diff --git a/bqplot_image_gl/viewlistener.py b/bqplot_image_gl/viewlistener.py index ef28f2d..7ce731c 100644 --- a/bqplot_image_gl/viewlistener.py +++ b/bqplot_image_gl/viewlistener.py @@ -2,10 +2,21 @@ from ipywidgets.widgets import widget_serialization from traitlets import Unicode, Dict, Instance from bqplot_image_gl._version import __version__ +from typing import cast, Dict as DictType +from typing_extensions import TypedDict __all__ = ['ViewListener'] +class ViewDataEntry(TypedDict): + x: float + y: float + width: float + height: float + resized_at: str # ISO 8601 + focused_at: str # ISO 8601 + + @widgets.register class ViewListener(widgets.DOMWidget): _view_name = Unicode('ViewListener').tag(sync=True) @@ -17,7 +28,7 @@ class ViewListener(widgets.DOMWidget): widget = Instance(widgets.Widget).tag(sync=True, **widget_serialization) css_selector = Unicode(None, allow_none=True).tag(sync=True) - view_data = Dict().tag(sync=True) + view_data = Dict(value_trait=cast(DictType[str, ViewDataEntry], {})).tag(sync=True) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/js/lib/ViewListener.js b/js/lib/ViewListener.js index 0095374..6cd945c 100644 --- a/js/lib/ViewListener.js +++ b/js/lib/ViewListener.js @@ -34,6 +34,9 @@ class ViewListenerModel extends base.DOMWidgetModel { } bind(this.get('widget')); window.lastViewListenerModel = this; + window.addEventListener('focus', () => { + this._updateAllViewData(); + }); } async _getViews() { const widgetModel = this.get('widget'); @@ -52,12 +55,12 @@ class ViewListenerModel extends base.DOMWidgetModel { this.set('view_data', {}) // clear data const selector = this.get('css_selector'); // initial fill - this._updateViewData(); + this._updateAllViewData(); // listen to element for resize events views.forEach((view) => { const resizeObserver = new ResizeObserver(entries => { - this._updateViewData(); + this._updateViewData(view, true); }); let el = view.el; el = selector ? el.querySelector(selector) : el; @@ -70,25 +73,38 @@ class ViewListenerModel extends base.DOMWidgetModel { } }) } - async _updateViewData() { - const views = await this._getViews(); + async _updateViewData(view, resized=false, focused=false) { const selector = this.get('css_selector'); + let el = view.el; + el = selector ? el.querySelector(selector) : el; + if(el) { + const {x, y, width, height} = el.getBoundingClientRect(); + const previousData = this.get('view_data'); + let resized_at = previousData[view.cid]?.resized_at; + let focused_at = previousData[view.cid]?.focused_at; + const currentDateTimeJSISO = (new Date()).toISOString(); + // Javascripts toISOString and Python's datetime.fromisoformat implement different parts of + // the ISO 8601 standard, so we replace Z (indicating Zulu time) with +00:00 to make it compatible with python + const currentDateTime = currentDateTimeJSISO.replace('Z', '+00:00'); + + resized_at = (resized || resized_at === undefined) ? currentDateTime : resized_at; + focused_at = (focused || focused_at === undefined) ? currentDateTime : focused_at; + this.send({ + event: 'set_view_data', + id: view.cid, + data: { x, y, width, height, resized_at, focused_at }, + }) + } else { + console.error('could not find element with css selector', selector); + } + } + async _updateAllViewData() { + const views = await this._getViews(); const currentViews = new Set(); views.forEach((view) => { currentViews.add(view.cid); this.knownViews.add(view.cid); - let el = view.el; - el = selector ? el.querySelector(selector) : el; - if(el) { - const {x, y, width, height} = el.getBoundingClientRect(); - this.send({ - event: 'set_view_data', - id: view.cid, - data: { x, y, width, height}, - }) - } else { - console.error('could not find element with css selector', selector); - } + this._updateViewData(view, true, true); }); const removeViews = [...this.knownViews].filter((cid) => !currentViews.has(cid)); removeViews.forEach((cid) => {