Comme j'ai récemment ajouté [TypeScript et WebPack encore à Grav] (/blog/adding-webpack-encore-and-typescript-to-grav), j'ai voulu trouver un moyen d'ajouter CSS et JavaScript uniquement à certaines pages.

Générer des sorties multiples pour les assets

Jusqu'à présent, j'ai généré une page assets.html qui contenait les liens vers les fichiers JavaScript et CSS générés par WebPack :

<script defer src="/user/themes/lingonberry-custom/build/app/app.js"></script>
<link href="/user/themes/lingonberry-custom/build/app/app.css" rel="stylesheet">

Pour générer différents fichiers, j'ai modifié le fichier webpack.config.js. J'ai trouvé la plupart des informations dans la [documentation Symfony] (https://symfony.com/doc/current/frontend/encore/advanced-config.html).

Cependant, comme je n'aime pas trop me répéter, j'ai créé une fonction :

function generateBaseConfig(name, buildPath, inputFile, outputFile, cleanOutput = true) {
        let itemBuildPath = path.join(BUILD_PATH_BASE, name);
        if (buildPath) {
                itemBuildPath = path.join(BUILD_PATH_BASE, buildPath);
        }

        if (!inputFile) {
                inputFile = `${name}.ts`
        }

        if (!outputFile) {
                outputFile = 'assets.html'
        }

        let config = Encore
                // directory where compiled assets will be stored
                .setOutputPath(itemBuildPath)
                // public path used by the web server to access the output path
                .setPublicPath(path.join('/', itemBuildPath))

                .addEntry(name, `./${path.join(PATH, 'js', inputFile)}`)

                // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
                //.splitEntryChunks()

                // will require an extra script tag for runtime.js
                // but, you probably want this, unless you're building a single-page app
                //.enableSingleRuntimeChunk()
                .disableSingleRuntimeChunk()

                .enableSourceMaps(!Encore.isProduction())
                // enables hashed filenames (e.g. app.abc123.css)
                .enableVersioning(Encore.isProduction())

                .addPlugin(new CompressionPlugin())
                // .addPlugin(new MiniCssExtractPlugin())

                .addPlugin(new HtmlWebpackPlugin({
                        inject: false,
                        filename: outputFile,
                        publicPath: path.join('/', itemBuildPath),
                        scriptLoading: 'defer',
                        templateContent: ({htmlWebpackPlugin}) => `
        ${htmlWebpackPlugin.tags.headTags}
        `
                }))

                // enables @babel/preset-env polyfills
                .configureBabelPresetEnv((config) => {
                        config.useBuiltIns = 'usage';
                        config.corejs = 3;
                })

                .enableSassLoader()

                .enableTypeScriptLoader();

        if (cleanOutput) {
                config.cleanupOutputBeforeBuild()
        }
        return config.getWebpackConfig()
}

Donc maintenant pour ajouter plusieurs entrées/sorties je n'ai plus qu'à ajouter quelques lignes :

const PATH = path.join('user', 'themes', 'lingonberry-custom')
const BUILD_PATH_BASE = path.join(PATH, 'build')

const baseEncoreConfiguration = generateBaseConfig('app');
baseEncoreConfiguration.name = 'baseEncoreConfiguration'
Encore.reset();

const animationsConfiguration = generateBaseConfig('animations');
animationsConfiguration.name = 'animationsConfiguration'

module.exports = [baseEncoreConfiguration, animationsConfiguration];

Si vous ajoutez seulement un nom lors de l'appel de la fonction generateBaseConfig, l'entrée sera de {{themeName}}/js/{{name}}.ts et la sortie sera {{themeName}}/build/{{name}}/assets.html. donc dans l'exemple précédent j'avais les fichiers suivants :

user/themes/lingonberry-custom
└── js
   ├── typewriter.ts
   └──  app.ts

Ce qui a généré la sortie suivante :

user/themes/lingonberry-custom
├── build
    ├── animations
    │   ├── animations.css
    │   ├── animations.js
    │   ├── animations.js.gz
    │   ├── assets.html
    │   ├── assets.html.gz
    │   ├── entrypoints.json
    │   ├── manifest.json
    │   └── manifest.json.gz
    ├── app
    │   ├── app.css
    │   ├── app.css.gz
    │   ├── app.js
    │   ├── app.js.gz
    │   ├── assets.html
    │   ├── assets.html.gz
    │   ├── entrypoints.json
    │   ├── fonts
    │   ├── images
    │   ├── manifest.json
    │   └── manifest.json.gz
    ├── entrypoints.json
    ├── manifest.json
    └── manifest.json.gz

Chaque entrée de fichier a un répertoire de sortie séparé. Sans cela la fonction cleanupOutputBeforeBuild d'Encore viderait toujours le répertoire principal de construction. Comme les constructions se déroulent en parallèle, il n'en resterait toujours qu'un à la fin, les autres étant supprimés.

Ajout des fichiers générés aux modèles twig

Maintenant qu'ils sont générés, les fichiers de sortie doivent être inclus dans nos modèles twig. Pour les assets principaux, j'ai simplement ajouté {% include 'app/assets.html' %} dans la balise header du fichier twig de base de mes templates.

J'ai également voulu simplifier l'ajout de fichiers à partir du backend pour une page de blog spécifique. Pour ce faire, j'ai [généré un nouveau plugin grav] (https://learn.getgrav.org/17/plugins/plugin-tutorial) Header Include.

Pour ajouter des options supplémentaires à une page, j'ai ajouté un blueprint plugins/header-include/blueprints/header-include.yaml :

form:
  fields:
    tabs:
      fields:
        options:
          type: tab

          fields:
            header-include:
              type: section
              title: HEADER_INCLUDE.PLUGIN.NAME
              underline: true

              fields:
                header.header-include.active:
                  type: toggle
                  toggleable: true
                  label: HEADER_INCLUDE.PLUGIN.ACTIVE
                  help: HEADER_INCLUDE.PLUGIN.ACTIVE_HELP
                  highlight: 1
                  # config-default@: plugins.header-include.active
                  options:
                    1: PLUGIN_ADMIN.YES
                    0: PLUGIN_ADMIN.NO
                  validate:
                    type: bool

                header.include:
                 type: multilevel
                 label: HEADER_INCLUDE.PLUGIN.ITEMS
                 value_only: true
                 validate:
                   type: array

Il contient juste un bouton on-off, et une liste d'éléments que nous voulons inclure dans notre en-tête. Pour inclure les nouveaux fichiers d'animation dans les options de cette page, j'ai défini la configuration suivante : Screenshot%20from%202021-07-03%2017-39-23

Maintenant pour s'assurer que tous les fichiers définis dans les options sont effectivement inclus, j'ai ajouté un modèle {{themeName}}/partials/includes.html.twig :

{% for filename in page.header.include %}
    {% include filename %}
{% endof %}

et j'ai inclus ce fichier dans le fichier de base de mon modèle.

Maintenant, lorsque j'ajoute quelques feuilles de style en cascade (CSS) au fichier animation.scss (que j'ai volé à CSS-Tricks), un peu de script, et je peux obtenir ces jolis effets, mais uniquement dans les pages qui en ont besoin.

