From 418054c298a510609e5ce5d207b31e3d28e2b90f Mon Sep 17 00:00:00 2001
From: Vjacheslav Trushkin
Date: Sat, 27 Jan 2024 13:07:16 +0200
Subject: [PATCH] chore: intergrate intersection observer in iconify-icon
---
iconify-icon/icon/README.md | 4 +-
iconify-icon/icon/demo/usage.html | 71 +++----
iconify-icon/icon/package.json | 2 +-
iconify-icon/icon/src/attributes/types.ts | 3 +
iconify-icon/icon/src/component.ts | 216 ++++++++++++++--------
iconify-icon/icon/src/render/icon.ts | 21 ++-
6 files changed, 189 insertions(+), 128 deletions(-)
diff --git a/iconify-icon/icon/README.md b/iconify-icon/icon/README.md
index 0b41266..c10a499 100644
--- a/iconify-icon/icon/README.md
+++ b/iconify-icon/icon/README.md
@@ -20,13 +20,13 @@ Iconify Icon web component renders icons.
Add this line to your page to load IconifyIcon (you can add it to `` section of the page or before ``):
```html
-
+
```
or
```html
-
+
```
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:
diff --git a/iconify-icon/icon/demo/usage.html b/iconify-icon/icon/demo/usage.html
index f2f4933..f119422 100644
--- a/iconify-icon/icon/demo/usage.html
+++ b/iconify-icon/icon/demo/usage.html
@@ -35,6 +35,17 @@
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 {
display: flex;
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);
})();
@@ -333,40 +334,22 @@
>
- Icon with innerHTML
-
- Keeping innerHTML without icon attribute:
-
-
-
-
-
- Keeping innerHTML,
- updating after 2 seconds...:
-
-
-
-
+ Hidden icons, appear on scroll
+
Scale icon
Using height="none" and CSS, animating width/height and color
diff --git a/iconify-icon/icon/package.json b/iconify-icon/icon/package.json
index 4b6f450..ef8444c 100644
--- a/iconify-icon/icon/package.json
+++ b/iconify-icon/icon/package.json
@@ -2,7 +2,7 @@
"name": "iconify-icon",
"description": "Icon web component that loads icon data on demand. Over 150,000 icons to choose from",
"author": "Vjacheslav Trushkin (https://iconify.design)",
- "version": "1.1.0-beta.4",
+ "version": "2.0.0-beta.1",
"publishConfig": {
"tag": "next"
},
diff --git a/iconify-icon/icon/src/attributes/types.ts b/iconify-icon/icon/src/attributes/types.ts
index 0cae2b2..a58488f 100644
--- a/iconify-icon/icon/src/attributes/types.ts
+++ b/iconify-icon/icon/src/attributes/types.ts
@@ -50,6 +50,9 @@ export interface IconifyIconProperties
// Inline mode
inline?: boolean;
+
+ // Use intersection observer
+ observe?: boolean;
}
/**
diff --git a/iconify-icon/icon/src/component.ts b/iconify-icon/icon/src/component.ts
index 0eeac32..a17e0f4 100644
--- a/iconify-icon/icon/src/component.ts
+++ b/iconify-icon/icon/src/component.ts
@@ -13,7 +13,7 @@ import { getInline } from './attributes/inline';
import { getRenderMode } from './attributes/mode';
import type { IconifyIconProperties } from './attributes/types';
import { exportFunctions, IconifyExportedFunctions } from './functions';
-import { renderIcon } from './render/icon';
+import { findIconElement, renderIcon } from './render/icon';
import { updateStyle } from './render/style';
import { IconState, setPendingState } from './state';
@@ -89,6 +89,7 @@ export function defineIconifyIcon(
// Mode
'mode',
'inline',
+ 'observe',
// Customisations
'width',
'height',
@@ -112,80 +113,54 @@ export function defineIconifyIcon(
// Attributes check queued
_checkQueued = false;
+ // Connected
+ _connected = false;
+
+ // Observer
+ _observer: IntersectionObserver | null = null;
+ _visible = true;
+
/**
* Constructor
*/
constructor() {
super();
- // Render old content if no icon attribute is set
- if (!this.getAttribute('icon')) {
- try {
- 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;
- }
+ // Attach shadow DOM
+ const root = (this._shadowRoot = this.attachShadow({
+ mode: 'open',
+ }));
- // Init DOM
- this._init();
+ // Add style
+ const inline = getInline(this);
+ updateStyle(root, inline);
+
+ // Create empty state
+ this._state = setPendingState(
+ {
+ value: '',
+ },
+ inline
+ );
// Queue icon render
this._queueCheck();
}
/**
- * Create shadow root
+ * Connected to DOM
*/
- _createShadowRoot() {
- // Attach shadow DOM
- if (!this._shadowRoot) {
- this._shadowRoot = this.attachShadow({
- mode: 'open',
- });
- }
- return this._shadowRoot;
+ connectedCallback() {
+ this._connected = true;
+ this.startObserver();
}
/**
- * Init state
+ * Disconnected from DOM
*/
- _init() {
- if (!this._initialised) {
- this._initialised = true;
-
- // Create root
- const root = this._createShadowRoot();
-
- // Add style
- const inline = getInline(this);
- updateStyle(root, inline);
-
- // Create empty state
- this._state = setPendingState(
- {
- value: '',
- },
- inline
- );
- }
+ disconnectedCallback() {
+ this._connected = false;
+ this.stopObserver();
}
/**
@@ -217,20 +192,32 @@ export function defineIconifyIcon(
* Attribute has changed
*/
attributeChangedCallback(name: string) {
- this._init();
-
- if (name === 'inline') {
- // Update immediately: not affected by other attributes
- const newInline = getInline(this);
- const state = this._state;
- if (newInline !== state.inline) {
- // Update style if inline mode changed
- state.inline = newInline;
- updateStyle(this._shadowRoot, newInline);
+ switch (name) {
+ case 'inline': {
+ // Update immediately: not affected by other attributes
+ const newInline = getInline(this);
+ const state = this._state;
+ if (newInline !== state.inline) {
+ // Update style if inline mode changed
+ state.inline = newInline;
+ updateStyle(this._shadowRoot, newInline);
+ }
+ break;
}
- } else {
- // Queue check for other attributes
- this._queueCheck();
+
+ case 'observer': {
+ 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
*/
restartAnimation() {
- this._init();
-
const state = this._state;
if (state.rendered) {
const root = this._shadowRoot;
@@ -297,7 +297,6 @@ export function defineIconifyIcon(
* Get status
*/
get status(): IconifyIconStatus {
- this._init();
const state = this._state;
return state.rendered
? 'rendered'
@@ -337,7 +336,7 @@ export function defineIconifyIcon(
}
// Ignore other attributes if icon is not rendered
- if (!state.rendered) {
+ if (!state.rendered || !this._visible) {
return;
}
@@ -346,7 +345,11 @@ export function defineIconifyIcon(
const customisations = getCustomisations(this);
if (
state.attrMode !== mode ||
- haveCustomisationsChanged(state.customisations, customisations)
+ haveCustomisationsChanged(
+ state.customisations,
+ customisations
+ ) ||
+ !findIconElement(this._shadowRoot)
) {
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
*/
@@ -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
diff --git a/iconify-icon/icon/src/render/icon.ts b/iconify-icon/icon/src/render/icon.ts
index d524883..a54952e 100644
--- a/iconify-icon/icon/src/render/icon.ts
+++ b/iconify-icon/icon/src/render/icon.ts
@@ -4,6 +4,20 @@ import type { RenderedState } from '../state';
import { renderSPAN } from './span';
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
*/
@@ -37,12 +51,7 @@ export function renderIcon(parent: Element | ShadowRoot, state: RenderedState) {
}
// Set element
- const oldNode = 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;
+ const oldNode = findIconElement(parent);
if (oldNode) {
// Replace old element
if (node.tagName === 'SPAN' && oldNode.tagName === node.tagName) {