Skip to content

Commit f566dcc

Browse files
authored
Merge pull request #267 from wechat-miniprogram/feat-resize-observer
Implement resize observer interface
2 parents 3062e31 + ac4a345 commit f566dcc

File tree

7 files changed

+188
-1
lines changed

7 files changed

+188
-1
lines changed

glass-easel-miniprogram-adapter/src/component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type utils as typeUtils } from './types'
33
import { type ComponentType, type GeneralBehavior, type TraitBehavior } from './behavior'
44
import { SelectorQuery } from './selector_query'
55
import { IntersectionObserver } from './intersection'
6+
import { ResizeObserver } from './resize'
67
import { MediaQueryObserver } from './media_query'
78

89
type DataList = typeUtils.DataList
@@ -362,6 +363,11 @@ export class ComponentCaller<
362363
)
363364
}
364365

366+
/** Create a resize observer */
367+
createResizeObserver(options?: { observeAll?: boolean }): ResizeObserver {
368+
return new ResizeObserver(this, !!options?.observeAll)
369+
}
370+
365371
/** Create an media query observer */
366372
createMediaQueryObserver(): MediaQueryObserver {
367373
return new MediaQueryObserver(this)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as glassEasel from 'glass-easel'
2+
import { type GeneralComponent } from './component'
3+
4+
export class ResizeObserver {
5+
private _$comp: GeneralComponent
6+
private _$observeAll: boolean
7+
private _$mode = glassEasel.backend.ResizeObserverMode.ContentBox
8+
private _$observers: glassEasel.backend.Observer[] = []
9+
10+
/** @internal */
11+
constructor(comp: GeneralComponent, observeAll: boolean) {
12+
this._$comp = comp
13+
this._$observeAll = observeAll
14+
}
15+
16+
contentBox(): this {
17+
this._$mode = glassEasel.backend.ResizeObserverMode.ContentBox
18+
return this
19+
}
20+
21+
borderBox(): this {
22+
this._$mode = glassEasel.backend.ResizeObserverMode.BorderBox
23+
return this
24+
}
25+
26+
observe(targetSelector: string, listener: (status: glassEasel.backend.ResizeStatus) => void) {
27+
const shadowRoot = this._$comp._$.getShadowRoot()
28+
let targets: glassEasel.Element[]
29+
if (!shadowRoot) {
30+
targets = []
31+
} else if (this._$observeAll) {
32+
targets = shadowRoot.querySelectorAll(targetSelector)
33+
} else {
34+
const elem = shadowRoot.querySelector(targetSelector)
35+
if (elem === null) {
36+
targets = []
37+
} else {
38+
targets = [elem]
39+
}
40+
}
41+
targets.forEach((target) => {
42+
const observer = target.createResizeObserver(this._$mode, listener)
43+
if (observer === null) {
44+
// TODO warn no observer attached
45+
} else {
46+
this._$observers.push(observer)
47+
}
48+
})
49+
}
50+
51+
disconnect() {
52+
const observers = this._$observers
53+
this._$observers = []
54+
observers.forEach((observer) => observer.disconnect())
55+
}
56+
}

glass-easel-miniprogram-adapter/tests/selector.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,46 @@ describe('intersection observer', () => {
700700
})
701701
})
702702

