# timOhjeet

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:

  1. tässä dokumentissa esiteltävä csPluginin alikieli JSFrame
  2. vähän vastaava csPluginin JS-alikieli.
  3. edellisten sovelluksena omaksi csPluginin alle julkaistavaksi alikieleksi. Katso esimerkiksi DFA tai vars.js
  4. kokonaan uuden pluginin tekeminen
  5. 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.

Tässä dokumentissa käytetään csPluginin jsframe-kielityyppiä. Sen avulla periaatteessa mikä tahansa html-pohjainen komponentti on muutettavissa TIM-komponentiksi.

  • jsframe-kielityypistä voit lukea lisää: jsframe -dokumentista
  • csPlugin käytöstä voit lukea lisää csPlugin-dokumentista

1. Palindromi

Tässä dokumentissa käytetään esimerkkinä yksinkertaista palindromitehtävää, jossa pitää osata kirjoittaa vähintään vaaditun mittainen palindromi.

# pali4html

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

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.

  1. tee jollakin tekstieditorilla tiedosto pali.html jonka sisältö on

    <!DOCTYPE html>
    <html lang="fi">
    <head>
        <meta charset="UTF-8">
        <title>Palindromi</title>
    </head>
    <body>
    <div>
        <label for="word">Anna sana:</label>
        <input type="text" id="word" name="word">
    </div>
    </body>
    </html>
  2. Avaa tiedosto jollakin selaimella. Jos selaimesta ei helpolla löydy Avaa tiedosto, niin rahaa tiedosto tiedostoselaimella (Explorer, Finder, jne.) selaimen ikkunaan.

  3. Kokeile toimintaa.

  4. Muokaa tiedostoa tekstieditorissa.

  5. 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):

  1. luo uusi projekti

  2. 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
  3. tee aluksi tiedosto pali.html

    <!DOCTYPE html>
    <html lang="fi">
    <head>
        <meta charset="UTF-8">
        <title>Palindromi</title>
    </head>
    <body>
    <div>
        <label for="word">Anna sana:</label>
        <input type="text" id="word" name="word">
    </div>
    </body>
    </html>
  4. 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.

# timuopotus

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:

