Yksinkertaisen JSFrame-komponentin tekeminen
Tässä dokumentissa tutustutaan miten voidaan tehdä yksinkertainen TIMiin upotettu JavaScript-pohjainen komponentti, jota voidaan uudelleen käyttää.
TIMiä voi laajentaa (eli tehdä uusia komponentteja) useilla eri tavoilla:
- tässä dokumentissa esiteltävä csPluginin alikieli JSFrame
- vähän vastaava csPluginin JS-alikieli.
- edellisten sovelluksena omaksi csPluginin alle julkaistavaksi alikieleksi. Katso esimerkiksi DFA tai vars.js
- kokonaan uuden pluginin tekeminen
- ominaisuuden upottaminen suoraan TIMiin
Valittu tapa riippuu pitkälle kehittäjän taitotasosta (vaatimus kasvaa em numeroiden mukana) sekä komponentin kommunikointitarpeesta TIMin kanssa. Jo vaihtoehdoilla 1 ja 2 voidaan tehdä uusia tehtäväkomponentteja tai erilaisia animaatioita.
1. Palindromi
Tässä dokumentissa käytetään esimerkkinä yksinkertaista palindromitehtävää, jossa pitää osata kirjoittaa vähintään vaaditun mittainen palindromi.
Toki vastaavan tehtävän voisi tehdä useilla eri tavoilla hyödyntäen TIMissä valmiina olevia komponentteja, mutta esimerkin yksinkertaisuuden takia siitä on helpointa ymmärtää itse käytetty tekniikka.
Dokumentin lukijalta edellytetään
- kykyä luoda ja muokata TIM-dokumentteja
- ainakin alkeellinen tuntemus HTML, CSS ja JavaScript-kielistä.
1.1 Kehittäminen lokaalissa koneessa ilman TIMiä
Kaiken tässä esitettävän voi tehdä myös suoraan TIMissä. Kuitenkin komponentin koodin ja debuggaustarpeen kasvaessa kehittäminen voi lopulta olla helpointa niin, että lokaalissa koneessa kehitetään ja debugataan komponettia ja sitten julkaistaan sitä sopivin ajoin TIMiin.
Tässä luvussa aloitetaan tällä tavalla. Jos kuitenkin halauta kokeilla suoraan TIMissä, voi hypätä aluksi seuraavan lukuunkin. Palaa kuitenkin tänne sitten, kun olet vakavamielisemmin kokeilemassa ja debuggaamassa.
Voit aluksi aloittaa jolla seuraavista tavoista:
- joku tekstieditori + selain
- html/js -kehitykseen tarkoitettu IDE
1.1.1 Jokin tekstieditori ja selaimen avulla debuggaaminen
Aivan alkuesimerkit voit tehdä lokaalisti millä tahansa tekstieditorilla ja tarvittaessa käyttää selaimen omaan debuggeria.
tee jollakin tekstieditorilla tiedosto
pali.html
jonka sisältö onAvaa tiedosto jollakin selaimella. Jos selaimesta ei helpolla löydy
Avaa tiedosto
, niin rahaa tiedosto tiedostoselaimella (Explorer, Finder, jne.) selaimen ikkunaan.Kokeile toimintaa.
Muokaa tiedostoa tekstieditorissa.
Virkistä selain.
1.1.2 PyCharm tai vastaava ympäristö
Oletetaan että sinulla on ainakin Pythonin jokin 3 versio asennettuna koneeseen. Katso komentoriviltä
python --version
Jos ei ole, niin asenna. Käytä vaikka tekoälyä apuna asentamisessa.
Ohjeet PyCharmille, toki voit vastaavalla tavalla tehdä myös esimerkiksi VSCodella (asenna silloin esim "Live Server" ja "JavaScript Debugger (Nightly)" -laajennus):
luo uusi projekti
Käynnistä terminaalissa lokaali palvelin joko Pythonilla (VSCodessa ei tarvitse em. laajennuksilla)
python -m http.server 8080
tai jos sinulla on NodeJS asennettuna, niin
http-server
tee aluksi tiedosto
pali.html
Valitse ajettavaksi
Current file
ja aja. Joskus tuntuu että debuggeria varten pitää Chrome sammuttaa ja kun on aloitettu alusta niin tappaa Taskmanagerilla ne Chrome-prosessit, joissa on vähiten säikeitä. Silloin PyCharm voi luoda uuden Chrome-ikkunan debuggaamista varten.
1.2 Helpoin tapa upottaa TIMiin
Komponentin kokeilemiseksi tee itsellesi omaan hakemistoosi
uusi tiedosto (dokumentti) vaikkapa nimelle pali
. Dokumentin alkuun voit lisätä vaikka ison otsikon
# Palindromitarkistus
Seuraavaksi lisää csPlugin
-komponentti, jonka alikielenä on jsfarme
ja sen fullhtml
-osaksi edellä ollut html-koodi. Eli lisää uusi lohko, jonka sisältönä on:
Nyt tämä näkyy sinulla TIM-sivulla seuraavasti:
Komponentin esityksen oletusleveys on 800px. Jos haluat komponentin keskitetyksi, niin lisää height
attribuutin jälkeen:
width: 300
Eli fullhtml
-osaan sama html-koodi, joka on lokaalissa koneessa kokeiltu toimivaksi. Joissakin TIM-dokumenteissa/esimerkeissä on käytetty myös srchtml
joka on alias tälle. Voit käyttää kumpaa attribuuttinimeä haluat.
1.3 Koodi eri tiedostoon
Voit jatkaa komponentin kehittämistä edellä kuvatulla tavalla ainakin siihen saakka, kun tulee tarve käyttää samaa komponenttia useammassa paikassa. Tällöin ongelmaksi tulee komponenttia kopioitaessa se, että jos koodia pitää muuttaa, on syntynyt useita paikkoja, joissa muutoksia pitäisi tehdä. Sen takia jossakin vaiheessa on järkevää erottaa tuo html-koodi omaksi tiedostokseen joka voi sijaita versionhallinnassa (esimerkiksi github) tai TIMissä. Tällöin itse komponentissa voidaan viitata tuohon tiedostoon ja on vain yksi paikka, jossa muutoksia tehdään.
Hyöty siitä että kooditiedosto on TIMIssä, on se ettei olla riippuvaisia muista palveluista ja komponentin muokkausoikeuksia on helpointa jakaa TIMin oikeuksia käyttäen.
Palindromissamme ei ole vielä mitään "järkevää" toimintaa. Ennen sen lisäämistä laitetaan malliksi html-koodi omaan tiedostoon, jota sitten jatkossa muokataan.
Tee jonnekin TIMissä tiedosto
pali.html
. Aluksi voit tehdä sen vaikka samaan hakemistoon kuin tuon kokeiludokumentinpali
.Myöhemmin jos osoittautuu, että olet tekemässä oikein yleishyödyllistä komponenttia, kannattaa komponentin tiedostot tallentaa hierarkiaan
Silloin voit ylläpidolta tim@jyu.fi pyytää itsellesi oman hakemiston tuonne ja sinne kirjoitusoikeudet. Tässä esimerkki tallennetaan nyt
Huomaa että vaikka esimerkeissä onpali1
jne. sen takia jotta välivaiheet jäävät hyvin talteen, voit omassa esimerkissä käyttää koko ajanpali.html
.Anna dokumentille
anonymous
-lukuoikeudet.Lisää dokumentin
pali.html
asetuksiin (Ratas +Edit settings
) rivi:textplain: true
Eli nyt asetukset pitäisi näyttää seuraavalta:
Tämän rivin ansiosta sisältö saadaan "raakana", kun URL-osoitteessa vaihdetaan
view
tilalleprint
:
Kopioi html-osuuden sisältö dokumentin uudeksi (ainoaksi) lohkoksi.
Sitten lisää kokeiludokumenttiisi
pali
uusi komponentti joka käyttää tuotahtml
-tiedostoa, eli esimerkiksi:``` {plugin="csPlugin" #pali1} type: jsframe width: 300 height: 60 fullhtmlurl: print/users/Anonymous/pali.html ```
Korjaa tarvittaessa tuo
fullhtmlurl
osoite, jos olet laittanutpali.html
muualle kuin oman hakmeistosi juureen. Osoitteessa voi toki olla myös koko polku kuten tuon aikaisemmanprint
-linkin tapauksessa. Tai myös
Nyt tuo komponentti pitäisi näkyä TIMissä:
Vastaavasti jos osoitteena on annettu
fullhtmlurl: https://raw.githubusercontent.com/vesal/cards/refs/heads/main/pali1.html
Vastaavasti jos osoite haluttaisiin JY:n gitlabista
, olisi se muotoa:
fullhtmlurl: https://gitlab.jyu.fi/tie/ohj2/esimerkit/cards/-/raw/main/pali1.html
Komponentilla ei ole vielä mitään järkevää toimintaa, mutta nyt meillä on hyvä toimiva pohja mistä jatkaa.
1.4 Muista tyhjentää CSPluginin välimuisti
Tiedoston hakemisen nopeuttamiseksi TIM tekee käteismuistia (cache) noista fullhtmlurl
haetuista tiedostoista. Jos muutat tiedoston sisältöä, niin muista tehdä
jolloin käteismuisti tyhjennetään.
Tosin itse kehittämisaikana voit käyttää komponentissa cachen kieltoa tyyliin:
``` {plugin="csPlugin" #pali1}
type: jsframe
width: 300
height: 60
usecache: false
fullhtmlurl: print/users/Anonymous/pali.html
```
Jos näin teet (kuten ehkä kannattaa), niin muista poistaa tuo rivi sitten, kun komponentin koodi stabiloituu. Toisaalta jos pidät tuota vain tässä kokeiludokumentissasi, niin ei se mitään haittaakaan. Kunhan muista tehdä tuon varsinaisen refresh
, jotta muut tuotannossa olevat komponentit päivittyvät.
Jatkossa puhutaan myös automaattisen dokumentaation tuottamisesta komponenttiin tulevista attribuuteista. Siihen liittyen muista myös muutosten jälkeen tyhjentää se cache:
Tuossa nimi hämää, vaikka komponentin koodi olisi gitissä tai TIMissä, niin silti sen sisältöä näyttää showFile
-komponentti joka on TIMissä vanhassa svn
-kontissa. Siksi nimessä on svn
.
1.5 Toiminnallisuutta JavaScriptillä
Lisätään seuraavaksi komponenttiin tarkistus siitä, onko kirjoitettu teksti palindromi vai ei. Unohdetaan aluksi "oikeat" palindromihienoudet kuten välilyöntien yms. poisto sekä isojen ja pienten kirjainten ero. Voit niitä lisätä itse sitten kun ymmärrät perusidean.
Lisätään koodiin ikoni, johon oikeellisuus näytetään punaisella tai vihreällä merkillä. Ja sitten lisätään koodia, joka tekee tarkistuksen aina kun sanassa tulee muutos. Huomaa että elementeille on täytynyt antaa id
, jotta ne löytyvät yksikäsitteisesti.
Eli muokataan pali.html
muotoon:
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<title>Palindromi</title>
</head>
<body>
<div>
<label for="word">Anna sana:</label>
<input id="word" type="text" name="word">
<span id="icon"></span>
</div>
<script>
const input = document.getElementById("word");
.addEventListener('input', () => this.checkPalindrome());
input
function checkPalindrome() {
const word = document.getElementById("word").value;
const icon = document.getElementById("icon");
let isPalindrome = true;
for (let i = 0, j = word.length - 1; i < j; i++, j--) {
if (word[i] !== word[j]) {
= false;
isPalindrome break;
}
}if (isPalindrome) {
.textContent = '✔';
icon.style.color = 'green';
iconelse {
} .textContent = '✘';
icon.style.color = 'red';
icon
}
}</script>
</body>
</html>
Toiminnallisuus on samanlainen lokaalisti kokeiltuna ja TIMissä:
Koodin tekemistä voi tarvittaessa jatkaa tällä tavalla. Oikeasti voi kuitenkin olla suuremmassa koodissa järkevää tehdä luokka varsinaisesta toiminnallisuudesta.
Siitä seuraavassa luvussa.
1.6 Toiminnallisuus omaan JavaScript luokkaan
Uudelleenkäytettävyyttä voidaan jatkossa helpottaa tekemällä toiminnallisuudesta oma JavaScript-luokka.
Samalla lisätään sisäinen this.settings
-olio, jota voidaan jatkossa käyttää siihen, että sen avulla voidaan toiminnallisuutta muuttaa. Nyt tehdyillä optioilla muutetaan siis itse tekstiä ja kuinka pitkä palindromi vähintään vaaditaan.
Eli muutetaan pali.html
-muotoon:
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<title>Palindromi</title>
</head>
<body>
<div id="container">
<label id="label" for="word">Anna sana:</label>
<input id="word" type="text" name="word">
<span id="icon"></span>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('container');
new Pali(container);
;
})
class Pali {
constructor(container) {
this.settings = {
inputChars: 10, // input alueen leveys
minChars: 0, // sanassa tarvittavien merkkien minimimäärä
labelText: "Anna sana:", // labelin teksti
}this.container = container;
this.createContent();
}createContent() {
const s = this.settings;
this.input = this.container.querySelector("#word");
this.icon = this.container.querySelector("#icon");
const label = this.container.querySelector("#label");
.textContent = s.labelText.replace('${count}', s.minChars);
label
this.input.style.width = s.inputChars + 'ch';
this.input.addEventListener('input', () => this.checkPalindrome());
}
checkPalindrome() {
const word = this.input.value;
let isPalindrome = true;
for (let i = 0, j = word.length - 1; i < j; i++, j--) {
if (word[i] !== word[j]) {
= false;
isPalindrome break;
}
}if (isPalindrome && this.settings.minChars <= word.length) {
this.icon.textContent = '✔';
this.icon.style.color = 'green';
else {
} this.icon.textContent = '✘';
this.icon.style.color = 'red';
}
}
}</script>
</body>
</html>
Toiminnallisuus on edelleen sama lokaalisti ja TIMissä.
1.7 Parametrit TIMistä
Kun halutaan kommunikoida TIMin kanssa niin, että aluksi voitaisiin edellä tehdyt optiot viedä komponentille, joudutaan hieman tekemään muutoksia erityisesti jos halutaan edelleen, että samaa koodia voi kehittää sekä lokaalisti että TIMissä.
Edellä tuo oma luokka luotiin kun dokumentti oli latautunut. TIMissä kutsutaan onInit
-funktiota kun iframen sisällä kaikki on valmista.
Sitten mahdollisten jsparams
-lohkoon kirjoitetut attribuutit löytyvät window.jsframedata
-oliosta. Siksi lokaalia kehitystä varten pitää matkia tuota tapaa ja tehdään tuo onInit
-funktio. Eli nyt pali.html on muotoa:
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<title>Palindromi</title>
<script>
if (window.self === window.top) { // lokaali ajo
window.jsframedata = { params: {
inputChars: 8,
minChars: 6,
labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
;
} }document.addEventListener('DOMContentLoaded', () => onInit() );
}</script>
</head>
<body>
<div id="container">
<p>
<label id="label" for="word">Anna sana:</label>
<input id="word" type="text" name="word">
<span id="icon"></span>
</p>
</div>
<script>
function onInit() {
const container = document.getElementById('container');
new Pali(container, window.jsframedata);
}
class Pali {
constructor(container, data) {
this.settings = {
inputChars: 10, // input alueen leveys
minChars: 0, // sanassa tarvittavien merkkien minimimäärä
labelText: "Anna sana:", // labelin teksti
}if (data) Object.assign(this.settings, data.params);
this.container = container;
this.createContent();
}createContent() {
const s = this.settings;
this.input = this.container.querySelector("#word");
this.icon = this.container.querySelector("#icon");
const label = this.container.querySelector("#label");
.textContent = s.labelText.replace('${count}', s.minChars);
label
this.input.style.width = s.inputChars + 'ch';
this.input.addEventListener('input', () => this.checkPalindrome());
}
checkPalindrome() {
const word = this.input.value;
let isPalindrome = true;
for (let i = 0, j = word.length - 1; i < j; i++, j--) {
if (word[i] !== word[j]) {
= false;
isPalindrome break;
}
}if (isPalindrome && this.settings.minChars <= word.length) {
this.icon.textContent = '✔';
this.icon.style.color = 'green';
else {
} this.icon.textContent = '✘';
this.icon.style.color = 'red';
}
}
}
</script>
</body>
</html>
Nyt siis jos koodia ajetaan lokaalissa koneessa, niin kutsutaan itse tuota onInit
, TIMIssä ajettuna TIM-lisää siihen koodin, jossa tuota kutsutaan.
Uutta on siis mm. tuo, että tuodaan muodostajalle asetukset (data
) ja sitten kopioidaan sieltä omiin asetukseen jos siellä on jotakin uutta.
TIMissä voidaan nyt laittaa (muuta omaa pali
-tiedostosi komponentin kutsua ja muista laittaa oma URL-polkusi):
Nyt komponentti näkyy TIMIssä:
Parametrit tulevat nyt siis window.jsframedata
-oliossa ja sieltä muodostajan data
-oliossa on:
{
"params": {
inputChars: 8,
minChars: 6,
labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
}
}
Nyt kun komponentista otetaan kopioita, niin jokaisessa kopiossa voidaan käyttää eri tekstiä, tekstilaatikon kokoa ja minimikirjainten määrää.
1.8 Parametrien dokumentointi
Näin tehtynä komponentin this.settings
kohtaan kirjoitetut asetukset saadaan näkymään TIMissä komponentin dokumentaatiodokumentissa seuraavalla TIM-koodilla (tämä vaatii että tiedostolla on anonyymi pääsy). Kokeile lisätä tuollainen koodi pali
-dokumenttiisi:
``` {plugin="showCode"}
file: "https://tim.jyu.fi/print/tim/components/pali/pali.html"
color: js
start: "this.settings ="
end: "}"
startn: 1
endn: -1
linefmt: ""
```
jolloin oletusparametrit näkyvät muodossa:
: 10, // input alueen leveys
inputChars: 0, // sanassa tarvittavien merkkien minimimäärä
minChars: "Anna sana:", // labelin teksti labelText
Jos halutaan "hienostella" vielä vähän enemmän, voidaan showCode
-pluginilla muokata tulosta niin, että se on valmista koodia kopioitavaksi komponentin käyttäjälle:
``` {plugin="showCode"}
stem: "Pali-komponentin parametrit ovat:"
file: "https://tim.jyu.fi/print/tim/components/pali/pali.html"
lineSed:
- 's|^ *([^/ ])| \1|' # alkutyhjät vähemmälle
- 's|,?( *)//|\1#|' # pilkku pois ja // tilalle #
- 's/.*this.*/jsparams:/' # otsikon vaihto
color: yaml
start: "this.settings ="
end: "}"
startn: 0
endn: -1
```
Tämä näkyisi siis TIM-koodissa:
Pali-komponentin parametrit ovat:
jsparams:
inputChars: 10 # input alueen leveys
minChars: 0 # sanassa tarvittavien merkkien minimimäärä
labelText: "Anna sana:" # labelin teksti
Näin komponenttiin voidaan nyt lisätä uusia parametreja ja ne näkyvät sitten automaattisesti (kunhan muistat tyhjentää svn-cachen) dokumentaatiossa. Toki voi joutua kirjoittamaan dokumentaatioon tarkempaa kuvausta ja esimerkkejä parametrien käytöstä.
1.9 Käyttäjän tietojen tallentaminen tehtävän vastauksena
Monessa komponentissa voi riittää edellisen kaltainen, missä on perus-html -tiedosto ja sen ulkoasua voi hieman säätää TIMin puolelta.
Usein kuitenkin halutaan tehdä tehtäväkomponentteja, joissa saadaan talteen käyttäjän vastaus ja seuraavalla käyttökerralla voidaan viimeisin vastaus antaa pohjaksi.
Tarvittavia muutoksia vastauksen palauttamiseksi on tehdä setData
-funktio, joka asettaa vanhan vastauksen näkyviin.
Lisäämällä saveButton
-painke, kutsuu se painettaessa getData
-funktiota ja sitten tallentaa vastaukset vastaustietokantaan.
Datan pitää olla muodossa:
Eli palindromin tapauksessa tämä voisi olla niin, että text
-attribuutilla toimitetaan käyttäjän kirjoittama sana suuntaan ja toiseen, eli datan muoto olisi:
Kun painetaan tallennuspainiketta, niin näyttöön tulee teksti saved
(jollei sitä muuteta) ja sen olisi hyvä hävitä, kun sanaan kirjoitetaan uusia merkkejä. Siksi tehdään vielä funktio update
. Pitää kuitenkin olla tarkkana, ettei update kutsuta liian herkästi.
Lisätään komponenttiin vielä niin, että tallennus tehdään myös kun painetaan return
. Harjoituksena tallennuksen voisi lisätä myös, jos komponentti menettää fokuksen.
Funktiot saveData
ja updataData
aiheuttavat ongelmia lokaalissa kehityksessä. Niiden sisältö olisi ehkä hyvä suojata tuolla
if (window.self === window.top) return;
Lopulta pali.html
on siis:
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<title>Palindromi</title>
<script>
if (window.self === window.top) { // lokaali ajo
window.jsframedata = { params: {
inputChars: 8,
minChars: 6,
labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
;
} }document.addEventListener('DOMContentLoaded', () => {
onInit();
setData({ c: { text: 'kissa' } });
;
} )
}</script>
</head>
<body>
<div id="container">
<label id="label" for="word">Anna sana:</label>
<input id="word" type="text" name="word">
<span id="icon"></span>
</div>
<script>
function onInit() {
const container = document.getElementById('container');
window.pali = new Pali(container, window.jsframedata);
}
function setData(data) {
if (!window.pali) onInit(); // varulta
window.pali.setData(data);
}
function saveData(data) {
window.port2.postMessage({ msg: "datasave", data: { ...data } });
}
function getData() {
return window.pali.getData();
}
function updateData(data) {
window.port2.postMessage({ msg: "update", data: { ...data } });
}
class Pali {
constructor(container, data) {
this.settings = {
inputChars: 10, // input alueen leveys
minChars: 0, // sanassa tarvittavien merkkien minimimäärä
labelText: "Anna sana:", // labelin teksti
}if (data) Object.assign(this.settings, data.params);
this.container = container;
this.createContent();
}createContent() {
const s = this.settings;
this.input = this.container.querySelector("#word");
this.icon = this.container.querySelector("#icon");
const label = this.container.querySelector("#label");
.textContent = s.labelText.replace('${count}', s.minChars);
label
this.input.style.width = s.inputChars + 'ch';
this.input.addEventListener('input', () => this.checkPalindrome());
this.input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
saveData(this.getData());
};
})
}
checkPalindrome() {
const word = this.input.value;
let isPalindrome = true;
for (let i = 0, j = word.length - 1; i < j; i++, j--) {
if (word[i] !== word[j]) {
= false;
isPalindrome break;
}
}if (isPalindrome && this.settings.minChars <= word.length) {
this.icon.textContent = '✔';
this.icon.style.color = 'green';
else {
} this.icon.textContent = '✘';
this.icon.style.color = 'red';
}updateData(getData());
}
getData() {
return { c: { text: this.input.value } };
}
setData(data) {
const newText = data.c.text;
if (this.input.value === newText) return;
this.input.value = newText;
this.checkPalindrome();
}
}</script>
</body>
</html>
Komponentin käyttöön oikeastaan ainoa lisäys on saveButton
-teksti. Tätäkään ei tarvittaisi jos tyydyttäisiin pelkkään return
-painikkeella tallentamiseen.
1.10 Tarkistus palvelimen päässä
Vaikka palindromi onkin tarkistettu jo selaimessa, niin sillä ei oikein voisi antaa pisteitä, koska taitava nörtti osaisi lähettää vastaavan pistejonon palvelimelle "käsin". Siksi lopullinen tarkistus on tehtävä palvelimen päässä. TIMillä tämä onnistuu kirjoittamalla komponenttiin postprogram
-koodi. Eli lisätään pluginiin attribuutti ja sille vaikka koodi:
postprogram: |!!
// print("\<pre\>" + JSON.stringify(data, null, 2) + "\</pre\>"); // debuggausta varten
function checkPalindrome(word) {
for (let i = 0, j = word.length - 1; i < j; i++, j--) {
if (word[i] !== word[j]) return false;
}
return true;
}
let len = 0;
let jsparams = data.answer_call_data.markup.jsparams;
if (jsparams) len = data.answer_call_data.markup.jsparams.minChars;
let word = data.save_object.c.text;
let p = 1;
if (word.length < len) { p = 0; data.web.console += " Liian lyhyt!"; }
if (!checkPalindrome(word)) { p = 0; data.web.console += " Ei ole palindromi!"; }
data.points = p;
return data;
!!
Katso lisää toiminnasta dokumentista Arvostelu JavaScriptillä.
Käytännössä toiminta näyttää nyt seuraavalta:
1.11 Tarkistusfunktio kirjastoon
Edellisessä on vielä se vika, että jokaiseen komponenttiin pitäisi taas kopioida sama tarkistuskoodi. Ja jos siinä havaitaan virheitä on päivitettäviä paikkoja paljon.
Eli kirjoitetaankin tarkastusfunktio tiedostoon palicheck.js
:
function checkPalindrome(word) {
for (let i = 0, j = word.length - 1; i < j; i++, j--) {
if (word[i] !== word[j]) return false;
}return true;
}
function checkPali(data, totalPoints=1, paliPoints=0, lenPoints=0)
{// print("\<pre\>" + JSON.stringify(data, null, 2) + "\</pre\>"); // debuggausta varten
let len = 0;
let jsparams = data.answer_call_data.markup.jsparams;
if (jsparams) len = data.answer_call_data.markup.jsparams.minChars;
let word = data.save_object.c.text;
let p = [0,0];
.web.console = "";
dataif (word.length < len) data.web.console += " Liian lyhyt!";
else p[0] = 1;
if (!checkPalindrome(word)) data.web.console += " Ei ole palindromi!";
else p[1] = 1;
if (p[0]+p[1] == 2) data.points = totalPoints;
else data.points = p[0]*lenPoints + p[1]*paliPoints;
.web.console += " Pisteitä: " + data.points;
datareturn data;
}
Samalla yleistettiin checkPali
funktiota niin, että voidaan antaa osapisteitä jos pituus on oikein tai on palindromi ja erikseen mitä saa jos molemmat ovat oikein.
Nyt itse komponentti oletusarvoilla tehtynä olisi:
1.12 Koristelua
Komponenttia voidaan toki koristella normaaleilla TIM-koristeilla:
``` {plugin="csPlugin" #pali5deco}
type: jsframe
jsparams:
inputChars: 8
minChars: 6
labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
width: 700
height: 60
usecache: false
saveButton: "Tallenna"
borders: true
header: Palindromi
stem: Palindromi on jono, joka on sama luettuna etu- ja takaperin.
fullhtmlurl: /print/tim/components/pali/pali.html
postlibraries:
- /print/tim/components/pali/palicheck.js
postprogram: return checkPali(data);
```
jolloin se näyttäisi seuraavalta:
1.13 Suurin osa koodista JavaScript-tiedostoon
Sellaisia tilanteita varten, jossa sisältöä luodaan dynaamisesti attribuuttien perusteella on syytä osata tehdä myös kaikki suoraan JavaScrptissä.
Totta kai totuus piilee jossakin näiden kahden tavan välillä.
Tekoäly osaa autaa paljon, kun kysyy miten tämä html tehtäisiin suoraan JavaScriptillä.
Tavoitteena on minimaalinen html, joka kutsuu JavaScript-koodia, joka on kirjoitettu omaan tiedostoonsa. Eli tehdään lokaalia kehitystä varten oma pali.html
:
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<title>Palindromi</title>
<script>
document.write('<script src="pali.js"><\/script>');
window.jsframedata = { params: {
minChars: 6,
labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
} };
document.addEventListener('DOMContentLoaded', () => onInit() );
</script>
</head>
<body>
<div id="container"></div>
</body>
</html>
TIMiä varten muokataan dokumentti pali.html
muotoon:
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<script src="https://tim.jyu.fi/print/tim/components/pali/pali.js"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
Sitten tehdään vastaavan sisältöinen TIM-dokumentti pali.js
. Ja sinne taas asetuksiin plaintext: true
.
function onInit() {
const container = document.getElementById('container');
window.pali = new Pali(container, window.jsframedata);
}
function setData(data) {
if (!window.pali) onInit(); // varulta
window.pali.setData(data);
}
function saveData(data) {
window.port2.postMessage({ msg: "datasave", data: { ...data } });
}
function getData() {
return window.pali.getData();
}
function updateData(data) {
window.port2.postMessage({ msg: "update", data: { ...data } });
}
= {
paliTranslations en: {
labelText: "Give a word:", // label text
}
}
class Pali {
constructor(container, data) {
this.settings = {
lang: "fi", // laguage, fi, en
inputChars: 10, // input alueen leveys
minChars: 0, // sanassa tarvittavien merkkien minimimäärä
labelText: "Anna sana:", // labelin teksti
}if (data) {
const lang = data.params?.lang ?? 'fi';
Object.assign(this.settings, paliTranslations[lang] ?? {}, );
Object.assign(this.settings, data.params);
}this.container = container;
this.createContent();
}
createContent() {
const s = this.settings;
const label = document.createElement('label');
.htmlFor = 'word';
label.textContent = label.textContent = s.labelText.replace('${count}', s.minChars);
labelthis.container.appendChild(label);
const input = document.createElement('input');
.type = 'text';
input.id = 'word';
input.name = 'word';
input.style.width = s.inputChars + 'ch';
inputthis.container.appendChild(input);
const icon = document.createElement('span');
.className = 'icon';
iconthis.container.appendChild(icon);
this.input = input;
this.icon = icon;
this.input.addEventListener('input', () => this.checkPalindrome());
this.input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
saveData(this.getData());
};
})
}
checkPalindrome() {
const word = this.input.value;
let isPalindrome = true;
for (let i = 0, j = word.length - 1; i < j; i++, j--) {
if (word[i] !== word[j]) {
= false;
isPalindrome break;
}
}if (isPalindrome && this.settings.minChars <= word.length) {
this.icon.textContent = '✔';
this.icon.style.color = 'green';
else {
} this.icon.textContent = '✘';
this.icon.style.color = 'red';
}updateData(getData());
}
getData() {
return { c: { text: this.input.value } };
}
setData(data) {
const newText = data.c.text;
if (this.input.value === newText) return;
this.input.value = newText;
this.checkPalindrome();
} }
Pluginin koodi kuten enne, paitsi viitataan sinne minimaaliseen pali.html
:
``` {plugin="csPlugin" #pali5js}
type: jsframe
jsparams:
inputChars: 8
minChars: 6
labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
width: 700
height: 60
usecache: false
saveButton: "Tallenna"
borders: true
header: Palindromi
stem: Palindromi on jono, joka on sama luettuna etu- ja takaperin.
fullhtmlurl: /print/tim/components/pali/pali.html
postlibraries:
- /print/tim/components/pali/palicheck.js
postprogram: return checkPali(data);
```
Kaiken pitäisi toimia lokaalisti ja TIMissä kuten ennenkin. Parametridokumentaatio otettaisiin nyt luonnollisesti pali.js
-tiedostosta. Mikäli on paljon jsparams
attribuutteja joiden arvoja pitää aina muuttaa samalla tavalla, niin kannattaa toki miettiä jo niiden oletusarvoja.
Nyt jatkossa tuota TIMissä olevaa pali.html
ei toivottavasti tarvitse muokata. Paitsi jos lisätään uusia JavaScript-kirjastoja kun myöhemmin selviää Korttipeli-esimerkeissä.
Lokaalissa versiossa eri parametreja kokeiltaessa niitä muokataan ja silloin lokaali pali.html
tokimuuttuu.
1.14 Lokaali kehitys kun on erillisiä JavaScript-tiedostoja mukana
Lokaalia ajamista varten kannattaa nyt tehdä PyCharmissa oma ajokonfiguraatio, koska etupäässä muokataan pali.js
ja silloinhan Current File
ei ole oikea ajettavaksi. Eli otetaan Current File
alta Edit Configurations
+
uuden lisäämiseksiJavaScript debug
- Name:
pali.html
- URL: etsitään kansion kuvalla
pali.html
, tulee jotakintyyliin:http://localhost:63342/kortit/pali.html
- Ruksitaan
Ensure breakpoints are detected...
- `OK``
Ja jatkossa pidetään syntynyt pali.html
ajon kohteena. Jos kehitetään useita html-tiedostoja samaan aikaan, kannattaa jokaiselle tehdä oma ajokonfiguraatio.
1.15 Kääntäminen toisille kielille
Mikäli komponettia pitää ylläpitää useilla kielillä, niin voisi lisätä seuraavanlaisesti pali.js
-tiedostoon:
= {
paliTranslations en: {
labelText: "Give a word:", // label text
}
}
class Pali {
constructor(container, data) {
this.settings = {
lang: "fi", // laguage, fi, en
inputChars: 10, // input alueen leveys
minChars: 0, // sanassa tarvittavien merkkien minimimäärä
labelText: "Anna sana:", // labelin teksti
}if (data) {
const lang = data.params?.lang ?? 'fi';
Object.assign(this.settings, paliTranslations[lang] ?? {}, );
Object.assign(this.settings, data.params);
}
Nythän kääntämisen voi tehdä TIMin komponentin asetuksissa, mutta jos on paljon tekstejä kuten esimerkiksi sortgame.js, niin kaikkia ei viitsi erikseen jokaisessa komponentin esiintymässä kääntää. Silloin minimissään toisen kieliseen versioon riittää:
Tiedostosta checkcards.js voi katsoa ideoita miten tarkistinfunktion käännökset voisi hoitaa.
2. Korttipelit
Tehdään samalla idealla korttipelejä varten ensin
joka hoitaa kaiken yleisen kottipeleihin liittyvän asian. Sitten tehdään esimerkiksi TIMiin hullu.html
:
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<script>
let ver = 1; // Math.random(); // laita tuotannossa vakioksi
document.write('<script src="https://tim.jyu.fi/print/tim/components/cards/cards.js?v=' + ver + '"><\/script>');
document.write('<script src="https://tim.jyu.fi/print/tim/components/cards/hullu.js?v=' + ver + '"><\/script>');
</script>
</head>
<body>
<div class="game"></div>
</body>
</html>
ja vastaava lokaalia kehitystä varten oleva hullu.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hullunpasianssi</title>
<script>
let ver = Math.random(); // laita tuotannossa vakioksi
document.write('<script src="cards.js?v=' + ver + '"><\/script>');
document.write('<script src="hullu.js?v=' + ver + '"><\/script>');
window.jsframedata = { params: {
imagePath: '/common/images/cards',
bgImages: ["back2"],
bgIndex: 0,
loosingText: "Häviö",
showCardsLeft: true,
handDeckText: "Käsi",
newGameText: "Uusi",
} };
document.addEventListener('DOMContentLoaded', () => onInit() );
</script>
</head>
<body>
<div id="game"></div>
</body>
</html>
Itse pelin logiikka ulkoasuineen:
Nyt TIMiin saadaan toteuma pelistä koodilla:
Tuolla COMPS
voi lyhentää TIMin komponentteihin viittaavan hakemiston.
Voit pelata alta peliä. Ainoa mitä voit tehdä on klikata jakopakkaa tai valita uuden pelin. Kun peli päättyy voittoon tai häviöön, tilanne tallentuu.
Korttipeleissä erona on palindromiin on se, että on kortteihin liittyviä "globaaleja" asetuksia ja pelin omia asetuksia. Siksi on tehty oma funktio
if (data) copyParamsValues(data.params, this.settings);
joka kopioi kortteihin kuuluvat asetukset sinne ja peliin kuuluvat omat asetukset tuonne this.settings
-olioon.
Pelien käytöstä lisää dokumentissa korttipelit.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.