@studiotitta/studio-editori 0.1.0
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/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/komponentit/Editori.d.ts +1 -0
- package/dist/komponentit/Editori.js +29 -0
- package/dist/komponentit/Muokattava.d.ts +10 -0
- package/dist/komponentit/Muokattava.js +7 -0
- package/dist/komponentit/index.d.ts +2 -0
- package/dist/komponentit/index.js +5 -0
- package/dist/maarittely.d.ts +215 -0
- package/dist/maarittely.js +170 -0
- package/dist/palvelin/apurit.d.ts +26 -0
- package/dist/palvelin/apurit.js +70 -0
- package/dist/palvelin/asetukset.d.ts +26 -0
- package/dist/palvelin/asetukset.js +55 -0
- package/dist/palvelin/github/avain.d.ts +15 -0
- package/dist/palvelin/github/avain.js +121 -0
- package/dist/palvelin/github/tiedostot.d.ts +56 -0
- package/dist/palvelin/github/tiedostot.js +144 -0
- package/dist/palvelin/github/yhteys.d.ts +28 -0
- package/dist/palvelin/github/yhteys.js +89 -0
- package/dist/palvelin/index.d.ts +15 -0
- package/dist/palvelin/index.js +19 -0
- package/dist/palvelin/istunto.d.ts +34 -0
- package/dist/palvelin/istunto.js +86 -0
- package/dist/palvelin/kaannos.d.ts +12 -0
- package/dist/palvelin/kaannos.js +102 -0
- package/dist/palvelin/kaannosreitti.d.ts +9 -0
- package/dist/palvelin/kaannosreitti.js +42 -0
- package/dist/palvelin/kirjautuminen.d.ts +25 -0
- package/dist/palvelin/kirjautuminen.js +183 -0
- package/dist/palvelin/kuvat.d.ts +29 -0
- package/dist/palvelin/kuvat.js +130 -0
- package/dist/palvelin/kuvateksti.d.ts +15 -0
- package/dist/palvelin/kuvateksti.js +95 -0
- package/dist/palvelin/lue.d.ts +6 -0
- package/dist/palvelin/lue.js +41 -0
- package/dist/palvelin/posti.d.ts +9 -0
- package/dist/palvelin/posti.js +45 -0
- package/dist/palvelin/reitit.d.ts +10 -0
- package/dist/palvelin/reitit.js +71 -0
- package/dist/palvelin/tallennus.d.ts +18 -0
- package/dist/palvelin/tallennus.js +177 -0
- package/dist/palvelin/tila.d.ts +21 -0
- package/dist/palvelin/tila.js +116 -0
- package/dist/palvelin/tunnus.d.ts +30 -0
- package/dist/palvelin/tunnus.js +84 -0
- package/dist/palvelin/varasto.d.ts +18 -0
- package/dist/palvelin/varasto.js +87 -0
- package/dist/selain/EditorinPalkki.d.ts +1 -0
- package/dist/selain/EditorinPalkki.js +35 -0
- package/dist/selain/Muokkaustila.d.ts +3 -0
- package/dist/selain/Muokkaustila.js +646 -0
- package/dist/selain/PalkkiLataaja.d.ts +1 -0
- package/dist/selain/PalkkiLataaja.js +19 -0
- package/dist/selain/Paneeli.d.ts +44 -0
- package/dist/selain/Paneeli.js +259 -0
- package/dist/selain/index.d.ts +4 -0
- package/dist/selain/index.js +8 -0
- package/dist/selain/kieli.d.ts +10 -0
- package/dist/selain/kieli.js +14 -0
- package/dist/selain/kuva.d.ts +7 -0
- package/dist/selain/kuva.js +45 -0
- package/dist/selain/polku.d.ts +21 -0
- package/dist/selain/polku.js +108 -0
- package/dist/selain/tyypit.d.ts +22 -0
- package/dist/selain/tyypit.js +2 -0
- package/dist/tarkistus.d.ts +26 -0
- package/dist/tarkistus.js +324 -0
- package/package.json +59 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* ==========================================================================
|
|
4
|
+
MUOKKAUSTILA SELAIMESSA
|
|
5
|
+
==========================================================================
|
|
6
|
+
Kirjautunut muokkaaja näkee sivun alareunassa pienen palkin. Kun hän
|
|
7
|
+
painaa *Muokkaa*, sivun muokattavat kohdat muuttuvat kirjoitettaviksi
|
|
8
|
+
siinä paikassa, jossa ne ovat, ja oikeaan reunaan avautuu paneeli sille,
|
|
9
|
+
mikä ei näy sivulla: ryhmäkoko, tila, näkyvyys, hakukonetekstit.
|
|
10
|
+
|
|
11
|
+
Kolme sääntöä, jotka ohjaavat tätä tiedostoa:
|
|
12
|
+
|
|
13
|
+
1. **Tavallinen kävijä ei lataa tästä riviäkään.** Sivusto renderöi
|
|
14
|
+
komponentin vain kirjautuneelle muokkaajalle.
|
|
15
|
+
2. **Sivuston ulkoasu ei saa vaikuttaa editoriin eikä editori siihen.**
|
|
16
|
+
Palkki ja paneeli elävät Shadow DOMissa, ja korostustyylit lisätään
|
|
17
|
+
`adoptedStyleSheets`-rajapinnalla — ei yhtään inline-tyyliä, joten
|
|
18
|
+
tiukka Content-Security-Policy riittää sellaisenaan.
|
|
19
|
+
3. **Mitään ei menetetä vahingossa.** Tallentamattomista muutoksista
|
|
20
|
+
varoitetaan lähtiessä, ja *Peru* palauttaa alkuperäiset tekstit.
|
|
21
|
+
========================================================================== */
|
|
22
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
23
|
+
import { valitseKieli } from "./kieli.js";
|
|
24
|
+
import { pienennaKuva } from "./kuva.js";
|
|
25
|
+
import { Paneeli } from "./Paneeli.js";
|
|
26
|
+
import { asetaArvo, kirjoitaKentanpolku, lueArvo, lueKentanpolku, vapaaTunnus, } from "./polku.js";
|
|
27
|
+
const PALKIN_TYYLIT = `
|
|
28
|
+
:host { all: initial; }
|
|
29
|
+
.palkki {
|
|
30
|
+
position: fixed; inset: auto 1rem 1rem auto; z-index: 2147483000;
|
|
31
|
+
display: flex; align-items: center; gap: .75rem;
|
|
32
|
+
padding: .6rem .75rem .6rem 1rem; border-radius: .5rem;
|
|
33
|
+
background: #1d1f1e; color: #fff;
|
|
34
|
+
font: 500 .875rem/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
35
|
+
box-shadow: 0 .5rem 1.5rem rgba(0,0,0,.25);
|
|
36
|
+
max-width: calc(100vw - 2rem);
|
|
37
|
+
}
|
|
38
|
+
.tila { opacity: .75; font-weight: 400; }
|
|
39
|
+
.tila[data-virhe="1"] { color: #ffb4a2; opacity: 1; }
|
|
40
|
+
button {
|
|
41
|
+
font: inherit; padding: .45rem .8rem; border-radius: .35rem;
|
|
42
|
+
border: 1px solid rgba(255,255,255,.25); background: transparent; color: inherit;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
button.ensisijainen { background: #fff; color: #1d1f1e; border-color: #fff; }
|
|
46
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
47
|
+
button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
|
48
|
+
|
|
49
|
+
/* Sivupaneeli */
|
|
50
|
+
.paneeli {
|
|
51
|
+
position: fixed; inset: 0 0 0 auto; z-index: 2147482999;
|
|
52
|
+
width: min(23rem, 100vw); overflow-y: auto;
|
|
53
|
+
padding: 1.25rem 1.25rem 6rem;
|
|
54
|
+
background: #fbfaf8; color: #1d1f1e;
|
|
55
|
+
border-left: 1px solid rgba(0,0,0,.12);
|
|
56
|
+
font: 400 .875rem/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
57
|
+
box-shadow: -.5rem 0 1.5rem rgba(0,0,0,.08);
|
|
58
|
+
}
|
|
59
|
+
.paneeli h2 { margin: 0 0 .75rem; font-size: .8125rem; text-transform: uppercase; letter-spacing: .06em; opacity: .6; }
|
|
60
|
+
.paneeli section + section { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid rgba(0,0,0,.1); }
|
|
61
|
+
.kentta { margin-bottom: 1rem; }
|
|
62
|
+
.kentta label { display: flex; gap: .5rem; align-items: baseline; margin-bottom: .25rem; font-weight: 600; }
|
|
63
|
+
.kieli { font-weight: 400; font-size: .75rem; padding: 0 .3rem; border-radius: .2rem; background: rgba(0,0,0,.08); text-transform: uppercase; }
|
|
64
|
+
.kentta input[type=text], .kentta input[type=number], .kentta input[type=date],
|
|
65
|
+
.kentta input[type=datetime-local], .kentta select, .kentta textarea {
|
|
66
|
+
width: 100%; padding: .45rem .55rem; border: 1px solid rgba(0,0,0,.25);
|
|
67
|
+
border-radius: .25rem; background: #fff; font: inherit; color: inherit;
|
|
68
|
+
}
|
|
69
|
+
.kentta textarea { resize: vertical; }
|
|
70
|
+
.kytkin { display: flex; gap: .5rem; align-items: center; font-weight: 400; }
|
|
71
|
+
.kielivalinta { display: grid; gap: .35rem; }
|
|
72
|
+
.kaannos button { color: inherit; border-color: rgba(0,0,0,.25); }
|
|
73
|
+
.ohje { margin: .25rem 0 0; font-size: .8125rem; opacity: .65; }
|
|
74
|
+
.laskuri { margin: .3rem 0 0; font-size: .8125rem; font-weight: 600; }
|
|
75
|
+
.virheet { padding: .75rem .9rem; border-radius: .3rem; background: #f6e8e3; color: #6d2b20; }
|
|
76
|
+
.virheet h2 { margin: 0 0 .4rem; opacity: 1; color: inherit; }
|
|
77
|
+
.virheet ul { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
78
|
+
.kuva img { display: block; width: 100%; height: auto; border-radius: .25rem; margin-bottom: .4rem; }
|
|
79
|
+
.kuva input[type=text] { width: 100%; padding: .45rem .55rem; border: 1px solid rgba(0,0,0,.25); border-radius: .25rem; font: inherit; }
|
|
80
|
+
/* Kuvan tekstikenttä on oma lohkonsa: label on .kentta-tasolla flex,
|
|
81
|
+
ja ilman tätä otsikko ja kenttä asettuisivat vierekkäin. */
|
|
82
|
+
.kentta label.alt, .kuva label.alt { display: block; font-weight: 600; margin-top: .5rem; }
|
|
83
|
+
.alt input { margin-top: .25rem; width: 100%; }
|
|
84
|
+
.huomio {
|
|
85
|
+
margin: .4rem 0 0; padding: .5rem .6rem; border-radius: .25rem;
|
|
86
|
+
background: #fdf3e7; color: #6b4a1f; font-size: .8125rem;
|
|
87
|
+
}
|
|
88
|
+
.huomio p { margin: 0 0 .4rem; }
|
|
89
|
+
.huomio button { color: inherit; border-color: rgba(107,74,31,.35); padding: .3rem .6rem; }
|
|
90
|
+
.lataus { display: inline-flex; margin: 0 0 .5rem; }
|
|
91
|
+
.lataus span {
|
|
92
|
+
padding: .45rem .8rem; border: 1px solid rgba(0,0,0,.25); border-radius: .35rem;
|
|
93
|
+
cursor: pointer; font-weight: 600;
|
|
94
|
+
}
|
|
95
|
+
.lataus input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
96
|
+
.virheviesti { margin: 0 0 .5rem; color: #8c3b2e; font-size: .8125rem; }
|
|
97
|
+
.tyhja { margin: 0; opacity: .6; }
|
|
98
|
+
.kohteet { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; gap: .25rem; }
|
|
99
|
+
.kohteet button {
|
|
100
|
+
width: 100%; text-align: left; color: inherit; border-color: rgba(0,0,0,.15);
|
|
101
|
+
display: flex; justify-content: space-between; gap: .5rem; align-items: center;
|
|
102
|
+
}
|
|
103
|
+
.kohteet button.valittu { background: #1d1f1e; color: #fff; border-color: #1d1f1e; }
|
|
104
|
+
.merkki { font-size: .75rem; opacity: .7; white-space: nowrap; }
|
|
105
|
+
/* Taitos näyttää painikkeelta ja nuoli osoittaa sinne, minne sisältö
|
|
106
|
+
aukeaa: alas kiinni ollessa, ylös auki ollessa. */
|
|
107
|
+
.ryhma { margin: 0 0 1rem; padding: .75rem .8rem; border-radius: .3rem; background: rgba(0,0,0,.035); }
|
|
108
|
+
.ryhma h3 { margin: 0 0 .5rem; font-size: .8125rem; text-transform: uppercase; letter-spacing: .05em; opacity: .65; }
|
|
109
|
+
.ryhma .kentta:last-child { margin-bottom: 0; }
|
|
110
|
+
.lisatiedot { margin: .25rem 0 1rem; border-top: 1px solid rgba(0,0,0,.1); }
|
|
111
|
+
.lisatiedot summary {
|
|
112
|
+
cursor: pointer; font-weight: 600; padding: .7rem 0; list-style: none;
|
|
113
|
+
display: flex; align-items: center; gap: .5rem;
|
|
114
|
+
}
|
|
115
|
+
.lisatiedot summary::-webkit-details-marker { display: none; }
|
|
116
|
+
.lisatiedot summary::after {
|
|
117
|
+
content: "▾"; font-size: 1.1rem; line-height: 1; margin-left: auto;
|
|
118
|
+
}
|
|
119
|
+
.lisatiedot[open] summary::after { content: "▴"; }
|
|
120
|
+
.lisatiedot summary:hover { text-decoration: underline; }
|
|
121
|
+
.lisatiedot summary:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
122
|
+
.kaannos { margin: 0 0 1rem; padding: .6rem .7rem; border-radius: .25rem; background: #eef1ee; }
|
|
123
|
+
.kaannos p { margin: 0 0 .5rem; }
|
|
124
|
+
/* Kenttien nimet omalle rivilleen: lause pysyy luettavana ja nimet erottuvat. */
|
|
125
|
+
.kaannos strong { display: block; margin-top: .15rem; }
|
|
126
|
+
.kaannos button { color: inherit; border-color: rgba(0,0,0,.25); }
|
|
127
|
+
.napit { display: flex; gap: .5rem; margin-bottom: 1rem; }
|
|
128
|
+
.napit button { color: inherit; border-color: rgba(0,0,0,.25); }
|
|
129
|
+
.napit button.vaarallinen { color: #8c3b2e; border-color: rgba(140,59,46,.4); }
|
|
130
|
+
`;
|
|
131
|
+
const KOROSTUS = `
|
|
132
|
+
[data-editori-kentta] { border-radius: .15rem; }
|
|
133
|
+
[data-editori-muokattavana="1"] {
|
|
134
|
+
outline: 1px dashed rgba(0,0,0,.35); outline-offset: .25rem; cursor: text;
|
|
135
|
+
}
|
|
136
|
+
[data-editori-muokattavana="1"]:hover { outline-style: solid; }
|
|
137
|
+
[data-editori-muokattavana="1"]:focus { outline: 2px solid currentColor; outline-offset: .25rem; }
|
|
138
|
+
[data-editori-muokattavana="1"] > p:last-child { margin-bottom: 0; }
|
|
139
|
+
body[data-editori-paneeli="1"] { padding-right: min(23rem, 100vw); }
|
|
140
|
+
`;
|
|
141
|
+
/** Lisää tyylit ilman inline-tyylejä, jotta CSP:tä ei tarvitse löysätä. */
|
|
142
|
+
function lisaaTyylit() {
|
|
143
|
+
if (typeof CSSStyleSheet === "undefined" || !("adoptedStyleSheets" in document))
|
|
144
|
+
return () => { };
|
|
145
|
+
const arkki = new CSSStyleSheet();
|
|
146
|
+
arkki.replaceSync(KOROSTUS);
|
|
147
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, arkki];
|
|
148
|
+
return () => {
|
|
149
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((muu) => muu !== arkki);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Kentän teksti elementistä.
|
|
154
|
+
*
|
|
155
|
+
* Kappalelistalla luetaan `innerText`, joka säilyttää kappalejaon
|
|
156
|
+
* rivinvaihtoina. Tavallisella tekstillä `textContent` riittää eikä se ole
|
|
157
|
+
* riippuvainen siitä, miten selain sattuu taittamaan sisällön.
|
|
158
|
+
*/
|
|
159
|
+
function lueTeksti(elementti, lista) {
|
|
160
|
+
return (lista ? elementti.innerText : elementti.textContent) ?? "";
|
|
161
|
+
}
|
|
162
|
+
/** Sivulla kirjoitettu kappalelista takaisin listaksi. */
|
|
163
|
+
function jaaKappaleiksi(teksti) {
|
|
164
|
+
return teksti
|
|
165
|
+
.split(/\n+/)
|
|
166
|
+
.map((kappale) => kappale.trim())
|
|
167
|
+
.filter((kappale) => kappale !== "");
|
|
168
|
+
}
|
|
169
|
+
/** Sivulla olevat muokattavat kohdat merkintöineen. */
|
|
170
|
+
function merkinnat() {
|
|
171
|
+
const tulos = [];
|
|
172
|
+
for (const elementti of document.querySelectorAll("[data-editori-kentta]")) {
|
|
173
|
+
const polku = lueKentanpolku(elementti.getAttribute("data-editori-kentta") ?? "");
|
|
174
|
+
if (polku)
|
|
175
|
+
tulos.push({ elementti, polku });
|
|
176
|
+
}
|
|
177
|
+
return tulos;
|
|
178
|
+
}
|
|
179
|
+
function kentanKieli(elementti, kielet) {
|
|
180
|
+
const lahin = elementti.closest("[lang]");
|
|
181
|
+
return valitseKieli(lahin?.getAttribute("lang") ?? document.documentElement.lang, kielet);
|
|
182
|
+
}
|
|
183
|
+
function onKaannettava(maaritys) {
|
|
184
|
+
return Boolean(maaritys && "kaannettava" in maaritys && maaritys.kaannettava === true);
|
|
185
|
+
}
|
|
186
|
+
export function Muokkaustila() {
|
|
187
|
+
const [tila, setTila] = useState("lataa");
|
|
188
|
+
const [viesti, setViesti] = useState("");
|
|
189
|
+
const [virheet, setVirheet] = useState([]);
|
|
190
|
+
const [sisalto, setSisalto] = useState(null);
|
|
191
|
+
const [valittu, setValittu] = useState(undefined);
|
|
192
|
+
const alkuperaiset = useRef(new Map());
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
let voimassa = true;
|
|
195
|
+
fetch("/api/editori/sisalto", { headers: { Accept: "application/json" } })
|
|
196
|
+
.then(async (vastaus) => {
|
|
197
|
+
if (!voimassa)
|
|
198
|
+
return;
|
|
199
|
+
/*
|
|
200
|
+
401 tarkoittaa, ettei istuntoa ole. Palkkia ei silloin näytetä
|
|
201
|
+
lainkaan: Draft Mode yksin ei tee kenestäkään muokkaajaa.
|
|
202
|
+
*/
|
|
203
|
+
if (vastaus.status === 401) {
|
|
204
|
+
setTila("piilossa");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!vastaus.ok)
|
|
208
|
+
throw new Error(String(vastaus.status));
|
|
209
|
+
setSisalto((await vastaus.json()));
|
|
210
|
+
setTila("valmis");
|
|
211
|
+
})
|
|
212
|
+
.catch(() => {
|
|
213
|
+
if (!voimassa)
|
|
214
|
+
return;
|
|
215
|
+
setTila("virhe");
|
|
216
|
+
setViesti("Sisältöä ei saatu haettua. Lataa sivu uudelleen.");
|
|
217
|
+
});
|
|
218
|
+
return () => {
|
|
219
|
+
voimassa = false;
|
|
220
|
+
};
|
|
221
|
+
}, []);
|
|
222
|
+
useEffect(() => lisaaTyylit(), []);
|
|
223
|
+
/* Sivu päätellään sivulla olevista merkinnöistä: sivuston ei tarvitse
|
|
224
|
+
kertoa erikseen, mitä sisältöä se näyttää. */
|
|
225
|
+
const sivunAvain = useMemo(() => {
|
|
226
|
+
if (typeof document === "undefined")
|
|
227
|
+
return undefined;
|
|
228
|
+
return merkinnat()[0]?.polku.sivu;
|
|
229
|
+
}, [tila]);
|
|
230
|
+
/*
|
|
231
|
+
Sivun kieli luetaan samasta paikasta kuin paikan päällä muokatessa:
|
|
232
|
+
ensimmäisen muokattavan kohdan lähimmästä lang-määreestä. Pelkkä
|
|
233
|
+
<html lang> kertoisi sivuston oletuskielen myös käännetyllä sivulla, ja
|
|
234
|
+
paneelissa tehty muutos menisi väärän kielen päälle.
|
|
235
|
+
*/
|
|
236
|
+
const sivunKieli = useMemo(() => {
|
|
237
|
+
if (typeof document === "undefined" || !sisalto)
|
|
238
|
+
return "fi";
|
|
239
|
+
const ensimmainen = merkinnat()[0]?.elementti;
|
|
240
|
+
return ensimmainen
|
|
241
|
+
? kentanKieli(ensimmainen, sisalto.kielet)
|
|
242
|
+
: valitseKieli(document.documentElement.lang, sisalto.kielet);
|
|
243
|
+
}, [sisalto, tila]);
|
|
244
|
+
const muokataan = tila === "muokkaa";
|
|
245
|
+
const tallentaa = tila === "tallentaa";
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (typeof document === "undefined")
|
|
248
|
+
return;
|
|
249
|
+
if (muokataan || tallentaa)
|
|
250
|
+
document.body.setAttribute("data-editori-paneeli", "1");
|
|
251
|
+
else
|
|
252
|
+
document.body.removeAttribute("data-editori-paneeli");
|
|
253
|
+
}, [muokataan, tallentaa]);
|
|
254
|
+
const onLista = useCallback((polku) => sisalto !== null && kentanMaaritys(sisalto, polku)?.tyyppi === "tekstilista", [sisalto]);
|
|
255
|
+
const paalle = useCallback(() => {
|
|
256
|
+
alkuperaiset.current = new Map();
|
|
257
|
+
for (const { elementti, polku } of merkinnat()) {
|
|
258
|
+
alkuperaiset.current.set(elementti, lueTeksti(elementti, onLista(polku)));
|
|
259
|
+
elementti.setAttribute("contenteditable", "plaintext-only");
|
|
260
|
+
elementti.setAttribute("data-editori-muokattavana", "1");
|
|
261
|
+
elementti.setAttribute("spellcheck", "true");
|
|
262
|
+
}
|
|
263
|
+
setTila("muokkaa");
|
|
264
|
+
setViesti("");
|
|
265
|
+
setVirheet([]);
|
|
266
|
+
}, [onLista]);
|
|
267
|
+
const pois = useCallback((palautaTekstit) => {
|
|
268
|
+
for (const { elementti } of merkinnat()) {
|
|
269
|
+
if (palautaTekstit) {
|
|
270
|
+
elementti.textContent = alkuperaiset.current.get(elementti) ?? elementti.textContent;
|
|
271
|
+
}
|
|
272
|
+
elementti.removeAttribute("contenteditable");
|
|
273
|
+
elementti.removeAttribute("data-editori-muokattavana");
|
|
274
|
+
elementti.removeAttribute("spellcheck");
|
|
275
|
+
}
|
|
276
|
+
}, []);
|
|
277
|
+
function sivullaMuuttunut() {
|
|
278
|
+
return merkinnat().some(({ elementti, polku }) => lueTeksti(elementti, onLista(polku)) !== alkuperaiset.current.get(elementti));
|
|
279
|
+
}
|
|
280
|
+
/* Tallentamattomat muutokset eivät saa kadota selaimen sulkemiseen. */
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (tila !== "muokkaa")
|
|
283
|
+
return;
|
|
284
|
+
const varoita = (tapahtuma) => {
|
|
285
|
+
if (!sivullaMuuttunut() && !muutettu.current)
|
|
286
|
+
return;
|
|
287
|
+
tapahtuma.preventDefault();
|
|
288
|
+
};
|
|
289
|
+
window.addEventListener("beforeunload", varoita);
|
|
290
|
+
return () => window.removeEventListener("beforeunload", varoita);
|
|
291
|
+
}, [tila]);
|
|
292
|
+
/** Onko paneelissa tehty muutoksia, joita ei ole vielä tallennettu? */
|
|
293
|
+
const muutettu = useRef(false);
|
|
294
|
+
const muuta = useCallback((polku, arvo, kaannettava) => {
|
|
295
|
+
setSisalto((vanha) => {
|
|
296
|
+
if (!vanha)
|
|
297
|
+
return vanha;
|
|
298
|
+
const sivunData = vanha.sivut[polku.sivu] ?? {};
|
|
299
|
+
const kieli = kaannettava ? sivunKieli : undefined;
|
|
300
|
+
muutettu.current = true;
|
|
301
|
+
return {
|
|
302
|
+
...vanha,
|
|
303
|
+
sivut: {
|
|
304
|
+
...vanha.sivut,
|
|
305
|
+
[polku.sivu]: asetaArvo(sivunData, polku, arvo, kieli, tunnusKentta(vanha, polku)),
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}, [sivunKieli]);
|
|
310
|
+
const lisaaKohde = useCallback((kentta, maaritys) => {
|
|
311
|
+
setSisalto((vanha) => {
|
|
312
|
+
if (!vanha || !sivunAvain)
|
|
313
|
+
return vanha;
|
|
314
|
+
const sivunData = vanha.sivut[sivunAvain] ?? {};
|
|
315
|
+
const lista = Array.isArray(sivunData[kentta]) ? [...sivunData[kentta]] : [];
|
|
316
|
+
const kaytossa = lista
|
|
317
|
+
.map((kohde) => kohde?.[maaritys.tunnus])
|
|
318
|
+
.filter((tunnus) => typeof tunnus === "string");
|
|
319
|
+
const tunnus = vapaaTunnus(kaytossa, "uusi");
|
|
320
|
+
const uusi = { [maaritys.tunnus]: tunnus };
|
|
321
|
+
/* Uusi kohde aloitetaan tyhjänä mutta oikean muotoisena, jotta
|
|
322
|
+
tarkistus osaa kertoa mitä siitä vielä puuttuu. */
|
|
323
|
+
for (const [avain, kenttä] of Object.entries(maaritys.kentat)) {
|
|
324
|
+
if (avain === maaritys.tunnus)
|
|
325
|
+
continue;
|
|
326
|
+
if (onKaannettava(kenttä))
|
|
327
|
+
uusi[avain] = {};
|
|
328
|
+
else if (kenttä.tyyppi === "kytkin")
|
|
329
|
+
uusi[avain] = false;
|
|
330
|
+
else if (kenttä.tyyppi === "tekstilista")
|
|
331
|
+
uusi[avain] = [];
|
|
332
|
+
else if (kenttä.tyyppi === "valinta")
|
|
333
|
+
uusi[avain] = kenttä.vaihtoehdot[0]?.arvo;
|
|
334
|
+
}
|
|
335
|
+
muutettu.current = true;
|
|
336
|
+
setValittu(tunnus);
|
|
337
|
+
return {
|
|
338
|
+
...vanha,
|
|
339
|
+
sivut: { ...vanha.sivut, [sivunAvain]: { ...sivunData, [kentta]: [...lista, uusi] } },
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
}, [sivunAvain]);
|
|
343
|
+
const poistaKohde = useCallback((kentta, tunnusKentanNimi, tunnus) => {
|
|
344
|
+
const yksikko = kohdelistanYksikko(sisalto, sivunAvain, kentta);
|
|
345
|
+
if (!window.confirm(`Poistetaanko ${yksikko} "${tunnus}" kokonaan? Sen voi palauttaa vain versiohistoriasta.`)) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
setSisalto((vanha) => {
|
|
349
|
+
if (!vanha || !sivunAvain)
|
|
350
|
+
return vanha;
|
|
351
|
+
const sivunData = vanha.sivut[sivunAvain] ?? {};
|
|
352
|
+
const lista = Array.isArray(sivunData[kentta]) ? sivunData[kentta] : [];
|
|
353
|
+
muutettu.current = true;
|
|
354
|
+
setValittu(undefined);
|
|
355
|
+
return {
|
|
356
|
+
...vanha,
|
|
357
|
+
sivut: {
|
|
358
|
+
...vanha.sivut,
|
|
359
|
+
[sivunAvain]: {
|
|
360
|
+
...sivunData,
|
|
361
|
+
[kentta]: lista.filter((kohde) => kohde?.[tunnusKentanNimi] !== tunnus),
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
}, [sivunAvain]);
|
|
367
|
+
/**
|
|
368
|
+
* Lataa kuvan: selain pienentää sen ensin, palvelin käsittelee ja
|
|
369
|
+
* tallentaa. Kuva menee sivustolle heti, ja kenttä alkaa osoittaa siihen
|
|
370
|
+
* vasta kun muokkaaja painaa Tallenna.
|
|
371
|
+
*/
|
|
372
|
+
const lataaKuva = useCallback(async (tiedosto, maaritys) => {
|
|
373
|
+
const pienennetty = await pienennaKuva(tiedosto);
|
|
374
|
+
const vastaus = await fetch("/api/editori/kuva", {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: { "Content-Type": "application/json" },
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
tiedosto: pienennetty.tavut,
|
|
379
|
+
kansio: maaritys.kansio ?? "yleiset",
|
|
380
|
+
...(maaritys.suhde === undefined ? {} : { suhde: maaritys.suhde }),
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
const tulos = await vastaus.json().catch(() => ({}));
|
|
384
|
+
if (!vastaus.ok || typeof tulos.src !== "string") {
|
|
385
|
+
throw new Error(typeof tulos.virhe === "string" ? tulos.virhe : "Kuvan lataus ei onnistunut.");
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
src: tulos.src,
|
|
389
|
+
ehdotus: typeof tulos.ehdotus === "string" ? tulos.ehdotus : null,
|
|
390
|
+
};
|
|
391
|
+
}, []);
|
|
392
|
+
/**
|
|
393
|
+
* Kentät, joilta puuttuu käännös tälle sivulle: lähdekielellä on tekstiä,
|
|
394
|
+
* nykyisellä kielellä ei.
|
|
395
|
+
*/
|
|
396
|
+
const kaannettavat = useMemo(() => {
|
|
397
|
+
if (!sisalto || !sivunAvain)
|
|
398
|
+
return [];
|
|
399
|
+
const malliSivu = sisalto.malli[sivunAvain];
|
|
400
|
+
if (!malliSivu)
|
|
401
|
+
return [];
|
|
402
|
+
const lahde = sisalto.kielet.find((kieli) => kieli !== sivunKieli);
|
|
403
|
+
if (!lahde)
|
|
404
|
+
return [];
|
|
405
|
+
const data = sisalto.sivut[sivunAvain] ?? {};
|
|
406
|
+
const loydot = [];
|
|
407
|
+
const tutki = (polku, kenttä) => {
|
|
408
|
+
if (!("kaannettava" in kenttä) || kenttä.kaannettava !== true)
|
|
409
|
+
return;
|
|
410
|
+
const arvo = lueArvo(data, polku, tunnusKentta(sisalto, polku));
|
|
411
|
+
if (typeof arvo !== "object" || arvo === null || Array.isArray(arvo))
|
|
412
|
+
return;
|
|
413
|
+
const kaannokset = arvo;
|
|
414
|
+
const nykyinen = kaannokset[sivunKieli];
|
|
415
|
+
const lahdeteksti = kaannokset[lahde];
|
|
416
|
+
const tyhja = nykyinen === undefined ||
|
|
417
|
+
nykyinen === "" ||
|
|
418
|
+
(Array.isArray(nykyinen) && nykyinen.length === 0);
|
|
419
|
+
const onLahde = (typeof lahdeteksti === "string" && lahdeteksti !== "") ||
|
|
420
|
+
(Array.isArray(lahdeteksti) && lahdeteksti.length > 0);
|
|
421
|
+
if (tyhja && onLahde) {
|
|
422
|
+
loydot.push({
|
|
423
|
+
polku,
|
|
424
|
+
arvo: lahdeteksti,
|
|
425
|
+
nimi: kenttä.nimi ?? polku.alikentta ?? polku.kentta,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
for (const [avain, kenttä] of Object.entries(malliSivu.kentat)) {
|
|
430
|
+
if (kenttä.tyyppi === "kohdelista") {
|
|
431
|
+
if (!valittu)
|
|
432
|
+
continue;
|
|
433
|
+
for (const [alikentta, alikenttä] of Object.entries(kenttä.kentat)) {
|
|
434
|
+
if (alikenttä.tyyppi === "ryhma") {
|
|
435
|
+
for (const [ryhmanKentta, ryhmanMaaritys] of Object.entries(alikenttä.kentat)) {
|
|
436
|
+
tutki({
|
|
437
|
+
sivu: sivunAvain,
|
|
438
|
+
kentta: avain,
|
|
439
|
+
tunnus: valittu,
|
|
440
|
+
alikentta: `${alikentta}.${ryhmanKentta}`,
|
|
441
|
+
}, ryhmanMaaritys);
|
|
442
|
+
}
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
tutki({ sivu: sivunAvain, kentta: avain, tunnus: valittu, alikentta }, alikenttä);
|
|
446
|
+
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
tutki({ sivu: sivunAvain, kentta: avain }, kenttä);
|
|
450
|
+
}
|
|
451
|
+
return loydot;
|
|
452
|
+
}, [sisalto, sivunAvain, sivunKieli, valittu, tila]);
|
|
453
|
+
/**
|
|
454
|
+
* Täyttää puuttuvat käännökset ehdotuksina. Ehdotukset menevät kenttiin,
|
|
455
|
+
* eivät suoraan sivustolle: muokkaaja lukee ne ja painaa Tallenna.
|
|
456
|
+
*/
|
|
457
|
+
const kaanna = useCallback(async (mihin) => {
|
|
458
|
+
const joukko = kaannettavat.filter(({ polku }) => mihin === "sivu" ? polku.tunnus === undefined : polku.tunnus !== undefined);
|
|
459
|
+
if (!sisalto || joukko.length === 0)
|
|
460
|
+
return "Kaikki on jo käännetty.";
|
|
461
|
+
const lahde = sisalto.kielet.find((kieli) => kieli !== sivunKieli);
|
|
462
|
+
if (!lahde)
|
|
463
|
+
return "Sivustolla on vain yksi kieli.";
|
|
464
|
+
const tekstit = {};
|
|
465
|
+
for (const { polku, arvo } of joukko)
|
|
466
|
+
tekstit[kirjoitaKentanpolku(polku)] = arvo;
|
|
467
|
+
const vastaus = await fetch("/api/editori/kaannos", {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: { "Content-Type": "application/json" },
|
|
470
|
+
body: JSON.stringify({ lahdekieli: lahde, kohdekieli: sivunKieli, tekstit }),
|
|
471
|
+
});
|
|
472
|
+
const tulos = await vastaus.json().catch(() => ({}));
|
|
473
|
+
if (!vastaus.ok || typeof tulos.kaannokset !== "object" || tulos.kaannokset === null) {
|
|
474
|
+
return typeof tulos.virhe === "string" ? tulos.virhe : "Käännöstä ei saatu.";
|
|
475
|
+
}
|
|
476
|
+
let maara = 0;
|
|
477
|
+
for (const { polku } of joukko) {
|
|
478
|
+
const kaannos = tulos.kaannokset[kirjoitaKentanpolku(polku)];
|
|
479
|
+
if (kaannos === undefined)
|
|
480
|
+
continue;
|
|
481
|
+
muuta(polku, kaannos, true);
|
|
482
|
+
maara++;
|
|
483
|
+
}
|
|
484
|
+
return maara === 0
|
|
485
|
+
? "Käännöstä ei saatu."
|
|
486
|
+
: `${maara} kohtaa käännetty. Lue ne läpi ennen tallennusta.`;
|
|
487
|
+
}, [sisalto, kaannettavat, sivunKieli, muuta]);
|
|
488
|
+
async function tallenna() {
|
|
489
|
+
if (!sisalto)
|
|
490
|
+
return;
|
|
491
|
+
setTila("tallentaa");
|
|
492
|
+
setViesti("");
|
|
493
|
+
setVirheet([]);
|
|
494
|
+
/* Sivulla tehdyt muutokset luetaan vasta tässä ja kirjoitetaan paneelin
|
|
495
|
+
muutosten päälle. Kentät, joita tämä sivu ei näytä, säilyvät ennallaan. */
|
|
496
|
+
const muutetut = {};
|
|
497
|
+
const pohja = (sivu) => muutetut[sivu] ?? { ...(sisalto.sivut[sivu] ?? {}) };
|
|
498
|
+
if (muutettu.current) {
|
|
499
|
+
for (const [sivu, data] of Object.entries(sisalto.sivut)) {
|
|
500
|
+
if (sivu === sivunAvain)
|
|
501
|
+
muutetut[sivu] = { ...data };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const { elementti, polku } of merkinnat()) {
|
|
505
|
+
const maaritys = kentanMaaritys(sisalto, polku);
|
|
506
|
+
const lista = maaritys?.tyyppi === "tekstilista";
|
|
507
|
+
const teksti = lueTeksti(elementti, lista);
|
|
508
|
+
if (teksti === alkuperaiset.current.get(elementti))
|
|
509
|
+
continue;
|
|
510
|
+
const kieli = onKaannettava(maaritys) ? kentanKieli(elementti, sisalto.kielet) : undefined;
|
|
511
|
+
muutetut[polku.sivu] = asetaArvo(pohja(polku.sivu), polku, lista ? jaaKappaleiksi(teksti) : teksti, kieli, tunnusKentta(sisalto, polku));
|
|
512
|
+
}
|
|
513
|
+
/* Uusi kohde sai tunnuksen "uusi". Nyt kun sillä on nimi, tunnus
|
|
514
|
+
johdetaan siitä: se näkyy osoitteessa, eikä kukaan halua /uusi-2. */
|
|
515
|
+
for (const [sivu, data] of Object.entries(muutetut)) {
|
|
516
|
+
const malliSivu = sisalto.malli[sivu];
|
|
517
|
+
if (!malliSivu)
|
|
518
|
+
continue;
|
|
519
|
+
for (const [kentta, maaritys] of Object.entries(malliSivu.kentat)) {
|
|
520
|
+
if (maaritys.tyyppi !== "kohdelista")
|
|
521
|
+
continue;
|
|
522
|
+
const lista = data[kentta];
|
|
523
|
+
if (!Array.isArray(lista))
|
|
524
|
+
continue;
|
|
525
|
+
const kaytossa = lista
|
|
526
|
+
.map((kohde) => kohde?.[maaritys.tunnus])
|
|
527
|
+
.filter((tunnus) => typeof tunnus === "string");
|
|
528
|
+
data[kentta] = lista.map((kohde) => {
|
|
529
|
+
const olio = kohde;
|
|
530
|
+
const tunnus = olio[maaritys.tunnus];
|
|
531
|
+
if (typeof tunnus !== "string" || !/^uusi(-\d+)?$/.test(tunnus))
|
|
532
|
+
return kohde;
|
|
533
|
+
const otsikko = olio[maaritys.otsikko];
|
|
534
|
+
const teksti = typeof otsikko === "string"
|
|
535
|
+
? otsikko
|
|
536
|
+
: typeof otsikko === "object" && otsikko !== null
|
|
537
|
+
? Object.values(otsikko).find((arvo) => typeof arvo === "string" && arvo !== "")
|
|
538
|
+
: undefined;
|
|
539
|
+
if (!teksti)
|
|
540
|
+
return kohde;
|
|
541
|
+
return {
|
|
542
|
+
...olio,
|
|
543
|
+
[maaritys.tunnus]: vapaaTunnus(kaytossa.filter((muu) => muu !== tunnus), teksti),
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (Object.keys(muutetut).length === 0) {
|
|
549
|
+
pois(false);
|
|
550
|
+
setTila("valmis");
|
|
551
|
+
setViesti("Ei muutoksia.");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const vastaus = await fetch("/api/editori/tallenna", {
|
|
556
|
+
method: "POST",
|
|
557
|
+
headers: { "Content-Type": "application/json" },
|
|
558
|
+
body: JSON.stringify({ perustuu: sisalto.versiot, sivut: muutetut }),
|
|
559
|
+
});
|
|
560
|
+
const tulos = await vastaus.json().catch(() => ({}));
|
|
561
|
+
if (!vastaus.ok) {
|
|
562
|
+
/* Muokkaustila jää päälle: muutokset ovat yhä sivulla ja paneelissa,
|
|
563
|
+
ja juuri paneelissa näkyy, mikä kenttä pysäytti tallennuksen. */
|
|
564
|
+
setTila("muokkaa");
|
|
565
|
+
setViesti(typeof tulos.virhe === "string"
|
|
566
|
+
? tulos.virhe
|
|
567
|
+
: "Tallennus ei onnistunut. Muutoksesi ovat yhä tässä.");
|
|
568
|
+
/* Kenttäkohtaiset virheet paneeliin: "Sisällössä on virheitä" ei
|
|
569
|
+
yksin auta ketään löytämään sitä tyhjää kenttää. */
|
|
570
|
+
if (Array.isArray(tulos.virheet))
|
|
571
|
+
setVirheet(tulos.virheet);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
setSisalto({
|
|
575
|
+
...sisalto,
|
|
576
|
+
versio: tulos.versio,
|
|
577
|
+
/* Tallennuksen jälkeen haetaan uudet tiedostoversiot, jotta seuraava
|
|
578
|
+
tallennus perustuu juuri kirjoitettuun eikä vanhaan. */
|
|
579
|
+
versiot: { ...sisalto.versiot, ...(tulos.versiot ?? {}) },
|
|
580
|
+
sivut: { ...sisalto.sivut, ...muutetut },
|
|
581
|
+
});
|
|
582
|
+
for (const { elementti, polku } of merkinnat()) {
|
|
583
|
+
alkuperaiset.current.set(elementti, lueTeksti(elementti, onLista(polku)));
|
|
584
|
+
}
|
|
585
|
+
muutettu.current = false;
|
|
586
|
+
pois(false);
|
|
587
|
+
setTila("tallennettu");
|
|
588
|
+
setViesti("Tallennettu. Sivusto rakennetaan uudelleen noin minuutissa.");
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
setTila("muokkaa");
|
|
592
|
+
setViesti("Verkkoyhteys katkesi. Muutoksesi ovat yhä tässä.");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function peru() {
|
|
596
|
+
const paneelissaMuutoksia = muutettu.current;
|
|
597
|
+
pois(true);
|
|
598
|
+
muutettu.current = false;
|
|
599
|
+
setTila("valmis");
|
|
600
|
+
setViesti("Muutokset peruttu.");
|
|
601
|
+
/* Paneelin muutokset perutaan hakemalla sisältö uudelleen: ne eivät ole
|
|
602
|
+
sivun tekstissä vaan editorin omassa muistissa. */
|
|
603
|
+
if (paneelissaMuutoksia)
|
|
604
|
+
location.reload();
|
|
605
|
+
}
|
|
606
|
+
if (tila === "piilossa")
|
|
607
|
+
return null;
|
|
608
|
+
const malli = sisalto && sivunAvain ? sisalto.malli[sivunAvain] : undefined;
|
|
609
|
+
const sivullaNakyvat = new Set(merkinnat().map(({ elementti }) => elementti.getAttribute("data-editori-kentta") ?? ""));
|
|
610
|
+
return (_jsxs(_Fragment, { children: [(muokataan || tallentaa) && sisalto && sivunAvain && malli ? (_jsx(Paneeli, { sivu: sivunAvain, malli: malli, data: sisalto.sivut[sivunAvain] ?? {}, kieli: sivunKieli, kielet: sisalto.kielet, sivullaNakyvat: sivullaNakyvat, valittu: valittu, virheet: virheet, muuta: muuta, lataaKuva: lataaKuva, kaanna: kaanna, kaannosKaytossa: sisalto.ominaisuudet?.kaannos ?? false, kaannettavia: {
|
|
611
|
+
sivu: kaannettavat
|
|
612
|
+
.filter(({ polku }) => polku.tunnus === undefined)
|
|
613
|
+
.map(({ nimi }) => nimi),
|
|
614
|
+
kohde: kaannettavat
|
|
615
|
+
.filter(({ polku }) => polku.tunnus !== undefined)
|
|
616
|
+
.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" }))] })) })] }));
|
|
618
|
+
}
|
|
619
|
+
/** Mitä yhtä kohdetta tässä listassa kutsutaan, esimerkiksi "kurssi". */
|
|
620
|
+
function kohdelistanYksikko(sisalto, sivu, kentta) {
|
|
621
|
+
if (!sisalto || !sivu)
|
|
622
|
+
return "kohde";
|
|
623
|
+
const maaritys = sisalto.malli[sivu]?.kentat[kentta];
|
|
624
|
+
return maaritys?.tyyppi === "kohdelista" ? (maaritys.yksikko ?? "kohde") : "kohde";
|
|
625
|
+
}
|
|
626
|
+
/** Minkä kentän arvo yksilöi kohteen tässä listassa. */
|
|
627
|
+
function tunnusKentta(sisalto, polku) {
|
|
628
|
+
const kentta = sisalto.malli[polku.sivu]?.kentat[polku.kentta];
|
|
629
|
+
return kentta?.tyyppi === "kohdelista" ? kentta.tunnus : "tunnus";
|
|
630
|
+
}
|
|
631
|
+
/** Kentän määrittely mallista, myös listan sisältä. */
|
|
632
|
+
function kentanMaaritys(sisalto, polku) {
|
|
633
|
+
const kentta = sisalto.malli[polku.sivu]?.kentat[polku.kentta];
|
|
634
|
+
if (!kentta)
|
|
635
|
+
return undefined;
|
|
636
|
+
if (polku.alikentta === undefined)
|
|
637
|
+
return kentta;
|
|
638
|
+
if (kentta.tyyppi !== "kohdelista")
|
|
639
|
+
return undefined;
|
|
640
|
+
const [ensimmainen, ryhmanKentta] = polku.alikentta.split(".");
|
|
641
|
+
const alikentta = kentta.kentat[ensimmainen];
|
|
642
|
+
if (ryhmanKentta === undefined)
|
|
643
|
+
return alikentta;
|
|
644
|
+
return alikentta?.tyyppi === "ryhma" ? alikentta.kentat[ryhmanKentta] : undefined;
|
|
645
|
+
}
|
|
646
|
+
export { PALKIN_TYYLIT };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function PalkkiLataaja(): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* ==========================================================================
|
|
4
|
+
PALKIN LATAAJA
|
|
5
|
+
==========================================================================
|
|
6
|
+
Ohut väli, joka pitää editorin käyttöliittymän omana latauspalanaan.
|
|
7
|
+
|
|
8
|
+
Ilman tätä kääntäjä niputtaa editorin selainkoodin samaan tiedostoon
|
|
9
|
+
sivuston oman koodin kanssa, ja tavallinen kävijä lataisi sen turhaan.
|
|
10
|
+
`next/dynamic` pakottaa erillisen palan, joka haetaan vasta kun palkki
|
|
11
|
+
oikeasti näytetään — eli vain kirjautuneelle muokkaajalle.
|
|
12
|
+
========================================================================== */
|
|
13
|
+
import dynamic from "next/dynamic";
|
|
14
|
+
const Palkki = dynamic(() => import("./EditorinPalkki.js").then((osa) => osa.EditorinPalkki), {
|
|
15
|
+
ssr: false,
|
|
16
|
+
});
|
|
17
|
+
export function PalkkiLataaja() {
|
|
18
|
+
return _jsx(Palkki, {});
|
|
19
|
+
}
|