2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-20 01:09:04 +00:00

Change behavior of scanDOM in SVG framework when used with custom root node

This commit is contained in:
Vjacheslav Trushkin 2020-08-02 21:48:53 +03:00
parent 4207fd254a
commit d1bd40bcb0
7 changed files with 372 additions and 96 deletions

View File

@ -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 iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon'; import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { getStorage, addIconSet } from '@iconify/core/lib/storage'; 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'; import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
const expect = chai.expect; const expect = chai.expect;
@ -42,6 +42,9 @@ describe('Scanning DOM', () => {
height: 24, height: 24,
}); });
// Sanity check before running tests
expect(getRootNodes()).to.be.eql([]);
it('Scan DOM with preloaded icons', () => { it('Scan DOM with preloaded icons', () => {
const node = getNode('scan-dom'); const node = getNode('scan-dom');
node.innerHTML = node.innerHTML =
@ -56,29 +59,46 @@ describe('Scanning DOM', () => {
'</li>' + '</li>' +
'</ul></div>'; '</ul></div>';
// Scan node
setRoot(node); setRoot(node);
scanDOM(); scanDOM();
// Find elements // Find elements
const elements = node.querySelectorAll('svg.iconify'); const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(4); 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', () => { it('Scan DOM with unattached root', () => {
const node = document.createElement('div'); const node = document.createElement('div');
node.innerHTML = '<span class="iconify" data-icon="mdi:home"></span>'; 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); scanDOM(node);
// Find elements // Find elements
const elements = node.querySelectorAll('svg.iconify'); const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1); 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', () => { it('Scan DOM with icon as root', () => {
const node = document.createElement('span'); const node = document.createElement('span');
node.setAttribute('data-icon', 'mdi:home'); node.setAttribute('data-icon', 'mdi:home');
// Scan node
scanDOM(node); scanDOM(node);
// Check node // Check node

View 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);
});
});

View File

