@studiotitta/studio-editori 0.1.9 → 0.1.10
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/komponentit/Muokattava.js +2 -1
- package/dist/komponentit/rivinvaihdot.d.ts +2 -0
- package/dist/komponentit/rivinvaihdot.js +30 -0
- package/dist/selain/Muokkaustila.d.ts +1 -1
- package/dist/selain/Muokkaustila.js +265 -24
- package/dist/selain/mitta.d.ts +46 -0
- package/dist/selain/mitta.js +94 -0
- package/dist/tarkistus.d.ts +5 -1
- package/dist/tarkistus.js +4 -1
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { draftMode } from "next/headers";
|
|
3
|
+
import { rivinvaihdoin } from "./rivinvaihdot.js";
|
|
3
4
|
export async function Muokattava({ kentta, as, children, ...muut }) {
|
|
4
5
|
const Elementti = as ?? "span";
|
|
5
6
|
const { isEnabled: muokkaustila } = await draftMode();
|
|
6
|
-
return (_jsx(Elementti, { ...muut, ...(muokkaustila ? { "data-editori-kentta": kentta } : {}), children: children }));
|
|
7
|
+
return (_jsx(Elementti, { ...muut, ...(muokkaustila ? { "data-editori-kentta": kentta } : {}), children: rivinvaihdoin(children) }));
|
|
7
8
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createElement, Fragment } from "react";
|
|
2
|
+
/* ==========================================================================
|
|
3
|
+
RIVINVAIHDOT JA KAPPALEVÄLIT
|
|
4
|
+
==========================================================================
|
|
5
|
+
Tekstialueeseen kirjoitettu rivinvaihto tallentuu tekstiin merkkinä "\n",
|
|
6
|
+
mutta HTML näyttää sen välilyöntinä. Tämä muuttaa sen sivulla <br>:ksi:
|
|
7
|
+
|
|
8
|
+
Enter → uusi rivi
|
|
9
|
+
tyhjä rivi → kappaleväli (<br><br>)
|
|
10
|
+
|
|
11
|
+
<br> eikä <p>, koska kenttä on usein itse <p> eikä kappaletta voi laittaa
|
|
12
|
+
kappaleen sisään. <br> toimii minkä tahansa elementin sisällä ilman, että
|
|
13
|
+
sivuston CSS:ää tarvitsee muuttaa — ja editori lukee sen takaisin samaksi
|
|
14
|
+
"\n":ksi (selain/mitta.ts, tekstiRiveina).
|
|
15
|
+
|
|
16
|
+
Vain merkkijonolapsi muuttuu. Jos sivusto rakentaa kentän itse
|
|
17
|
+
kappaleiksi tai listaksi, sen rakenne jää koskematta.
|
|
18
|
+
========================================================================== */
|
|
19
|
+
export function rivinvaihdoin(lapset) {
|
|
20
|
+
if (typeof lapset !== "string" || !/[\r\n]/.test(lapset))
|
|
21
|
+
return lapset;
|
|
22
|
+
const rivit = lapset
|
|
23
|
+
.replace(/\r\n?/g, "\n")
|
|
24
|
+
/* Kolme tyhjää riviä on sama kappaleväli kuin yksi: sivun rytmi pysyy
|
|
25
|
+
designin mukaisena, vaikka Enteriä painaisi monta kertaa. */
|
|
26
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
27
|
+
.replace(/^\n+|\n+$/g, "")
|
|
28
|
+
.split("\n");
|
|
29
|
+
return rivit.map((rivi, sija) => createElement(Fragment, { key: sija }, sija > 0 ? createElement("br") : null, rivi));
|
|
30
|
+
}
|
|
@@ -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 /* 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";
|
|
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\n /* Kent\u00E4n mitta: pieni lappu aktiivisen kent\u00E4n alla. Kertoo, mihin\n kentt\u00E4\u00E4n kirjoitetaan ja kuinka paljon siihen on suunniteltu teksti\u00E4.\n Sijainti asetetaan CSSOM:lla (elementti.style), jota CSP ei rajoita. */\n .mitta {\n position: fixed; top: 0; left: 0; z-index: 2147483001;\n max-width: min(24rem, calc(100vw - 1rem));\n padding: .4rem .6rem; border-radius: .35rem;\n background: #1d1f1e; color: #fff;\n font: 400 .75rem/1.45 -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n box-shadow: 0 .25rem .75rem rgba(0,0,0,.25);\n pointer-events: none;\n }\n .mitta p { margin: 0; }\n .mitta p + p { margin-top: .2rem; }\n .mitta .nimi { font-weight: 600; }\n .mitta .maara { font-variant-numeric: tabular-nums; opacity: .8; }\n .mitta .ohje { opacity: .75; }\n .mitta[data-yli=\"1\"] { background: #fdf3e7; color: #6b4a1f; border: 1px solid rgba(107,74,31,.35); }\n .mitta[data-yli=\"1\"] .maara { opacity: 1; font-weight: 700; }\n";
|
|
2
2
|
export declare function Muokkaustila(): import("react").JSX.Element | null;
|
|
3
3
|
export { PALKIN_TYYLIT };
|
|
@@ -19,9 +19,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
19
19
|
3. **Mitään ei menetetä vahingossa.** Tallentamattomista muutoksista
|
|
20
20
|
varoitetaan lähtiessä, ja *Peru* palauttaa alkuperäiset tekstit.
|
|
21
21
|
========================================================================== */
|
|
22
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
22
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
23
23
|
import { valitseKieli } from "./kieli.js";
|
|
24
24
|
import { pienennaKuva } from "./kuva.js";
|
|
25
|
+
import { jaaKappaleiksi, kentanMitta, onYksirivinen, tekstiRiveina, yhdeksiRiviksi, } from "./mitta.js";
|
|
25
26
|
import { Paneeli } from "./Paneeli.js";
|
|
26
27
|
import { asetaArvo, kirjoitaKentanpolku, lueArvo, lueKentanpolku, vapaaTunnus, } from "./polku.js";
|
|
27
28
|
const PALKIN_TYYLIT = `
|
|
@@ -137,15 +138,53 @@ const PALKIN_TYYLIT = `
|
|
|
137
138
|
.napit { display: flex; gap: .5rem; margin-bottom: 1rem; }
|
|
138
139
|
.napit button { color: inherit; border-color: rgba(0,0,0,.25); }
|
|
139
140
|
.napit button.vaarallinen { color: #8c3b2e; border-color: rgba(140,59,46,.4); }
|
|
141
|
+
|
|
142
|
+
/* Kentän mitta: pieni lappu aktiivisen kentän alla. Kertoo, mihin
|
|
143
|
+
kenttään kirjoitetaan ja kuinka paljon siihen on suunniteltu tekstiä.
|
|
144
|
+
Sijainti asetetaan CSSOM:lla (elementti.style), jota CSP ei rajoita. */
|
|
145
|
+
.mitta {
|
|
146
|
+
position: fixed; top: 0; left: 0; z-index: 2147483001;
|
|
147
|
+
max-width: min(24rem, calc(100vw - 1rem));
|
|
148
|
+
padding: .4rem .6rem; border-radius: .35rem;
|
|
149
|
+
background: #1d1f1e; color: #fff;
|
|
150
|
+
font: 400 .75rem/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
151
|
+
box-shadow: 0 .25rem .75rem rgba(0,0,0,.25);
|
|
152
|
+
pointer-events: none;
|
|
153
|
+
}
|
|
154
|
+
.mitta p { margin: 0; }
|
|
155
|
+
.mitta p + p { margin-top: .2rem; }
|
|
156
|
+
.mitta .nimi { font-weight: 600; }
|
|
157
|
+
.mitta .maara { font-variant-numeric: tabular-nums; opacity: .8; }
|
|
158
|
+
.mitta .ohje { opacity: .75; }
|
|
159
|
+
.mitta[data-yli="1"] { background: #fdf3e7; color: #6b4a1f; border: 1px solid rgba(107,74,31,.35); }
|
|
160
|
+
.mitta[data-yli="1"] .maara { opacity: 1; font-weight: 700; }
|
|
140
161
|
`;
|
|
141
162
|
const KOROSTUS = `
|
|
142
163
|
[data-editori-kentta] { border-radius: .15rem; }
|
|
164
|
+
/* Kehys tekstin omalla värillä: musta katkoviiva katosi tummilla
|
|
165
|
+
sivustoilla, eikä muokkaaja nähnyt, missä kentät ovat. Tekstin väri
|
|
166
|
+
erottuu taustasta aina, koska sivun design on tehty niin. */
|
|
143
167
|
[data-editori-muokattavana="1"] {
|
|
144
|
-
outline: 1px dashed
|
|
168
|
+
outline: 1px dashed color-mix(in srgb, currentColor 45%, transparent);
|
|
169
|
+
outline-offset: .25rem; cursor: text;
|
|
145
170
|
}
|
|
146
171
|
[data-editori-muokattavana="1"]:hover { outline-style: solid; }
|
|
147
172
|
[data-editori-muokattavana="1"]:focus { outline: 2px solid currentColor; outline-offset: .25rem; }
|
|
148
173
|
[data-editori-muokattavana="1"] > p:last-child { margin-bottom: 0; }
|
|
174
|
+
/* Tyhjä kenttä ei saa kadota: ilman tekstiä sen korkeus olisi nolla ja
|
|
175
|
+
katkoviiva kutistuisi viivaksi. Paikkamerkki on kentän nimi, ja se
|
|
176
|
+
latoutuu kentän omalla fontilla, joten se kertoo myös mittakaavan. */
|
|
177
|
+
[data-editori-tyhja] { min-height: 1.4em; min-width: 12ch; }
|
|
178
|
+
/* Kirjoittaessa Enter lisää tekstiin rivinvaihdon. Chrome ja Safari
|
|
179
|
+
näyttävät sen muokattavassa kentässä itsekin; tämä varmistaa saman
|
|
180
|
+
muissa selaimissa. Julkinen sivu näyttää rivinvaihdon <br>:nä
|
|
181
|
+
(komponentit/rivinvaihdot.ts). */
|
|
182
|
+
[data-editori-monirivinen] { white-space: pre-line; }
|
|
183
|
+
span[data-editori-tyhja] { display: inline-block; }
|
|
184
|
+
[data-editori-tyhja]::before {
|
|
185
|
+
content: attr(data-editori-nimi) " – kirjoita tähän";
|
|
186
|
+
opacity: .45; font-style: italic; pointer-events: none;
|
|
187
|
+
}
|
|
149
188
|
body[data-editori-paneeli="1"] { padding-right: min(23rem, 100vw); }
|
|
150
189
|
`;
|
|
151
190
|
/** Lisää tyylit ilman inline-tyylejä, jotta CSP:tä ei tarvitse löysätä. */
|
|
@@ -159,25 +198,39 @@ function lisaaTyylit() {
|
|
|
159
198
|
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((muu) => muu !== arkki);
|
|
160
199
|
};
|
|
161
200
|
}
|
|
201
|
+
/** Tyhjä kenttä saa paikkamerkin; ks. KOROSTUS. */
|
|
202
|
+
function merkitseTyhja(elementti) {
|
|
203
|
+
elementti.toggleAttribute("data-editori-tyhja", (elementti.textContent ?? "").trim() === "");
|
|
204
|
+
}
|
|
205
|
+
/** Kursori kentän tekstin loppuun, kun kenttään siirrytään Enterillä. */
|
|
206
|
+
function kursoriLoppuun(elementti) {
|
|
207
|
+
const valinta = window.getSelection();
|
|
208
|
+
if (!valinta)
|
|
209
|
+
return;
|
|
210
|
+
const alue = document.createRange();
|
|
211
|
+
alue.selectNodeContents(elementti);
|
|
212
|
+
alue.collapse(false);
|
|
213
|
+
valinta.removeAllRanges();
|
|
214
|
+
valinta.addRange(alue);
|
|
215
|
+
}
|
|
216
|
+
/** Muokattava kenttä, jonka sisällä tapahtuma on. */
|
|
217
|
+
function muokattavaKentta(kohde) {
|
|
218
|
+
return kohde instanceof HTMLElement
|
|
219
|
+
? kohde.closest('[data-editori-muokattavana="1"]')
|
|
220
|
+
: null;
|
|
221
|
+
}
|
|
162
222
|
/**
|
|
163
223
|
* Kentän teksti elementistä.
|
|
164
224
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
225
|
+
* Monirivisellä kentällä (lista, tekstialue) kappalejako luetaan sivun
|
|
226
|
+
* rakenteesta, ks. tekstiRiveina. Yksirivisellä `textContent` riittää.
|
|
227
|
+
* Kumpikaan ei riipu siitä, miten sivu on muotoiltu tai mitä on näkyvissä.
|
|
168
228
|
*/
|
|
169
|
-
function lueTeksti(elementti,
|
|
170
|
-
return
|
|
229
|
+
function lueTeksti(elementti, monirivinen) {
|
|
230
|
+
return monirivinen ? tekstiRiveina(elementti) : (elementti.textContent ?? "");
|
|
171
231
|
}
|
|
172
|
-
|
|
173
|
-
|
|
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 !== "");
|
|
232
|
+
function onMonirivinen(maaritys) {
|
|
233
|
+
return maaritys?.tyyppi === "tekstilista" || maaritys?.tyyppi === "tekstialue";
|
|
181
234
|
}
|
|
182
235
|
/** Sivulla olevat muokattavat kohdat merkintöineen. */
|
|
183
236
|
function merkinnat() {
|
|
@@ -203,6 +256,12 @@ export function Muokkaustila() {
|
|
|
203
256
|
const [sisalto, setSisalto] = useState(null);
|
|
204
257
|
const [valittu, setValittu] = useState(undefined);
|
|
205
258
|
const alkuperaiset = useRef(new Map());
|
|
259
|
+
/** Peru palauttaa kentän rakenteen (kappaleet), ei pelkkää tekstiä. */
|
|
260
|
+
const alkuperainenRakenne = useRef(new Map());
|
|
261
|
+
/** Kenttä, jossa kursori on, ja hetken näkyvä vihje sen yhteydessä. */
|
|
262
|
+
const [aktiivinen, setAktiivinen] = useState(null);
|
|
263
|
+
/** Kasvaa jokaisella näppäilyllä, jotta mitta päivittyy kirjoittaessa. */
|
|
264
|
+
const [, setNappailyt] = useState(0);
|
|
206
265
|
useEffect(() => {
|
|
207
266
|
let voimassa = true;
|
|
208
267
|
fetch("/api/editori/sisalto", { headers: { Accept: "application/json" } })
|
|
@@ -264,29 +323,126 @@ export function Muokkaustila() {
|
|
|
264
323
|
else
|
|
265
324
|
document.body.removeAttribute("data-editori-paneeli");
|
|
266
325
|
}, [muokataan, tallentaa]);
|
|
267
|
-
const onLista = useCallback((polku) => sisalto !== null && kentanMaaritys(sisalto, polku)
|
|
326
|
+
const onLista = useCallback((polku) => sisalto !== null && onMonirivinen(kentanMaaritys(sisalto, polku)), [sisalto]);
|
|
268
327
|
const paalle = useCallback(() => {
|
|
269
328
|
alkuperaiset.current = new Map();
|
|
329
|
+
alkuperainenRakenne.current = new Map();
|
|
270
330
|
for (const { elementti, polku } of merkinnat()) {
|
|
271
331
|
alkuperaiset.current.set(elementti, lueTeksti(elementti, onLista(polku)));
|
|
332
|
+
alkuperainenRakenne.current.set(elementti, elementti.innerHTML);
|
|
272
333
|
elementti.setAttribute("contenteditable", "plaintext-only");
|
|
273
334
|
elementti.setAttribute("data-editori-muokattavana", "1");
|
|
274
335
|
elementti.setAttribute("spellcheck", "true");
|
|
336
|
+
const maaritys = sisalto ? kentanMaaritys(sisalto, polku) : undefined;
|
|
337
|
+
elementti.setAttribute("data-editori-nimi", maaritys?.nimi ?? polku.alikentta ?? polku.kentta);
|
|
338
|
+
if (onYksirivinen(maaritys))
|
|
339
|
+
elementti.setAttribute("data-editori-rivi", "1");
|
|
340
|
+
if (maaritys?.tyyppi === "tekstialue")
|
|
341
|
+
elementti.setAttribute("data-editori-monirivinen", "");
|
|
342
|
+
merkitseTyhja(elementti);
|
|
275
343
|
}
|
|
276
344
|
setTila("muokkaa");
|
|
277
345
|
setViesti("");
|
|
278
346
|
setVirheet([]);
|
|
279
|
-
}, [onLista]);
|
|
347
|
+
}, [onLista, sisalto]);
|
|
280
348
|
const pois = useCallback((palautaTekstit) => {
|
|
281
349
|
for (const { elementti } of merkinnat()) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
350
|
+
const rakenne = alkuperainenRakenne.current.get(elementti);
|
|
351
|
+
if (palautaTekstit && rakenne !== undefined)
|
|
352
|
+
elementti.innerHTML = rakenne;
|
|
285
353
|
elementti.removeAttribute("contenteditable");
|
|
286
354
|
elementti.removeAttribute("data-editori-muokattavana");
|
|
287
355
|
elementti.removeAttribute("spellcheck");
|
|
356
|
+
elementti.removeAttribute("data-editori-nimi");
|
|
357
|
+
elementti.removeAttribute("data-editori-rivi");
|
|
358
|
+
elementti.removeAttribute("data-editori-monirivinen");
|
|
359
|
+
elementti.removeAttribute("data-editori-tyhja");
|
|
288
360
|
}
|
|
361
|
+
setAktiivinen(null);
|
|
289
362
|
}, []);
|
|
363
|
+
/*
|
|
364
|
+
Kirjoittaminen sivulla. Kolme asiaa, joita selain ei tee itse:
|
|
365
|
+
|
|
366
|
+
1. Enter yksirivisessä kentässä (otsikko, painike) siirtää seuraavaan
|
|
367
|
+
kenttään. Ennen se lisäsi rivinvaihdon, jonka tallennus hukkasi
|
|
368
|
+
hiljaa, ja lauseet sulautuivat yhteen ("kysymys?Seuraava lause").
|
|
369
|
+
2. Monirivinen teksti liitetään yksiriviseen kenttään yhtenä rivinä.
|
|
370
|
+
3. Tyhjä kenttä saa paikkamerkin heti, kun sen viimeinen merkki
|
|
371
|
+
poistetaan — ei vasta seuraavalla avauksella.
|
|
372
|
+
*/
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (!muokataan)
|
|
375
|
+
return;
|
|
376
|
+
const kohdistus = (tapahtuma) => {
|
|
377
|
+
const kentta = muokattavaKentta(tapahtuma.target);
|
|
378
|
+
if (kentta)
|
|
379
|
+
setAktiivinen((nyt) => (nyt?.elementti === kentta ? nyt : { elementti: kentta }));
|
|
380
|
+
};
|
|
381
|
+
const poistuminen = (tapahtuma) => {
|
|
382
|
+
if (!muokattavaKentta(tapahtuma.relatedTarget))
|
|
383
|
+
setAktiivinen(null);
|
|
384
|
+
};
|
|
385
|
+
const kirjoitus = (tapahtuma) => {
|
|
386
|
+
const kentta = muokattavaKentta(tapahtuma.target);
|
|
387
|
+
if (!kentta)
|
|
388
|
+
return;
|
|
389
|
+
merkitseTyhja(kentta);
|
|
390
|
+
setNappailyt((n) => n + 1);
|
|
391
|
+
};
|
|
392
|
+
const nappain = (tapahtuma) => {
|
|
393
|
+
if (tapahtuma.key !== "Enter" || tapahtuma.isComposing)
|
|
394
|
+
return;
|
|
395
|
+
const kentta = muokattavaKentta(tapahtuma.target);
|
|
396
|
+
if (!kentta || kentta.getAttribute("data-editori-rivi") !== "1")
|
|
397
|
+
return;
|
|
398
|
+
tapahtuma.preventDefault();
|
|
399
|
+
const nimi = kentta.getAttribute("data-editori-nimi") ?? "Kenttä";
|
|
400
|
+
const kaikki = merkinnat()
|
|
401
|
+
.map(({ elementti }) => elementti)
|
|
402
|
+
.filter((elementti) => elementti.getAttribute("data-editori-muokattavana") === "1");
|
|
403
|
+
const seuraava = kaikki[kaikki.indexOf(kentta) + 1];
|
|
404
|
+
if (!seuraava) {
|
|
405
|
+
setAktiivinen({ elementti: kentta, vihje: `${nimi} on yksi rivi, eikä siihen voi lisätä rivinvaihtoa.` });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
seuraava.focus();
|
|
409
|
+
kursoriLoppuun(seuraava);
|
|
410
|
+
setAktiivinen({
|
|
411
|
+
elementti: seuraava,
|
|
412
|
+
vihje: `${nimi} on yksi rivi, joten jatkat tästä kentästä.`,
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const liitto = (tapahtuma) => {
|
|
416
|
+
const kentta = muokattavaKentta(tapahtuma.target);
|
|
417
|
+
if (!kentta || kentta.getAttribute("data-editori-rivi") !== "1")
|
|
418
|
+
return;
|
|
419
|
+
const teksti = tapahtuma.clipboardData?.getData("text/plain") ?? "";
|
|
420
|
+
if (!/[\r\n]/.test(teksti))
|
|
421
|
+
return;
|
|
422
|
+
tapahtuma.preventDefault();
|
|
423
|
+
/* execCommand säilyttää kumoamisen (Cmd+Z), toisin kuin suora DOM-muutos. */
|
|
424
|
+
document.execCommand("insertText", false, yhdeksiRiviksi(teksti));
|
|
425
|
+
};
|
|
426
|
+
document.addEventListener("focusin", kohdistus);
|
|
427
|
+
document.addEventListener("focusout", poistuminen);
|
|
428
|
+
document.addEventListener("input", kirjoitus);
|
|
429
|
+
document.addEventListener("keydown", nappain, true);
|
|
430
|
+
document.addEventListener("paste", liitto, true);
|
|
431
|
+
return () => {
|
|
432
|
+
document.removeEventListener("focusin", kohdistus);
|
|
433
|
+
document.removeEventListener("focusout", poistuminen);
|
|
434
|
+
document.removeEventListener("input", kirjoitus);
|
|
435
|
+
document.removeEventListener("keydown", nappain, true);
|
|
436
|
+
document.removeEventListener("paste", liitto, true);
|
|
437
|
+
};
|
|
438
|
+
}, [muokataan]);
|
|
439
|
+
/* Vihje on kertaluonteinen: se katoaa hetken päästä, mitta jää. */
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (!aktiivinen?.vihje)
|
|
442
|
+
return;
|
|
443
|
+
const ajastin = window.setTimeout(() => setAktiivinen((nyt) => (nyt?.vihje ? { elementti: nyt.elementti } : nyt)), 5000);
|
|
444
|
+
return () => window.clearTimeout(ajastin);
|
|
445
|
+
}, [aktiivinen]);
|
|
290
446
|
function sivullaMuuttunut() {
|
|
291
447
|
return merkinnat().some(({ elementti, polku }) => lueTeksti(elementti, onLista(polku)) !== alkuperaiset.current.get(elementti));
|
|
292
448
|
}
|
|
@@ -532,7 +688,7 @@ export function Muokkaustila() {
|
|
|
532
688
|
for (const { elementti, polku } of merkinnat()) {
|
|
533
689
|
const maaritys = kentanMaaritys(sisalto, polku);
|
|
534
690
|
const lista = maaritys?.tyyppi === "tekstilista";
|
|
535
|
-
const teksti = lueTeksti(elementti,
|
|
691
|
+
const teksti = lueTeksti(elementti, onMonirivinen(maaritys));
|
|
536
692
|
if (teksti === alkuperaiset.current.get(elementti))
|
|
537
693
|
continue;
|
|
538
694
|
let arvo = teksti;
|
|
@@ -634,7 +790,14 @@ export function Muokkaustila() {
|
|
|
634
790
|
muutettu.current = false;
|
|
635
791
|
pois(false);
|
|
636
792
|
setTila("tallennettu");
|
|
637
|
-
|
|
793
|
+
/* Pituusvaroitukset sanotaan ääneen. Ennen ne palasivat palvelimelta
|
|
794
|
+
eikä niitä näytetty missään, joten kaksinkertainen otsikko meni
|
|
795
|
+
läpi kenenkään huomaamatta. Vain tässä tallennuksessa muutetut
|
|
796
|
+
kentät: vanhat ylitykset toistuisivat joka kerta meluna. */
|
|
797
|
+
const pitkat = pituusvaroitukset(sisalto, muutetut, tulos.varoitukset);
|
|
798
|
+
setViesti(pitkat.length > 0
|
|
799
|
+
? `Tallennettu. Suunniteltua pidempi: ${pitkat.join(", ")}. Tarkista, miltä se näyttää puhelimella.`
|
|
800
|
+
: "Tallennettu. Sivusto rakennetaan uudelleen noin minuutissa.");
|
|
638
801
|
}
|
|
639
802
|
catch {
|
|
640
803
|
setTila("muokkaa");
|
|
@@ -655,6 +818,12 @@ export function Muokkaustila() {
|
|
|
655
818
|
if (tila === "piilossa")
|
|
656
819
|
return null;
|
|
657
820
|
const malli = sisalto && sivunAvain ? sisalto.malli[sivunAvain] : undefined;
|
|
821
|
+
const aktiivisenMaaritys = (() => {
|
|
822
|
+
if (!aktiivinen || !sisalto)
|
|
823
|
+
return undefined;
|
|
824
|
+
const polku = lueKentanpolku(aktiivinen.elementti.getAttribute("data-editori-kentta") ?? "");
|
|
825
|
+
return polku ? kentanMaaritys(sisalto, polku) : undefined;
|
|
826
|
+
})();
|
|
658
827
|
const sivullaNakyvat = new Set(merkinnat().map(({ elementti }) => elementti.getAttribute("data-editori-kentta") ?? ""));
|
|
659
828
|
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: {
|
|
660
829
|
sivu: kaannettavat
|
|
@@ -663,7 +832,79 @@ export function Muokkaustila() {
|
|
|
663
832
|
kohde: kaannettavat
|
|
664
833
|
.filter(({ polku }) => polku.tunnus !== undefined)
|
|
665
834
|
.map(({ nimi }) => nimi),
|
|
666
|
-
}, valitse: setValittu, lisaaKohde: lisaaKohde, poistaKohde: poistaKohde })) : null,
|
|
835
|
+
}, valitse: setValittu, lisaaKohde: lisaaKohde, poistaKohde: poistaKohde })) : null, muokataan && aktiivinen ? (_jsx(KentanMittaLappu, { elementti: aktiivinen.elementti, nimi: aktiivinen.elementti.getAttribute("data-editori-nimi") ?? "", mitta: kentanMitta(aktiivisenMaaritys, lueTeksti(aktiivinen.elementti, onMonirivinen(aktiivisenMaaritys))), vihje: aktiivinen.vihje, ohje: aktiivisenMaaritys?.tyyppi === "tekstialue"
|
|
836
|
+
? "Enter tekee uuden rivin, tyhjä rivi kappalevälin."
|
|
837
|
+
: aktiivisenMaaritys?.tyyppi === "tekstilista"
|
|
838
|
+
? "Jokainen rivi on oma kohtansa."
|
|
839
|
+
: undefined })) : 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" }))] })) })] }));
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Lappu aktiivisen kentän alla: nimi, täyttöaste ja hetkellinen vihje.
|
|
843
|
+
*
|
|
844
|
+
* Lappu sijoitetaan kentän alle, tai yläpuolelle jos alla ei ole tilaa.
|
|
845
|
+
* Jos kenttä on koko ruudun kokoinen, lappu jää ruudun alareunaan palkin
|
|
846
|
+
* yläpuolelle, jotta se on aina näkyvissä kirjoittaessa.
|
|
847
|
+
*/
|
|
848
|
+
function KentanMittaLappu({ elementti, nimi, mitta, vihje, ohje, }) {
|
|
849
|
+
const lappu = useRef(null);
|
|
850
|
+
useLayoutEffect(() => {
|
|
851
|
+
const sijoita = () => {
|
|
852
|
+
const oma = lappu.current;
|
|
853
|
+
if (!oma)
|
|
854
|
+
return;
|
|
855
|
+
const alue = elementti.getBoundingClientRect();
|
|
856
|
+
const vali = 8;
|
|
857
|
+
/* Palkki vie ruudun alareunasta noin 4,5 rem. */
|
|
858
|
+
const alin = window.innerHeight - oma.offsetHeight - 72;
|
|
859
|
+
let yla = alue.bottom + vali;
|
|
860
|
+
if (yla > alin)
|
|
861
|
+
yla = alue.top - oma.offsetHeight - vali;
|
|
862
|
+
if (yla < vali)
|
|
863
|
+
yla = alin;
|
|
864
|
+
const vasen = Math.min(Math.max(vali, alue.left), window.innerWidth - oma.offsetWidth - vali);
|
|
865
|
+
oma.style.transform = `translate(${Math.round(vasen)}px, ${Math.round(yla)}px)`;
|
|
866
|
+
};
|
|
867
|
+
sijoita();
|
|
868
|
+
let kehys = 0;
|
|
869
|
+
const ajoita = () => {
|
|
870
|
+
cancelAnimationFrame(kehys);
|
|
871
|
+
kehys = requestAnimationFrame(sijoita);
|
|
872
|
+
};
|
|
873
|
+
window.addEventListener("scroll", ajoita, true);
|
|
874
|
+
window.addEventListener("resize", ajoita);
|
|
875
|
+
return () => {
|
|
876
|
+
cancelAnimationFrame(kehys);
|
|
877
|
+
window.removeEventListener("scroll", ajoita, true);
|
|
878
|
+
window.removeEventListener("resize", ajoita);
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
const yli = mitta !== null && mitta.nyt > mitta.raja;
|
|
882
|
+
return (_jsxs("div", { ref: lappu, className: "mitta", "data-yli": yli ? "1" : "0", children: [_jsxs("p", { children: [_jsx("span", { className: "nimi", children: nimi }), mitta ? (_jsxs("span", { className: "maara", children: [" · ", mitta.nyt, " / ", mitta.raja, " ", mitta.yksikko] })) : null] }), yli ? (_jsx("p", { children: mitta.yksikko === "merkkiä"
|
|
883
|
+
? "Pidempi kuin tähän kohtaan on suunniteltu. Tarkista, miltä se näyttää puhelimella."
|
|
884
|
+
: `Tähän mahtuu enintään ${mitta.raja} ${mitta.yksikko}. Ylimääräiset estävät tallennuksen.` })) : null, ohje ? _jsx("p", { className: "ohje", children: ohje }) : null, vihje ? _jsx("p", { role: "status", children: vihje }) : null] }));
|
|
885
|
+
}
|
|
886
|
+
/** Tässä tallennuksessa muutettujen, suunniteltua pidempien kenttien nimet. */
|
|
887
|
+
function pituusvaroitukset(sisalto, muutetut, varoitukset) {
|
|
888
|
+
if (!Array.isArray(varoitukset))
|
|
889
|
+
return [];
|
|
890
|
+
const nimet = new Set();
|
|
891
|
+
for (const varoitus of varoitukset) {
|
|
892
|
+
if (!varoitus || typeof varoitus !== "object" || varoitus.syy !== "pituus")
|
|
893
|
+
continue;
|
|
894
|
+
const kohde = typeof varoitus.kohde === "string" ? varoitus.kohde : "";
|
|
895
|
+
const polku = lueKentanpolku(kohde);
|
|
896
|
+
if (!polku)
|
|
897
|
+
continue;
|
|
898
|
+
const uusi = muutetut[polku.sivu];
|
|
899
|
+
if (!uusi)
|
|
900
|
+
continue;
|
|
901
|
+
const tunnus = tunnusKentta(sisalto, polku);
|
|
902
|
+
const ennen = lueArvo(sisalto.sivut[polku.sivu] ?? {}, polku, tunnus);
|
|
903
|
+
if (JSON.stringify(ennen) === JSON.stringify(lueArvo(uusi, polku, tunnus)))
|
|
904
|
+
continue;
|
|
905
|
+
nimet.add(kentanMaaritys(sisalto, polku)?.nimi ?? kohde);
|
|
906
|
+
}
|
|
907
|
+
return [...nimet];
|
|
667
908
|
}
|
|
668
909
|
/** Mitä yhtä kohdetta tässä listassa kutsutaan, esimerkiksi "kurssi". */
|
|
669
910
|
function kohdelistanYksikko(sisalto, sivu, kentta) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Kentta } from "../maarittely.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Yhden rivin kenttä: otsikot, painikkeet, luvut. Enter ei tee näihin
|
|
4
|
+
* rivinvaihtoa, koska palvelin hylkäisi sen ja selain hukkaisi sen
|
|
5
|
+
* hiljaa ("kysymys?Seuraava lause").
|
|
6
|
+
*/
|
|
7
|
+
export declare function onYksirivinen(maaritys: Kentta | undefined): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Sivulla kirjoitettu lista takaisin listaksi.
|
|
10
|
+
*
|
|
11
|
+
* Oletuksena yksi rivi on yksi kohta. Jos sivusto näyttää listan yhtenä
|
|
12
|
+
* tekstinä (esim. "Lounas, Materiaalit"), erotin tulee sisältömallista.
|
|
13
|
+
*/
|
|
14
|
+
export declare function jaaKappaleiksi(teksti: string, erotin?: string): string[];
|
|
15
|
+
export interface Mitta {
|
|
16
|
+
nyt: number;
|
|
17
|
+
raja: number;
|
|
18
|
+
yksikko: "merkkiä" | "riviä" | "kohtaa";
|
|
19
|
+
}
|
|
20
|
+
/** Kentän täyttöaste, jos kentälle on suunniteltu raja; muuten null. */
|
|
21
|
+
export declare function kentanMitta(maaritys: Kentta | undefined, teksti: string): Mitta | null;
|
|
22
|
+
/**
|
|
23
|
+
* Liitetty monirivinen teksti yhdeksi riviksi: rivinvaihto → välilyönti.
|
|
24
|
+
*
|
|
25
|
+
* Alun rivinvaihto jää välilyönniksi, koska teksti liitetään usein
|
|
26
|
+
* edellisen lauseen perään. Lopun rivinvaihto on kopioinnin sivutuote
|
|
27
|
+
* ja poistetaan.
|
|
28
|
+
*/
|
|
29
|
+
export declare function yhdeksiRiviksi(teksti: string): string;
|
|
30
|
+
/** Se osa DOM-solmusta, jota rivien lukeminen tarvitsee (testattavissa ilman selainta). */
|
|
31
|
+
export interface Solmu {
|
|
32
|
+
nodeType: number;
|
|
33
|
+
nodeName: string;
|
|
34
|
+
textContent: string | null;
|
|
35
|
+
childNodes: ArrayLike<Solmu>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Monirivisen kentän teksti sivun rakenteesta: kappale, listan kohta ja
|
|
39
|
+
* <br> ovat rivinvaihtoja.
|
|
40
|
+
*
|
|
41
|
+
* Ei `innerText`: se lukee tekstin sellaisena kuin se näkyy ruudulla.
|
|
42
|
+
* Piilossa olevasta kohdasta (esim. vierityksen mukaan vaihtuva osio) se
|
|
43
|
+
* palauttaa tyhjää ja versaaliksi muotoillusta ("text-transform") isoja
|
|
44
|
+
* kirjaimia — ja tallennus kirjoitti ne tiedostoon muutoksina.
|
|
45
|
+
*/
|
|
46
|
+
export declare function tekstiRiveina(juuri: Solmu): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { merkkimaara } from "../tarkistus.js";
|
|
2
|
+
/* ==========================================================================
|
|
3
|
+
KENTÄN MITTA
|
|
4
|
+
==========================================================================
|
|
5
|
+
Muokkaaja kirjoittaa suoraan sivulle, joten hän ei näe lomakkeen
|
|
6
|
+
reunoja. Nämä apurit kertovat, millainen kenttä on: mahtuuko siihen
|
|
7
|
+
yksi rivi vai kappaleita, ja kuinka paljon tekstiä siihen on suunniteltu.
|
|
8
|
+
|
|
9
|
+
Raja on ohje, ei lukko — sama periaate kuin palvelimen tarkistuksessa.
|
|
10
|
+
Mittaus tehdään samalla merkkimäärällä kuin siellä, jotta sivulla näkyvä
|
|
11
|
+
luku ja tallennuksen varoitus eivät koskaan ole eri mieltä.
|
|
12
|
+
========================================================================== */
|
|
13
|
+
/**
|
|
14
|
+
* Yhden rivin kenttä: otsikot, painikkeet, luvut. Enter ei tee näihin
|
|
15
|
+
* rivinvaihtoa, koska palvelin hylkäisi sen ja selain hukkaisi sen
|
|
16
|
+
* hiljaa ("kysymys?Seuraava lause").
|
|
17
|
+
*/
|
|
18
|
+
export function onYksirivinen(maaritys) {
|
|
19
|
+
return maaritys?.tyyppi === "teksti" || maaritys?.tyyppi === "luku";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sivulla kirjoitettu lista takaisin listaksi.
|
|
23
|
+
*
|
|
24
|
+
* Oletuksena yksi rivi on yksi kohta. Jos sivusto näyttää listan yhtenä
|
|
25
|
+
* tekstinä (esim. "Lounas, Materiaalit"), erotin tulee sisältömallista.
|
|
26
|
+
*/
|
|
27
|
+
export function jaaKappaleiksi(teksti, erotin) {
|
|
28
|
+
const osat = erotin ? teksti.split(erotin.trim()) : teksti.split(/\n+/);
|
|
29
|
+
return osat.map((osa) => osa.trim()).filter((osa) => osa !== "");
|
|
30
|
+
}
|
|
31
|
+
/** Kentän täyttöaste, jos kentälle on suunniteltu raja; muuten null. */
|
|
32
|
+
export function kentanMitta(maaritys, teksti) {
|
|
33
|
+
if (!maaritys)
|
|
34
|
+
return null;
|
|
35
|
+
if ((maaritys.tyyppi === "teksti" || maaritys.tyyppi === "tekstialue") &&
|
|
36
|
+
maaritys.suositusPituus !== undefined) {
|
|
37
|
+
return { nyt: merkkimaara(teksti), raja: maaritys.suositusPituus, yksikko: "merkkiä" };
|
|
38
|
+
}
|
|
39
|
+
if (maaritys.tyyppi === "tekstilista" && maaritys.enintaan !== undefined) {
|
|
40
|
+
return {
|
|
41
|
+
nyt: jaaKappaleiksi(teksti, maaritys.erotin).length,
|
|
42
|
+
raja: maaritys.enintaan,
|
|
43
|
+
yksikko: maaritys.erotin ? "kohtaa" : "riviä",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Liitetty monirivinen teksti yhdeksi riviksi: rivinvaihto → välilyönti.
|
|
50
|
+
*
|
|
51
|
+
* Alun rivinvaihto jää välilyönniksi, koska teksti liitetään usein
|
|
52
|
+
* edellisen lauseen perään. Lopun rivinvaihto on kopioinnin sivutuote
|
|
53
|
+
* ja poistetaan.
|
|
54
|
+
*/
|
|
55
|
+
export function yhdeksiRiviksi(teksti) {
|
|
56
|
+
return teksti.replace(/\s+$/, "").replace(/\s*[\r\n]+\s*/g, " ");
|
|
57
|
+
}
|
|
58
|
+
const LOHKO = /^(P|DIV|LI|H[1-6]|BLOCKQUOTE|PRE)$/;
|
|
59
|
+
/**
|
|
60
|
+
* Monirivisen kentän teksti sivun rakenteesta: kappale, listan kohta ja
|
|
61
|
+
* <br> ovat rivinvaihtoja.
|
|
62
|
+
*
|
|
63
|
+
* Ei `innerText`: se lukee tekstin sellaisena kuin se näkyy ruudulla.
|
|
64
|
+
* Piilossa olevasta kohdasta (esim. vierityksen mukaan vaihtuva osio) se
|
|
65
|
+
* palauttaa tyhjää ja versaaliksi muotoillusta ("text-transform") isoja
|
|
66
|
+
* kirjaimia — ja tallennus kirjoitti ne tiedostoon muutoksina.
|
|
67
|
+
*/
|
|
68
|
+
export function tekstiRiveina(juuri) {
|
|
69
|
+
let tulos = "";
|
|
70
|
+
const rivinvaihto = () => {
|
|
71
|
+
if (tulos !== "" && !tulos.endsWith("\n"))
|
|
72
|
+
tulos += "\n";
|
|
73
|
+
};
|
|
74
|
+
const kay = (solmu) => {
|
|
75
|
+
if (solmu.nodeType === 3) {
|
|
76
|
+
tulos += solmu.textContent ?? "";
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (solmu.nodeType !== 1)
|
|
80
|
+
return;
|
|
81
|
+
if (solmu.nodeName === "BR") {
|
|
82
|
+
tulos += "\n";
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const lohko = LOHKO.test(solmu.nodeName);
|
|
86
|
+
if (lohko)
|
|
87
|
+
rivinvaihto();
|
|
88
|
+
Array.from(solmu.childNodes).forEach(kay);
|
|
89
|
+
if (lohko)
|
|
90
|
+
rivinvaihto();
|
|
91
|
+
};
|
|
92
|
+
Array.from(juuri.childNodes).forEach(kay);
|
|
93
|
+
return tulos.replace(/\n+$/, "");
|
|
94
|
+
}
|
package/dist/tarkistus.d.ts
CHANGED
|
@@ -11,8 +11,12 @@ export interface Huomio {
|
|
|
11
11
|
* Ero on tarpeen, koska piilotetussa kohteessa tyhjä kenttä on varoitus
|
|
12
12
|
* eikä virhe: keskeneräisen luonnoksen pitää voida tallentaa. Väärä muoto
|
|
13
13
|
* on virhe silloinkin — sitä ei kirjoiteta tiedostoon missään tilanteessa.
|
|
14
|
+
*
|
|
15
|
+
* "pituus" = teksti on suunniteltua pidempi. Editori nostaa nämä esiin
|
|
16
|
+
* tallennuksen jälkeen; muut varoitukset (esim. puuttuva käännös) eivät
|
|
17
|
+
* koske juuri kirjoitettua tekstiä.
|
|
14
18
|
*/
|
|
15
|
-
syy?: "puuttuu";
|
|
19
|
+
syy?: "puuttuu" | "pituus";
|
|
16
20
|
}
|
|
17
21
|
export interface Tarkistustulos {
|
|
18
22
|
virheet: Huomio[];
|
package/dist/tarkistus.js
CHANGED
|
@@ -39,7 +39,10 @@ function tarkistaTeksti(teksti, maaritys, kohde, kieli, tulos) {
|
|
|
39
39
|
if (suositus !== undefined) {
|
|
40
40
|
const yli = merkkimaara(teksti) - suositus;
|
|
41
41
|
if (yli > 0) {
|
|
42
|
-
tulos.varoitukset.push(
|
|
42
|
+
tulos.varoitukset.push({
|
|
43
|
+
...huomio(`Teksti on ${yli} merkkiä pidempi kuin suunniteltu. Tarkista, miltä se näyttää puhelimella.`),
|
|
44
|
+
syy: "pituus",
|
|
45
|
+
});
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
}
|
package/package.json
CHANGED