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) {