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
   ├── animations.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 :) 📝 😃