2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-19 16:59:02 +00:00

Rebuild observer in SVG framework. Now it supports multiple root nodes

This commit is contained in:
Vjacheslav Trushkin 2020-08-04 18:30:24 +03:00
parent 0266760091
commit 7645047431
22 changed files with 635 additions and 309 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@iconify/iconify-browser-tests",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -11,9 +11,9 @@
"dev": true
},
"@iconify/core": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/@iconify/core/-/core-1.0.0-beta.2.tgz",
"integrity": "sha512-PIPWM7Z0R8CClnl9kx8c3G8qe4gF+WGvro6IOcYRLw5YsVt0JyagGh5MgTHH8yfhMWlpAgiiROm65tJpKHctQw==",
"version": "1.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@iconify/core/-/core-1.0.0-beta.3.tgz",
"integrity": "sha512-VqdW1vu7uWBI4QhOijVbT4vxDUKLjGNOye7SbJxvB2XQpJIQe9ZKnNqIyX1fO1olSV+Zf4Y1CQhWjw/sOxW+sQ==",
"dev": true,
"requires": {
"@cyberalien/redundancy": "^1.0.0",
@ -22,9 +22,9 @@
}
},
"@iconify/iconify": {
"version": "2.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-beta.3.tgz",
"integrity": "sha512-s+R3tKl+5+21Foj/JtODPtoAZmxCBt+NLTS0oTwDaibl8VFkomoPDuMPdT+twSVD7iJG81+MqERr0zTk8HyFfg==",
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-beta.4.tgz",
"integrity": "sha512-/uUDM3D2rYWa5JmfC9DIfoznGbodBwwsMcpMOyG5xx5vgMZzfJYrgBWA1kCT+ec5NJ3bcH2etZBzkLOVS3sJAA==",
"dev": true
},
"@iconify/types": {

View File

@ -3,7 +3,7 @@
"private": true,
"description": "Browser tests for @iconify/iconify package",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"license": "(Apache-2.0 OR GPL-2.0)",
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",
@ -19,8 +19,8 @@
},
"devDependencies": {
"@cyberalien/redundancy": "^1.0.0",
"@iconify/core": "^1.0.0-beta.2",
"@iconify/iconify": "^2.0.0-beta.3",
"@iconify/core": "^1.0.0-beta.3",
"@iconify/iconify": "^2.0.0-beta.4",
"@iconify/types": "^1.0.3",
"@rollup/plugin-buble": "^0.21.1",
"@rollup/plugin-commonjs": "^11.0.2",

View File

@ -1,11 +1,10 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { setRoot } from '@iconify/iconify/lib/modules/root';
import { getNode, setRoot } from './node';
import { listRootNodes } from '@iconify/iconify/lib/modules/root';
import {
initObserver,
isObserverPaused,
pauseObserver,
} from '@iconify/iconify/lib/modules/observer';
@ -16,28 +15,40 @@ describe('Testing observer creation', () => {
const node = getNode('observer-creation');
setRoot(node);
// Get node
const list = listRootNodes();
expect(list.length).to.be.equal(1);
const item = list[0];
expect(item.node).to.be.equal(node);
expect(item.observer).to.be.equal(void 0);
// Do test
let counter = 0;
node.innerHTML = '<div></div><ul><li>test</li><li>test2</li></ul>';
initObserver((root) => {
expect(root).to.be.equal(node);
expect(root.node).to.be.equal(node);
counter++;
// Should be called only once
expect(counter).to.be.equal(1);
expect(isObserverPaused()).to.be.equal(false);
// Check if observer is paused
expect(item.observer).to.not.be.equal(void 0);
expect(item.observer.paused).to.be.equal(0);
// Pause observer
pauseObserver();
expect(isObserverPaused()).to.be.equal(true);
expect(item.observer.paused).to.be.equal(1);
done();
});
// Add few nodes to trigger observer
expect(isObserverPaused()).to.be.equal(false);
expect(item.observer).to.not.be.equal(void 0);
expect(item.observer.paused).to.be.equal(0);
node.querySelector('div').innerHTML =
'<span class="test">Some text</span><i>!</i>';
});

View File

@ -1,14 +1,12 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { getNode, setRoot } from './node';
import { elementFinderProperty } from '@iconify/iconify/lib/modules/element';
import { setRoot } from '@iconify/iconify/lib/modules/root';
import {
initObserver,
pauseObserver,
resumeObserver,
isObserverPaused,
} from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
@ -24,7 +22,7 @@ describe('Testing observer with DOM manipulation', () => {
node.innerHTML =
'<div></div><ul><li>test</li><li>test2</li></ul><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg); vertical-align: -0.125em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi-home" data-inline="false" class="iconify"><path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"></path></svg>';
initObserver((root) => {
expect(root).to.be.equal(node);
expect(root.node).to.be.equal(node);
expect(waitingCallback).to.be.equal(true);
counter++;
@ -108,7 +106,6 @@ describe('Testing observer with DOM manipulation', () => {
});
// Add few nodes to trigger observer
expect(isObserverPaused()).to.be.equal(false);
node.querySelector('div').innerHTML =
'<span class="test">Some text</span><i>!</i>';
});

View File

@ -1,13 +1,14 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { getNode, setRoot } 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 { listRootNodes } from '@iconify/iconify/lib/modules/root';
import { scanDOM, scanElement } from '@iconify/iconify/lib/modules/scanner';
import { removeObservedNode } from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
@ -43,10 +44,12 @@ describe('Scanning DOM', () => {
});
// Sanity check before running tests
expect(getRootNodes()).to.be.eql([]);
expect(listRootNodes()).to.be.eql([]);
it('Scan DOM with preloaded icons', () => {
const node = getNode('scan-dom');
setRoot(node);
node.innerHTML =
'<div><p>Testing scanning DOM</p><ul>' +
'<li>Valid icons:' +
@ -60,49 +63,66 @@ describe('Scanning DOM', () => {
'</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,
},
]);
// Check root nodes list
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
});
it('Scan DOM with unattached root', () => {
const fakeNode = getNode('scan-dom');
setRoot(fakeNode);
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();
// Check root nodes list
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
// Scan node
scanDOM(node);
scanElement(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);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
});
it('Scan DOM with icon as root', () => {
const fakeNode = getNode('scan-dom');
setRoot(fakeNode);
const node = document.createElement('span');
node.setAttribute('data-icon', 'mdi:home');
// Check root nodes list
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
// Scan node
scanDOM(node);
scanElement(node);
// Check node
expect(node.tagName).to.be.equal('SPAN');
expect(node.innerHTML).to.be.equal('');
// Make sure tempoary node was not added as root
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
});
});

View File

@ -1,14 +1,18 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { getNode, setRoot } 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 { listRootNodes } from '@iconify/iconify/lib/modules/root';
import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
import { initObserver } from '@iconify/iconify/lib/modules/observer';
import {
initObserver,
observeNode,
removeObservedNode,
} from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
@ -45,30 +49,38 @@ describe('Observe DOM', () => {
it('Basic test', (done) => {
const node = getNode('observe-dom');
const ignoredNode = getNode('observe-dom');
// Set root and init observer
setRoot(node);
initObserver(scanDOM);
// Test getRootNodes
expect(getRootNodes()).to.be.eql([
{
node: node,
temporary: false,
},
]);
// Test listRootNodes
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// Set HTML
node.innerHTML =
'<p>Testing observing DOM</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
ignoredNode.innerHTML =
'<p>This node should be ignored</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Test nodes
setTimeout(() => {
// Find elements
const elements = node.querySelectorAll('svg.iconify');
let elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
elements = ignoredNode.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
0,
'Looks like document.body is observed!'
);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
@ -114,4 +126,112 @@ describe('Observe DOM', () => {
}, 100);
}, 100);
});
it('Adding node to observe', (done) => {
const baseNode = getNode('observe-dom');
const node = getNode('observe-dom');
// Set root and init observer
setRoot(baseNode);
initObserver(scanDOM);
// Test listRootNodes
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
// Observe another node
observeNode(node);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(2);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
expect(nodes[1].node).to.be.equal(node);
expect(nodes[1].temporary).to.be.equal(false);
// Set HTML
baseNode.innerHTML =
'<p>Testing observing 2 nodes (1)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
node.innerHTML =
'<p>Testing observing 2 nodes (2)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Test nodes
setTimeout(() => {
// Find elements
let elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
elements = baseNode.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);
expect(baseNode.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(
-1
);
done();
}, 100);
});
it('Stop observing node', (done) => {
const baseNode = getNode('observe-dom');
const node = getNode('observe-dom');
// Set root and init observer
setRoot(baseNode);
initObserver(scanDOM);
// Test listRootNodes
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
// Observe another node
observeNode(node);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(2);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
expect(nodes[1].node).to.be.equal(node);
expect(nodes[1].temporary).to.be.equal(false);
// Stop observing baseNode
removeObservedNode(baseNode);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// Set HTML
baseNode.innerHTML =
'<p>Testing observing 2 nodes (1)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
node.innerHTML =
'<p>Testing observing 2 nodes (2)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Test nodes
setTimeout(() => {
// Find elements
let elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
elements = baseNode.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(0);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
expect(baseNode.innerHTML.indexOf('20v-6h4v6h5v')).to.be.equal(-1);
done();
}, 100);
});
});

