mirror of
https://github.com/iconify/iconify.git
synced 2024-11-09 23:00:56 +00:00
Change behavior of scanDOM in SVG framework when used with custom root node
This commit is contained in:
parent
4207fd254a
commit
d1bd40bcb0
@ -6,7 +6,7 @@ import { addFinder } from '@iconify/iconify/lib/modules/finder';
|
||||
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
|
||||
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
|
||||
import { getStorage, addIconSet } from '@iconify/core/lib/storage';
|
||||
import { setRoot } from '@iconify/iconify/lib/modules/root';
|
||||
import { setRoot, getRootNodes } from '@iconify/iconify/lib/modules/root';
|
||||
import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
|
||||
|
||||
const expect = chai.expect;
|
||||
@ -42,6 +42,9 @@ describe('Scanning DOM', () => {
|
||||
height: 24,
|
||||
});
|
||||
|
||||
// Sanity check before running tests
|
||||
expect(getRootNodes()).to.be.eql([]);
|
||||
|
||||
it('Scan DOM with preloaded icons', () => {
|
||||
const node = getNode('scan-dom');
|
||||
node.innerHTML =
|
||||
@ -56,29 +59,46 @@ describe('Scanning DOM', () => {
|
||||
'</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
// Scan node
|
||||
setRoot(node);
|
||||
scanDOM();
|
||||
|
||||
// Find elements
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
expect(elements.length).to.be.equal(4);
|
||||
|
||||
// Make sure tempoary node was not added as root, but new root node was
|
||||
expect(getRootNodes()).to.be.eql([
|
||||
{
|
||||
node: node,
|
||||
temporary: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Scan DOM with unattached root', () => {
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = '<span class="iconify" data-icon="mdi:home"></span>';
|
||||
|
||||
// Get old root nodes. It should not be empty because of previous test(s)
|
||||
const oldRoot = getRootNodes();
|
||||
|
||||
// Scan node
|
||||
scanDOM(node);
|
||||
|
||||
// Find elements
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
expect(elements.length).to.be.equal(1);
|
||||
|
||||
// Make sure tempoary node was not added as root
|
||||
expect(getRootNodes()).to.be.eql(oldRoot);
|
||||
});
|
||||
|
||||
it('Scan DOM with icon as root', () => {
|
||||
const node = document.createElement('span');
|
||||
node.setAttribute('data-icon', 'mdi:home');
|
||||
|
||||
// Scan node
|
||||
scanDOM(node);
|
||||
|
||||
// Check node
|
||||
|
117
packages/browser-tests/tests/21-observe-dom-test.ts
Normal file
117
packages/browser-tests/tests/21-observe-dom-test.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import mocha from 'mocha';
|
||||
import chai from 'chai';
|
||||
|
||||
import { getNode } from './node';
|
||||
import { addFinder } from '@iconify/iconify/lib/modules/finder';
|
||||
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
|
||||
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
|
||||
import { getStorage, addIconSet } from '@iconify/core/lib/storage';
|
||||
import { setRoot, getRootNodes } from '@iconify/iconify/lib/modules/root';
|
||||
import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
|
||||
import { initObserver } from '@iconify/iconify/lib/modules/observer';
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
// Add finders
|
||||
addFinder(iconifyFinder);
|
||||
addFinder(iconifyIconFinder);
|
||||
|
||||
describe('Observe DOM', () => {
|
||||
// Add mentioned icons to storage
|
||||
const storage = getStorage('', 'mdi');
|
||||
addIconSet(storage, {
|
||||
prefix: 'mdi',
|
||||
icons: {
|
||||
'account-box': {
|
||||
body:
|
||||
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
|
||||
},
|
||||
'account-cash': {
|
||||
body:
|
||||
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
|
||||
},
|
||||
'account': {
|
||||
body:
|
||||
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
|
||||
},
|
||||
'home': {
|
||||
body:
|
||||
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
|
||||
},
|
||||
},
|
||||
width: 24,
|
||||
height: 24,
|
||||
});
|
||||
|
||||
it('Basic test', (done) => {
|
||||
const node = getNode('observe-dom');
|
||||
|
||||
// Set root and init observer
|
||||
setRoot(node);
|
||||
initObserver(scanDOM);
|
||||
|
||||
// Test getRootNodes
|
||||
expect(getRootNodes()).to.be.eql([
|
||||
{
|
||||
node: node,
|
||||
temporary: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// Set HTML
|
||||
node.innerHTML =
|
||||
'<p>Testing observing DOM</p>' +
|
||||
'<span class="iconify" data-icon="mdi:home"></span>';
|
||||
|
||||
// Test nodes
|
||||
setTimeout(() => {
|
||||
// Find elements
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
expect(elements.length).to.be.equal(1);
|
||||
|
||||
// Test for "home" icon contents
|
||||
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
|
||||
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('Change icon', (done) => {
|
||||
const node = getNode('observe-dom');
|
||||
|
||||
// Set root and init observer
|
||||
setRoot(node);
|
||||
initObserver(scanDOM);
|
||||
|
||||
// Set HTML
|
||||
node.innerHTML =
|
||||
'<p>Testing observing DOM</p>' +
|
||||
'<span class="iconify" data-icon="mdi:home"></span>';
|
||||
|
||||
// Test nodes
|
||||
setTimeout(() => {
|
||||
// Find elements
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
expect(elements.length).to.be.equal(1);
|
||||
|
||||
// Test for "home" icon contents
|
||||
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
|
||||
|
||||
// Change icon
|
||||
elements[0].setAttribute('data-icon', 'mdi:account');
|
||||
|
||||
// Test nodes after timer
|
||||
setTimeout(() => {
|
||||
// Find elements
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
expect(elements.length).to.be.equal(1);
|
||||
|
||||
// Test for "home" icon contents
|
||||
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.be.equal(-1);
|
||||
expect(node.innerHTML.indexOf('M12 4a4')).to.not.be.equal(-1);
|
||||
|
||||
done();
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
});
|
@ -10,7 +10,7 @@ import { setAPIConfig } from '@iconify/core/lib/api/config';
|
||||
import { coreModules } from '@iconify/core/lib/modules';
|
||||
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
|
||||
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
|
||||
import { setRoot } from '@iconify/iconify/lib/modules/root';
|
||||
import { setRoot, getRootNodes } from '@iconify/iconify/lib/modules/root';
|
||||
import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
|
||||
|
||||
const expect = chai.expect;
|
||||
@ -114,10 +114,18 @@ describe('Scanning DOM with API', () => {
|
||||
'</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
// Scan DOM
|
||||
setRoot(node);
|
||||
|
||||
scanDOM();
|
||||
|
||||
// Test getRootNodes
|
||||
expect(getRootNodes()).to.be.eql([
|
||||
{
|
||||
node: node,
|
||||
temporary: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// First API response should have loaded
|
||||
setTimeout(() => {
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
@ -241,10 +249,18 @@ describe('Scanning DOM with API', () => {
|
||||
'</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
// Scan DOM
|
||||
setRoot(node);
|
||||
|
||||
scanDOM();
|
||||
|
||||
// Test getRootNodes
|
||||
expect(getRootNodes()).to.be.eql([
|
||||
{
|
||||
node: node,
|
||||
temporary: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// Make sure no icons were rendered yet
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
expect(elements.length).to.be.equal(
|
||||
@ -369,8 +385,8 @@ describe('Scanning DOM with API', () => {
|
||||
'</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
// Scan DOM
|
||||
setRoot(node);
|
||||
|
||||
scanDOM();
|
||||
|
||||
// Change icon name
|
||||
@ -429,18 +445,47 @@ describe('Scanning DOM with API', () => {
|
||||
prefix +
|
||||
':home"></span>';
|
||||
|
||||
console.log('\nUnattached node test start');
|
||||
// Set root node, test nodes list
|
||||
setRoot(fakeRoot);
|
||||
expect(getRootNodes()).to.be.eql([
|
||||
{
|
||||
node: fakeRoot,
|
||||
temporary: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// Scan different node
|
||||
scanDOM(node);
|
||||
|
||||
// Test nodes list
|
||||
expect(getRootNodes()).to.be.eql([
|
||||
{
|
||||
node: fakeRoot,
|
||||
temporary: false,
|
||||
},
|
||||
{
|
||||
node: node,
|
||||
temporary: true,
|
||||
},
|
||||
]);
|
||||
|
||||
// API response should have loaded
|
||||
setTimeout(() => {
|
||||
const elements = node.querySelectorAll('svg.iconify');
|
||||
console.log('Unattached node test:', node.innerHTML);
|
||||
expect(elements.length).to.be.equal(
|
||||
1,
|
||||
'Expected to find 1 rendered SVG element'
|
||||
);
|
||||
|
||||
// Test nodes list: temporary node should have been removed
|
||||
expect(getRootNodes()).to.be.eql([
|
||||
{
|
||||
node: fakeRoot,
|
||||
temporary: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// Done
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
@ -66,7 +66,6 @@ describe('Testing Iconify object', () => {
|
||||
expect(node).to.not.be.equal(null);
|
||||
|
||||
const html = node.outerHTML;
|
||||
console.log('Rendered SVG:', html);
|
||||
expect(html.indexOf('<svg')).to.be.equal(0);
|
||||
|
||||
// Get HTML
|
||||
|
@ -66,7 +66,6 @@ describe('Testing Iconify object (without API)', () => {
|
||||
expect(node).to.not.be.equal(null);
|
||||
|
||||
const html = node.outerHTML;
|
||||
console.log('Rendered SVG:', html);
|
||||
expect(html.indexOf('<svg')).to.be.equal(0);
|
||||
|
||||
// Get HTML
|
||||
|
@ -1,6 +1,15 @@
|
||||
// Root element
|
||||
// Default root element
|
||||
let root: HTMLElement;
|
||||
|
||||
// Interface for extra root nodes
|
||||
export interface ExtraRootNode {
|
||||
node: HTMLElement;
|
||||
temporary: boolean; // True if node should be removed when all placeholders have been replaced
|
||||
}
|
||||
|
||||
// Additional root elements
|
||||
let customRoot: ExtraRootNode[] = [];
|
||||
|
||||
/**
|
||||
* Get root element
|
||||
*/
|
||||
@ -14,3 +23,58 @@ export function getRoot(): HTMLElement {
|
||||
export function setRoot(node: HTMLElement): void {
|
||||
root = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add extra root node
|
||||
*/
|
||||
export function addRoot(node: HTMLElement, autoRemove = false): ExtraRootNode {
|
||||
if (root === node) {
|
||||
return {
|
||||
node: root,
|
||||
temporary: false,
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < customRoot.length; i++) {
|
||||
const item = customRoot[i];
|
||||
if (item.node === node) {
|
||||
// Found matching node
|
||||
if (!autoRemove && item.temporary) {
|
||||
// Change to permanent
|
||||
item.temporary = false;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
// Add item
|
||||
const item = {
|
||||
node,
|
||||
temporary: autoRemove,
|
||||
};
|
||||
customRoot.push(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove root node
|
||||
*/
|
||||
export function removeRoot(node: HTMLElement): void {
|
||||
customRoot = customRoot.filter((item) => item.node !== node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all root nodes
|
||||
*/
|
||||
export function getRootNodes(): ExtraRootNode[] {
|
||||
return (root
|
||||
? [
|
||||
{
|
||||
node: root,
|
||||
temporary: false,
|
||||
},
|
||||
]
|
||||
: []
|
||||
).concat(customRoot);
|
||||
}
|
||||
|
@ -6,7 +6,13 @@ import { findPlaceholders } from './finder';
|
||||
import { IconifyElementData, elementDataProperty } from './element';
|
||||
import { renderIcon } from './render';
|
||||
import { pauseObserver, resumeObserver } from './observer';
|
||||
import { getRoot } from './root';
|
||||
import {
|
||||
getRoot,
|
||||
getRootNodes,
|
||||
addRoot,
|
||||
removeRoot,
|
||||
ExtraRootNode,
|
||||
} from './root';
|
||||
|
||||
/**
|
||||
* Flag to avoid scanning DOM too often
|
||||
@ -48,108 +54,138 @@ const compareIcons = (
|
||||
/**
|
||||
* Scan DOM for placeholders
|
||||
*/
|
||||
export function scanDOM(root?: HTMLElement): void {
|
||||
export function scanDOM(customRoot?: HTMLElement): void {
|
||||
scanQueued = false;
|
||||
|
||||
// Observer
|
||||
let paused = false;
|
||||
|
||||
// List of icons to load: [provider][prefix][name] = boolean
|
||||
const loadIcons: Record<
|
||||
string,
|
||||
Record<string, Record<string, boolean>>
|
||||
> = Object.create(null);
|
||||
|
||||
// Add temporary root node
|
||||
let customRootItem: ExtraRootNode;
|
||||
if (customRoot) {
|
||||
customRootItem = addRoot(customRoot, true);
|
||||
}
|
||||
|
||||
// Get root node and placeholders
|
||||
if (!root) {
|
||||
root = getRoot();
|
||||
}
|
||||
if (!root || !root.querySelectorAll) {
|
||||
return;
|
||||
}
|
||||
findPlaceholders(root).forEach((item) => {
|
||||
const element = item.element;
|
||||
const iconName = item.name;
|
||||
const provider = iconName.provider;
|
||||
const prefix = iconName.prefix;
|
||||
const name = iconName.name;
|
||||
let data: IconifyElementData = element[elementDataProperty];
|
||||
|
||||
// Icon has not been updated since last scan
|
||||
if (data !== void 0 && compareIcons(data.name, iconName)) {
|
||||
// Icon name was not changed and data is set - quickly return if icon is missing or still loading
|
||||
switch (data.status) {
|
||||
case 'missing':
|
||||
return;
|
||||
|
||||
case 'loading':
|
||||
if (
|
||||
coreModules.api &&
|
||||
coreModules.api.isPending({ provider, prefix, name })
|
||||
) {
|
||||
// Pending
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check icon
|
||||
const storage = getStorage(provider, prefix);
|
||||
if (storage.icons[name] !== void 0) {
|
||||
// Icon exists - replace placeholder
|
||||
if (!paused) {
|
||||
pauseObserver(root);
|
||||
paused = true;
|
||||
}
|
||||
|
||||
// Get customisations
|
||||
const customisations =
|
||||
item.customisations !== void 0
|
||||
? item.customisations
|
||||
: item.finder.customisations(element);
|
||||
|
||||
// Render icon
|
||||
renderIcon(
|
||||
item,
|
||||
customisations,
|
||||
getIcon(storage, name) as FullIconifyIcon
|
||||
);
|
||||
const rootNodes: ExtraRootNode[] = customRoot
|
||||
? [customRootItem]
|
||||
: getRootNodes();
|
||||
rootNodes.forEach((rootItem) => {
|
||||
const root = rootItem.node;
|
||||
|
||||
if (!root || !root.querySelectorAll) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (storage.missing[name]) {
|
||||
// Mark as missing
|
||||
// Track placeholders
|
||||
let hasPlaceholders = false;
|
||||
|
||||
// Observer
|
||||
let paused = false;
|
||||
|
||||
// Find placeholders
|
||||
findPlaceholders(root).forEach((item) => {
|
||||
const element = item.element;
|
||||
const iconName = item.name;
|
||||
const provider = iconName.provider;
|
||||
const prefix = iconName.prefix;
|
||||
const name = iconName.name;
|
||||
let data: IconifyElementData = element[elementDataProperty];
|
||||
|
||||
// Icon has not been updated since last scan
|
||||
if (data !== void 0 && compareIcons(data.name, iconName)) {
|
||||
// Icon name was not changed and data is set - quickly return if icon is missing or still loading
|
||||
switch (data.status) {
|
||||
case 'missing':
|
||||
return;
|
||||
|
||||
case 'loading':
|
||||
if (
|
||||
coreModules.api &&
|
||||
coreModules.api.isPending({
|
||||
provider,
|
||||
prefix,
|
||||
name,
|
||||
})
|
||||
) {
|
||||
// Pending
|
||||
hasPlaceholders = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check icon
|
||||
const storage = getStorage(provider, prefix);
|
||||
if (storage.icons[name] !== void 0) {
|
||||
// Icon exists - replace placeholder
|
||||
if (!paused && !rootItem.temporary) {
|
||||
pauseObserver(root);
|
||||
paused = true;
|
||||
}
|
||||
|
||||
// Get customisations
|
||||
const customisations =
|
||||
item.customisations !== void 0
|
||||
? item.customisations
|
||||
: item.finder.customisations(element);
|
||||
|
||||
// Render icon
|
||||
renderIcon(
|
||||
item,
|
||||
customisations,
|
||||
getIcon(storage, name) as FullIconifyIcon
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (storage.missing[name]) {
|
||||
// Mark as missing
|
||||
data = {
|
||||
name: iconName,
|
||||
status: 'missing',
|
||||
customisations: {},
|
||||
};
|
||||
element[elementDataProperty] = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if (coreModules.api) {
|
||||
if (!coreModules.api.isPending({ provider, prefix, name })) {
|
||||
// Add icon to loading queue
|
||||
if (loadIcons[provider] === void 0) {
|
||||
loadIcons[provider] = Object.create(null);
|
||||
}
|
||||
const providerLoadIcons = loadIcons[provider];
|
||||
if (providerLoadIcons[prefix] === void 0) {
|
||||
providerLoadIcons[prefix] = Object.create(null);
|
||||
}
|
||||
providerLoadIcons[prefix][name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as loading
|
||||
data = {
|
||||
name: iconName,
|
||||
status: 'missing',
|
||||
status: 'loading',
|
||||
customisations: {},
|
||||
};
|
||||
element[elementDataProperty] = data;
|
||||
return;
|
||||
hasPlaceholders = true;
|
||||
});
|
||||
|
||||
// Remove temporay node
|
||||
if (rootItem.temporary && !hasPlaceholders) {
|
||||
removeRoot(root);
|
||||
}
|
||||
|
||||
if (coreModules.api) {
|
||||
if (!coreModules.api.isPending({ provider, prefix, name })) {
|
||||
// Add icon to loading queue
|
||||
if (loadIcons[provider] === void 0) {
|
||||
loadIcons[provider] = Object.create(null);
|
||||
}
|
||||
const providerLoadIcons = loadIcons[provider];
|
||||
if (providerLoadIcons[prefix] === void 0) {
|
||||
providerLoadIcons[prefix] = Object.create(null);
|
||||
}
|
||||
providerLoadIcons[prefix][name] = true;
|
||||
}
|
||||
if (paused && !rootItem.temporary) {
|
||||
resumeObserver(root);
|
||||
}
|
||||
|
||||
// Mark as loading
|
||||
data = {
|
||||
name: iconName,
|
||||
status: 'loading',
|
||||
customisations: {},
|
||||
};
|
||||
element[elementDataProperty] = data;
|
||||
});
|
||||
|
||||
// Load icons
|
||||
@ -172,8 +208,4 @@ export function scanDOM(root?: HTMLElement): void {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (paused) {
|
||||
resumeObserver(root);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user