2
0
mirror of https://github.com/iconify/iconify.git synced 2024-12-13 06:07:50 +00:00

chore: intergrate intersection observer in iconify-icon

This commit is contained in:
Vjacheslav Trushkin 2024-01-27 13:07:16 +02:00
parent 53a229305e
commit ce7ed322c6
6 changed files with 189 additions and 128 deletions

View File

@ -20,13 +20,13 @@ Iconify Icon web component renders icons.
Add this line to your page to load IconifyIcon (you can add it to `<head>` section of the page or before `</body>`): Add this line to your page to load IconifyIcon (you can add it to `<head>` section of the page or before `</body>`):
```html ```html
<script src="https://code.iconify.design/iconify-icon/1.1.0-beta.4/iconify-icon.min.js"></script> <script src="https://code.iconify.design/iconify-icon/2.0.0-beta.1/iconify-icon.min.js"></script>
``` ```
or or
```html ```html
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@1.1.0-beta.4/dist/iconify-icon.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.0.0-beta.1/dist/iconify-icon.min.js"></script>
``` ```
or, if you are building a project with a bundler, you can include the script by installing `iconify-icon` as a dependency and importing it in your project: or, if you are building a project with a bundler, you can include the script by installing `iconify-icon` as a dependency and importing it in your project:

View File

@ -35,6 +35,17 @@
color: red; color: red;
} }
.observer-test {
display: flex;
width: 100px;
height: 32px;
overflow-y: auto;
background-color: rgba(0, 0, 0, 0.1);
}
.observer-test > div {
margin-top: 100px;
}
.unset-size { .unset-size {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -103,16 +114,6 @@
}, },
}, },
}); });
setTimeout(() => {
const span = document.querySelector('.test-2sec');
if (span) {
const icon =
span.parentElement.querySelector('iconify-icon');
span.remove();
icon.setAttribute('icon', 'test:icon');
}
}, 2000);
})(); })();
</script> </script>
<script src="../dist/iconify-icon.min.js"></script> <script src="../dist/iconify-icon.min.js"></script>
@ -333,40 +334,22 @@
></iconify-icon> ></iconify-icon>
</p> </p>
<h2>Icon with innerHTML</h2> <h2>Hidden icons, appear on scroll</h2>
<p> <div class="observer-test">
Keeping innerHTML without icon attribute: <div>
<iconify-icon> <iconify-icon
<svg icon="test2:icon"
xmlns="http://www.w3.org/2000/svg" flip="horizontal,vertical"
width="1em" mode="style"
height="1em" ></iconify-icon>
viewBox="0 0 24 24" <iconify-icon
> observe="false"
<path icon="test2:icon"
fill="currentColor" flip="horizontal,vertical"
d="M16.5 12.5v1q0 .2.15.35T17 14q.2 0 .35-.15t.15-.35v-1h1q.2 0 .35-.15T19 12q0-.2-.15-.35t-.35-.15h-1v-1q0-.2-.15-.35T17 10q-.2 0-.35.15t-.15.35v1h-1q-.2 0-.35.15T15 12q0 .2.15.35t.35.15zm-4 .25l1.55 1.975q.05.075.55.275q.425 0 .625-.387t-.075-.738L13.75 12l1.425-1.9q.275-.35.075-.725T14.6 9q-.175 0-.312.075t-.238.2L12.5 11.25v-1.5q0-.325-.213-.538T11.75 9q-.325 0-.537.213T11 9.75v4.5q0 .325.213.538t.537.212q.325 0 .538-.213t.212-.537zm-5 .75v-1H9q.425 0 .713-.288T10 11.5V10q0-.425-.288-.712T9 9H6.75q-.325 0-.537.213T6 9.75q0 .325.213.538t.537.212H8.5v1H7q-.425 0-.712.288T6 12.5v1.75q0 .325.213.538T6.75 15h2.5q.325 0 .538-.213T10 14.25q0-.325-.213-.537T9.25 13.5zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21z" mode="style"
/> ></iconify-icon>
</svg> </div>
</iconify-icon> </div>
</p>
<p>
Keeping innerHTML,
<span class="test-2sec">updating after 2 seconds...</span>:
<iconify-icon>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M16.5 12.5v1q0 .2.15.35T17 14q.2 0 .35-.15t.15-.35v-1h1q.2 0 .35-.15T19 12q0-.2-.15-.35t-.35-.15h-1v-1q0-.2-.15-.35T17 10q-.2 0-.35.15t-.15.35v1h-1q-.2 0-.35.15T15 12q0 .2.15.35t.35.15zm-4 .25l1.55 1.975q.05.075.55.275q.425 0 .625-.387t-.075-.738L13.75 12l1.425-1.9q.275-.35.075-.725T14.6 9q-.175 0-.312.075t-.238.2L12.5 11.25v-1.5q0-.325-.213-.538T11.75 9q-.325 0-.537.213T11 9.75v4.5q0 .325.213.538t.537.212q.325 0 .538-.213t.212-.537zm-5 .75v-1H9q.425 0 .713-.288T10 11.5V10q0-.425-.288-.712T9 9H6.75q-.325 0-.537.213T6 9.75q0 .325.213.538t.537.212H8.5v1H7q-.425 0-.712.288T6 12.5v1.75q0 .325.213.538T6.75 15h2.5q.325 0 .538-.213T10 14.25q0-.325-.213-.537T9.25 13.5zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21z"
/>
</svg>
</iconify-icon>
</p>
<h2>Scale icon</h2> <h2>Scale icon</h2>
<p>Using height="none" and CSS, animating width/height and color</p> <p>Using height="none" and CSS, animating width/height and color</p>

View File

@ -2,7 +2,7 @@
"name": "iconify-icon", "name": "iconify-icon",
"description": "Icon web component that loads icon data on demand. Over 150,000 icons to choose from", "description": "Icon web component that loads icon data on demand. Over 150,000 icons to choose from",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)", "author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "1.1.0-beta.4", "version": "2.0.0-beta.1",
"publishConfig": { "publishConfig": {
"tag": "next" "tag": "next"
}, },

View File

@ -50,6 +50,9 @@ export interface IconifyIconProperties
// Inline mode // Inline mode
inline?: boolean; inline?: boolean;
// Use intersection observer
observe?: boolean;
} }
/** /**

View File

@ -13,7 +13,7 @@ import { getInline } from './attributes/inline';
import { getRenderMode } from './attributes/mode'; import { getRenderMode } from './attributes/mode';
import type { IconifyIconProperties } from './attributes/types'; import type { IconifyIconProperties } from './attributes/types';
import { exportFunctions, IconifyExportedFunctions } from './functions'; import { exportFunctions, IconifyExportedFunctions } from './functions';
import { renderIcon } from './render/icon'; import { findIconElement, renderIcon } from './render/icon';
import { updateStyle } from './render/style'; import { updateStyle } from './render/style';
import { IconState, setPendingState } from './state'; import { IconState, setPendingState } from './state';
@ -89,6 +89,7 @@ export function defineIconifyIcon(
// Mode // Mode
'mode', 'mode',
'inline', 'inline',
'observe',
// Customisations // Customisations
'width', 'width',
'height', 'height',
@ -112,80 +113,54 @@ export function defineIconifyIcon(
// Attributes check queued // Attributes check queued
_checkQueued = false; _checkQueued = false;
// Connected
_connected = false;
// Observer
_observer: IntersectionObserver | null = null;
_visible = true;
/** /**
* Constructor * Constructor
*/ */
constructor() { constructor() {
super(); super();
// Render old content if no icon attribute is set // Attach shadow DOM
if (!this.getAttribute('icon')) { const root = (this._shadowRoot = this.attachShadow({
try { mode: 'open',
if (document.readyState == 'complete') { }));
// DOM already loaded
const html = this.innerHTML;
if (html) {
this._createShadowRoot().innerHTML = html;
}
} else {
// Do it when DOM is loaded
window.onload = () => {
if (!this.getAttribute('icon')) {
const html = this.innerHTML;
if (html) {
this._createShadowRoot().innerHTML = html;
}
}
};
}
} catch (err) {
//
}
return;
}
// Init DOM // Add style
this._init(); const inline = getInline(this);
updateStyle(root, inline);
// Create empty state
this._state = setPendingState(
{
value: '',
},
inline
);
// Queue icon render // Queue icon render
this._queueCheck(); this._queueCheck();
} }
/** /**
* Create shadow root * Connected to DOM
*/ */
_createShadowRoot() { connectedCallback() {
// Attach shadow DOM this._connected = true;
if (!this._shadowRoot) { this.startObserver();
this._shadowRoot = this.attachShadow({
mode: 'open',
});
}
return this._shadowRoot;
} }
/** /**
* Init state * Disconnected from DOM
*/ */
_init() { disconnectedCallback() {
if (!this._initialised) { this._connected = false;
this._initialised = true; this.stopObserver();
// Create root
const root = this._createShadowRoot();
// Add style
const inline = getInline(this);
updateStyle(root, inline);
// Create empty state
this._state = setPendingState(
{
value: '',
},
inline
);
}
} }
/** /**
@ -217,20 +192,32 @@ export function defineIconifyIcon(
* Attribute has changed * Attribute has changed
*/ */
attributeChangedCallback(name: string) { attributeChangedCallback(name: string) {
this._init(); switch (name) {
case 'inline': {
if (name === 'inline') { // Update immediately: not affected by other attributes
// Update immediately: not affected by other attributes const newInline = getInline(this);
const newInline = getInline(this); const state = this._state;
const state = this._state; if (newInline !== state.inline) {
if (newInline !== state.inline) { // Update style if inline mode changed
// Update style if inline mode changed state.inline = newInline;
state.inline = newInline; updateStyle(this._shadowRoot, newInline);
updateStyle(this._shadowRoot, newInline); }
break;
} }
} else {
// Queue check for other attributes case 'observer': {
this._queueCheck(); const value = this.observer;
if (value) {
this.startObserver();
} else {
this.stopObserver();
}
break;
}
default:
// Queue check for other attributes
this._queueCheck();
} }
} }
@ -271,12 +258,25 @@ export function defineIconifyIcon(
} }
} }
/**
* Get/set observer
*/
get observer(): boolean {
return this.hasAttribute('observer');
}
set observer(value: boolean) {
if (value) {
this.setAttribute('observer', 'true');
} else {
this.removeAttribute('observer');
}
}
/** /**
* Restart animation * Restart animation
*/ */
restartAnimation() { restartAnimation() {
this._init();
const state = this._state; const state = this._state;
if (state.rendered) { if (state.rendered) {
const root = this._shadowRoot; const root = this._shadowRoot;
@ -297,7 +297,6 @@ export function defineIconifyIcon(
* Get status * Get status
*/ */
get status(): IconifyIconStatus { get status(): IconifyIconStatus {
this._init();
const state = this._state; const state = this._state;
return state.rendered return state.rendered
? 'rendered' ? 'rendered'
@ -337,7 +336,7 @@ export function defineIconifyIcon(
} }
// Ignore other attributes if icon is not rendered // Ignore other attributes if icon is not rendered
if (!state.rendered) { if (!state.rendered || !this._visible) {
return; return;
} }
@ -346,7 +345,11 @@ export function defineIconifyIcon(
const customisations = getCustomisations(this); const customisations = getCustomisations(this);
if ( if (
state.attrMode !== mode || state.attrMode !== mode ||
haveCustomisationsChanged(state.customisations, customisations) haveCustomisationsChanged(
state.customisations,
customisations
) ||
!findIconElement(this._shadowRoot)
) { ) {
this._renderIcon(state.icon, customisations, mode); this._renderIcon(state.icon, customisations, mode);
} }
@ -393,6 +396,23 @@ export function defineIconifyIcon(
} }
} }
/**
* Force render icon on state change
*/
_forceRender() {
if (!this._visible) {
// Remove icon
const node = findIconElement(this._shadowRoot);
if (node) {
this._shadowRoot.removeChild(node);
}
return;
}
// Re-render icon
this._queueCheck();
}
/** /**
* Got new icon data, icon is ready to (re)render * Got new icon data, icon is ready to (re)render
*/ */
@ -432,6 +452,52 @@ export function defineIconifyIcon(
}) })
); );
} }
/**
* Start observer
*/
startObserver() {
if (!this._observer) {
try {
this._observer = new IntersectionObserver((entries) => {
const intersecting = entries.some(
(entry) => entry.isIntersecting
);
if (intersecting !== this._visible) {
this._visible = intersecting;
this._forceRender();
}
});
this._observer.observe(this);
} catch (err) {
// Something went wrong, possibly observer is not supported
if (this._observer) {
try {
this._observer.disconnect();
} catch (err) {
//
}
this._observer = null;
}
}
}
}
/**
* Stop observer
*/
stopObserver() {
if (this._observer) {
this._observer.disconnect();
this._observer = null;
this._visible = true;
if (this._connected) {
// Render icon
this._forceRender();
}
}
}
}; };
// Add getters and setters // Add getters and setters

View File

@ -4,6 +4,20 @@ import type { RenderedState } from '../state';
import { renderSPAN } from './span'; import { renderSPAN } from './span';
import { renderSVG } from './svg'; import { renderSVG } from './svg';
/**
* Find icon node
*/
export function findIconElement(
parent: Element | ShadowRoot
): HTMLElement | undefined {
return Array.from(parent.childNodes).find((node) => {
const tag =
(node as HTMLElement).tagName &&
(node as HTMLElement).tagName.toUpperCase();
return tag === 'SPAN' || tag === 'SVG';
}) as HTMLElement | undefined;
}
/** /**
* Render icon * Render icon
*/ */
@ -37,12 +51,7 @@ export function renderIcon(parent: Element | ShadowRoot, state: RenderedState) {
} }
// Set element // Set element
const oldNode = Array.from(parent.childNodes).find((node) => { const oldNode = findIconElement(parent);
const tag =
(node as HTMLElement).tagName &&
(node as HTMLElement).tagName.toUpperCase();
return tag === 'SPAN' || tag === 'SVG';
}) as HTMLElement | undefined;
if (oldNode) { if (oldNode) {
// Replace old element // Replace old element
if (node.tagName === 'SPAN' && oldNode.tagName === node.tagName) { if (node.tagName === 'SPAN' && oldNode.tagName === node.tagName) {