@use 'variables' as *;
.typewriter {
  max-width: fit-content;
  display: block;
  text-align: left;
  overflow: hidden; /* Ensures the content is not revealed until the animation */
  border-right: .15em solid $primary-color;/* The typwriter cursor */
  white-space: nowrap; /* Keeps the content on a single line */
  margin: 0 auto; /* Gives that scrolling effect as the typing happens */
  letter-spacing: .15em; /* Adjust as needed */

  animation:
          typing 3.5s steps(38, end),
          blink-caret .75s step-end infinite;
  &.slow {
    animation:
            typing 7s steps(52, end),
            blink-caret 1.5s step-end infinite;
  }
}

/* The typing effect */
@keyframes typing {
  from { width: 0 }
  to { width: 100% }
}

/* The typewriter cursor effect */
@keyframes blink-caret {
  from, to { border-color: transparent }
  50% { border-color: $primary-color}
}

.retype-btn {
  border-radius: 5px;
  padding: 0.6rem 1.2rem;
  cursor: pointer;
  background-color: $darkmode-bg-color;
  color: $primary-color;
  transition: color 200ms ease-in-out,
  background-color 200ms ease-out;
;
  &:hover {
    color: $darkmode-bg-color;
    background-color: $primary-color;
  }
}
//components/typewriter.ts
export default class Typewriter {

    retypeButtons: HTMLButtonElement[]
    typewriters: HTMLElement[]

    constructor() {
        this.retypeButtons = <HTMLButtonElement[]><any>document.getElementsByClassName('retype-btn');
        this.typewriters = <HTMLElement[]><any>document.getElementsByClassName('typewriter');
        this.retypeButtons.forEach((item) => {
            item.addEventListener('click', (event) => {
                this.typewriters.forEach((item) => {
                    const clone = item.cloneNode(true)
                    item.parentNode?.replaceChild(clone, item)
                })
            })
        })
    }
}
//typewriter.ts
import '../css/animations.scss'

import Typewriter from './components/typewriter'

document.addEventListener("DOMContentLoaded", function () {
    new Typewriter();
});

Ici, dans l'éditeur markdown de Grav admin pour ma page de blog, j'ajoute

<span class="typewriter">I'm a Typewriter !!</span>
<span class="typewriter slow">I'm a sloooow Typewriter !!</span>
<button id="retype-btn" class="retype-btn">Retype</button>

I'm a Typewriter !! I'm a sloooow Typewriter !!

Les CSS et JavaScript sont maintenant appliqués mais seulement à cette page :) 📝 😃