Last active
August 12, 2025 21:55
-
-
Save tak-dcxi/91c0e143e57a8a482c9ae9e216cba86e to your computer and use it in GitHub Desktop.
RectObserver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 要素の位置情報を監視してCSS変数として:rootに設定するWeb Component | |
| * AnchorPositioningのような位置指定に使用可能 | |
| * | |
| * @example | |
| * ```html | |
| * <rect-observer var-prefix="global-header"> | |
| * <header>...</header> | |
| * </rect-observer> | |
| * ``` | |
| */ | |
| export class RectObserverElement extends HTMLElement { | |
| private prefixValue: string | null = null; | |
| private resizeObserver: ResizeObserver | null = null; | |
| private updateScheduled: boolean = false; | |
| /** | |
| * イベントリスナーのオプション | |
| */ | |
| private static readonly PASSIVE_OPTIONS: AddEventListenerOptions = { | |
| passive: true, | |
| capture: false | |
| }; | |
| /** | |
| * アクティブなprefixとその要素をMapで管理する | |
| */ | |
| private static activePrefixes: Map< | |
| string, | |
| Set<RectObserverElement> | |
| > = new Map(); | |
| /** | |
| * 監視する属性のリスト | |
| */ | |
| static get observedAttributes(): string[] { | |
| return ["var-prefix"]; | |
| } | |
| /** | |
| * 要素がDOMに接続されたときの処理 | |
| */ | |
| connectedCallback(): void { | |
| if (!this.validatePrefix()) return; | |
| this.registerPrefix(); | |
| this.setupObservers(); | |
| this.scheduleUpdate(); | |
| } | |
| /** | |
| * 要素がDOMから切断されたときの処理 | |
| */ | |
| disconnectedCallback(): void { | |
| this.cleanupObservers(); | |
| this.unregisterPrefix(); | |
| this.clearProperties(); | |
| } | |
| /** | |
| * 属性が変更されたときの処理 | |
| */ | |
| attributeChangedCallback( | |
| name: string, | |
| oldValue: string | null, | |
| newValue: string | null | |
| ): void { | |
| if (name === "var-prefix" && oldValue !== newValue) { | |
| // 古いprefixの登録解除とプロパティクリア | |
| if (oldValue) { | |
| this.unregisterPrefix(oldValue); | |
| this.clearProperties(oldValue); | |
| } | |
| // 新しいprefixを検証 | |
| if (!this.validatePrefix()) { | |
| this.cleanupObservers(); | |
| return; | |
| } | |
| // 新しいprefixを登録 | |
| this.registerPrefix(); | |
| // 監視が設定されていない場合は設定 | |
| if (!this.resizeObserver) { | |
| this.setupObservers(); | |
| } | |
| this.scheduleUpdate(); | |
| } | |
| } | |
| /** | |
| * prefix属性の検証 | |
| * @returns 有効なprefixが設定されているかどうか | |
| */ | |
| private validatePrefix(): boolean { | |
| const prefix = this.getAttribute("var-prefix"); | |
| if (!prefix) { | |
| console.warn("RectObserverElement: var-prefix属性は必須です"); | |
| return false; | |
| } | |
| this.prefixValue = prefix; | |
| return true; | |
| } | |
| /** | |
| * prefixの登録と重複チェック | |
| */ | |
| private registerPrefix(): void { | |
| if (!this.prefixValue) return; | |
| // 既存のSetを取得 | |
| const existingElements = RectObserverElement.activePrefixes.get( | |
| this.prefixValue | |
| ); | |
| // 新規作成が必要な場合 | |
| if (!existingElements) { | |
| const newSet = new Set<RectObserverElement>(); | |
| newSet.add(this); | |
| RectObserverElement.activePrefixes.set(this.prefixValue, newSet); | |
| return; | |
| } | |
| // 自分以外の要素が既に存在する場合は警告 | |
| if (!existingElements.has(this) && existingElements.size > 0) { | |
| console.warn( | |
| `RectObserverElement: var-prefix="${this.prefixValue}"が重複しています。最後に存在する要素の値が適用されます。` | |
| ); | |
| } | |
| existingElements.add(this); | |
| } | |
| /** | |
| * prefixの登録解除 | |
| */ | |
| private unregisterPrefix(prefix?: string): void { | |
| const targetPrefix = prefix ?? this.prefixValue; | |
| if (!targetPrefix) return; | |
| const elements = RectObserverElement.activePrefixes.get(targetPrefix); | |
| if (elements) { | |
| elements.delete(this); | |
| // Setが空になったらMapからも削除 | |
| if (elements.size === 0) { | |
| RectObserverElement.activePrefixes.delete(targetPrefix); | |
| } | |
| } | |
| } | |
| /** | |
| * 監視の設定 | |
| */ | |
| private setupObservers(): void { | |
| // ResizeObserverで自身のサイズ変更を監視 | |
| this.resizeObserver = new ResizeObserver(() => { | |
| this.scheduleUpdate(); | |
| }); | |
| this.resizeObserver.observe(this); | |
| // スクロールとウィンドウリサイズを監視 | |
| // 静的に定義されたオプションを使用 | |
| window.addEventListener( | |
| "scroll", | |
| this.handleViewportChange, | |
| RectObserverElement.PASSIVE_OPTIONS | |
| ); | |
| window.addEventListener( | |
| "resize", | |
| this.handleViewportChange, | |
| RectObserverElement.PASSIVE_OPTIONS | |
| ); | |
| } | |
| /** | |
| * 監視のクリーンアップ | |
| */ | |
| private cleanupObservers(): void { | |
| if (this.resizeObserver) { | |
| this.resizeObserver.disconnect(); | |
| this.resizeObserver = null; | |
| } | |
| // removeEventListenerでも同じオプションを指定 | |
| window.removeEventListener( | |
| "scroll", | |
| this.handleViewportChange, | |
| RectObserverElement.PASSIVE_OPTIONS as EventListenerOptions | |
| ); | |
| window.removeEventListener( | |
| "resize", | |
| this.handleViewportChange, | |
| RectObserverElement.PASSIVE_OPTIONS as EventListenerOptions | |
| ); | |
| } | |
| /** | |
| * ビューポート変更時のハンドラ | |
| * イベントオブジェクトは使用しないため、preventDefaultの呼び出しなし | |
| */ | |
| private handleViewportChange = (): void => { | |
| this.scheduleUpdate(); | |
| }; | |
| /** | |
| * 更新のスケジューリング | |
| */ | |
| private scheduleUpdate(): void { | |
| if (this.updateScheduled || !this.prefixValue) return; | |
| this.updateScheduled = true; | |
| requestAnimationFrame(() => { | |
| this.updateRect(); | |
| this.updateScheduled = false; | |
| }); | |
| } | |
| /** | |
| * 位置情報の更新とCSS変数の設定 | |
| */ | |
| private updateRect(): void { | |
| if (!this.prefixValue) return; | |
| const rect = this.getBoundingClientRect(); | |
| // ビューポート相対位置とサイズ | |
| const properties = { | |
| [`--${this.prefixValue}-rect-left`]: `${rect.left}px`, | |
| [`--${this.prefixValue}-rect-right`]: `${rect.right}px`, | |
| [`--${this.prefixValue}-rect-top`]: `${rect.top}px`, | |
| [`--${this.prefixValue}-rect-bottom`]: `${rect.bottom}px`, | |
| [`--${this.prefixValue}-rect-width`]: `${rect.width}px`, | |
| [`--${this.prefixValue}-rect-height`]: `${rect.height}px` | |
| }; | |
| // すべてのプロパティを:rootに設定 | |
| Object.entries(properties).forEach(([property, value]) => { | |
| document.documentElement.style.setProperty(property, value); | |
| }); | |
| } | |
| /** | |
| * CSS変数のクリア | |
| * @param prefix - クリアするプロパティのprefix(省略時は現在のprefixValueを使用) | |
| */ | |
| private clearProperties(prefix?: string): void { | |
| const targetPrefix = prefix ?? this.prefixValue; | |
| if (!targetPrefix) return; | |
| const propertiesToClear = [ | |
| `--${targetPrefix}-rect-left`, | |
| `--${targetPrefix}-rect-right`, | |
| `--${targetPrefix}-rect-top`, | |
| `--${targetPrefix}-rect-bottom`, | |
| `--${targetPrefix}-rect-width`, | |
| `--${targetPrefix}-rect-height` | |
| ]; | |
| propertiesToClear.forEach((property) => { | |
| document.documentElement.style.removeProperty(property); | |
| }); | |
| } | |
| } | |
| if (!customElements.get("rect-observer")) { | |
| customElements.define("rect-observer", RectObserverElement); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment