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:
parent
53a229305e
commit
ce7ed322c6
@ -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:
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -50,6 +50,9 @@ export interface IconifyIconProperties
|
|||||||
|
|
||||||
// Inline mode
|
// Inline mode
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
|
|
||||||
|
// Use intersection observer
|
||||||
|
observe?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user