@studiotitta/studio-editori 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/maarittely.d.ts +23 -4
- package/dist/palvelin/kuvat.js +10 -1
- package/dist/selain/Muokkaustila.d.ts +1 -1
- package/dist/selain/Muokkaustila.js +62 -9
- package/dist/selain/Paneeli.d.ts +1 -0
- package/dist/selain/Paneeli.js +91 -21
- package/dist/selain/polku.d.ts +7 -1
- package/dist/selain/polku.js +34 -11
- package/package.json +1 -1
package/dist/maarittely.d.ts
CHANGED
|
@@ -102,6 +102,14 @@ export interface TekstilistaKentta<K extends boolean = boolean> extends KentanPe
|
|
|
102
102
|
/** Monirivinen kohta listassa. */
|
|
103
103
|
monirivinen?: boolean;
|
|
104
104
|
enintaan?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Miten lista näkyy sivulla yhtenä tekstinä, esimerkiksi ", ".
|
|
107
|
+
*
|
|
108
|
+
* Kun sivusto näyttää listan pilkuilla eroteltuna ("Lounas, Materiaalit"),
|
|
109
|
+
* sen voi silti muokata paikallaan: editori jakaa tekstin takaisin listaksi
|
|
110
|
+
* tällä erottimella. Ilman tätä jokainen rivi on oma kohtansa.
|
|
111
|
+
*/
|
|
112
|
+
erotin?: string;
|
|
105
113
|
}
|
|
106
114
|
/**
|
|
107
115
|
* Kenttäryhmä, esimerkiksi hakukonetiedot.
|
|
@@ -110,9 +118,9 @@ export interface TekstilistaKentta<K extends boolean = boolean> extends KentanPe
|
|
|
110
118
|
* Editorissa se näkyy omana lohkonaan, jotta sivun kentät eivät sekoitu
|
|
111
119
|
* hakukoneteksteihin.
|
|
112
120
|
*/
|
|
113
|
-
export interface RyhmaKentta extends KentanPerus {
|
|
121
|
+
export interface RyhmaKentta<R extends Record<string, Kentta> = Record<string, Kentta>> extends KentanPerus {
|
|
114
122
|
tyyppi: "ryhma";
|
|
115
|
-
kentat:
|
|
123
|
+
kentat: R;
|
|
116
124
|
}
|
|
117
125
|
/**
|
|
118
126
|
* Lista samanlaisia kohteita, esimerkiksi kurssit.
|
|
@@ -134,6 +142,14 @@ export interface KohdelistaKentta extends KentanPerus {
|
|
|
134
142
|
* puhuttaisiin kohteista, mikä ei ole kenenkään sana omasta työstään.
|
|
135
143
|
*/
|
|
136
144
|
yksikko?: string;
|
|
145
|
+
/**
|
|
146
|
+
* Kohteen oma osoite sivustolla, esimerkiksi "/kurssit/{tunnus}".
|
|
147
|
+
*
|
|
148
|
+
* Editori näyttää linkin, jolla kohteen sivun pääsee katsomaan. Uuden
|
|
149
|
+
* kohteen sivu syntyy vasta tallennuksen jälkeen, joten linkki kertoo
|
|
150
|
+
* myös, mistä työn tuloksen näkee.
|
|
151
|
+
*/
|
|
152
|
+
osoite?: string;
|
|
137
153
|
kentat: Record<string, Kentta>;
|
|
138
154
|
}
|
|
139
155
|
export type Kentta = TekstiKentta | TekstialueKentta | LukuKentta | ValintaKentta | KytkinKentta | AikaKentta | KuvaKentta | KielivalintaKentta | TekstilistaKentta | RyhmaKentta | KohdelistaKentta;
|
|
@@ -151,7 +167,7 @@ export declare function kielivalinta(asetukset?: Omit<KielivalintaKentta, "tyypp
|
|
|
151
167
|
export declare function tekstilista<K extends boolean = false>(asetukset?: Omit<TekstilistaKentta<K>, "tyyppi" | "kaannettava"> & {
|
|
152
168
|
kaannettava?: K;
|
|
153
169
|
}): TekstilistaKentta<K>;
|
|
154
|
-
export declare function ryhma(asetukset: Omit<RyhmaKentta
|
|
170
|
+
export declare function ryhma<R extends Record<string, Kentta>>(asetukset: Omit<RyhmaKentta<R>, "tyyppi">): RyhmaKentta<R>;
|
|
155
171
|
export declare function kohdelista(asetukset: Omit<KohdelistaKentta, "tyyppi">): KohdelistaKentta;
|
|
156
172
|
export interface SivunMaarittely {
|
|
157
173
|
/** Nimi, jonka muokkaaja näkee editorissa. Oletuksena sivun avain. */
|
|
@@ -193,7 +209,10 @@ export type KentanArvo<K extends Kentta> = K extends {
|
|
|
193
209
|
tyyppi: "kohdelista";
|
|
194
210
|
} ? Record<string, unknown>[] : K extends {
|
|
195
211
|
tyyppi: "ryhma";
|
|
196
|
-
|
|
212
|
+
kentat: infer R extends Record<string, Kentta>;
|
|
213
|
+
} ? {
|
|
214
|
+
[A in keyof R]: KentanArvo<R[A]>;
|
|
215
|
+
} : string;
|
|
197
216
|
/** Kentän arvo sellaisena kuin se on tiedostossa. */
|
|
198
217
|
type TallennettuArvo<K extends Kentta, L extends string> = K extends {
|
|
199
218
|
kaannettava: true;
|
package/dist/palvelin/kuvat.js
CHANGED
|
@@ -125,6 +125,15 @@ export function kuvienReitti(yliajot) {
|
|
|
125
125
|
console.error("[editori] Kuvan tallennus epäonnistui:", virhe);
|
|
126
126
|
return json({ virhe: "Kuvaa ei saatu tallennettua." }, 502);
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
/*
|
|
129
|
+
Esikatselu palautetaan kuvana, ei osoitteena: tiedosto on repossa, mutta
|
|
130
|
+
sivusto tarjoilee sen vasta seuraavan julkaisun jälkeen. Ilman tätä
|
|
131
|
+
muokkaaja näkee rikkinäisen kuvan eikä tiedä, menikö lataus läpi.
|
|
132
|
+
*/
|
|
133
|
+
return json({
|
|
134
|
+
src: kuvanOsoite(polku),
|
|
135
|
+
esikatselu: `data:image/${tunnistaTyyppi(valmis) ?? "jpeg"};base64,${Buffer.from(valmis).toString("base64")}`,
|
|
136
|
+
ehdotus: await ehdotus,
|
|
137
|
+
});
|
|
129
138
|
};
|
|
130
139
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare const PALKIN_TYYLIT = "\n :host { all: initial; }\n .palkki {\n position: fixed; inset: auto 1rem 1rem auto; z-index: 2147483000;\n display: flex; align-items: center; gap: .75rem;\n padding: .6rem .75rem .6rem 1rem; border-radius: .5rem;\n background: #1d1f1e; color: #fff;\n font: 500 .875rem/1.4 -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n box-shadow: 0 .5rem 1.5rem rgba(0,0,0,.25);\n max-width: calc(100vw - 2rem);\n }\n .tila { opacity: .75; font-weight: 400; }\n .tila[data-virhe=\"1\"] { color: #ffb4a2; opacity: 1; }\n button {\n font: inherit; padding: .45rem .8rem; border-radius: .35rem;\n border: 1px solid rgba(255,255,255,.25); background: transparent; color: inherit;\n cursor: pointer;\n }\n button.ensisijainen { background: #fff; color: #1d1f1e; border-color: #fff; }\n button:disabled { opacity: .5; cursor: default; }\n button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n\n /* Sivupaneeli */\n .paneeli {\n position: fixed; inset: 0 0 0 auto; z-index: 2147482999;\n width: min(23rem, 100vw); overflow-y: auto;\n padding: 1.25rem 1.25rem 6rem;\n background: #fbfaf8; color: #1d1f1e;\n border-left: 1px solid rgba(0,0,0,.12);\n font: 400 .875rem/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n box-shadow: -.5rem 0 1.5rem rgba(0,0,0,.08);\n }\n .paneeli h2 { margin: 0 0 .75rem; font-size: .8125rem; text-transform: uppercase; letter-spacing: .06em; opacity: .6; }\n .paneeli section + section { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid rgba(0,0,0,.1); }\n .kentta { margin-bottom: 1rem; }\n .kentta label { display: flex; gap: .5rem; align-items: baseline; margin-bottom: .25rem; font-weight: 600; }\n .kieli { font-weight: 400; font-size: .75rem; padding: 0 .3rem; border-radius: .2rem; background: rgba(0,0,0,.08); text-transform: uppercase; }\n .kentta input[type=text], .kentta input[type=number], .kentta input[type=date],\n .kentta input[type=datetime-local], .kentta select, .kentta textarea {\n width: 100%; padding: .45rem .55rem; border: 1px solid rgba(0,0,0,.25);\n border-radius: .25rem; background: #fff; font: inherit; color: inherit;\n }\n .kentta textarea { resize: vertical; }\n .kytkin { display: flex; gap: .5rem; align-items: center; font-weight: 400; }\n .kielivalinta { display: grid; gap: .35rem; }\n .kaannos button { color: inherit; border-color: rgba(0,0,0,.25); }\n .ohje { margin: .25rem 0 0; font-size: .8125rem; opacity: .65; }\n .laskuri { margin: .3rem 0 0; font-size: .8125rem; font-weight: 600; }\n .virheet { padding: .75rem .9rem; border-radius: .3rem; background: #f6e8e3; color: #6d2b20; }\n .virheet h2 { margin: 0 0 .4rem; opacity: 1; color: inherit; }\n .virheet ul { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n .kuva img { display: block; width: 100%; height: auto; border-radius: .25rem; margin-bottom: .4rem; }\n .kuva input[type=text] { width: 100%; padding: .45rem .55rem; border: 1px solid rgba(0,0,0,.25); border-radius: .25rem; font: inherit; }\n /* Kuvan tekstikentt\u00E4 on oma lohkonsa: label on .kentta-tasolla flex,\n ja ilman t\u00E4t\u00E4 otsikko ja kentt\u00E4 asettuisivat vierekk\u00E4in. */\n .kentta label.alt, .kuva label.alt { display: block; font-weight: 600; margin-top: .5rem; }\n .alt input { margin-top: .25rem; width: 100%; }\n .huomio {\n margin: .4rem 0 0; padding: .5rem .6rem; border-radius: .25rem;\n background: #fdf3e7; color: #6b4a1f; font-size: .8125rem;\n }\n .huomio p { margin: 0 0 .4rem; }\n .huomio button { color: inherit; border-color: rgba(107,74,31,.35); padding: .3rem .6rem; }\n .lataus { display: inline-flex; margin: 0 0 .5rem; }\n .lataus span {\n padding: .45rem .8rem; border: 1px solid rgba(0,0,0,.25); border-radius: .35rem;\n cursor: pointer; font-weight: 600;\n }\n .lataus input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; }\n .virheviesti { margin: 0 0 .5rem; color: #8c3b2e; font-size: .8125rem; }\n .tyhja { margin: 0; opacity: .6; }\n .kohteet { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; gap: .25rem; }\n .kohteet button {\n width: 100%; text-align: left; color: inherit; border-color: rgba(0,0,0,.15);\n display: flex; justify-content: space-between; gap: .5rem; align-items: center;\n }\n .kohteet button.valittu { background: #1d1f1e; color: #fff; border-color: #1d1f1e; }\n .merkki { font-size: .75rem; opacity: .7; white-space: nowrap; }\n /* Taitos n\u00E4ytt\u00E4\u00E4 painikkeelta ja nuoli osoittaa sinne, minne sis\u00E4lt\u00F6\n aukeaa: alas kiinni ollessa, yl\u00F6s auki ollessa. */\n .ryhma { margin: 0 0 1rem; padding: .75rem .8rem; border-radius: .3rem; background: rgba(0,0,0,.035); }\n .ryhma h3 { margin: 0 0 .5rem; font-size: .8125rem; text-transform: uppercase; letter-spacing: .05em; opacity: .65; }\n .ryhma .kentta:last-child { margin-bottom: 0; }\n .lisatiedot { margin: .25rem 0 1rem; border-top: 1px solid rgba(0,0,0,.1); }\n .lisatiedot summary {\n cursor: pointer; font-weight: 600; padding: .7rem 0; list-style: none;\n display: flex; align-items: center; gap: .5rem;\n }\n .lisatiedot summary::-webkit-details-marker { display: none; }\n .lisatiedot summary::after {\n content: \"\u25BE\"; font-size: 1.1rem; line-height: 1; margin-left: auto;\n }\n .lisatiedot[open] summary::after { content: \"\u25B4\"; }\n .lisatiedot summary:hover { text-decoration: underline; }\n .lisatiedot summary:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n .kaannos { margin: 0 0 1rem; padding: .6rem .7rem; border-radius: .25rem; background: #eef1ee; }\n .kaannos p { margin: 0 0 .5rem; }\n /* Kenttien nimet omalle rivilleen: lause pysyy luettavana ja nimet erottuvat. */\n .kaannos strong { display: block; margin-top: .15rem; }\n .kaannos button { color: inherit; border-color: rgba(0,0,0,.25); }\n .napit { display: flex; gap: .5rem; margin-bottom: 1rem; }\n .napit button { color: inherit; border-color: rgba(0,0,0,.25); }\n .napit button.vaarallinen { color: #8c3b2e; border-color: rgba(140,59,46,.4); }\n";
|
|
1
|
+
declare const PALKIN_TYYLIT = "\n :host { all: initial; }\n .palkki {\n position: fixed; inset: auto 1rem 1rem auto; z-index: 2147483000;\n display: flex; align-items: center; gap: .75rem;\n padding: .6rem .75rem .6rem 1rem; border-radius: .5rem;\n background: #1d1f1e; color: #fff;\n font: 500 .875rem/1.4 -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n box-shadow: 0 .5rem 1.5rem rgba(0,0,0,.25);\n max-width: calc(100vw - 2rem);\n }\n .tila { opacity: .75; font-weight: 400; }\n .tila[data-virhe=\"1\"] { color: #ffb4a2; opacity: 1; }\n button {\n font: inherit; padding: .45rem .8rem; border-radius: .35rem;\n border: 1px solid rgba(255,255,255,.25); background: transparent; color: inherit;\n cursor: pointer;\n }\n button.ensisijainen { background: #fff; color: #1d1f1e; border-color: #fff; }\n button:disabled { opacity: .5; cursor: default; }\n button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n\n /* Sivupaneeli */\n .paneeli {\n position: fixed; inset: 0 0 0 auto; z-index: 2147482999;\n width: min(23rem, 100vw); overflow-y: auto;\n padding: 1.25rem 1.25rem 6rem;\n background: #fbfaf8; color: #1d1f1e;\n border-left: 1px solid rgba(0,0,0,.12);\n font: 400 .875rem/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n box-shadow: -.5rem 0 1.5rem rgba(0,0,0,.08);\n }\n .paneeli h2 { margin: 0 0 .75rem; font-size: .8125rem; text-transform: uppercase; letter-spacing: .06em; opacity: .6; }\n .paneeli section + section { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid rgba(0,0,0,.1); }\n .kentta { margin-bottom: 1rem; }\n .kentta label { display: flex; gap: .5rem; align-items: baseline; margin-bottom: .25rem; font-weight: 600; }\n .kieli { font-weight: 400; font-size: .75rem; padding: 0 .3rem; border-radius: .2rem; background: rgba(0,0,0,.08); text-transform: uppercase; }\n .kentta input[type=text], .kentta input[type=number], .kentta input[type=date],\n .kentta input[type=datetime-local], .kentta select, .kentta textarea {\n width: 100%; padding: .45rem .55rem; border: 1px solid rgba(0,0,0,.25);\n border-radius: .25rem; background: #fff; font: inherit; color: inherit;\n }\n .kentta textarea { resize: vertical; }\n .kytkin { display: flex; gap: .5rem; align-items: center; font-weight: 400; }\n .kielivalinta { display: grid; gap: .35rem; }\n .kaannos button { color: inherit; border-color: rgba(0,0,0,.25); }\n .ohje { margin: .25rem 0 0; font-size: .8125rem; opacity: .65; }\n .laskuri { margin: .3rem 0 0; font-size: .8125rem; font-weight: 600; }\n /* Pakollinen kentt\u00E4: punainen t\u00E4hti on tuttu merkki, eik\u00E4 se vie tilaa. */\n .pakko { color: #b3261e; font-weight: 700; }\n .piilossa {\n position: absolute; width: 1px; height: 1px; overflow: hidden;\n clip-path: inset(50%); white-space: nowrap;\n }\n .linkki {\n background: none; border: 0; padding: 0; margin: 0; font: inherit;\n color: inherit; text-decoration: underline; cursor: pointer;\n }\n .virheet { padding: .75rem .9rem; border-radius: .3rem; background: #f6e8e3; color: #6d2b20; }\n .virheet h2 { margin: 0 0 .4rem; opacity: 1; color: inherit; }\n .virheet ul { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n .kuva img { display: block; width: 100%; height: auto; border-radius: .25rem; margin-bottom: .4rem; }\n .kuva input[type=text] { width: 100%; padding: .45rem .55rem; border: 1px solid rgba(0,0,0,.25); border-radius: .25rem; font: inherit; }\n /* Kuvan tekstikentt\u00E4 on oma lohkonsa: label on .kentta-tasolla flex,\n ja ilman t\u00E4t\u00E4 otsikko ja kentt\u00E4 asettuisivat vierekk\u00E4in. */\n .kentta label.alt, .kuva label.alt { display: block; font-weight: 600; margin-top: .5rem; }\n .alt input { margin-top: .25rem; width: 100%; }\n .huomio {\n margin: .4rem 0 0; padding: .5rem .6rem; border-radius: .25rem;\n background: #fdf3e7; color: #6b4a1f; font-size: .8125rem;\n }\n .huomio p { margin: 0 0 .4rem; }\n .huomio button { color: inherit; border-color: rgba(107,74,31,.35); padding: .3rem .6rem; }\n .lataus { display: inline-flex; margin: 0 0 .5rem; }\n .lataus span {\n padding: .45rem .8rem; border: 1px solid rgba(0,0,0,.25); border-radius: .35rem;\n cursor: pointer; font-weight: 600;\n }\n .lataus input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; }\n .virheviesti { margin: 0 0 .5rem; color: #8c3b2e; font-size: .8125rem; }\n .tyhja { margin: 0; opacity: .6; }\n .kohteet { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; gap: .25rem; }\n .kohteet button {\n width: 100%; text-align: left; color: inherit; border-color: rgba(0,0,0,.15);\n display: flex; justify-content: space-between; gap: .5rem; align-items: center;\n }\n .kohteet button.valittu { background: #1d1f1e; color: #fff; border-color: #1d1f1e; }\n .merkki { font-size: .75rem; opacity: .7; white-space: nowrap; }\n /* Taitos n\u00E4ytt\u00E4\u00E4 painikkeelta ja nuoli osoittaa sinne, minne sis\u00E4lt\u00F6\n aukeaa: alas kiinni ollessa, yl\u00F6s auki ollessa. */\n .ryhma { margin: 0 0 1rem; padding: .75rem .8rem; border-radius: .3rem; background: rgba(0,0,0,.035); }\n .ryhma h3 { margin: 0 0 .5rem; font-size: .8125rem; text-transform: uppercase; letter-spacing: .05em; opacity: .65; }\n .ryhma .kentta:last-child { margin-bottom: 0; }\n .lisatiedot { margin: .25rem 0 1rem; border-top: 1px solid rgba(0,0,0,.1); }\n .lisatiedot summary {\n cursor: pointer; font-weight: 600; padding: .7rem 0; list-style: none;\n display: flex; align-items: center; gap: .5rem;\n }\n .lisatiedot summary::-webkit-details-marker { display: none; }\n .lisatiedot summary::after {\n content: \"\u25BE\"; font-size: 1.1rem; line-height: 1; margin-left: auto;\n }\n .lisatiedot[open] summary::after { content: \"\u25B4\"; }\n .lisatiedot summary:hover { text-decoration: underline; }\n .lisatiedot summary:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n .kaannos { margin: 0 0 1rem; padding: .6rem .7rem; border-radius: .25rem; background: #eef1ee; }\n .kaannos p { margin: 0 0 .5rem; }\n /* Kenttien nimet omalle rivilleen: lause pysyy luettavana ja nimet erottuvat. */\n .kaannos strong { display: block; margin-top: .15rem; }\n .kaannos button { color: inherit; border-color: rgba(0,0,0,.25); }\n .napit { display: flex; gap: .5rem; margin-bottom: 1rem; }\n .napit button { color: inherit; border-color: rgba(0,0,0,.25); }\n .napit button.vaarallinen { color: #8c3b2e; border-color: rgba(140,59,46,.4); }\n";
|
|
2
2
|
export declare function Muokkaustila(): import("react").JSX.Element | null;
|
|
3
3
|
export { PALKIN_TYYLIT };
|
|
@@ -72,6 +72,16 @@ const PALKIN_TYYLIT = `
|
|
|
72
72
|
.kaannos button { color: inherit; border-color: rgba(0,0,0,.25); }
|
|
73
73
|
.ohje { margin: .25rem 0 0; font-size: .8125rem; opacity: .65; }
|
|
74
74
|
.laskuri { margin: .3rem 0 0; font-size: .8125rem; font-weight: 600; }
|
|
75
|
+
/* Pakollinen kenttä: punainen tähti on tuttu merkki, eikä se vie tilaa. */
|
|
76
|
+
.pakko { color: #b3261e; font-weight: 700; }
|
|
77
|
+
.piilossa {
|
|
78
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden;
|
|
79
|
+
clip-path: inset(50%); white-space: nowrap;
|
|
80
|
+
}
|
|
81
|
+
.linkki {
|
|
82
|
+
background: none; border: 0; padding: 0; margin: 0; font: inherit;
|
|
83
|
+
color: inherit; text-decoration: underline; cursor: pointer;
|
|
84
|
+
}
|
|
75
85
|
.virheet { padding: .75rem .9rem; border-radius: .3rem; background: #f6e8e3; color: #6d2b20; }
|
|
76
86
|
.virheet h2 { margin: 0 0 .4rem; opacity: 1; color: inherit; }
|
|
77
87
|
.virheet ul { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
@@ -159,12 +169,15 @@ function lisaaTyylit() {
|
|
|
159
169
|
function lueTeksti(elementti, lista) {
|
|
160
170
|
return (lista ? elementti.innerText : elementti.textContent) ?? "";
|
|
161
171
|
}
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Sivulla kirjoitettu lista takaisin listaksi.
|
|
174
|
+
*
|
|
175
|
+
* Oletuksena yksi rivi on yksi kohta. Jos sivusto näyttää listan yhtenä
|
|
176
|
+
* tekstinä (esim. "Lounas, Materiaalit"), erotin tulee sisältömallista.
|
|
177
|
+
*/
|
|
178
|
+
function jaaKappaleiksi(teksti, erotin) {
|
|
179
|
+
const osat = erotin ? teksti.split(erotin.trim()) : teksti.split(/\n+/);
|
|
180
|
+
return osat.map((osa) => osa.trim()).filter((osa) => osa !== "");
|
|
168
181
|
}
|
|
169
182
|
/** Sivulla olevat muokattavat kohdat merkintöineen. */
|
|
170
183
|
function merkinnat() {
|
|
@@ -325,8 +338,10 @@ export function Muokkaustila() {
|
|
|
325
338
|
continue;
|
|
326
339
|
if (onKaannettava(kenttä))
|
|
327
340
|
uusi[avain] = {};
|
|
341
|
+
/* Uusi kohde syntyy piilotettuna, kun mallissa on piilottava
|
|
342
|
+
kytkin: keskeneräinen kurssi ei kuulu kävijän silmien eteen. */
|
|
328
343
|
else if (kenttä.tyyppi === "kytkin")
|
|
329
|
-
uusi[avain] =
|
|
344
|
+
uusi[avain] = kenttä.piilottaa === true;
|
|
330
345
|
else if (kenttä.tyyppi === "tekstilista")
|
|
331
346
|
uusi[avain] = [];
|
|
332
347
|
else if (kenttä.tyyppi === "valinta")
|
|
@@ -386,6 +401,9 @@ export function Muokkaustila() {
|
|
|
386
401
|
}
|
|
387
402
|
return {
|
|
388
403
|
src: tulos.src,
|
|
404
|
+
/* Esikatselu on itse kuva: tallennettu tiedosto näkyy osoitteessaan
|
|
405
|
+
vasta seuraavan julkaisun jälkeen. */
|
|
406
|
+
esikatselu: typeof tulos.esikatselu === "string" ? tulos.esikatselu : null,
|
|
389
407
|
ehdotus: typeof tulos.ehdotus === "string" ? tulos.ehdotus : null,
|
|
390
408
|
};
|
|
391
409
|
}, []);
|
|
@@ -446,6 +464,13 @@ export function Muokkaustila() {
|
|
|
446
464
|
}
|
|
447
465
|
continue;
|
|
448
466
|
}
|
|
467
|
+
/* Sivun oma ryhmä: kentät ovat ryhmän sisällä, eivät sivun juuressa. */
|
|
468
|
+
if (kenttä.tyyppi === "ryhma") {
|
|
469
|
+
for (const [ryhmanKentta, ryhmanMaaritys] of Object.entries(kenttä.kentat)) {
|
|
470
|
+
tutki({ sivu: sivunAvain, kentta: avain, alikentta: ryhmanKentta }, ryhmanMaaritys);
|
|
471
|
+
}
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
449
474
|
tutki({ sivu: sivunAvain, kentta: avain }, kenttä);
|
|
450
475
|
}
|
|
451
476
|
return loydot;
|
|
@@ -501,14 +526,38 @@ export function Muokkaustila() {
|
|
|
501
526
|
muutetut[sivu] = { ...data };
|
|
502
527
|
}
|
|
503
528
|
}
|
|
529
|
+
/* Sivulla kirjoitetut luvut tarkistetaan tässä: palvelimelle ei kannata
|
|
530
|
+
lähettää "kymmenen", kun virheen näkee jo selaimessa. */
|
|
531
|
+
const sivunVirheet = [];
|
|
504
532
|
for (const { elementti, polku } of merkinnat()) {
|
|
505
533
|
const maaritys = kentanMaaritys(sisalto, polku);
|
|
506
534
|
const lista = maaritys?.tyyppi === "tekstilista";
|
|
507
535
|
const teksti = lueTeksti(elementti, lista);
|
|
508
536
|
if (teksti === alkuperaiset.current.get(elementti))
|
|
509
537
|
continue;
|
|
538
|
+
let arvo = teksti;
|
|
539
|
+
if (lista && maaritys?.tyyppi === "tekstilista") {
|
|
540
|
+
arvo = jaaKappaleiksi(teksti, maaritys.erotin);
|
|
541
|
+
}
|
|
542
|
+
else if (maaritys?.tyyppi === "luku") {
|
|
543
|
+
const luku = Number(teksti.replace(",", ".").trim());
|
|
544
|
+
if (teksti.trim() === "" || !Number.isFinite(luku)) {
|
|
545
|
+
sivunVirheet.push({
|
|
546
|
+
kohde: maaritys.nimi ?? polku.alikentta ?? polku.kentta,
|
|
547
|
+
viesti: "Kirjoita pelkkä numero.",
|
|
548
|
+
});
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
arvo = luku;
|
|
552
|
+
}
|
|
510
553
|
const kieli = onKaannettava(maaritys) ? kentanKieli(elementti, sisalto.kielet) : undefined;
|
|
511
|
-
muutetut[polku.sivu] = asetaArvo(pohja(polku.sivu), polku,
|
|
554
|
+
muutetut[polku.sivu] = asetaArvo(pohja(polku.sivu), polku, arvo, kieli, tunnusKentta(sisalto, polku));
|
|
555
|
+
}
|
|
556
|
+
if (sivunVirheet.length > 0) {
|
|
557
|
+
setTila("muokkaa");
|
|
558
|
+
setViesti("Tarkista sivulla merkityt kohdat. Muutoksesi ovat yhä tässä.");
|
|
559
|
+
setVirheet(sivunVirheet);
|
|
560
|
+
return;
|
|
512
561
|
}
|
|
513
562
|
/* Uusi kohde sai tunnuksen "uusi". Nyt kun sillä on nimi, tunnus
|
|
514
563
|
johdetaan siitä: se näkyy osoitteessa, eikä kukaan halua /uusi-2. */
|
|
@@ -614,7 +663,7 @@ export function Muokkaustila() {
|
|
|
614
663
|
kohde: kaannettavat
|
|
615
664
|
.filter(({ polku }) => polku.tunnus !== undefined)
|
|
616
665
|
.map(({ nimi }) => nimi),
|
|
617
|
-
}, valitse: setValittu, lisaaKohde: lisaaKohde, poistaKohde: poistaKohde })) : null, _jsx("div", { className: "palkki", role: "region", "aria-label": "Sis\u00E4ll\u00F6n muokkaus", children: tila === "lataa" ? (_jsx("span", { className: "tila", children: "Editori avautuu\u2026" })) : muokataan || tallentaa ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "tila", role: "status", "data-virhe": viesti ? "1" : "0", children: tallentaa ? "Tallennetaan…" : (viesti ?? "") !== "" ? viesti : "Muokkaat sivua" }), _jsx("button", { type: "button", onClick: peru, disabled: tallentaa, children: "Peru" }), _jsx("button", { type: "button", className: "ensisijainen", onClick: tallenna, disabled: tallentaa, children: "Tallenna" })] })) : (_jsxs(_Fragment, { children: [viesti ? (_jsx("span", { className: "tila", role: "status", "data-virhe": tila === "virhe" ? "1" : "0", children: viesti })) : null, tila === "tallennettu" ? (_jsx("button", { type: "button", onClick: () => location.reload(), children: "P\u00E4ivit\u00E4 sivu" })) : null, sisalto ? (_jsx("button", { type: "button", className: "ensisijainen", onClick: paalle, children: "Muokkaa" })) : (_jsx("button", { type: "button", onClick: () => location.reload(), children: "Lataa uudelleen" }))] })) })] }));
|
|
666
|
+
}, valitse: setValittu, lisaaKohde: lisaaKohde, poistaKohde: poistaKohde })) : null, _jsx("div", { className: "palkki", role: "region", "aria-label": "Sis\u00E4ll\u00F6n muokkaus", children: tila === "lataa" ? (_jsx("span", { className: "tila", children: "Editori avautuu\u2026" })) : muokataan || tallentaa ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "tila", role: "status", "data-virhe": viesti ? "1" : "0", children: tallentaa ? "Tallennetaan…" : (viesti ?? "") !== "" ? viesti : "Muokkaat sivua" }), _jsx("button", { type: "button", onClick: peru, disabled: tallentaa, children: "Peru" }), _jsx("button", { type: "button", className: "ensisijainen", onClick: tallenna, disabled: tallentaa, children: "Tallenna" })] })) : (_jsxs(_Fragment, { children: [viesti ? (_jsx("span", { className: "tila", role: "status", "data-virhe": tila === "virhe" ? "1" : "0", children: viesti })) : null, tila === "tallennettu" ? (_jsx("button", { type: "button", onClick: () => location.reload(), children: "P\u00E4ivit\u00E4 sivu" })) : null, sisalto && malli ? (_jsx("button", { type: "button", className: "ensisijainen", onClick: paalle, children: "Muokkaa" })) : sisalto ? (_jsx("span", { className: "tila", children: "T\u00E4ll\u00E4 sivulla ei ole muokattavaa sis\u00E4lt\u00F6\u00E4." })) : (_jsx("button", { type: "button", onClick: () => location.reload(), children: "Lataa uudelleen" }))] })) })] }));
|
|
618
667
|
}
|
|
619
668
|
/** Mitä yhtä kohdetta tässä listassa kutsutaan, esimerkiksi "kurssi". */
|
|
620
669
|
function kohdelistanYksikko(sisalto, sivu, kentta) {
|
|
@@ -635,6 +684,10 @@ function kentanMaaritys(sisalto, polku) {
|
|
|
635
684
|
return undefined;
|
|
636
685
|
if (polku.alikentta === undefined)
|
|
637
686
|
return kentta;
|
|
687
|
+
/* Ilman tunnusta alikenttä on sivun oman ryhmän kenttä. */
|
|
688
|
+
if (polku.tunnus === undefined) {
|
|
689
|
+
return kentta.tyyppi === "ryhma" ? kentta.kentat[polku.alikentta] : undefined;
|
|
690
|
+
}
|
|
638
691
|
if (kentta.tyyppi !== "kohdelista")
|
|
639
692
|
return undefined;
|
|
640
693
|
const [ensimmainen, ryhmanKentta] = polku.alikentta.split(".");
|
package/dist/selain/Paneeli.d.ts
CHANGED
package/dist/selain/Paneeli.js
CHANGED
|
@@ -85,15 +85,15 @@ function Kentta_({ polku, maaritys, tiedot, }) {
|
|
|
85
85
|
const tunniste = `kentta-${polku.kentta}-${polku.tunnus ?? ""}-${(polku.alikentta ?? "").replace(".", "-")}`;
|
|
86
86
|
const kaannettava = "kaannettava" in maaritys && maaritys.kaannettava === true;
|
|
87
87
|
const arvo = nykyinenArvo(maaritys, lueArvo(tiedot.data, polku, tunnuksenNimi(tiedot, polku.kentta)), tiedot.kieli);
|
|
88
|
-
return (_jsxs("div", { className: "kentta", children: [_jsxs("label", { htmlFor: tunniste, children: [nimi, kaannettava ? _jsx("span", { className: "kieli", children: tiedot.kieli }) : null] }), _jsx(Sisalto, { maaritys: maaritys, arvo: arvo, tunniste: tunniste, kielet: tiedot.kielet, muuta: (uusi) => tiedot.muuta(polku, uusi, kaannettava), lataa: maaritys.tyyppi === "kuva"
|
|
88
|
+
return (_jsxs("div", { className: "kentta", children: [_jsxs("label", { htmlFor: tunniste, children: [nimi, maaritys.pakollinen ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pakko", "aria-hidden": "true", title: "Pakollinen tieto", children: "*" }), _jsx("span", { className: "piilossa", children: " (pakollinen)" })] })) : null, kaannettava ? _jsx("span", { className: "kieli", children: tiedot.kieli }) : null] }), _jsx(Sisalto, { maaritys: maaritys, arvo: arvo, tunniste: tunniste, kielet: tiedot.kielet, muuta: (uusi) => tiedot.muuta(polku, uusi, kaannettava), lataa: maaritys.tyyppi === "kuva"
|
|
89
89
|
? async (tiedosto) => {
|
|
90
|
-
const { src, ehdotus } = await tiedot.lataaKuva(tiedosto, maaritys);
|
|
90
|
+
const { src, ehdotus, esikatselu } = await tiedot.lataaKuva(tiedosto, maaritys);
|
|
91
91
|
const vanha = onOlio(arvo) ? arvo : {};
|
|
92
92
|
/* Tyhjä teksti täytetään ehdotuksella suoraan; olemassa
|
|
93
93
|
olevaa ei ylikirjoiteta, vaan ehdotus tarjotaan. */
|
|
94
94
|
const alt = typeof vanha.alt === "string" ? vanha.alt.trim() : "";
|
|
95
95
|
tiedot.muuta(polku, { ...vanha, src, ...(alt === "" && ehdotus ? { alt: ehdotus } : {}) }, false);
|
|
96
|
-
return ehdotus;
|
|
96
|
+
return { ehdotus, esikatselu };
|
|
97
97
|
}
|
|
98
98
|
: undefined }), _jsx(Vapaana, { polku: polku, maaritys: maaritys, arvo: arvo, tiedot: tiedot }), maaritys.ohje ? _jsx("p", { className: "ohje", children: maaritys.ohje }) : null] }));
|
|
99
99
|
}
|
|
@@ -115,6 +115,75 @@ function Vapaana({ polku, maaritys, arvo, tiedot, }) {
|
|
|
115
115
|
const vapaana = Math.max(0, yla - arvo);
|
|
116
116
|
return (_jsx("p", { className: "laskuri", role: "status", children: vapaana === 0 ? "Täynnä" : `Vapaana ${vapaana} / ${yla} paikkaa` }));
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Kohteen nimi listassa.
|
|
120
|
+
*
|
|
121
|
+
* Nykyinen kieli ensin, mutta kääntämätön kurssi näkyy sillä kielellä, jolla
|
|
122
|
+
* se on kirjoitettu — ei nimettömänä.
|
|
123
|
+
*/
|
|
124
|
+
/** Onko kohde vasta lisätty eikä sitä ole vielä tallennettu? */
|
|
125
|
+
function uusi(tunnus) {
|
|
126
|
+
return /^uusi(-\d+)?$/.test(tunnus);
|
|
127
|
+
}
|
|
128
|
+
function kohteenOtsikko(maaritys, kohde, kieli) {
|
|
129
|
+
const arvo = kohde[maaritys.otsikko];
|
|
130
|
+
if (typeof arvo === "string")
|
|
131
|
+
return arvo === "" ? "(nimetön)" : arvo;
|
|
132
|
+
if (!onOlio(arvo))
|
|
133
|
+
return "(nimetön)";
|
|
134
|
+
const omalla = arvo[kieli];
|
|
135
|
+
if (typeof omalla === "string" && omalla !== "")
|
|
136
|
+
return omalla;
|
|
137
|
+
const joku = Object.values(arvo).find((muu) => typeof muu === "string" && muu !== "");
|
|
138
|
+
return joku ?? "(nimetön)";
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Virheen kohde muokkaajan sanoin.
|
|
142
|
+
*
|
|
143
|
+
* Palvelin kertoo polun ("kurssit.lista.testi.dateLabel"), mutta muokkaaja
|
|
144
|
+
* tuntee kentät niiden nimillä. Nimi haetaan sisältömallista, ja listan
|
|
145
|
+
* sisällä mukaan tulee se, minkä kohteen kentästä on kyse — muuten viisi
|
|
146
|
+
* virhettä näyttää samalta.
|
|
147
|
+
*/
|
|
148
|
+
function virheenKohde(tiedot, kohde) {
|
|
149
|
+
const osat = kohde.split(".");
|
|
150
|
+
const kentta = osat[1];
|
|
151
|
+
const maaritys = kentta ? tiedot.malli.kentat[kentta] : undefined;
|
|
152
|
+
if (!maaritys)
|
|
153
|
+
return { nimi: osat.at(-1) ?? kohde, sivulla: false };
|
|
154
|
+
if (maaritys.tyyppi === "ryhma") {
|
|
155
|
+
const ryhmanKentta = osat[2];
|
|
156
|
+
const sisempi = ryhmanKentta ? maaritys.kentat[ryhmanKentta] : undefined;
|
|
157
|
+
return {
|
|
158
|
+
nimi: sisempi?.nimi ?? ryhmanKentta ?? maaritys.nimi ?? kentta,
|
|
159
|
+
sivulla: tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${kentta}.${ryhmanKentta}`),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (maaritys.tyyppi !== "kohdelista") {
|
|
163
|
+
return {
|
|
164
|
+
nimi: maaritys.nimi ?? kentta,
|
|
165
|
+
sivulla: tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${kentta}`),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const tunnus = osat[2];
|
|
169
|
+
const alikentta = osat[3];
|
|
170
|
+
const alimaaritys = alikentta ? maaritys.kentat[alikentta] : undefined;
|
|
171
|
+
let nimi = alimaaritys?.nimi ?? alikentta ?? maaritys.nimi ?? kentta;
|
|
172
|
+
if (alimaaritys?.tyyppi === "ryhma" && osat[4]) {
|
|
173
|
+
const sisempi = alimaaritys.kentat[osat[4]];
|
|
174
|
+
nimi = `${alimaaritys.nimi ?? alikentta}: ${sisempi?.nimi ?? osat[4]}`;
|
|
175
|
+
}
|
|
176
|
+
const kohteet = Array.isArray(tiedot.data[kentta])
|
|
177
|
+
? tiedot.data[kentta]
|
|
178
|
+
: [];
|
|
179
|
+
const osuma = kohteet.find((yksi) => yksi[maaritys.tunnus] === tunnus);
|
|
180
|
+
return {
|
|
181
|
+
nimi,
|
|
182
|
+
...(osuma ? { kohteenNimi: kohteenOtsikko(maaritys, osuma, tiedot.kieli) } : {}),
|
|
183
|
+
...(tunnus === undefined ? {} : { tunnus }),
|
|
184
|
+
sivulla: tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${kentta}[${tunnus}].${alikentta}`),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
118
187
|
export function Paneeli(tiedot) {
|
|
119
188
|
const kentat = Object.entries(tiedot.malli.kentat);
|
|
120
189
|
const lista = kentat.find(([, maaritys]) => maaritys.tyyppi === "kohdelista");
|
|
@@ -122,10 +191,20 @@ export function Paneeli(tiedot) {
|
|
|
122
191
|
/* Kielivalinta on turha, jos sivustolla on vain yksi kieli. */
|
|
123
192
|
if (maaritys.tyyppi === "kielivalinta" && tiedot.kielet.length < 2)
|
|
124
193
|
return false;
|
|
194
|
+
/* Ryhmä, jonka jokainen kenttä muokataan jo sivulla, ei kuulu paneeliin:
|
|
195
|
+
tyhjä otsikko näyttäisi siltä, että jotain puuttuu. */
|
|
196
|
+
if (maaritys.tyyppi === "ryhma") {
|
|
197
|
+
return Object.keys(maaritys.kentat).some((ryhmanKentta) => !tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}.${ryhmanKentta}`));
|
|
198
|
+
}
|
|
125
199
|
return !tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}`);
|
|
126
200
|
};
|
|
127
201
|
const omat = kentat.filter(([avain, maaritys]) => maaritys.tyyppi !== "kohdelista" && nayta([avain, maaritys]));
|
|
128
|
-
return (_jsxs("aside", { className: "paneeli", "aria-label": "Sis\u00E4ll\u00F6n tiedot", children: [tiedot.virheet && tiedot.virheet.length > 0 ? (_jsxs("section", { className: "virheet", role: "alert", children: [_jsx("h2", { children: "Tallennus pys\u00E4htyi" }), _jsx("ul", { children: tiedot.virheet.slice(0, 6).map((virhe) =>
|
|
202
|
+
return (_jsxs("aside", { className: "paneeli", "aria-label": "Sis\u00E4ll\u00F6n tiedot", children: [tiedot.virheet && tiedot.virheet.length > 0 ? (_jsxs("section", { className: "virheet", role: "alert", children: [_jsx("h2", { children: "Tallennus pys\u00E4htyi" }), _jsx("ul", { children: tiedot.virheet.slice(0, 6).map((virhe) => {
|
|
203
|
+
const tiedot_ = virheenKohde(tiedot, virhe.kohde);
|
|
204
|
+
return (_jsxs("li", { children: [_jsx("strong", { children: tiedot_.nimi }), " ", virhe.viesti, tiedot_.kohteenNimi ? (_jsx("button", { type: "button", className: "linkki", onClick: () => tiedot_.tunnus && tiedot.valitse(tiedot_.tunnus), children: tiedot_.kohteenNimi })) : null, tiedot_.sivulla ? _jsx("span", { className: "merkki", children: " Muokataan sivulla." }) : null] }, `${virhe.kohde}-${virhe.viesti}`));
|
|
205
|
+
}) })] })) : null, omat.length > 0 || tiedot.kaannettavia.sivu.length > 0 ? (_jsxs("section", { children: [_jsx("h2", { children: tiedot.malli.nimi }), omat.map(([avain, maaritys]) => maaritys.tyyppi === "ryhma" ? (_jsxs("div", { className: "ryhma", children: [_jsx("h3", { children: maaritys.nimi ?? avain }), maaritys.ohje ? _jsx("p", { className: "ohje", children: maaritys.ohje }) : null, Object.entries(maaritys.kentat)
|
|
206
|
+
.filter(([ryhmanKentta]) => !tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}.${ryhmanKentta}`))
|
|
207
|
+
.map(([ryhmanKentta, ryhmanMaaritys]) => (_jsx(Kentta_, { polku: { sivu: tiedot.sivu, kentta: avain, alikentta: ryhmanKentta }, maaritys: ryhmanMaaritys, tiedot: tiedot }, ryhmanKentta)))] }, avain)) : (_jsx(Kentta_, { polku: { sivu: tiedot.sivu, kentta: avain }, maaritys: maaritys, tiedot: tiedot }, avain))), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "sivu" }) : null] })) : null, lista ? _jsx(Kohteet, { avain: lista[0], maaritys: lista[1], tiedot: tiedot }) : null] }));
|
|
129
208
|
}
|
|
130
209
|
const KIELELLE = {
|
|
131
210
|
fi: "suomeksi",
|
|
@@ -169,20 +248,7 @@ function Kohteet({ avain, maaritys, tiedot, }) {
|
|
|
169
248
|
const kohteet = Array.isArray(tiedot.data[avain])
|
|
170
249
|
? tiedot.data[avain]
|
|
171
250
|
: [];
|
|
172
|
-
const nimi = (kohde) =>
|
|
173
|
-
const arvo = kohde[maaritys.otsikko];
|
|
174
|
-
if (typeof arvo === "string")
|
|
175
|
-
return arvo === "" ? "(nimetön)" : arvo;
|
|
176
|
-
if (!onOlio(arvo))
|
|
177
|
-
return "(nimetön)";
|
|
178
|
-
/* Nykyinen kieli ensin, mutta kääntämätön kurssi näkyy listassa sillä
|
|
179
|
-
kielellä, jolla se on kirjoitettu — ei nimettömänä. */
|
|
180
|
-
const omalla = arvo[tiedot.kieli];
|
|
181
|
-
if (typeof omalla === "string" && omalla !== "")
|
|
182
|
-
return omalla;
|
|
183
|
-
const joku = Object.values(arvo).find((muu) => typeof muu === "string" && muu !== "");
|
|
184
|
-
return joku ?? "(nimetön)";
|
|
185
|
-
};
|
|
251
|
+
const nimi = (kohde) => kohteenOtsikko(maaritys, kohde, tiedot.kieli);
|
|
186
252
|
/**
|
|
187
253
|
* Merkinnät kohteen nimen perässä: piilotus, huomionarvoinen tila ja
|
|
188
254
|
* täyttyminen. Näin listasta näkee tilanteen avaamatta jokaista kohdetta.
|
|
@@ -228,7 +294,7 @@ function Kohteet({ avain, maaritys, tiedot, }) {
|
|
|
228
294
|
const tunnus = String(kohde[maaritys.tunnus] ?? "");
|
|
229
295
|
const omatMerkinnat = merkinnat(kohde);
|
|
230
296
|
return (_jsx("li", { children: _jsxs("button", { type: "button", className: tunnus === valitunTunnus ? "valittu" : undefined, "aria-current": tunnus === valitunTunnus ? "true" : undefined, onClick: () => tiedot.valitse(tunnus), children: [_jsx("span", { children: nimi(kohde) }), omatMerkinnat.length > 0 ? (_jsx("span", { className: "merkki", children: omatMerkinnat.join(" · ") })) : null] }) }, tunnus));
|
|
231
|
-
}) }), _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", onClick: () => tiedot.lisaaKohde(avain, maaritys), children: ["+ Lis\u00E4\u00E4 ", yksikko] }) }), valittu ? null : (_jsxs("p", { className: "ohje", children: ["Valitse ", yksikko, ", niin n\u00E4et sen tiedot."] }))] }), valittu && valitunTunnus ? (_jsxs("section", { children: [_jsx("h2", { children: nimi(valittu) }), ensisijaiset.map(kentta), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "kohde" }) : null, lisatiedot.length > 0 ? (_jsxs("details", { className: "lisatiedot", children: [_jsx("summary", { children: "Lis\u00E4tiedot" }), lisatiedot.map(kentta)] })) : null, _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", className: "vaarallinen", onClick: () => tiedot.poistaKohde(avain, maaritys.tunnus, valitunTunnus), children: ["Poista ", yksikko] }) })] })) : null] }));
|
|
297
|
+
}) }), _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", onClick: () => tiedot.lisaaKohde(avain, maaritys), children: ["+ Lis\u00E4\u00E4 ", yksikko] }) }), valittu ? null : (_jsxs("p", { className: "ohje", children: ["Valitse ", yksikko, ", niin n\u00E4et sen tiedot."] }))] }), valittu && valitunTunnus ? (_jsxs("section", { children: [_jsx("h2", { children: nimi(valittu) }), maaritys.osoite ? (uusi(valitunTunnus) ? (_jsx("p", { className: "ohje", children: "Oma sivu syntyy, kun tallennat." })) : (_jsx("p", { className: "ohje", children: _jsx("a", { className: "linkki", href: maaritys.osoite.replace("{tunnus}", valitunTunnus), children: "Avaa sivu" }) }))) : null, ensisijaiset.map(kentta), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "kohde" }) : null, lisatiedot.length > 0 ? (_jsxs("details", { className: "lisatiedot", children: [_jsx("summary", { children: "Lis\u00E4tiedot" }), lisatiedot.map(kentta)] })) : null, _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", className: "vaarallinen", onClick: () => tiedot.poistaKohde(avain, maaritys.tunnus, valitunTunnus), children: ["Poista ", yksikko] }) })] })) : null] }));
|
|
232
298
|
}
|
|
233
299
|
/**
|
|
234
300
|
* Kuvakenttä: nykyinen kuva, vaihto ja vaihtoehtoinen teksti.
|
|
@@ -242,6 +308,9 @@ function Kuvakentta({ maaritys, arvo, muuta, lataa, }) {
|
|
|
242
308
|
/* Vasta vaihdettu kuva: vanha teksti kuvaa yhä vanhaa kuvaa. */
|
|
243
309
|
const [vaihdettu, setVaihdettu] = useState(false);
|
|
244
310
|
const [ehdotus, setEhdotus] = useState(null);
|
|
311
|
+
/* Juuri ladattu kuva näytetään sellaisenaan: tallennettu tiedosto ilmestyy
|
|
312
|
+
osoitteeseensa vasta, kun sivusto on rakennettu uudelleen. */
|
|
313
|
+
const [esikatselu, setEsikatselu] = useState(null);
|
|
245
314
|
const kuva = onOlio(arvo) ? arvo : undefined;
|
|
246
315
|
async function valitse(tiedosto) {
|
|
247
316
|
if (!tiedosto)
|
|
@@ -251,9 +320,10 @@ function Kuvakentta({ maaritys, arvo, muuta, lataa, }) {
|
|
|
251
320
|
setEhdotus(null);
|
|
252
321
|
try {
|
|
253
322
|
const nykyinenAlt = typeof kuva?.alt === "string" ? kuva.alt.trim() : "";
|
|
254
|
-
const uusiEhdotus = await lataa(tiedosto);
|
|
323
|
+
const { ehdotus: uusiEhdotus, esikatselu: uusiKuva } = await lataa(tiedosto);
|
|
255
324
|
setTila("valmis");
|
|
256
325
|
setVaihdettu(true);
|
|
326
|
+
setEsikatselu(uusiKuva);
|
|
257
327
|
/* Jos kentässä oli jo teksti, ehdotusta tarjotaan mutta ei tyrkytetä. */
|
|
258
328
|
if (uusiEhdotus && nykyinenAlt !== "")
|
|
259
329
|
setEhdotus(uusiEhdotus);
|
|
@@ -263,7 +333,7 @@ function Kuvakentta({ maaritys, arvo, muuta, lataa, }) {
|
|
|
263
333
|
setVirhe(syy instanceof Error ? syy.message : "Kuvan lataus ei onnistunut.");
|
|
264
334
|
}
|
|
265
335
|
}
|
|
266
|
-
return (_jsxs("div", { className: "kuva", children: [typeof kuva?.src === "string" ? (_jsx("img", { src: kuva
|
|
336
|
+
return (_jsxs("div", { className: "kuva", children: [esikatselu ?? (typeof kuva?.src === "string" ? kuva.src : null) ? (_jsxs(_Fragment, { children: [_jsx("img", { src: esikatselu ?? kuva?.src, alt: "" }), esikatselu ? (_jsx("p", { className: "ohje", children: "Uusi kuva. Se tulee sivulle, kun tallennat." })) : null] })) : (_jsx("p", { className: "tyhja", children: "Ei kuvaa." })), _jsxs("label", { className: "lataus", children: [_jsx("span", { children: tila === "lataa" ? "Käsitellään…" : "Vaihda kuva" }), _jsx("input", { type: "file", accept: "image/*", disabled: tila === "lataa", onChange: (tapahtuma) => valitse(tapahtuma.target.files?.[0]) })] }), tila === "virhe" ? _jsx("p", { className: "virheviesti", children: virhe }) : null, _jsxs("label", { className: "alt", children: ["Kuvan kuvaus", _jsx("input", { type: "text", placeholder: "Esimerkiksi: kynttil\u00E4 ikkunalaudalla", value: typeof kuva?.alt === "string" ? kuva.alt : "", onChange: (tapahtuma) => {
|
|
267
337
|
setVaihdettu(false);
|
|
268
338
|
muuta({ ...(kuva ?? {}), alt: tapahtuma.target.value });
|
|
269
339
|
} })] }), ehdotus ? (_jsxs("div", { className: "huomio", role: "status", children: [_jsxs("p", { children: ["Ehdotus: \u201C", ehdotus, "\u201D"] }), _jsx("button", { type: "button", onClick: () => {
|
package/dist/selain/polku.d.ts
CHANGED
|
@@ -3,7 +3,13 @@ export interface Kentanpolku {
|
|
|
3
3
|
kentta: string;
|
|
4
4
|
/** Kohteen tunnus, jos kenttä on listan sisällä. */
|
|
5
5
|
tunnus?: string;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Kentän nimi ryhmän sisällä.
|
|
8
|
+
*
|
|
9
|
+
* Ilman tunnusta tämä on sivun oman ryhmän kenttä ("hero.otsikko" →
|
|
10
|
+
* kentta "hero", alikenttä "otsikko"). Tunnuksen kanssa se on kohteen
|
|
11
|
+
* kenttä, joka voi olla myös kohteen ryhmässä ("meta.title").
|
|
12
|
+
*/
|
|
7
13
|
alikentta?: string;
|
|
8
14
|
}
|
|
9
15
|
export declare function lueKentanpolku(polku: string): Kentanpolku | null;
|
package/dist/selain/polku.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
Sivulla oleva merkintä kertoo, mitä kohtaa sisällöstä elementti näyttää:
|
|
5
5
|
|
|
6
6
|
kurssit.otsikko sivun oma kenttä
|
|
7
|
+
etusivu.hero.otsikko kenttä sivun ryhmässä
|
|
7
8
|
kurssit.lista[joogaretriitti].nimi yhden kohteen kenttä listassa
|
|
8
9
|
|
|
9
10
|
Hakasulkeissa on kohteen tunnus, ei järjestysnumero. Näin muokkaus osuu
|
|
@@ -13,23 +14,31 @@
|
|
|
13
14
|
Tämä tiedosto on tarkoituksella pelkkää logiikkaa ilman selainta, jotta
|
|
14
15
|
sen voi testata suoraan.
|
|
15
16
|
========================================================================== */
|
|
16
|
-
/*
|
|
17
|
-
|
|
17
|
+
/* Kolme muotoa: sivun kenttä, sivun ryhmän kenttä ja kohteen kenttä
|
|
18
|
+
listassa. Kohteen kenttä voi olla vielä kohteen oman ryhmän sisällä:
|
|
19
|
+
"kurssit.lista[tunnus].meta.title". */
|
|
20
|
+
const MUOTO = /^([a-zA-Z][a-zA-Z0-9]*)\.([a-zA-Z][a-zA-Z0-9]*)(?:\[([^\]]+)\]\.([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*)?)|\.([a-zA-Z][a-zA-Z0-9]*))?$/;
|
|
18
21
|
export function lueKentanpolku(polku) {
|
|
19
22
|
const osuma = MUOTO.exec(polku.trim());
|
|
20
23
|
if (!osuma)
|
|
21
24
|
return null;
|
|
22
|
-
const [, sivu, kentta, tunnus,
|
|
25
|
+
const [, sivu, kentta, tunnus, kohteenKentta, ryhmanKentta] = osuma;
|
|
23
26
|
if (!sivu || !kentta)
|
|
24
27
|
return null;
|
|
25
|
-
if (tunnus !== undefined
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
if (tunnus !== undefined) {
|
|
29
|
+
if (!kohteenKentta)
|
|
30
|
+
return null;
|
|
31
|
+
return { sivu, kentta, tunnus, alikentta: kohteenKentta };
|
|
32
|
+
}
|
|
33
|
+
return ryhmanKentta === undefined ? { sivu, kentta } : { sivu, kentta, alikentta: ryhmanKentta };
|
|
28
34
|
}
|
|
29
35
|
export function kirjoitaKentanpolku(polku) {
|
|
30
|
-
|
|
36
|
+
if (polku.tunnus !== undefined) {
|
|
37
|
+
return `${polku.sivu}.${polku.kentta}[${polku.tunnus}].${polku.alikentta}`;
|
|
38
|
+
}
|
|
39
|
+
return polku.alikentta === undefined
|
|
31
40
|
? `${polku.sivu}.${polku.kentta}`
|
|
32
|
-
: `${polku.sivu}.${polku.kentta}
|
|
41
|
+
: `${polku.sivu}.${polku.kentta}.${polku.alikentta}`;
|
|
33
42
|
}
|
|
34
43
|
/* ---- Arvon asettaminen --------------------------------------------------- */
|
|
35
44
|
function onOlio(arvo) {
|
|
@@ -49,7 +58,17 @@ export function asetaArvo(sivunData, polku, arvo, kieli, tunnusKentta = "tunnus"
|
|
|
49
58
|
return { ...pohja, [kieli]: arvo };
|
|
50
59
|
};
|
|
51
60
|
if (polku.tunnus === undefined) {
|
|
52
|
-
|
|
61
|
+
if (polku.alikentta === undefined) {
|
|
62
|
+
return { ...sivunData, [polku.kentta]: yhdista(sivunData[polku.kentta]) };
|
|
63
|
+
}
|
|
64
|
+
/* Sivun ryhmä: muut ryhmän kentät säilyvät ennallaan. */
|
|
65
|
+
const ryhma = onOlio(sivunData[polku.kentta])
|
|
66
|
+
? sivunData[polku.kentta]
|
|
67
|
+
: {};
|
|
68
|
+
return {
|
|
69
|
+
...sivunData,
|
|
70
|
+
[polku.kentta]: { ...ryhma, [polku.alikentta]: yhdista(ryhma[polku.alikentta]) },
|
|
71
|
+
};
|
|
53
72
|
}
|
|
54
73
|
const lista = Array.isArray(sivunData[polku.kentta]) ? [...sivunData[polku.kentta]] : [];
|
|
55
74
|
const sija = lista.findIndex((kohde) => onOlio(kohde) && kohde[tunnusKentta] === polku.tunnus);
|
|
@@ -73,8 +92,12 @@ export function asetaArvo(sivunData, polku, arvo, kieli, tunnusKentta = "tunnus"
|
|
|
73
92
|
}
|
|
74
93
|
/** Lukee yhden arvon sivun sisällöstä. */
|
|
75
94
|
export function lueArvo(sivunData, polku, tunnusKentta = "tunnus") {
|
|
76
|
-
if (polku.tunnus === undefined)
|
|
77
|
-
|
|
95
|
+
if (polku.tunnus === undefined) {
|
|
96
|
+
const arvo = sivunData[polku.kentta];
|
|
97
|
+
if (polku.alikentta === undefined)
|
|
98
|
+
return arvo;
|
|
99
|
+
return onOlio(arvo) ? arvo[polku.alikentta] : undefined;
|
|
100
|
+
}
|
|
78
101
|
const lista = sivunData[polku.kentta];
|
|
79
102
|
if (!Array.isArray(lista))
|
|
80
103
|
return undefined;
|
package/package.json
CHANGED