Skip to content

Commit

Permalink
Merge pull request #55 from swup/feat/attributes
Browse files Browse the repository at this point in the history
Update additional head attributes
  • Loading branch information
daun authored Oct 19, 2024
2 parents 11c58fe + bc4b46f commit f61c824
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 69 deletions.
16 changes: 10 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Handler } from 'swup';
import Plugin from '@swup/plugin';

import mergeHeadContents from './mergeHeadContents.js';
import updateLangAttribute from './updateLangAttribute.js';
import updateAttributes from './updateAttributes.js';
import waitForAssets from './waitForAssets.js';

type Options = {
Expand All @@ -12,6 +12,8 @@ type Options = {
persistTags: boolean | string | ((el: Element) => boolean);
/** Delay the transition to the new page until all newly added assets have finished loading. Default: `false` */
awaitAssets: boolean;
/** Additional attributes of the head element to update. Default: ['lang', 'dir']. */
attributes: (string | RegExp)[];
/** How long to wait for assets before continuing anyway. Only applies if `awaitAssets` is enabled. Default: `3000` */
timeout: number;
};
Expand All @@ -25,6 +27,7 @@ export default class SwupHeadPlugin extends Plugin {
persistTags: false,
persistAssets: false,
awaitAssets: false,
attributes: ['lang', 'dir'],
timeout: 3000
};
options: Options;
Expand All @@ -45,6 +48,8 @@ export default class SwupHeadPlugin extends Plugin {
}

updateHead: Handler<'content:replace'> = async (visit, { page: { html } }) => {
const { awaitAssets, attributes, timeout } = this.options;

const newDocument = visit.to.document!;

const { removed, added } = mergeHeadContents(document.head, newDocument.head, {
Expand All @@ -53,13 +58,12 @@ export default class SwupHeadPlugin extends Plugin {

this.swup.log(`Removed ${removed.length} / added ${added.length} tags in head`);

const lang = updateLangAttribute(document.documentElement, newDocument.documentElement);
if (lang) {
this.swup.log(`Updated lang attribute: ${lang}`);
if (attributes?.length) {
updateAttributes(document.documentElement, newDocument.documentElement, attributes);
}

if (this.options.awaitAssets) {
const assetLoadPromises = waitForAssets(added, this.options.timeout);
if (awaitAssets) {
const assetLoadPromises = waitForAssets(added, timeout);
if (assetLoadPromises.length) {
this.swup.log(`Waiting for ${assetLoadPromises.length} assets to load`);
await Promise.all(assetLoadPromises);
Expand Down
29 changes: 29 additions & 0 deletions src/updateAttributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default function updateAttributes(
target: Element,
source: Element,
filters: (string | RegExp)[] = []
): void {
const keep = new Set<string>();

for (const { name, value } of getAttributes(source, filters)) {
target.setAttribute(name, value);
keep.add(name);
}

for (const { name } of getAttributes(target, filters)) {
if (!keep.has(name)) {
target.removeAttribute(name);
}
}
}

function getAttributes(el: Element, filters: (string | RegExp)[] = []): Attr[] {
const all = Array.from(el.attributes);
if (!filters.length) return all;

return all.filter(({ name }) =>
filters.some((pattern) =>
pattern instanceof RegExp ? pattern.test(name) : name === pattern
)
);
}
11 changes: 0 additions & 11 deletions src/updateLangAttribute.ts

This file was deleted.

14 changes: 8 additions & 6 deletions tests/unit/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Swup, { Visit } from 'swup';
import SwupHeadPlugin from '../../src/index.js';

vitest.mock('../../src/mergeHeadContents.js');
vitest.mock('../../src/updateLangAttribute.js');
vitest.mock('../../src/updateAttributes.js');
vitest.mock('../../src/waitForAssets.js');

const page = { page: { html: '', url: '/' } };
Expand Down Expand Up @@ -63,15 +63,17 @@ describe('SwupHeadPlugin', () => {
});

it('updates lang attr from content:replace hook handler', async () => {
const updateLangAttribute = await import('../../src/updateLangAttribute.js');
updateLangAttribute.default = vitest.fn().mockImplementation(() => 'fr');
const updateAttributes = await import('../../src/updateAttributes.js');
updateAttributes.default = vitest.fn().mockImplementation(() => 'fr');

swup.use(plugin);
plugin.options.attributes = ['lang', /^data-/];
plugin.updateHead(visit, page);
expect(updateLangAttribute.default).toHaveBeenCalledOnce();
expect(updateLangAttribute.default).toHaveBeenCalledWith(
expect(updateAttributes.default).toHaveBeenCalledOnce();
expect(updateAttributes.default).toHaveBeenCalledWith(
document.documentElement,
visit.to.document!.documentElement
visit.to.document!.documentElement,
['lang', /^data-/]
);
});

Expand Down
72 changes: 72 additions & 0 deletions tests/unit/updateAttributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest';
import updateAttributes from '../../src/updateAttributes.js';

function createElement(html: string): Element {
const el = document.createElement('div');
el.innerHTML = html;
return el.firstElementChild!;
};

const mergeAttributes = (currentEl: string, incomingEl: string, ...args): string => {
const current = createElement(currentEl);
const incoming = createElement(incomingEl);
updateAttributes(current, incoming, ...args);
return current.outerHTML;
};

describe('updateAttributes', () => {
describe('attributes', () => {
it('adds attributes', () => {
expect(mergeAttributes('<div></div>', '<div a="b"></div>')).toBe('<div a="b"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>')).toBe('<div a="b" b="c"></div>');
});

it('updates attributes', () => {
expect(mergeAttributes('<div a="b" b="c"></div>', '<div a="c" b="c"></div>')).toBe('<div a="c" b="c"></div>');
});

it('removes attributes', () => {
expect(mergeAttributes('<div a="b"></div>', '<div></div>')).toBe('<div></div>');
expect(mergeAttributes('<div a="b" b="c"></div>', '<div a="b"></div>')).toBe('<div a="b"></div>');
});

it('allows filtering attributes', () => {
expect(mergeAttributes('<div></div>', '<div a="b"></div>', [''])).toBe('<div></div>');
expect(mergeAttributes('<div></div>', '<div a="b"></div>', ['b'])).toBe('<div></div>');
expect(mergeAttributes('<div></div>', '<div b="c"></div>', ['a'])).toBe('<div></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>', ['a'])).toBe('<div a="b"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>', ['b'])).toBe('<div b="c"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>', ['a', 'b'])).toBe('<div a="b" b="c"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" abc="d" bcd="e"></div>', [/^ab/])).toBe('<div abc="d"></div>');
});

it('handles boolean attributes', () => {
expect(mergeAttributes('<div disabled></div>', '<div hidden></div>')).toBe('<div hidden=""></div>');
expect(mergeAttributes('<div></div>', '<div disabled></div>')).toBe('<div disabled=""></div>');
});
});

describe('types', () => {
const el = document.createElement('div');

it('returns nothing', () => {
expect(updateAttributes(el, el)).toBeUndefined();
});

it('only accepts elements', () => {
try {
updateAttributes(el, el);
// @ts-expect-error
updateAttributes('', el);
// @ts-expect-error
updateAttributes(el, '');
// @ts-expect-error
updateAttributes(el, 1);
// @ts-expect-error
updateAttributes(el, []);
// @ts-expect-error
updateAttributes(el);
} catch (error) {}
});
});
});
46 changes: 0 additions & 46 deletions tests/unit/updateLangAttribute.test.ts

This file was deleted.

0 comments on commit f61c824

Please sign in to comment.