View File

@ -1,7 +1,7 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { getNode, setRoot } from './node';
import { addFinder } from '@iconify/iconify/lib/modules/finder';
import { FakeData, setFakeData, prepareQuery, sendQuery } from './fake-api';
import { API } from '@iconify/core/lib/api/';
@ -10,8 +10,8 @@ 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, getRootNodes } from '@iconify/iconify/lib/modules/root';
import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
import { listRootNodes } from '@iconify/iconify/lib/modules/root';
import { scanDOM, scanElement } from '@iconify/iconify/lib/modules/scanner';
const expect = chai.expect;
@ -86,6 +86,7 @@ describe('Scanning DOM with API', () => {
setFakeData(provider, prefix2, data2);
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM with API</p><ul>' +
'<li>Inline icons:' +
@ -118,13 +119,11 @@ describe('Scanning DOM with API', () => {
setRoot(node);
scanDOM();
// Test getRootNodes
expect(getRootNodes()).to.be.eql([
{
node: node,
temporary: false,
},
]);
// Test listRootNodes
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// First API response should have loaded
setTimeout(() => {
@ -253,13 +252,11 @@ describe('Scanning DOM with API', () => {
setRoot(node);
scanDOM();
// Test getRootNodes
expect(getRootNodes()).to.be.eql([
{
node: node,
temporary: false,
},
]);
// Test listRootNodes
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// Make sure no icons were rendered yet
const elements = node.querySelectorAll('svg.iconify');
@ -447,27 +444,22 @@ describe('Scanning DOM with API', () => {
// Set root node, test nodes list
setRoot(fakeRoot);
expect(getRootNodes()).to.be.eql([
{
node: fakeRoot,
temporary: false,
},
]);
// Test listRootNodes
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeRoot);
expect(nodes[0].temporary).to.be.equal(false);
// Scan different node
scanDOM(node);
scanElement(node);
// Test nodes list
expect(getRootNodes()).to.be.eql([
{
node: fakeRoot,
temporary: false,
},
{
node: node,
temporary: true,
},
]);
// Test listRootNodes
nodes = listRootNodes();
expect(nodes.length).to.be.equal(2);
expect(nodes[0].node).to.be.equal(fakeRoot);
expect(nodes[1].node).to.be.equal(node);
expect(nodes[1].temporary).to.be.equal(true);
// API response should have loaded
setTimeout(() => {
@ -478,12 +470,9 @@ describe('Scanning DOM with API', () => {
);
// Test nodes list: temporary node should have been removed
expect(getRootNodes()).to.be.eql([
{
node: fakeRoot,
temporary: false,
},
]);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeRoot);
// Done
done();

View File

@ -9,10 +9,16 @@ const expect = chai.expect;
const selector =
'span.iconify, i.iconify, span.iconify-inline, i.iconify-inline';
const node = getNode('iconify-api');
Iconify.setRoot(node);
// Do not observe document.body!
Iconify.stopObserving(document.body);
node.innerHTML =
// Create node to observe
const observedNode = getNode('iconify-api');
const ignoredNode = getNode('iconify-api');
Iconify.observe(observedNode);
observedNode.innerHTML =
'<div><p>Testing Iconify with API</p><ul>' +
'<li>Inline icons:' +
' <span class="iconify-inline" data-icon="mdi:home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
@ -24,13 +30,23 @@ node.innerHTML =
'</li>' +
'</ul></div>';
describe('Testing Iconify object', () => {
ignoredNode.innerHTML =
'<div>This node should not have icons! <span class="iconify-inline" data-icon="mdi:home" style="color: red; box-shadow: 0 0 2px black;"></span>';
describe('Testing Iconify object with API', () => {
it('Rendering icons with API', () => {
// Icons should have been replaced by now
let list = node.querySelectorAll(selector);
let list = observedNode.querySelectorAll(selector);
expect(list.length).to.be.equal(0);
list = node.querySelectorAll('svg.iconify');
list = observedNode.querySelectorAll('svg.iconify');
expect(list.length).to.be.equal(4);
// Icons in ignored node should not have been replaced
list = ignoredNode.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
list = ignoredNode.querySelectorAll('svg.iconify');
expect(list.length).to.be.equal(0);
});
});

View File

@ -12,8 +12,11 @@ const selector =
const node1 = getNode('iconify-basic');
const node2 = getNode('iconify-basic');
// Do not observe document.body!
Iconify.stopObserving(document.body);
// Set root node
Iconify.setRoot(node1);
Iconify.observe(node1);
describe('Testing Iconify object', () => {
const prefix = 'invalid-' + Date.now();

View File

@ -12,8 +12,11 @@ const selector =
const node1 = getNode('iconify-basic');
const node2 = getNode('iconify-basic');
// Do not observe document.body!
Iconify.stopObserving(document.body);
// Set root node
Iconify.setRoot(node1);
Iconify.observe(node1);
describe('Testing Iconify object (without API)', () => {
const prefix = 'invalid-' + Date.now();

View File

@ -1,3 +1,7 @@
import { addRootNode, listRootNodes } from '@iconify/iconify/lib/modules/root';
import { removeObservedNode } from '@iconify/iconify/lib/modules/observer';
import { ObservedNode } from '@iconify/iconify/lib/modules/observed-node';
let counter = 0;
/**
@ -12,3 +16,15 @@ export function getNode(prefix = 'test') {
document.getElementById('debug').appendChild(node);
return node;
}
/**
* Set root node, remove old nodes
*/
export function setRoot(node: HTMLElement): ObservedNode {
listRootNodes().forEach((node) => {
if (typeof node.node !== 'function') {
removeObservedNode(node.node);
}
});
return addRootNode(node);
}

View File

@ -1,6 +1,6 @@
{
"name": "@iconify/core",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -2,7 +2,7 @@
"name": "@iconify/core",
"description": "Reusable files used by multiple Iconify packages",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"license": "(Apache-2.0 OR GPL-2.0)",
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",

View File

@ -21,13 +21,13 @@ Iconify SVG framework is designed to be as easy to use as possible.
Add this line to your page to load Iconify SVG framework (you can add it to `<head>` section of the page or before `</body>`):
```html
<script src="https://code.iconify.design/2/2.0.0-beta.3/iconify.min.js"></script>
<script src="https://code.iconify.design/2/2.0.0-beta.4/iconify.min.js"></script>
```
or
```html
<script src="https://cdn.jsdelivr.net/npm/@iconify/iconify@2.0.0-beta.3/dist/iconify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@iconify/iconify@2.0.0-beta.4/dist/iconify.min.js"></script>
```
or, if you are building a project with something like WebPack or Rollup, you can include the script by installing `@iconify/iconify` as a dependency and importing it in your project:

View File

@ -1,6 +1,6 @@
{
"name": "@iconify/iconify",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -31,9 +31,9 @@
"dev": true
},
"@iconify/core": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/@iconify/core/-/core-1.0.0-beta.2.tgz",
"integrity": "sha512-PIPWM7Z0R8CClnl9kx8c3G8qe4gF+WGvro6IOcYRLw5YsVt0JyagGh5MgTHH8yfhMWlpAgiiROm65tJpKHctQw==",
"version": "1.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@iconify/core/-/core-1.0.0-beta.3.tgz",
"integrity": "sha512-VqdW1vu7uWBI4QhOijVbT4vxDUKLjGNOye7SbJxvB2XQpJIQe9ZKnNqIyX1fO1olSV+Zf4Y1CQhWjw/sOxW+sQ==",
"dev": true,
"requires": {
"@cyberalien/redundancy": "^1.0.0",

View File

@ -2,7 +2,7 @@
"name": "@iconify/iconify",
"description": "Unified SVG framework with over 50,000 icons to choose from",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"license": "(Apache-2.0 OR GPL-2.0)",
"main": "./dist/iconify.min.js",
"types": "./dist/iconify.d.ts",
@ -24,7 +24,7 @@
},
"devDependencies": {
"@cyberalien/redundancy": "^1.0.0",
"@iconify/core": "^1.0.0-beta.2",
"@iconify/core": "^1.0.0-beta.3",
"@iconify/types": "^1.0.3",
"@microsoft/api-extractor": "^7.9.2",
"@rollup/plugin-buble": "^0.21.3",

View File

@ -25,13 +25,16 @@ import {
initObserver,
pauseObserver,
resumeObserver,
observeNode,
removeObservedNode,
} from './modules/observer';
import { scanDOM } from './modules/scanner';
import { scanDOM, scanElement } from './modules/scanner';
// Finders
import { addFinder } from './modules/finder';
import { finder as iconifyFinder } from './finders/iconify';
import { setRoot } from './modules/root';
import { findRootNode, addRootNode, addBodyNode } from './modules/root';
import { onReady } from './modules/ready';
// import { finder as iconifyIconFinder } from './finders/iconify-icon';
/**
@ -194,23 +197,28 @@ export interface IconifyGlobal {
/**
* Scan DOM
*/
scanDOM: typeof scanDOM;
scan: (root?: HTMLElement) => void;
/**
* Set root node
* Add root node
*/
setRoot: (root: HTMLElement) => void;
observe: (root: HTMLElement) => void;
/**
* Remove root node
*/
stopObserving: (root: HTMLElement) => void;
/* Observer */
/**
* Pause observer
*/
pauseObserver: typeof pauseObserver;
pauseObserver: (root?: HTMLElement) => void;
/**
* Resume observer
*/
resumeObserver: typeof resumeObserver;
resumeObserver: (root?: HTMLElement) => void;
}
/**
@ -296,30 +304,56 @@ export const IconifyCommon: IconifyGlobal = {
replaceIDs,
// Scan DOM
scanDOM,
scan: (root?: HTMLElement) => {
if (root) {
scanElement(root);
} else {
scanDOM();
}
},
// Set root node
setRoot: (root: HTMLElement) => {
setRoot(root);
// Add root node
observe: (root: HTMLElement) => {
observeNode(root);
},
// Restart observer
initObserver(scanDOM);
// Scan DOM on next tick
setTimeout(scanDOM);
// Remove root node
stopObserving: (root: HTMLElement) => {
removeObservedNode(root);
},
// Pause observer
pauseObserver,
pauseObserver: (root?: HTMLElement) => {
if (root) {
const node = findRootNode(root);
if (node) {
pauseObserver(node);
}
} else {
pauseObserver();
}
},
// Resume observer
resumeObserver,
resumeObserver: (root?: HTMLElement) => {
if (root) {
const node = findRootNode(root);
if (node) {
resumeObserver(node);
}
} else {
resumeObserver();
}
},
};
/**
* Initialise stuff
*/
if (typeof document !== 'undefined' && typeof window !== 'undefined') {
// Add document.body node
addBodyNode();
// Add finder modules
// addFinder(iconifyIconFinder);
addFinder(iconifyFinder);
@ -362,7 +396,7 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
// Load observer and scan DOM on next tick
setTimeout(() => {
scanDOM();
initObserver(scanDOM);
scanDOM();
});
}

View File

@ -0,0 +1,27 @@
/**
* Callback
*/
export type GetHTMLElement = () => HTMLElement | null;
/**
* Observed node type
*/
export interface ObservedNode {
// Node
node: HTMLElement | GetHTMLElement;
// True if node should be removed when all placeholders have been replaced
temporary?: boolean;
// Observer data
observer?: {
// MutationObserver instance
instance?: MutationObserver;
// Number instead of boolean to allow multiple pause/resume calls. Observer is resumed only when pause reaches 0
paused: number;
// Timer for pending scan
pendingScan?: number;
};
}

View File

@ -1,15 +1,17 @@
import { elementFinderProperty, IconifyElement } from './element';
import { getRoot } from './root';
/**
* MutationObserver instance, null until DOM is ready
*/
let instance: MutationObserver | null = null;
import { ObservedNode } from './observed-node';
import {
listRootNodes,
addRootNode,
findRootNode,
removeRootNode,
} from './root';
import { onReady } from './ready';
/**
* Observer callback function
*/
export type ObserverCallback = (root: HTMLElement) => void;
export type ObserverCallback = (item: ObservedNode) => void;
/**
* Callback
@ -25,32 +27,20 @@ const observerParams: MutationObserverInit = {
attributes: true,
};
/**
* Pause. Number instead of boolean to allow multiple pause/resume calls. Observer is resumed only when pause reaches 0
*/
let paused = 0;
/**
* Scan is pending when observer is resumed
*/
let scanPending = false;
/**
* Scan is already queued
*/
let scanQueued = false;
/**
* Queue DOM scan
*/
function queueScan(): void {
if (!scanQueued) {
scanQueued = true;
setTimeout(() => {
scanQueued = false;
scanPending = false;
function queueScan(node: ObservedNode): void {
if (!node.observer) {
return;
}
const observer = node.observer;
if (!observer.pendingScan) {
observer.pendingScan = setTimeout(() => {
delete observer.pendingScan;
if (callback) {
callback(getRoot());
callback(node);
}
});
}
@ -59,8 +49,13 @@ function queueScan(): void {
/**
* Check mutations for added nodes
*/
function checkMutations(mutations: MutationRecord[]): void {
if (!scanPending) {
function checkMutations(node: ObservedNode, mutations: MutationRecord[]): void {
if (!node.observer) {
return;
}
const observer = node.observer;
if (!observer.pendingScan) {
for (let i = 0; i < mutations.length; i++) {
const item = mutations[i];
if (
@ -71,9 +66,8 @@ function checkMutations(mutations: MutationRecord[]): void {
(item.target as IconifyElement)[elementFinderProperty] !==
void 0)
) {
scanPending = true;
if (!paused) {
queueScan();
if (!observer.paused) {
queueScan(node);
}
return;
}
@ -84,108 +78,173 @@ function checkMutations(mutations: MutationRecord[]): void {
/**
* Start/resume observer
*/
function observe(): void {
if (instance) {
instance.observe(getRoot(), observerParams);
}
function observe(node: ObservedNode, root: HTMLElement): void {
node.observer.instance.observe(root, observerParams);
}
/**
* Start mutation observer
*/
function startObserver(): void {
if (instance !== null) {
function startObserver(node: ObservedNode): void {
let observer = node.observer;
if (observer && observer.instance) {
// Already started
return;
}
scanPending = true;
instance = new MutationObserver(checkMutations);
observe();
if (!paused) {
queueScan();
const root = typeof node.node === 'function' ? node.node() : node.node;
if (!root) {
// document.body is not available yet
return;
}
if (!observer) {
observer = {
paused: 0,
};
node.observer = observer;
}
// Create new instance, observe
observer.instance = new MutationObserver(checkMutations.bind(null, node));
observe(node, root);
// Scan immediately
if (!observer.paused) {
queueScan(node);
}
}
// Fake interface to test old IE properties
interface OldIEElement extends HTMLElement {
doScroll?: boolean;
/**
* Start all observers
*/
function startObservers(): void {
listRootNodes().forEach(startObserver);
}
/**
* Export module
* Stop observer
*/
function stopObserver(node: ObservedNode): void {
if (!node.observer) {
return;
}
const observer = node.observer;
// Stop scan
if (observer.pendingScan) {
clearTimeout(observer.pendingScan);
delete observer.pendingScan;
}
// Disconnect observer
if (observer.instance) {
observer.instance.disconnect();
delete observer.instance;
}
}
/**
* Start observer when DOM is ready
*/
export function initObserver(cb: ObserverCallback): void {
callback = cb;
let isRestart = callback !== void 0;
if (instance && !paused) {
// Restart observer
instance.disconnect();
observe();
if (callback !== cb) {
// Change callback and stop all pending observers
callback = cb;
if (isRestart) {
listRootNodes().forEach(stopObserver);
}
}
if (isRestart) {
// Restart instances
startObservers();
return;
}
setTimeout(() => {
const doc = document;
if (
doc.readyState === 'complete' ||
(doc.readyState !== 'loading' &&
!(doc.documentElement as OldIEElement).doScroll)
) {
startObserver();
} else {
doc.addEventListener('DOMContentLoaded', startObserver);
window.addEventListener('load', startObserver);
}
});
// Start observers when document is ready
onReady(startObservers);
}
/**
* Pause observer
*/
export function pauseObserver(root?: HTMLElement): void {
if (root && getRoot() !== root) {
// Invalid root node
return;
}
export function pauseObserver(node?: ObservedNode): void {
(node ? [node] : listRootNodes()).forEach((node) => {
if (!node.observer) {
node.observer = {
paused: 1,
};
return;
}
paused++;
if (paused > 1 || instance === null) {
return;
}
const observer = node.observer;
observer.paused++;
if (observer.paused > 1 || !observer.instance) {
return;
}
// Check pending records, stop observer
checkMutations(instance.takeRecords());
instance.disconnect();
// Disconnect observer
const instance = observer.instance;
// checkMutations(node, instance.takeRecords());
instance.disconnect();
});
}
/**
* Resume observer
*/
export function resumeObserver(root?: HTMLElement): void {
if (root && getRoot() !== root) {
// Invalid root node
return;
}
if (!paused) {
return;
}
paused--;
if (!paused && instance) {
observe();
if (scanPending) {
queueScan();
export function resumeObserver(observer?: ObservedNode): void {
(observer ? [observer] : listRootNodes()).forEach((node) => {
if (!node.observer) {
// Start observer
startObserver(node);
return;
}
}
const observer = node.observer;
if (observer.paused) {
observer.paused--;
if (!observer.paused) {
// Start / resume
const root =
typeof node.node === 'function' ? node.node() : node.node;
if (!root) {
return;
} else if (observer.instance) {
observe(node, root);
} else {
startObserver(node);
}
}
}
});
}
/**
* Check if observer is paused
* Observe node
*/
export function isObserverPaused(): boolean {
return paused > 0;
export function observeNode(
root: HTMLElement,
autoRemove = false
): ObservedNode {
const node = addRootNode(root, autoRemove);
startObserver(node);
return node;
}
/**
* Remove observed node
*/
export function removeObservedNode(root: HTMLElement): void {
const node = findRootNode(root);
if (node) {
stopObserver(node);
removeRootNode(root);
}
}

View File

@ -0,0 +1,21 @@
// Fake interface to test old IE properties
interface OldIEElement extends HTMLElement {
doScroll?: boolean;
}
/**
* Execute function when DOM is ready
*/
export function onReady(callback): void {
const doc = document;
if (
doc.readyState === 'complete' ||
(doc.readyState !== 'loading' &&
!(doc.documentElement as OldIEElement).doScroll)
) {
callback();
} else {
doc.addEventListener('DOMContentLoaded', callback);
window.addEventListener('load', callback);
}
}

View File

@ -1,80 +1,77 @@
// 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[] = [];
import { ObservedNode } from './observed-node';
/**
* Get root element
* List of root nodes
*/
export function getRoot(): HTMLElement {
return root ? root : (document.querySelector('body') as HTMLElement);
}
let nodes: ObservedNode[] = [];
/**
* Set root element
* Find node
*/
export function setRoot(node: HTMLElement): void {
root = node;
export function findRootNode(node: HTMLElement): ObservedNode | undefined {
for (let i = 0; i < nodes.length; i++) {
const item = nodes[i];
let root = typeof item.node === 'function' ? item.node() : item.node;
if (root === node) {
return item;
}
}
}
/**
* 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;
export function addRootNode(
root: HTMLElement,
autoRemove = false
): ObservedNode {
let node = findRootNode(root);
if (node) {
// Node already exist: switch type if needed
if (node.temporary) {
node.temporary = autoRemove;
}
return node;
}
// Add item
const item = {
node,
// Create item, add it to list, start observer
node = {
node: root,
temporary: autoRemove,
};
customRoot.push(item);
nodes.push(node);
return item;
return node;
}
/**
* Add document.body node
*/
export function addBodyNode(): ObservedNode {
if (document.body) {
return addRootNode(document.body);
}
nodes.push({
node: () => {
return document.body;
},
});
}
/**
* Remove root node
*/
export function removeRoot(node: HTMLElement): void {
customRoot = customRoot.filter((item) => item.node !== node);
export function removeRootNode(root: HTMLElement): void {
nodes = nodes.filter((node) => {
const element =
typeof node.node === 'function' ? node.node() : node.node;
return root !== element;
});
}
/**
* Get all root nodes
* Get list of root nodes
*/
export function getRootNodes(): ExtraRootNode[] {
return (root
? [
{
node: root,
temporary: false,
},
]
: []
).concat(customRoot);
export function listRootNodes(): ObservedNode[] {
return nodes;
}

View File

@ -5,14 +5,14 @@ import { FullIconifyIcon } from '@iconify/core/lib/icon';
import { findPlaceholders } from './finder';
import { IconifyElementData, elementDataProperty } from './element';
import { renderIcon } from './render';
import { pauseObserver, resumeObserver } from './observer';
import { ObservedNode } from './observed-node';
import {
getRoot,
getRootNodes,
addRoot,
removeRoot,
ExtraRootNode,
} from './root';
pauseObserver,
resumeObserver,
removeObservedNode,
observeNode,
} from './observer';
import { findRootNode, addRootNode, listRootNodes } from './root';
/**
* Flag to avoid scanning DOM too often
@ -51,10 +51,29 @@ const compareIcons = (
);
};
/**
* Scan node for placeholders
*/
export function scanElement(root: HTMLElement): void {
// Add temporary node
let node = findRootNode(root);
if (!node) {
scanDOM(
{
node: root,
temporary: true,
},
true
);
} else {
scanDOM(node);
}
}
/**
* Scan DOM for placeholders
*/
export function scanDOM(customRoot?: HTMLElement): void {
export function scanDOM(node?: ObservedNode, addTempNode = false): void {
scanQueued = false;
// List of icons to load: [provider][prefix][name] = boolean
@ -63,18 +82,9 @@ export function scanDOM(customRoot?: HTMLElement): void {
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
const rootNodes: ExtraRootNode[] = customRoot
? [customRootItem]
: getRootNodes();
rootNodes.forEach((rootItem) => {
const root = rootItem.node;
// Get placeholders
(node ? [node] : listRootNodes()).forEach((node) => {
const root = typeof node.node === 'function' ? node.node() : node.node;
if (!root || !root.querySelectorAll) {
return;
@ -121,9 +131,9 @@ export function scanDOM(customRoot?: HTMLElement): void {
// Check icon
const storage = getStorage(provider, prefix);
if (storage.icons[name] !== void 0) {
// Icon exists - replace placeholder
if (!paused && !rootItem.temporary) {
pauseObserver(root);
// Icon exists - pause observer before replacing placeholder
if (!paused && node.observer) {
pauseObserver(node);
paused = true;
}
@ -178,13 +188,16 @@ export function scanDOM(customRoot?: HTMLElement): void {
hasPlaceholders = true;
});
// Remove temporay node
if (rootItem.temporary && !hasPlaceholders) {
removeRoot(root);
}
if (paused && !rootItem.temporary) {
resumeObserver(root);
// Node stuff
if (node.temporary && !hasPlaceholders) {
// Remove temporary node
removeObservedNode(root);
} else if (addTempNode && hasPlaceholders) {
// Add new temporary node
observeNode(root, true);
} else if (paused && node.observer) {
// Resume observer
resumeObserver(node);
}
});