diff --git a/packages/browser-tests/tests/20-scan-dom-test.ts b/packages/browser-tests/tests/20-scan-dom-test.ts index 250b40d..512931e 100644 --- a/packages/browser-tests/tests/20-scan-dom-test.ts +++ b/packages/browser-tests/tests/20-scan-dom-test.ts @@ -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', () => { '' + ''; + // 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 = ''; + // 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 diff --git a/packages/browser-tests/tests/21-observe-dom-test.ts b/packages/browser-tests/tests/21-observe-dom-test.ts new file mode 100644 index 0000000..f2af93b --- /dev/null +++ b/packages/browser-tests/tests/21-observe-dom-test.ts @@ -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: + '', + }, + 'account-cash': { + body: + '', + }, + 'account': { + body: + '', + }, + 'home': { + body: + '', + }, + }, + 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 = + '

Testing observing DOM

' + + ''; + + // 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 = + '

Testing observing DOM

' + + ''; + + // 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); + }); +}); diff --git a/packages/browser-tests/tests/21-scan-dom-api-test.ts b/packages/browser-tests/tests/21-scan-dom-api-test.ts index 9e3f940..e83ee88 100644 --- a/packages/browser-tests/tests/21-scan-dom-api-test.ts +++ b/packages/browser-tests/tests/21-scan-dom-api-test.ts @@ -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', () => { '' + ''; + // 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', () => { '' + ''; + // 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', () => { '' + ''; + // Scan DOM setRoot(node); - scanDOM(); // Change icon name @@ -429,18 +445,47 @@ describe('Scanning DOM with API', () => { prefix + ':home">'; - 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); }); diff --git a/packages/browser-tests/tests/30-iconify-basic-test.ts b/packages/browser-tests/tests/30-iconify-basic-test.ts index 2c09228..e23251e 100644 --- a/packages/browser-tests/tests/30-iconify-basic-test.ts +++ b/packages/browser-tests/tests/30-iconify-basic-test.ts @@ -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(' { expect(node).to.not.be.equal(null); const html = node.outerHTML; - console.log('Rendered SVG:', html); expect(html.indexOf(' item.node !== node); +} + +/** + * Get all root nodes + */ +export function getRootNodes(): ExtraRootNode[] { + return (root + ? [ + { + node: root, + temporary: false, + }, + ] + : [] + ).concat(customRoot); +} diff --git a/packages/iconify/src/modules/scanner.ts b/packages/iconify/src/modules/scanner.ts index 4de40ee..2c31160 100644 --- a/packages/iconify/src/modules/scanner.ts +++ b/packages/iconify/src/modules/scanner.ts @@ -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> > = 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); - } }