From 0227374d19f3f0a9f2558789377bfeca605ce451 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Tue, 8 Sep 2020 19:10:11 +0200 Subject: [PATCH 1/2] temp --- examples/demo/demo.ts | 2 +- examples/demo/layout.xml | 1 + packages/bundle-basic-editor/BasicEditor.ts | 17 +++ packages/plugin-commandbar/src/ActionBar.ts | 112 ++++++++++++++++++ .../src/ActionBarDomObjectRenderer.ts | 56 +++++++++ .../plugin-commandbar/src/ActionBarNode.ts | 15 +++ .../src/ActionBarXmlDomParser.ts | 19 +++ .../plugin-commandbar/test/CommandBar.test.ts | 0 packages/utils/src/EventMixin2.ts | 67 +++++++++++ packages/utils/src/ReactiveValue.ts | 4 +- 10 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-commandbar/src/ActionBar.ts create mode 100644 packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts create mode 100644 packages/plugin-commandbar/src/ActionBarNode.ts create mode 100644 packages/plugin-commandbar/src/ActionBarXmlDomParser.ts create mode 100644 packages/plugin-commandbar/test/CommandBar.test.ts create mode 100644 packages/utils/src/EventMixin2.ts diff --git a/examples/demo/demo.ts b/examples/demo/demo.ts index b1ae2df1a..82242934f 100644 --- a/examples/demo/demo.ts +++ b/examples/demo/demo.ts @@ -26,7 +26,7 @@ target.innerHTML = template; const editor = new BasicEditor({ editable: target }); editor.load(FontAwesome); -// editor.load(DevTools); +editor.load(DevTools); editor.configure(DomLayout, { location: [target, 'replace'], components: [ diff --git a/examples/demo/layout.xml b/examples/demo/layout.xml index 8d8d016ce..91ad9e51b 100644 --- a/examples/demo/layout.xml +++ b/examples/demo/layout.xml @@ -1,6 +1,7 @@ + diff --git a/packages/bundle-basic-editor/BasicEditor.ts b/packages/bundle-basic-editor/BasicEditor.ts index f30a3d827..9acd7f1b5 100644 --- a/packages/bundle-basic-editor/BasicEditor.ts +++ b/packages/bundle-basic-editor/BasicEditor.ts @@ -51,6 +51,7 @@ import { FontSize } from '../plugin-font-size/src/FontSize'; import { Button } from '../plugin-button/src/Button'; import { HorizontalRule } from '../plugin-horizontal-rule/src/HorizontalRule'; import { Strikethrough } from '../plugin-strikethrough/src/Strikethrough'; +import { ActionBar } from '../plugin-commandbar/src/ActionBar'; export class BasicEditor extends JWEditor { constructor(params?: { editable?: HTMLElement }) { @@ -163,5 +164,21 @@ export class BasicEditor extends JWEditor { ['UndoButton', 'RedoButton'], ], }); + this.configure(ActionBar, { + actions: [ + { + name: 'action1', + }, + { + name: 'action2', + }, + { + name: 'foo', + }, + { + name: 'bar', + }, + ], + }); } } diff --git a/packages/plugin-commandbar/src/ActionBar.ts b/packages/plugin-commandbar/src/ActionBar.ts new file mode 100644 index 000000000..64471af94 --- /dev/null +++ b/packages/plugin-commandbar/src/ActionBar.ts @@ -0,0 +1,112 @@ +import { JWPluginConfig, JWPlugin } from '../../core/src/JWPlugin'; +import { Parser } from '../../plugin-parser/src/Parser'; +import { Loadables } from '../../core/src/JWEditor'; +import { Renderer } from '../../plugin-renderer/src/Renderer'; +import { InsertTextParams } from '../../plugin-char/src/Char'; +import { CharNode } from '../../plugin-char/src/CharNode'; +import { VNode } from '../../core/src/VNodes/VNode'; +import JWEditor from '../../core/src/JWEditor'; +import { ReactiveValue } from '../../utils/src/ReactiveValue'; +import { ActionBarXmlDomParser } from './ActionBarXmlDomParser'; +import { ActionBarDomObjectRenderer } from './ActionBarDomObjectRenderer'; +import { CommandParams } from '../../core/src/Dispatcher'; +import { setSelection } from '../../utils/src/testUtils'; + +export interface ActionItem { + name: string; +} + +interface CommandBarConfig extends JWPluginConfig { + // /** + // * Take a function that will receive a filtering term and should return true + // * if it has at least one match. + // */ + // filter: (term: string) => boolean; + // /** + // * Callback to be executed to open the external command bar. + // */ + // open: () => void; + // /** + // * Callback to be executed to close the external command bar. + // */ + // close: () => void; + actions?: ActionItem[]; +} +// export class Color extends JWPlugin { + +export class ActionBar extends JWPlugin { + // static dependencies = [Inline]; + readonly loadables: Loadables = { + parsers: [ActionBarXmlDomParser], + renderers: [ActionBarDomObjectRenderer], + }; + commands = { + // insertText: { + // handler: this.insertText, + // }, + }; + commandHooks = { + insertText: this._insertText.bind(this), + deleteWord: this._check.bind(this), + deleteBackward: this._check.bind(this), + deleteForward: this._check.bind(this), + setSelection: this._check.bind(this), + }; + availableActions = new ReactiveValue([]); + private _opened = false; + private _actions: T['actions']; + + constructor(public editor: JWEditor, public configuration: Partial = {}) { + super(editor, configuration); + if (!configuration.actions) { + throw new Error( + 'Impossible to load the ActionBar without actions in the configuration.', + ); + } + this._actions = configuration.actions; + } + + _insertText(params: InsertTextParams): void { + if (this._opened) { + this._check(params); + } else { + const range = params.context.range; + const beforeStart = range.start.previousSibling(); + const isSlash = beforeStart instanceof CharNode && beforeStart.char === '/'; + // If there is no previous sibling before start, it means beforeStart is + // the first node of the container. + if (range.isCollapsed() && isSlash && !beforeStart.previousSibling()) { + this._opened = true; + this.availableActions.set(this._actions); + } + } + } + + private _check(params: CommandParams): void { + console.warn('check'); + const range = this.editor.selection.range; + const chars: string[] = []; + let node: VNode = range.end; + while ((node = node.previousSibling())) { + if (node instanceof CharNode) chars.unshift(node.char); + } + const term = chars.slice(1).join(''); + + const actions = this._actions.filter(a => a.name.startsWith(term)); + + if (!actions.length) { + this.close(); + console.log('close'); + } else { + this.availableActions.set(actions); + } + } + + close(): void { + this._opened = false; + this.availableActions.set([]); + } + // _openCommandBar(): void { + // this._opened = true; + // } +} diff --git a/packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts b/packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts new file mode 100644 index 000000000..ba1072ff1 --- /dev/null +++ b/packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts @@ -0,0 +1,56 @@ +import { + DomObjectRenderingEngine, + DomObject, +} from '../../plugin-renderer-dom-object/src/DomObjectRenderingEngine'; +import { NodeRenderer } from '../../plugin-renderer/src/NodeRenderer'; +import { Predicate } from '../../core/src/VNodes/VNode'; +import { ActionBarNode } from './ActionBarNode'; +import { ActionBar, ActionItem } from './ActionBar'; +import { Color } from '../../plugin-color/src/Color'; +import { RenderingEngine } from '../../plugin-renderer/src/RenderingEngine'; +import { ReactiveValue } from '../../utils/src/ReactiveValue'; + +export class ActionBarDomObjectRenderer extends NodeRenderer { + static id = DomObjectRenderingEngine.id; + engine: DomObjectRenderingEngine; + predicate: Predicate = ActionBarNode; + availableActions: ReactiveValue; + + constructor(engine: RenderingEngine) { + super(engine); + this.engine.editor.plugins.get(Color); + this.availableActions = this.engine.editor.plugins.get(ActionBar).availableActions; + } + + async render(): Promise { + const domNode = document.createElement('jw-actionbar'); + const updateActions = (): void => { + const actionItems = this.availableActions.get(); + if (actionItems.length) { + domNode.innerHTML = ''; + for (const node of this._renderActionNode(actionItems)) { + domNode.appendChild(node); + } + } else { + domNode.style.display = 'none'; + } + }; + const attach = (): void => { + this.availableActions.on('set', updateActions); + }; + const detach = (): void => { + this.availableActions.off('set', updateActions); + }; + return { dom: [domNode], attach, detach }; + } + + _renderActionNode(actionItems: ActionItem[]): HTMLElement[] { + return actionItems.map(item => { + const element = document.createElement('jw-action-item'); + element.innerHTML = ` + ${item.name} + `; + return element; + }); + } +} diff --git a/packages/plugin-commandbar/src/ActionBarNode.ts b/packages/plugin-commandbar/src/ActionBarNode.ts new file mode 100644 index 000000000..195d03b37 --- /dev/null +++ b/packages/plugin-commandbar/src/ActionBarNode.ts @@ -0,0 +1,15 @@ +import { AtomicNode } from '../../core/src/VNodes/AtomicNode'; +import { ReactiveValue } from '../../utils/src/ReactiveValue'; +import { ActionItem } from './ActionBar'; + +// interface ActionBarNodeParams { +// actionsItems: ReactiveValue; +// } + +export class ActionBarNode extends AtomicNode { + // actionsItems: ActionBarNodeParams['actionsItems']; + // constructor(params: ActionBarNodeParams) { + // super(); + // this.actionsItems = params.actionsItems; + // } +} diff --git a/packages/plugin-commandbar/src/ActionBarXmlDomParser.ts b/packages/plugin-commandbar/src/ActionBarXmlDomParser.ts new file mode 100644 index 000000000..5c9ca0a17 --- /dev/null +++ b/packages/plugin-commandbar/src/ActionBarXmlDomParser.ts @@ -0,0 +1,19 @@ +import { removeFormattingSpace } from '../../utils/src/formattingSpace'; +import { AbstractParser } from '../../plugin-parser/src/AbstractParser'; +import { XmlDomParsingEngine } from '../../plugin-xml/src/XmlDomParsingEngine'; +import { VNode } from '../../core/src/VNodes/VNode'; +import { nodeName } from '../../utils/src/utils'; +import { ActionBarNode } from './ActionBarNode'; + +export class ActionBarXmlDomParser extends AbstractParser { + static id = XmlDomParsingEngine.id; + engine: XmlDomParsingEngine; + + predicate = (item: Node): boolean => { + return item instanceof Element && nodeName(item) === 'T-ACTIONBAR'; + }; + + async parse(item: Node): Promise { + return [new ActionBarNode()]; + } +} diff --git a/packages/plugin-commandbar/test/CommandBar.test.ts b/packages/plugin-commandbar/test/CommandBar.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/utils/src/EventMixin2.ts b/packages/utils/src/EventMixin2.ts new file mode 100644 index 000000000..de03b833e --- /dev/null +++ b/packages/utils/src/EventMixin2.ts @@ -0,0 +1,67 @@ +import { VersionableObject } from '../../core/src/Memory/VersionableObject'; +import { VersionableArray } from '../../core/src/Memory/VersionableArray'; +import { makeVersionable } from '../../core/src/Memory/Versionable'; +import { VersionableSet } from '../../core/src/Memory/VersionableSet'; + +/** + * Abstract class to add event mechanism. + */ +export class EventMixin2 { + _eventCallbacks: Record; + _callbackWorking: Set; + + /** + * Subscribe to an event with a callback. + * + * @param eventName + * @param callback + */ + on(eventName: string, callback: Function): void { + if (!this._eventCallbacks) { + this._eventCallbacks = {}; + } + if (!this._eventCallbacks[eventName]) { + this._eventCallbacks[eventName] = []; + } + this._eventCallbacks[eventName].push(callback); + } + + /** + * Stop listening to an event for a specific callback. If no callback is + * provided, remove stop listening all event `eventName`. + * + * @param eventName + * @param callback + */ + off(eventName: string, callback?: Function): void { + if (callback) { + const index = this._eventCallbacks[eventName].indexOf(callback); + if (index !== -1) { + this._eventCallbacks[eventName].splice(index, 1); + } + } else { + this._eventCallbacks[eventName].splice(0); + } + } + + /** + * Fire an event for of this object and all ancestors. + * + * @param eventName + * @param args + */ + trigger(eventName: string, args?: A): void { + if (this._eventCallbacks?.[eventName]) { + if (!this._callbackWorking) { + this._callbackWorking = new Set(); + } + for (const callback of this._eventCallbacks[eventName]) { + if (!this._callbackWorking.has(callback)) { + this._callbackWorking.add(callback); + callback(args); + this._callbackWorking.delete(callback); + } + } + } + } +} diff --git a/packages/utils/src/ReactiveValue.ts b/packages/utils/src/ReactiveValue.ts index b2e084142..da2b255c3 100644 --- a/packages/utils/src/ReactiveValue.ts +++ b/packages/utils/src/ReactiveValue.ts @@ -1,6 +1,6 @@ -import { EventMixin } from './EventMixin'; +import { EventMixin2 } from './EventMixin2'; -export class ReactiveValue extends EventMixin { +export class ReactiveValue extends EventMixin2 { constructor(private _value?: T) { super(); } From d1533ac00c16157f57fe14b9f5f93c7cd3da6ed9 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Wed, 9 Sep 2020 12:05:24 +0200 Subject: [PATCH 2/2] temp --- packages/plugin-commandbar/src/ActionBar.ts | 49 ++-- .../src/ActionBarDomObjectRenderer.ts | 38 ++- packages/utils/src/EventMixin2.ts | 5 - packages/utils/src/Positionable.ts | 64 ++++- packages/utils/src/ResizeObserver.d.ts | 239 ++++++++++++++++++ 5 files changed, 364 insertions(+), 31 deletions(-) create mode 100644 packages/utils/src/ResizeObserver.d.ts diff --git a/packages/plugin-commandbar/src/ActionBar.ts b/packages/plugin-commandbar/src/ActionBar.ts index 64471af94..b6485b409 100644 --- a/packages/plugin-commandbar/src/ActionBar.ts +++ b/packages/plugin-commandbar/src/ActionBar.ts @@ -11,6 +11,7 @@ import { ActionBarXmlDomParser } from './ActionBarXmlDomParser'; import { ActionBarDomObjectRenderer } from './ActionBarDomObjectRenderer'; import { CommandParams } from '../../core/src/Dispatcher'; import { setSelection } from '../../utils/src/testUtils'; +import { MarkerNode } from '../../core/src/VNodes/MarkerNode'; export interface ActionItem { name: string; @@ -55,6 +56,8 @@ export class ActionBar extends JW availableActions = new ReactiveValue([]); private _opened = false; private _actions: T['actions']; + private _initialNode: MarkerNode; + private _lastNode: MarkerNode; constructor(public editor: JWEditor, public configuration: Partial = {}) { super(editor, configuration); @@ -75,7 +78,14 @@ export class ActionBar extends JW const isSlash = beforeStart instanceof CharNode && beforeStart.char === '/'; // If there is no previous sibling before start, it means beforeStart is // the first node of the container. - if (range.isCollapsed() && isSlash && !beforeStart.previousSibling()) { + if (range.isCollapsed() && isSlash) { + console.log('open'); + const firstMarker = new MarkerNode(); + beforeStart.parent.insertBefore(firstMarker, beforeStart); + this._initialNode = firstMarker; + const lastMarker = new MarkerNode(); + beforeStart.parent.insertAfter(lastMarker, beforeStart); + this._lastNode = lastMarker; this._opened = true; this.availableActions.set(this._actions); } @@ -83,27 +93,36 @@ export class ActionBar extends JW } private _check(params: CommandParams): void { - console.warn('check'); - const range = this.editor.selection.range; - const chars: string[] = []; - let node: VNode = range.end; - while ((node = node.previousSibling())) { - if (node instanceof CharNode) chars.unshift(node.char); - } - const term = chars.slice(1).join(''); + if (this._opened) { + console.warn('check'); + // const range = this.editor.selection.range; + + const chars: string[] = []; + let index = this._initialNode.parent.childVNodes.indexOf(this._initialNode) + 1; + const allNodes = this._initialNode.parent.childVNodes; + let node: VNode; + while ((node = allNodes[index])) { + if (node instanceof CharNode) chars.push(node.char); + index++; + } + const term = chars.slice(1).join(''); + const termFuzzyRegex = term.split('').join('.*'); - const actions = this._actions.filter(a => a.name.startsWith(term)); + const actions = this._actions.filter(a => a.name.match(termFuzzyRegex)); - if (!actions.length) { - this.close(); - console.log('close'); - } else { - this.availableActions.set(actions); + if (!actions.length) { + this.close(); + console.log('close'); + } else { + this.availableActions.set(actions); + } } } close(): void { this._opened = false; + this._initialNode.remove(); + this._lastNode.remove(); this.availableActions.set([]); } // _openCommandBar(): void { diff --git a/packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts b/packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts index ba1072ff1..85e109f13 100644 --- a/packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts +++ b/packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts @@ -9,6 +9,10 @@ import { ActionBar, ActionItem } from './ActionBar'; import { Color } from '../../plugin-color/src/Color'; import { RenderingEngine } from '../../plugin-renderer/src/RenderingEngine'; import { ReactiveValue } from '../../utils/src/ReactiveValue'; +import { Positionable, PositionableVerticalAlignment } from '../../utils/src/Positionable'; +import { DomLayout } from '../../plugin-dom-layout/src/DomLayout'; +import { Layout } from '../../plugin-layout/src/Layout'; +import { DomLayoutEngine } from '../../plugin-dom-layout/src/DomLayoutEngine'; export class ActionBarDomObjectRenderer extends NodeRenderer { static id = DomObjectRenderingEngine.id; @@ -23,27 +27,51 @@ export class ActionBarDomObjectRenderer extends NodeRenderer { } async render(): Promise { - const domNode = document.createElement('jw-actionbar'); + const actionBar = document.createElement('jw-actionbar'); + let positionable: Positionable; + const updateActions = (): void => { + const container = this.engine.editor.selection.range.startContainer; + const layout = this.engine.editor.plugins.get(Layout); + const domLayoutEngine = layout.engines.dom as DomLayoutEngine; + const selectedDomNode = domLayoutEngine.getDomNodes(container)[0] as HTMLElement; + + console.log('selectedDomNode:', selectedDomNode); + + positionable.resetRelativeElement(selectedDomNode); + positionable.resetPositionedElement(); + const actionItems = this.availableActions.get(); if (actionItems.length) { - domNode.innerHTML = ''; + actionBar.style.display = 'block'; + actionBar.innerHTML = ''; for (const node of this._renderActionNode(actionItems)) { - domNode.appendChild(node); + actionBar.appendChild(node); } } else { - domNode.style.display = 'none'; + actionBar.style.display = 'none'; } }; const attach = (): void => { + console.log('attach'); + positionable = new Positionable({ + positionedElement: actionBar, + verticalAlignment: PositionableVerticalAlignment.BOTTOM, + }); + positionable.bind(); this.availableActions.on('set', updateActions); + setTimeout(() => updateActions(), 1000); }; const detach = (): void => { + console.log('detach'); + positionable.destroy(); this.availableActions.off('set', updateActions); }; - return { dom: [domNode], attach, detach }; + return { dom: [actionBar], attach, detach }; } + _positionNode() {} + _renderActionNode(actionItems: ActionItem[]): HTMLElement[] { return actionItems.map(item => { const element = document.createElement('jw-action-item'); diff --git a/packages/utils/src/EventMixin2.ts b/packages/utils/src/EventMixin2.ts index de03b833e..9604821a6 100644 --- a/packages/utils/src/EventMixin2.ts +++ b/packages/utils/src/EventMixin2.ts @@ -1,8 +1,3 @@ -import { VersionableObject } from '../../core/src/Memory/VersionableObject'; -import { VersionableArray } from '../../core/src/Memory/VersionableArray'; -import { makeVersionable } from '../../core/src/Memory/Versionable'; -import { VersionableSet } from '../../core/src/Memory/VersionableSet'; - /** * Abstract class to add event mechanism. */ diff --git a/packages/utils/src/Positionable.ts b/packages/utils/src/Positionable.ts index 5f2c96b69..b6a96d8f1 100644 --- a/packages/utils/src/Positionable.ts +++ b/packages/utils/src/Positionable.ts @@ -24,15 +24,32 @@ function getBoundingClientRect(elem: HTMLElement): BoundingRect { const POSITIONABLE_TAG_NAME = 'jw-positionable'; const POSITIONED_TAG_NAME = 'jw-positionned'; +export enum PositionableVerticalAlignment { + TOP = 'TOP', + BOTTOM = 'BOTTOM', +} +export enum PositionableHorizontalAlignment { + LEFT = 'LEFT', + RIGHT = 'RIGHT', +} + interface PositionableOptions { /** * The element in relative in which we base the position. */ - relativeElement: HTMLElement; + relativeElement?: HTMLElement; /** * The element to position. */ positionedElement: HTMLElement; + /** + * Vertical alignment. + */ + verticalAlignment?: PositionableVerticalAlignment; + /** + * Vertical alignment. + */ + horizontalAlignment?: PositionableHorizontalAlignment; /** * The container into which the box is held. If not provided, A * jw-positionable node will be appended in the body. @@ -40,13 +57,24 @@ interface PositionableOptions { container?: HTMLElement; } export class Positionable { - private _relativeElement: PositionableOptions['relativeElement']; + private _relativeElement?: PositionableOptions['relativeElement']; private _positionedElement: PositionableOptions['positionedElement']; private _positionedElementContainer: HTMLElement; private _container: HTMLElement; + private _verticalAlignment: PositionableOptions['verticalAlignment']; + private _horizontalAlignment: PositionableOptions['horizontalAlignment']; + private _resizeObserver: ResizeObserver; + constructor(options: PositionableOptions) { - this._relativeElement = options.relativeElement; + this._resizeObserver = new ResizeObserver(this.resetPositionedElement.bind(this)); + if (options.relativeElement) { + this.resetRelativeElement(options.relativeElement); + } this._positionedElement = options.positionedElement; + this._verticalAlignment = options.verticalAlignment || PositionableVerticalAlignment.TOP; + this._horizontalAlignment = + options.horizontalAlignment || PositionableHorizontalAlignment.LEFT; + if (options.container) { this._container = options.container; } else { @@ -70,21 +98,44 @@ export class Positionable { this.bind(); setTimeout(this.resetPositionedElement.bind(this), 0); } + resetRelativeElement(element: HTMLElement): void { + if (this._relativeElement) { + this._resizeObserver.unobserve(this._relativeElement); + } + this._relativeElement = element; + this._resizeObserver.observe(this._relativeElement); + } resetPositionedElement(): void { + if (!this._relativeElement) return; + const coords1 = getBoundingClientRect(this._relativeElement); const coords2 = getBoundingClientRect(this._positionedElement); - // right top position - const x = coords1.right - coords2.width; - const y = coords1.top - coords2.height; + let x: number; + let y: number; + if (this._verticalAlignment === PositionableVerticalAlignment.TOP) { + y = coords1.top - coords2.height; + } else { + y = coords1.top + coords2.height; + console.log('coords1.top:', coords1.top); + console.log('coords2.height:', coords2.height); + console.log('y', y); + } + if (this._horizontalAlignment === PositionableHorizontalAlignment.RIGHT) { + x = coords1.right - coords2.width; + } else { + x = coords1.left; + } this._positionedElementContainer.style.left = x + 'px'; this._positionedElementContainer.style.top = y + 'px'; } bind(): void { + console.log('bind'); document.body.addEventListener('scroll', this._onScroll, true); } unbind(): void { + console.log('unbind'); document.body.removeEventListener('scroll', this._onScroll, true); } destroy(): void { @@ -98,6 +149,7 @@ export class Positionable { } } private _onScroll(): void { + console.log('resetpositioned'); this.resetPositionedElement(); } } diff --git a/packages/utils/src/ResizeObserver.d.ts b/packages/utils/src/ResizeObserver.d.ts new file mode 100644 index 000000000..982a9e361 --- /dev/null +++ b/packages/utils/src/ResizeObserver.d.ts @@ -0,0 +1,239 @@ +/** + * The **ResizeObserver** interface reports changes to the dimensions of an + * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)'s content + * or border box, or the bounding box of an + * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement). + * + * > **Note**: The content box is the box in which content can be placed, + * > meaning the border box minus the padding and border width. The border box + * > encompasses the content, padding, and border. See + * > [The box model](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model) + * > for further explanation. + * + * `ResizeObserver` avoids infinite callback loops and cyclic dependencies that + * are often created when resizing via a callback function. It does this by only + * processing elements deeper in the DOM in subsequent frames. Implementations + * should, if they follow the specification, invoke resize events before paint + * and after layout. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver + */ +declare class ResizeObserver { + /** + * The **ResizeObserver** constructor creates a new `ResizeObserver` object, + * which can be used to report changes to the content or border box of an + * `Element` or the bounding box of an `SVGElement`. + * + * @example + * var ResizeObserver = new ResizeObserver(callback) + * + * @param callback + * The function called whenever an observed resize occurs. The function is + * called with two parameters: + * * **entries** + * An array of + * [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) + * objects that can be used to access the new dimensions of the element + * after each change. + * * **observer** + * A reference to the `ResizeObserver` itself, so it will definitely be + * accessible from inside the callback, should you need it. This could be + * used for example to automatically unobserve the observer when a certain + * condition is reached, but you can omit it if you don't need it. + * + * The callback will generally follow a pattern along the lines of: + * ```js + * function(entries, observer) { + * for (let entry of entries) { + * // Do something to each entry + * // and possibly something to the observer itself + * } + * } + * ``` + * + * The following snippet is taken from the + * [resize-observer-text.html](https://mdn.github.io/dom-examples/resize-observer/resize-observer-text.html) + * ([see source](https://github.com/mdn/dom-examples/blob/master/resize-observer/resize-observer-text.html)) + * example: + * @example + * const resizeObserver = new ResizeObserver(entries => { + * for (let entry of entries) { + * if(entry.contentBoxSize) { + * h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem'; + * pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem'; + * } else { + * h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem'; + * pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem'; + * } + * } + * }); + * + * resizeObserver.observe(divElem); + */ + constructor(callback: ResizeObserverCallback); + + /** + * The **disconnect()** method of the + * [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) + * interface unobserves all observed + * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or + * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) + * targets. + */ + disconnect: () => void; + + /** + * The `observe()` method of the + * [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) + * interface starts observing the specified + * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or + * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement). + * + * @example + * resizeObserver.observe(target, options); + * + * @param target + * A reference to an + * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or + * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) + * to be observed. + * + * @param options + * An options object allowing you to set options for the observation. + * Currently this only has one possible option that can be set. + */ + observe: (target: Element, options?: ResizeObserverObserveOptions) => void; + + /** + * The **unobserve()** method of the + * [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) + * interface ends the observing of a specified + * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or + * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement). + */ + unobserve: (target: Element) => void; +} + +interface ResizeObserverObserveOptions { + /** + * Sets which box model the observer will observe changes to. Possible values + * are `content-box` (the default), and `border-box`. + * + * @default "content-box" + */ + box?: 'content-box' | 'border-box'; +} + +/** + * The function called whenever an observed resize occurs. The function is + * called with two parameters: + * + * @param entries + * An array of + * [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) + * objects that can be used to access the new dimensions of the element after + * each change. + * + * @param observer + * A reference to the `ResizeObserver` itself, so it will definitely be + * accessible from inside the callback, should you need it. This could be used + * for example to automatically unobserve the observer when a certain condition + * is reached, but you can omit it if you don't need it. + * + * The callback will generally follow a pattern along the lines of: + * @example + * function(entries, observer) { + * for (let entry of entries) { + * // Do something to each entry + * // and possibly something to the observer itself + * } + * } + * + * @example + * const resizeObserver = new ResizeObserver(entries => { + * for (let entry of entries) { + * if(entry.contentBoxSize) { + * h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem'; + * pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem'; + * } else { + * h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem'; + * pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem'; + * } + * } + * }); + * + * resizeObserver.observe(divElem); + */ +type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void; + +/** + * The **ResizeObserverEntry** interface represents the object passed to the + * [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver) + * constructor's callback function, which allows you to access the new + * dimensions of the + * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or + * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) + * being observed. + */ +interface ResizeObserverEntry { + /** + * An object containing the new border box size of the observed element when + * the callback is run. + */ + readonly borderBoxSize: ResizeObserverEntryBoxSize; + + /** + * An object containing the new content box size of the observed element when + * the callback is run. + */ + readonly contentBoxSize: ResizeObserverEntryBoxSize; + + /** + * A [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly) + * object containing the new size of the observed element when the callback is + * run. Note that this is better supported than the above two properties, but + * it is left over from an earlier implementation of the Resize Observer API, + * is still included in the spec for web compat reasons, and may be deprecated + * in future versions. + */ + // node_modules/typescript/lib/lib.dom.d.ts + readonly contentRect: DOMRectReadOnly; + + /** + * A reference to the + * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or + * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) + * being observed. + */ + readonly target: Element; +} + +/** + * The **borderBoxSize** read-only property of the + * [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) + * interface returns an object containing the new border box size of the + * observed element when the callback is run. + */ +interface ResizeObserverEntryBoxSize { + /** + * The length of the observed element's border box in the block dimension. For + * boxes with a horizontal + * [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), + * this is the vertical dimension, or height; if the writing-mode is vertical, + * this is the horizontal dimension, or width. + */ + blockSize: number; + + /** + * The length of the observed element's border box in the inline dimension. + * For boxes with a horizontal + * [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), + * this is the horizontal dimension, or width; if the writing-mode is + * vertical, this is the vertical dimension, or height. + */ + inlineSize: number; +} + +interface Window { + ResizeObserver: typeof ResizeObserver; +}