703+
describe('resize observer', () => {
704+
test('create resize observer', () => {
705+
const env = new MiniProgramEnv()
706+
const codeSpace = env.createCodeSpace('', true)
707+
708+
codeSpace.addComponentStaticConfig('path/to/comp', {
709+
usingComponents: {},
710+
})
711+
codeSpace.addCompiledTemplate(
712+
'path/to/comp',
713+
tmpl(`
714+
<div id="a" />
715+
<div id="a" />
716+
`),
717+
)
718+
// eslint-disable-next-line arrow-body-style
719+
codeSpace.componentEnv('path/to/comp', ({ Component }) => {
720+
return Component()
721+
.lifetime('attached', function () {
722+
const o1 = this.createResizeObserver()
723+
o1.observe('#a', () => {
724+
/* empty */
725+
})
726+
const o2 = this.createResizeObserver({ observeAll: true })
727+
o2.borderBox().observe('#a', () => {
728+
/* empty */
729+
})
730+
o1.disconnect()
731+
o2.disconnect()
732+
})
733+
.register()
734+
})
735+
736+
const backendContext = new glassEasel.EmptyComposedBackendContext()
737+
const ab = env.associateBackend(backendContext)
738+
const root = ab.createRoot('body', codeSpace, 'path/to/comp')
739+
glassEasel.Element.pretendAttached(root.getComponent())
740+
})
741+
})
742+
703743
describe('media query observer', () => {
704744
test('create media query observer', () => {
705745
const env = new MiniProgramEnv()

glass-easel/src/backend/current_window_backend_context.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
type MediaQueryStatus,
1414
type Observer,
1515
} from './shared'
16-
import type * as shared from './shared'
16+
import * as shared from './shared'
1717

1818
const DELEGATE_EVENTS = [
1919
'touchstart',
@@ -453,6 +453,47 @@ export class CurrentWindowBackendContext implements Context {
453453
}
454454
}
455455

456+
createResizeObserver(
457+
targetElement: Element,
458+
mode: shared.ResizeObserverMode,
459+
listener: (res: shared.ResizeStatus) => void,
460+
): Observer {
461+
const observer = new ResizeObserver((entries) => {
462+
entries.forEach((entry) => {
463+
const elem = entry.target
464+
const isVertialWritingMode = window
465+
.getComputedStyle(elem)
466+
.writingMode.startsWith('vertical-')
467+
if (entry.contentBoxSize.length !== 1 || entry.borderBoxSize.length !== 1) {
468+
// eslint-disable-next-line no-console
469+
console.error('currently multiple boxes are not supported in resize observer')
470+
return
471+
}
472+
const contentBox = entry.contentBoxSize[0]!
473+
const borderBox = entry.borderBoxSize[0]!
474+
listener({
475+
boundingContentBoxWidth: isVertialWritingMode
476+
? contentBox.blockSize
477+
: contentBox.inlineSize,
478+
boundingContentBoxHeight: isVertialWritingMode
479+
? contentBox.inlineSize
480+
: contentBox.blockSize,
481+
boundingBorderBoxWidth: isVertialWritingMode ? borderBox.blockSize : borderBox.inlineSize,
482+
boundingBorderBoxHeight: isVertialWritingMode
483+
? borderBox.inlineSize
484+
: borderBox.blockSize,
485+
})
486+
})
487+
})
488+
const box = mode === shared.ResizeObserverMode.BorderBox ? 'border-box' : 'content-box'
489+
observer.observe(targetElement as unknown as HTMLElement, { box })
490+
return {
491+
disconnect() {
492+
observer.disconnect()
493+
},
494+
}
495+
}
496+
456497
createMediaQueryObserver(
457498
status: MediaQueryStatus,
458499
listener: (res: { matches: boolean }) => void,

glass-easel/src/backend/shared.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ export type IntersectionStatus = {
7979
time: number
8080
}
8181

82+
export const enum ResizeObserverMode {
83+
ContentBox = 1,
84+
BorderBox = 2,
85+
}
86+
87+
export type ResizeStatus = {
88+
boundingContentBoxWidth: number
89+
boundingContentBoxHeight: number
90+
boundingBorderBoxWidth: number
91+
boundingBorderBoxHeight: number
92+
}
93+
8294
export type MediaQueryStatus = {
8395
minWidth?: number
8496
maxWidth?: number

glass-easel/src/backend/suggested_backend_protocol.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
type IntersectionStatus,
1010
type MediaQueryStatus,
1111
type Observer,
12+
type ResizeObserverMode,
13+
type ResizeStatus,
1214
type ScrollOffset,
1315
} from './shared'
1416

@@ -37,6 +39,7 @@ export type Element<E> = {
3739
thresholds: number[],
3840
listener: (res: IntersectionStatus) => void,
3941
): Observer
42+
createResizeObserver(mode: ResizeObserverMode, listener: (res: ResizeStatus) => void): Observer
4043
getMatchedRules(cb: (res: GetMatchedRulesResponses) => void): void
4144
getPseudoMatchedRules(pseudoType: string, cb: (res: GetMatchedRulesResponses) => void): void
4245
getInheritedRules(cb: (res: GetInheritedRulesResponses) => void): void

glass-easel/src/element.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
type GeneralBackendElement,
77
type IntersectionStatus,
88
type Observer,
9+
type ResizeObserverMode,
10+
type ResizeStatus,
911
type ScrollOffset,
1012
type backend,
1113
type composedBackend,
@@ -3257,6 +3259,33 @@ export class Element implements NodeCast {
32573259
return null
32583260
}
32593261

3262+
/**
3263+
* Create a resize observer
3264+
*
3265+
* It is possible to choose to observe either the content box or the border box.
3266+
*/
3267+
createResizeObserver(
3268+
mode: ResizeObserverMode,
3269+
listener: ((res: ResizeStatus) => void) | null,
3270+
): Observer | null {
3271+
const backendElement = this._$backendElement
3272+
if (backendElement) {
3273+
if (BM.DOMLIKE || (BM.DYNAMIC && this.getBackendMode() === BackendMode.Domlike)) {
3274+
const context = this._$nodeTreeContext as domlikeBackend.Context | null
3275+
if (!context || !context.createResizeObserver) return null
3276+
return context.createResizeObserver(
3277+
backendElement as domlikeBackend.Element,
3278+
mode,
3279+
listener!,
3280+
)
3281+
}
3282+
const be = backendElement as backend.Element | composedBackend.Element
3283+
if (!be.createResizeObserver) return null
3284+
return be.createResizeObserver(mode, listener as any)
3285+
}
3286+
return null
3287+
}
3288+
32603289
/**
32613290
* Get an interactive context
32623291
*/

0 commit comments

Comments
 (0)