``` {plugin="csPlugin" #html1}
type: jsframe
height: 60
fullhtml: |!!
<!DOCTYPE html>
<html lang="fi">
<head>
    <meta charset="UTF-8">
    <title>Palindromi</title>
</head>
<body>
<div>
    <label for="word">Anna sana:</label>
    <input type="text" id="word" name="word">
</div>
</body>
</html>
!!

Nyt tämä näkyy sinulla TIM-sivulla seuraavasti:

# html1

Komponentin esityksen oletusleveys on 800px. Jos haluat komponentin keskitetyksi, niin lisää height attribuutin jälkeen:

width: 300
# html2

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.

  1. Tee jonnekin TIMissä tiedosto pali.html. Aluksi voit tehdä sen vaikka samaan hakemistoon kuin tuon kokeiludokumentin pali.

  2. Myöhemmin jos osoittautuu, että olet tekemässä oikein yleishyödyllistä komponenttia, kannattaa komponentin tiedostot tallentaa hierarkiaan


    Silloin voit ylläpidolta pyytää itsellesi oman hakemiston tuonne ja sinne kirjoitusoikeudet. Tässä esimerkki tallennetaan nyt


    Huomaa että vaikka esimerkeissä on pali1 jne. sen takia jotta välivaiheet jäävät hyvin talteen, voit omassa esimerkissä käyttää koko ajan pali.html.

  3. Anna dokumentille anonymous-lukuoikeudet.

  4. Lisää dokumentin pali.html asetuksiin (Ratas + Edit settings) rivi:

    textplain: true

    Eli nyt asetukset pitäisi näyttää seuraavalta:

    ``` {settings=""}
    auto_number_headings: 0
    textplain: true
    ```

    Tämän rivin ansiosta sisältö saadaan "raakana", kun URL-osoitteessa vaihdetaan view tilalle print:


  5. Kopioi html-osuuden sisältö dokumentin uudeksi (ainoaksi) lohkoksi.

  6. Sitten lisää kokeiludokumenttiisi pali uusi komponentti joka käyttää tuota html-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 laittanut pali.html muualle kuin oman hakmeistosi juureen. Osoitteessa voi toki olla myös koko polku kuten tuon aikaisemman print-linkin tapauksessa. Tai myös

Nyt tuo komponentti pitäisi näkyä TIMissä:

# pali1

Vastaavasti jos osoitteena on annettu

fullhtmlurl: https://raw.githubusercontent.com/vesal/cards/refs/heads/main/pali1.html
# pali1git

Vastaavasti jos osoite haluttaisiin JY:n gitlabista, olisi se muotoa:

fullhtmlurl: https://gitlab.jyu.fi/tie/ohj2/esimerkit/cards/-/raw/main/pali1.html
# pali1gitlab

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");
input.addEventListener('input', () => this.checkPalindrome());

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]) {
              isPalindrome = false;
              break;
          }
      }
      if (isPalindrome) {
          icon.textContent = '✔';
          icon.style.color = 'green';
      } else {
          icon.textContent = '✘';
          icon.style.color = 'red';
      }
}
</script>
</body>
</html>

Toiminnallisuus on samanlainen lokaalisti kokeiltuna ja TIMissä:

# pali2html

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");
        label.textContent = s.labelText.replace('${count}', s.minChars);

        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]) {
                isPalindrome = false;
                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ä.

# pali3html

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");
        label.textContent = s.labelText.replace('${count}', s.minChars);

        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]) {
                isPalindrome = false;
                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.

        if (data) Object.assign(this.settings, data.params);

TIMissä voidaan nyt laittaa (muuta omaa pali-tiedostosi komponentin kutsua ja muista laittaa oma URL-polkusi):

``` {plugin="csPlugin" #pali5}
type: js
jsparams: 
  inputChars: 8
  minChars: 6
  labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
width: 700
height: 60
fullhtmlurl: /print/tim/components/pali/pali.html
```

Nyt komponentti näkyy TIMIssä:

# pali4html

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:

            inputChars: 10, // input alueen leveys
            minChars: 0, // sanassa tarvittavien merkkien minimimäärä
            labelText: "Anna sana:", // labelin teksti

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:

{'c': { pluginin_data }  }

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:

{'c': { 'text': 'abba' } } 

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");
        label.textContent = s.labelText.replace('${count}', s.minChars);

        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]) {
                isPalindrome = false;
                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.

``` {plugin="csPlugin" #pali5}
type: jsframe
jsparams: 
  inputChars: 8
  minChars: 6
  labelText: 'Anna palindromi, jossa vähintään ${count} kirjainta:'
height: 60
width: 700
saveButton: "Tallenna"
fullhtmlurl: /print/tim/components/pali/pali.html
```
# pali5html

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:

# pali6html

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];
    data.web.console = "";
    if (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;
    data.web.console += " Pisteitä: " + data.points;
    return 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:

``` {plugin="csPlugin" #pali5check}
type: jsframe
width: 700
height: 60
saveButton: "Tallenna"
fullhtmlurl: /print/tim/components/pali/pali.html
postlibraries: 
  - /print/tim/components/pali/palicheck.js
postprogram: return checkPali(data);
```

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:

# pali5deco

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.

Tiedosto pali.js:

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.

# pali5js

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

  1. + uuden lisäämiseksi
  2. JavaScript debug
  3. Name: pali.html
  4. URL: etsitään kansion kuvalla pali.html, tulee jotakintyyliin: http://localhost:63342/kortit/pali.html
  5. Ruksitaan Ensure breakpoints are detected...
  6. `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ää:

``` {plugin="csPlugin" #pali5en}
type: jsframe
jsparams: 
  lang:en
fullhtmlurl: COMPS/pali/pali.html
height: 40
```
# pali5en

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:

``` {plugin="csPlugin" #hullu1 .sortgame}
type: jsframe
jsparams:
    bgIndex: 1
    showCardsLeft: true
    loosingText: "Olitpa huono!"

width: 500
height: 250
fullhtmlurl: COMPS/cards/hullu.html
```

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.

# hullu1

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.