@ -10,7 +10,7 @@ import { setAPIConfig } from '@iconify/core/lib/api/config';
import { coreModules } from '@iconify/core/lib/modules'; import { coreModules } from '@iconify/core/lib/modules';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify'; import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon'; 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'; import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
const expect = chai.expect; const expect = chai.expect;
@ -114,10 +114,18 @@ describe('Scanning DOM with API', () => {
'</li>' + '</li>' +
'</ul></div>'; '</ul></div>';
// Scan DOM
setRoot(node); setRoot(node);
scanDOM(); scanDOM();
// Test getRootNodes
expect(getRootNodes()).to.be.eql([
{
node: node,
temporary: false,
},
]);
// First API response should have loaded // First API response should have loaded
setTimeout(() => { setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify'); const elements = node.querySelectorAll('svg.iconify');
@ -241,10 +249,18 @@ describe('Scanning DOM with API', () => {
'</li>' + '</li>' +
'</ul></div>'; '</ul></div>';
// Scan DOM
setRoot(node); setRoot(node);
scanDOM(); scanDOM();
// Test getRootNodes
expect(getRootNodes()).to.be.eql([
{
node: node,
temporary: false,
},
]);
// Make sure no icons were rendered yet // Make sure no icons were rendered yet
const elements = node.querySelectorAll('svg.iconify'); const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal( expect(elements.length).to.be.equal(
@ -369,8 +385,8 @@ describe('Scanning DOM with API', () => {
'</li>' + '</li>' +
'</ul></div>'; '</ul></div>';
// Scan DOM
setRoot(node); setRoot(node);
scanDOM(); scanDOM();
// Change icon name // Change icon name
@ -429,18 +445,47 @@ describe('Scanning DOM with API', () => {
prefix + prefix +
':home"></span>'; ':home"></span>';
console.log('\nUnattached node test start'); // Set root node, test nodes list
setRoot(fakeRoot); setRoot(fakeRoot);
expect(getRootNodes()).to.be.eql([
{
node: fakeRoot,
temporary: false,
},
]);
// Scan different node
scanDOM(node); scanDOM(node);
// Test nodes list
expect(getRootNodes()).to.be.eql([
{
node: fakeRoot,
temporary: false,
},
{
node: node,
temporary: true,
},
]);
// API response should have loaded // API response should have loaded
setTimeout(() => { setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify'); const elements = node.querySelectorAll('svg.iconify');
console.log('Unattached node test:', node.innerHTML);
expect(elements.length).to.be.equal( expect(elements.length).to.be.equal(
1, 1,
'Expected to find 1 rendered SVG element' '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(); done();
}, 200); }, 200);
}); });

View File

@ -66,7 +66,6 @@ describe('Testing Iconify object', () => {
expect(node).to.not.be.equal(null); expect(node).to.not.be.equal(null);
const html = node.outerHTML; const html = node.outerHTML;
console.log('Rendered SVG:', html);
expect(html.indexOf('<svg')).to.be.equal(0); expect(html.indexOf('<svg')).to.be.equal(0);
// Get HTML // Get HTML

View File

@ -66,7 +66,6 @@ describe('Testing Iconify object (without API)', () => {
expect(node).to.not.be.equal(null); expect(node).to.not.be.equal(null);
const html = node.outerHTML; const html = node.outerHTML;
console.log('Rendered SVG:', html);
expect(html.indexOf('<svg')).to.be.equal(0); expect(html.indexOf('<svg')).to.be.equal(0);
// Get HTML // Get HTML

View File

@ -1,6 +1,15 @@
// Root element // Default root element
let root: HTMLElement; 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 * Get root element
*/ */
@ -14,3 +23,58 @@ export function getRoot(): HTMLElement {
export function setRoot(node: HTMLElement): void { export function setRoot(node: HTMLElement): void {
root = node; 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);
}

View File

@ -6,7 +6,13 @@ import { findPlaceholders } from './finder';
import { IconifyElementData, elementDataProperty } from './element'; import { IconifyElementData, elementDataProperty } from './element';
import { renderIcon } from './render'; import { renderIcon } from './render';
import { pauseObserver, resumeObserver } from './observer'; import { pauseObserver, resumeObserver } from './observer';
import { getRoot } from './root'; import {
getRoot,
getRootNodes,
addRoot,
removeRoot,
ExtraRootNode,
} from './root';
/** /**
* Flag to avoid scanning DOM too often * Flag to avoid scanning DOM too often
@ -48,108 +54,138 @@ const compareIcons = (
/** /**
* Scan DOM for placeholders * Scan DOM for placeholders
*/ */
export function scanDOM(root?: HTMLElement): void { export function scanDOM(customRoot?: HTMLElement): void {
scanQueued = false; scanQueued = false;
// Observer
let paused = false;
// List of icons to load: [provider][prefix][name] = boolean // List of icons to load: [provider][prefix][name] = boolean
const loadIcons: Record< const loadIcons: Record<
string, string,
Record<string, Record<string, boolean>> Record<string, Record<string, boolean>>
> = Object.create(null); > = Object.create(null);
// Add temporary root node
let customRootItem: ExtraRootNode;
if (customRoot) {
customRootItem = addRoot(customRoot, true);
}
// Get root node and placeholders // Get root node and placeholders
if (!root) { const rootNodes: ExtraRootNode[] = customRoot
root = getRoot(); ? [customRootItem]
} : getRootNodes();
if (!root || !root.querySelectorAll) { rootNodes.forEach((rootItem) => {
return; const root = rootItem.node;
}
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
);
if (!root || !root.querySelectorAll) {
return; return;
} }
if (storage.missing[name]) { // Track placeholders
// Mark as missing 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 = { data = {
name: iconName, name: iconName,
status: 'missing', status: 'loading',
customisations: {}, customisations: {},
}; };
element[elementDataProperty] = data; element[elementDataProperty] = data;
return; hasPlaceholders = true;
});
// Remove temporay node
if (rootItem.temporary && !hasPlaceholders) {
removeRoot(root);
} }
if (coreModules.api) { if (paused && !rootItem.temporary) {
if (!coreModules.api.isPending({ provider, prefix, name })) { resumeObserver(root);
// 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: 'loading',
customisations: {},
};
element[elementDataProperty] = data;
}); });
// Load icons // Load icons
@ -172,8 +208,4 @@ export function scanDOM(root?: HTMLElement): void {
}); });
}); });
} }
if (paused) {
resumeObserver(root);
}
} }