Dans l'article précédent, j'ai ajouté quelques animations CSS simples pour montrer comment fonctionne la séparation de mes assets. Maintenant que tout cela est en place, plongeons un peu plus dans le JavaScript et créons une animation de machine à écrire encore meilleure en fonction de vos données !


Le HTML (à l'intérieur de Grav markdown)

Pour que nos CSS et JavaScript interagissent avec cette page de blog, nous devons mettre en place certains éléments HTML, comme j'utilise l'éditeur de contenu Grav, je dois saisir certains éléments en particulier.

<label for="typewriter-input-data">Input</label>
<textarea id='typewriter-input-data'>Default input data</textarea>

<label for="delay-input">Delay <small>(ms)</small></label>
<input type="number" id="delay-input" name="speed" value="50"/>

<br>
<button class="type-btn" id="type-btn">Type !</button>
<button class="type-btn" id="cancel-btn" disabled>Cancel</button>
<ul id="typewriter-errors" class="errors"></ul>
<div id="typewriter-errors" class="banner"></div>

J'ai maintenant :

  • Une zone de texte pour la saisie
  • Une entrée numérique pour le délai
  • Un bouton de saisie pour déclencher le JavaScript
  • Un bouton d'annulation pour arrêter l'écriture
  • Une div de sortie

TypeScript

D'abord, je vais chercher tous les éléments dont j'ai besoin :

export default class Typewriter {
    private typeWriterInput: HTMLTextAreaElement
    private typeWriterOutput: HTMLElement
    private typeButton: HTMLButtonElement;
    private delayInput: HTMLInputElement;
    private errorElement: HTMLUListElement;
    private cancelButton: HTMLButtonElement;

    constructor() {
        this.typeWriterInput = document.getElementById('typewriter-input-data') as HTMLTextAreaElement
        this.typeWriterOutput = document.getElementById('typewriter-output') as HTMLElement
        this.typeButton = document.getElementById('type-btn') as HTMLButtonElement
        this.cancelButton = document.getElementById('cancel-btn') as HTMLButtonElement
        this.delayInput = document.getElementById('delay-input') as HTMLInputElement
        this.errorElement = document.getElementById('typewriter-errors') as HTMLUListElement
    }
}

Ensuite, nous devons faire en sorte que quelque chose se passe dès que le bouton Type ! est cliqué, dans le constructeur j'ai ajouté l'écouteur d'événement qui appellera une autre fonction de ma classe, typeEvent :

export default class Typewriter {
    constructor() {
//...
        this.typeButton.addEventListener('click', () => {
            this.typeEvent()
        })
//...
    }

    typeEvent() {
        this.resetTypewriter()
        let delay = <any>this.delayInput.value as number;
        let errors: string[] = [];
        if (delay < 10) {
            errors.push("Delay has to be 10 or more")
        }

        const text = this.typeWriterInput.value
        if(text.length < 4) {
            errors.push("Add more than 3 characters to the text input")
        }

       if (errors.length !== 0) {
            this.errorElement.classList.add('error')
            this.errorElement.innerHTML = '';
            let errorList = document.createElement('ul')
            this.errorElement.appendChild(errorList);
            errors.forEach((error: string) => {
                let errorSpan = document.createElement('li')
                errorSpan.innerHTML = error;
                errorList.appendChild(errorSpan);
            })
            return;
        }

        this.typeWriterOutput.innerHTML = '';
        this.disableTypewriter();
        this.JSTypewriter(text, delay);
    }

    //...

    enableTypewriter() 
    {
        this.typeButton.disabled = false
        this.typeWriterInput.disabled = false
        this.cancelButton.disabled = true
    }

    disableTypewriter() {
        this.typeButton.disabled = true
        this.cancelButton.disabled = false
        this.typeWriterInput.disabled = true
    }

    resetTypewriter() {
        this.errorElement.classList.remove('error')
        this.typeWriterOutput.innerHTML = ''
        this.typeWriterOutput.classList.remove('typewriter-output-blinker')
    }
}

Il utilisera la fonction resetTypewriter pour s'assurer que tout est réinitialisé à chaque nouveau clic. Il récupérera les valeurs de la zone de texte et de l'entrée numérique, et vérifiera si les valeurs sont correctes. Si l'une d'entre elles est fausse, une erreur sera ajoutée, les erreurs seront affichées et aucune saisie ne sera effectuée.

Si tout est correct, le texte précédemment tapé sera supprimé et les entrées ainsi que le bouton de saisie seront désactivés tandis que le bouton d'annulation deviendra disponible.

Ensuite, le JSTypewriter sera déclenché, c'est lui qui est responsable de la saisie d'un caractère à la fois.

setTimeout

Afin d'obtenir ce délai entre les caractères ajoutés, j'ai utilisé la fonction setTimeout. Il m'a fallu un peu de temps pour m'y habituer dans cette situation. En regardant la documentation W3schools, ils ajoutent le caractère suivant chaque fois que la fonction est appelée après le setTimeout et augmentent une variable de compteur (i).

Ici, dans ma classe, je ne voulais pas ajouter une propriété juste pour cela. C'est pourquoi mon JSTypewriter a tout ce dont il a besoin dans les paramètres.

Cependant, lorsque j'ai essayé d'appeler la classe en passant des paramètres dans la fonction setTimeout, elle s'est plantée. Il s'avère que pour être capable de passer les paramètres, vous devez créer un clone de la fonction et lui bind les paramètres.

Cela fonctionne maintenant, et j'ai ajouté l'instruction if pour réactiver la machine à écrire une fois le texte terminé.

Il y avait un autre petit problème, les sauts de ligne ne voulaient pas être ajoutés. C'est parce que l'ajout au innerHTML d'un élément ne peut ajouter que des éléments HTML, donc des caractères ou des éléments uniques. C'est pourquoi j'ai ajouté un char = char.replace(/\r?\n/g, '<br />'); pour m'assurer qu'un saut de ligne serait transformé en un élément HTML <br />.

Je voulais être en mesure d'arrêter la saisie pendant qu'elle se produisait, donc pour cela j'ai ajouté une propriété NodeJS.Timeout à ma classe, voici le code :

export default class Typewriter {
    //...
    private typewriterTimer?: NodeJS.Timeout;

    JSTypewriter(text: string, delay: number, count: number = 0) {
        if (text.length > count) {
            this.typeWriterOutput.classList.add('typewriter-output-blinker')
            let char = text.charAt(count);
            char = char.replace(/\r?\n/g, '<br />');
            this.typeWriterOutput.innerHTML += char
            count++
            this.typewriterTimer = setTimeout(this.JSTypewriter.bind(this, text, delay, count), delay)
        } else {
            this.enableTypewriter()
        }
    }
}

Maintenant pour ajouter la possibilité d'arrêter la saisie, j'ai ajouté un écouteur au bouton Cancel et utilisé le paramètre typewriterTimer pour l'arrêter.

export default class Typewriter {
    constructor() {
        //...
        this.cancelButton.addEventListener('click', (event) => {
            if(this.typewriterTimer) {
                clearTimeout(this.typewriterTimer)
            }
            this.resetTypewriter()
            this.enableTypewriter()
        })
    }
}

Cette fonction clearTimeout va arrêter le setTimeout et ensuite nous allons réinitialiser la machine à écrire.

Un peu de style, SCSS

Maintenant, pour rendre la chose un peu plus jolie, il faut ajouter du SCSS :

@keyframes blink-char {
  from, to { color: transparent }
  50% { color: inherit}
}

.retype-btn, .type-btn {
  border-radius: 5px;
  padding: 0.6rem 1.2rem;
  cursor: pointer;
  background-color: $darkmode-bg-color;
  color: $primary-color;
  margin-bottom: 2rem;
  transition: color 200ms ease-in-out,
  background-color 200ms ease-out;
;
  &:hover {
    color: $darkmode-bg-color;
    background-color: $primary-color;
  }
}
.animations-post {
  textarea, input {
    margin-top: 0.5rem;
    &:focus, &:focus-visible {
      box-shadow: 0 0 5px $primary-color-dark;
      border: 1px $bg-color-extra-dark solid;
    }
  }

  label {
    margin: 1rem;
  }
  button {
    &:disabled {
      background-color: $bg-color-dark;
      color: $primary-color-extra-dark;
    }
  }
}

.typewriter-output-blinker {
  &::after {
    color: rgba(0,0,0,0.8);
    content: '_';
    font-weight: 1000;
    margin-left: 0.2rem;
    animation: blink-char 0.75s step-end infinite;
  }
}

.typewriter-output {
  border-bottom: 2px solid $bg-color-extra-dark;
}

La plus grande partie est assez simple et utilise mon fichier _variables.scss pour obtenir les couleurs correctes. La partie la plus importante est le caractère clignotant à la fin du texte de sortie pendant qu'il est tapé. C'est là que le .typewriter-output-blinker::after entre en jeu. Le contenu est défini comme vide mais, il a un border-left et une animation pour cette propriété de bordure.

Comme je ne voulais pas que cela clignote pendant que rien n'est tapé, le fichier TypeScript ajoute et supprime cette classe quand c'est nécessaire.

Enfin, je voulais que cela ressemble à du vieux papier, j'ai donc utilisé le CSS que j'ai trouvé dans CodePen et, j'ai ajouté une belle police de caractères que j'ai trouvée sur dafont. J'ai utilisé cloudConvert pour obtenir une police de type woff2 à partir du fichier TFF original.

Pour ajouter cette police, j'ai ajouté un SCSS Mixin.

//_fonts.scss
@mixin font($font-family, $font-file) {
    @font-face {
        font-family: $font-family;
        src: url('../fonts/' + $font-file + '.woff2') format('woff2'),
        url('../fonts/' + $font-file + '.ttf') format('truetype');
        font-weight: normal;
        font-style: normal;
    }
}

Dans le fichier SCSS de ma machine à écrire, j'ai ajouté la police :

@use 'fonts' as *;

@include font('ATWriter', 'atwriter/atwriter');

//...

.typewriter-output {
  font-family: ATWriter, sans-serif;
}

La dernière chose à faire était de s'assurer que les fichiers SCSS puissent accéder aux polices. Pour ce faire, j'ai copié mes polices téléchargées dans le répertoire {{themeName}}/fonts/atwriter. Ce répertoire contient maintenant :

  • atwriter.tff
  • atwriter.woff2

Pour les copier dans {themeName}}/build/fonts/atwriter j'ai ajouté une autre configuration webpack :

const CopyFiles = Encore
        .setOutputPath(path.join(BUILD_PATH_BASE))
        .setPublicPath(path.join('/', BUILD_PATH_BASE))
        .disableSingleRuntimeChunk()
        .copyFiles({
                from: path.join(PATH, 'fonts'),
                to: 'fonts/[path][name].[ext]',
        })
        .getWebpackConfig()

CopyFiles.name = 'CopyFiles'

Encore.reset();

//...
module.exports = [CopyFiles, baseEncoreConfiguration, animationsConfiguration];