Skip to content

Commit 68edb2f

Browse files
authored
Merge pull request #11 from jayfreestone/preserve-original-menu
Retain original menu classes/ attributes
2 parents 8d16708 + 644fbf7 commit 68edb2f

File tree

4 files changed

+125
-16
lines changed

4 files changed

+125
-16
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,15 @@ If your menu is part of an auto-sized flex-child, it will probably need a positi
302302
flex-grow: 1;
303303
}
304304
```
305+
306+
### Navigation event listeners
307+
308+
priorityPlus makes a copy of your menu, rather than reusing the original. Classes and attributes are carried over, but not event listeners. This means that any additional libraries or JavaScript which operate on the menu and its children needs to be run (or re-run) after initialization:
309+
310+
```javascript
311+
priorityPlus(document.querySelector('.js-p-target'));
312+
// .js-p-target is *not* the same element, but has been cloned and replaced
313+
loadLibrary(document.querySelector('.js-p-target'));
314+
```
315+
316+
If your use-case is not covered by this, please raise an issue.

cypress/integration/clonedMenu.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import priorityPlus from '../../src/priorityPlus';
2+
3+
describe('Cloned menu', () => {
4+
it('should preserve the original menu\'s attributes', () => {
5+
const original = document.createRange().createContextualFragment(`
6+
<nav>
7+
<ul class="menu-items test" data-depth="1" data-test>
8+
<li class="menu-items__item menu-items__item_article menu-items__item_active" data-custom>
9+
<a href="#">
10+
Item One
11+
</a>
12+
</li>
13+
</ul>
14+
</nav>
15+
`);
16+
17+
const [menu, items] = getMenuElements(original);
18+
19+
priorityPlus(menu);
20+
21+
const [instanceMenu, instanceItems] = getMenuElements(original);
22+
23+
assertEnhancedContainsAttributes(menu, instanceMenu);
24+
assertEnhancedContainsClasses(menu, instanceMenu);
25+
26+
items.forEach((item, i) => {
27+
assertEnhancedContainsAttributes(item, instanceItems[i]);
28+
assertEnhancedContainsClasses(item, instanceItems[i]);
29+
});
30+
});
31+
});
32+
33+
function getMenuElements(elem: DocumentFragment): [HTMLElement, HTMLLIElement[]] {
34+
const menu = elem.querySelector('ul') as HTMLElement;
35+
const items = Array.from(elem.querySelectorAll('li')) as HTMLLIElement[];
36+
return [menu, items];
37+
}
38+
39+
// Assert that each class on the original element is present on the copy.
40+
function assertEnhancedContainsClasses(original: HTMLElement, enhanced: HTMLElement) {
41+
Array.from(original.classList).forEach(className => {
42+
expect(Array.from(enhanced.classList), 'class').to.contain(className)
43+
});
44+
}
45+
46+
// Assert that every easily comparable attribute (e.g. not class) on the
47+
// original is present on the copy.
48+
function assertEnhancedContainsAttributes(original: HTMLElement, enhanced: HTMLElement) {
49+
const [originalAttrs, enhancedAttrs] = [original, enhanced]
50+
.map(getAttributes)
51+
.map(retainComparable);
52+
const originalDict = Object.fromEntries(originalAttrs);
53+
54+
// Filter out attributes added by priorityPlus.
55+
const preserved = enhancedAttrs.filter(([key]) => key in originalDict);
56+
57+
expect(preserved, 'attributes').to.deep.equal(originalAttrs);
58+
}
59+
60+
// Filter out hard to compare attributes.
61+
function retainComparable(attrList: string[][]) {
62+
return attrList.filter(([name]) => name !== 'class');
63+
}
64+
65+
function getAttributes(elem: HTMLElement) {
66+
return Object.keys(elem.attributes).map(key => {
67+
const attr = elem.attributes[Number(key)] as Attr;
68+
return [attr.name, attr.value];
69+
});
70+
}
71+

src/priorityPlus.ts

+41-15
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ interface ElementRefs {
3838
};
3939
primary: {
4040
[El.Main]: HTMLElement;
41+
[El.PrimaryNavWrapper]: HTMLElement;
4142
[El.PrimaryNav]: HTMLElement;
42-
[El.NavItems]: HTMLElement[];
43+
[El.NavItems]: HTMLLIElement[];
4344
[El.OverflowNav]: HTMLElement;
4445
[El.ToggleBtn]: HTMLElement;
4546
};
@@ -144,21 +145,12 @@ function priorityPlus(targetElem: HTMLElement, userOptions: DeepPartial<Options>
144145
}
145146

146147
/**
147-
* Generates the HTML to use in-place of the user's supplied element.
148+
* Generates the HTML wrapper to use in-place of the user's supplied menu.
148149
*/
149150
function createMarkup(): string {
150151
return `
151152
<div ${dv(El.Main)} class="${cn(El.Main)}">
152-
<div class="${cn(El.PrimaryNavWrapper)}">
153-
<${targetElem.tagName}
154-
${dv(El.PrimaryNav)}
155-
class="${cn(El.PrimaryNav)}"
156-
>
157-
${Array.from(targetElem.children).map((elem: Element) => (
158-
`<li ${dv(El.NavItems)} class="${cn(El.NavItems)}">${elem.innerHTML}</li>`
159-
)).join('')}
160-
</${targetElem.tagName}>
161-
</div>
153+
<div class="${cn(El.PrimaryNavWrapper)}" ${dv(El.PrimaryNavWrapper)}></div>
162154
<button
163155
${dv(El.ToggleBtn)}
164156
class="${cn(El.ToggleBtn)}"
@@ -174,6 +166,36 @@ function priorityPlus(targetElem: HTMLElement, userOptions: DeepPartial<Options>
174166
`;
175167
}
176168

169+
/**
170+
* Clones the target menu and enhances it with additional properties, such
171+
* as data attributes and classes.
172+
*/
173+
function cloneNav(elem: HTMLElement): HTMLElement {
174+
const targetClone = elem.cloneNode(true) as HTMLElement;
175+
enhanceOriginalMenu(targetClone);
176+
177+
const navItems = Array.from(targetClone.children) as HTMLLIElement[];
178+
navItems.forEach(enhanceOriginalNavItem)
179+
180+
return targetClone;
181+
}
182+
183+
/**
184+
* Enhance the original list element with classes/attributes.
185+
*/
186+
function enhanceOriginalMenu(elem: HTMLElement) {
187+
elem.classList.add(...classNames[El.PrimaryNav])
188+
elem.setAttribute(dv(El.PrimaryNav), '');
189+
}
190+
191+
/**
192+
* Enhance an original menu list-item with classes/attributes.
193+
*/
194+
function enhanceOriginalNavItem(elem: HTMLLIElement) {
195+
elem.classList.add(...classNames[El.NavItems])
196+
elem.setAttribute(dv(El.NavItems), '');
197+
}
198+
177199
/**
178200
* Replaces the navigation with the two clones and populates the 'el' object.
179201
*/
@@ -186,22 +208,26 @@ function priorityPlus(targetElem: HTMLElement, userOptions: DeepPartial<Options>
186208
el[El.Container] = container;
187209

188210
const original = document.createRange().createContextualFragment(markup);
211+
212+
// Setup the wrapper and clone/enhance the original menu.
213+
el.primary[El.PrimaryNavWrapper] = original.querySelector(`[${dv(El.PrimaryNavWrapper)}]`) as HTMLElement;
214+
el.primary[El.PrimaryNavWrapper].appendChild(cloneNav(targetElem))
215+
189216
const cloned = original.cloneNode(true) as Element;
190217

218+
// Establish references. By this point the menu is fully built.
191219
el.primary[El.Main] = original.querySelector(`[${dv(El.Main)}]`) as HTMLElement;
192220
el.primary[El.PrimaryNav] = original.querySelector(`[${dv(El.PrimaryNav)}]`) as HTMLElement;
193-
el.primary[El.NavItems] = Array.from(original.querySelectorAll(`[${dv(El.NavItems)}]`)) as HTMLElement[];
221+
el.primary[El.NavItems] = Array.from(original.querySelectorAll(`[${dv(El.NavItems)}]`)) as HTMLLIElement[];
194222
el.primary[El.OverflowNav] = original.querySelector(`[${dv(El.OverflowNav)}]`) as HTMLElement;
195223
el.primary[El.ToggleBtn] = original.querySelector(`[${dv(El.ToggleBtn)}]`) as HTMLElement;
196224

197225
el.clone[El.Main] = cloned.querySelector(`[${dv(El.Main)}]`) as HTMLElement;
198226
el.clone[El.NavItems] = Array.from(cloned.querySelectorAll(`[${dv(El.NavItems)}]`)) as HTMLElement[];
199227
el.clone[El.ToggleBtn] = cloned.querySelector(`[${dv(El.ToggleBtn)}]`) as HTMLElement;
200-
201228
el.clone[El.Main].setAttribute('aria-hidden', 'true');
202229
el.clone[El.Main].setAttribute('data-clone', 'true');
203230
el.clone[El.Main].classList.add(`${classNames[El.Main][0]}--clone`);
204-
205231
el.clone[El.Main].classList.add(`${classNames[El.Main][0]}--${StateModifiers.ButtonVisible}`);
206232

207233
container.appendChild(original);

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"target": "es6",
99
"lib": [
1010
"dom",
11-
"es6"
11+
"es2017"
1212
]
1313
},
1414
"compileOnSave": false,

0 commit comments

Comments
 (0)