diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7a5215..2b38e0da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## next +- Added `setStageTitle` method to the prepare context API to display additional text on the progress bar: +```js +export function async prepare(input, { setStageTitle }) { + await setStageTitle('phase 1'); + // ... + await setStageTitle('phase 2'); + // ... +} +``` - Fixed crashing the entire render tree on an exception in a view's `render` function; now, crashes are isolated to the affected view - Fixed unnecessary view rendering when returning to the discovery page - Fixed hiding a popup with `hideOnResize: true` when scrolling outside of the popup element diff --git a/src/core/utils/progressbar.ts b/src/core/utils/progressbar.ts index 2b44765d..af8708af 100644 --- a/src/core/utils/progressbar.ts +++ b/src/core/utils/progressbar.ts @@ -137,6 +137,8 @@ export default class Progressbar extends Observer { appearanceDelay: number; domReady: Promise; el: HTMLElement; + #titleEl: HTMLElement; + #stepEl: HTMLElement; constructor({ onTiming, onFinish, delay, domReady }: ProgressbarOptions) { super({ stage: 'inited', progress: null, error: null }); @@ -152,7 +154,10 @@ export default class Progressbar extends Observer { this.domReady = domReady || Promise.resolve(); this.el = createElement('div', 'view-progress init', [ - createElement('div', 'title'), + createElement('div', 'content main-secondary', [ + this.#titleEl = createElement('span', 'main'), + this.#stepEl = createElement('span', 'secondary') + ]), createElement('div', 'progress') ]); } @@ -173,6 +178,16 @@ export default class Progressbar extends Observer { this.onTiming(entry); } + async #awaitRenderIfNeeded(enforce = false, now = performance.now()) { + const timeSinceAwaitRepaint = now - (this.awaitRepaint || 0); + const timeSinceLastStageStart = now - (this.lastStageStart || 0); + + if (enforce || (timeSinceAwaitRepaint > 65 && timeSinceLastStageStart > 200)) { + await letRepaintIfNeeded(); + this.awaitRepaint = performance.now(); + } + } + async setState(state: Partial) { const { stage = this.lastStage, progress = null, error = null } = state; @@ -224,18 +239,20 @@ export default class Progressbar extends Observer { } const { title, progressValue } = decodeStageProgress(stage, progress); - const titleEl = this.el.querySelector('.title'); this.el.style.setProperty('--progress', String(progressValue)); - if (titleEl !== null) { - titleEl.textContent = title; - } + this.#titleEl.textContent = title; + this.#stepEl.textContent = ''; - if (stageChanged || (now - (this.awaitRepaint || 0) > 65 && now - (this.lastStageStart || 0) > 200)) { - await letRepaintIfNeeded(); - this.awaitRepaint = performance.now(); - } + return this.#awaitRenderIfNeeded(stageChanged, now); + } + + async setStateStep(name: string) { + this.#titleEl.textContent = (this.#titleEl.textContent || '').replace(/(\.{3}|:)?$/, ':'); + this.#stepEl.textContent = name; + + return this.#awaitRenderIfNeeded(true); } finish(error?: Error) { diff --git a/src/main/model-extension-api.ts b/src/main/model-extension-api.ts index 00b7e945..e5ae0d1f 100644 --- a/src/main/model-extension-api.ts +++ b/src/main/model-extension-api.ts @@ -1,14 +1,15 @@ -import jora from 'jora'; -import type { Model, ModelOptions, PageParams, PageRef, PrepareContextApiWrapper, SetupMethods } from './model.js'; +import type { Model, ModelOptions, PageParams, PageRef, PrepareContextApiWrapper, SetDataOptions, SetupMethods } from './model.js'; import type { ObjectMarkerConfig } from '../core/object-marker.js'; +import jora from 'jora'; -export function createExtensionApi(host: Model): PrepareContextApiWrapper { +export function createExtensionApi(host: Model, options?: SetDataOptions): PrepareContextApiWrapper { return { before() { host.objectMarkers.reset(); }, contextApi: { markers: host.objectMarkers.markerMap(), + setStageTitle: options?.setPrepareStepTitle || (() => Promise.resolve()), rejectData(message: string, renderContent: any) { throw Object.assign(new Error(message), { renderContent }); } diff --git a/src/main/model-legacy-extension-api.ts b/src/main/model-legacy-extension-api.ts index c48d96ac..5fab1089 100644 --- a/src/main/model-legacy-extension-api.ts +++ b/src/main/model-legacy-extension-api.ts @@ -1,13 +1,14 @@ -import jora from 'jora'; -import ObjectMarker, { ObjectMarkerConfig } from '../core/object-marker.js'; -import type { LegacyPrepareContextApi, PrepareContextApiWrapper, Model, Query, PageRef, PageParams } from './model.js'; +import type { LegacyPrepareContextApi, PrepareContextApiWrapper, Model, Query, PageRef, PageParams, SetDataOptions } from './model.js'; import type { ValueAnnotationContext, Widget } from './widget.js'; +import ObjectMarker, { ObjectMarkerConfig } from '../core/object-marker.js'; +import jora from 'jora'; -export function createLegacyExtensionApi(host: Model): PrepareContextApiWrapper { +export function createLegacyExtensionApi(host: Model, options?: SetDataOptions): PrepareContextApiWrapper { const objectMarkers = new ObjectMarker(); const linkResolvers: Model['linkResolvers'] = []; const annotations: Widget['annotations'] = []; const contextApi: LegacyPrepareContextApi = { + setStageTitle: options?.setPrepareStepTitle || (() => Promise.resolve()), rejectData(message: string, renderContent: any) { throw Object.assign(new Error(message), { renderContent }); }, diff --git a/src/main/model.ts b/src/main/model.ts index cb7462eb..bb0fb915 100644 --- a/src/main/model.ts +++ b/src/main/model.ts @@ -45,6 +45,7 @@ export type ModelDataset = Dataset | RawDataDataset; export type GetDecodeParams = (pageId: string) => (entries: [string, any][]) => object; export interface SetDataOptions { + setPrepareStepTitle?: (name: string) => Promise; dataset?: Dataset; } @@ -64,10 +65,12 @@ export type PrepareContextApiWrapper = { contextApi: PrepareContextApi | LegacyPrepareContextApi; }; export interface PrepareContextApi { + setStageTitle: (name: string) => Promise; rejectData: (message: string, extra: any) => void; markers: Record void>; } export interface LegacyPrepareContextApi { + setStageTitle: (name: string) => Promise; rejectData: (message: string, extra: any) => void; defineObjectMarker(name: string, options: ObjectMarkerConfig): ObjectMarker['mark']; lookupObjectMarker(value: any, type?: string): ObjectMarkerDescriptor | null; @@ -227,7 +230,7 @@ export class Model< this.prepare = fn; } - setData(data: unknown, options: SetDataOptions) { + setData(data: unknown, options?: SetDataOptions) { options = options || {}; // mark as last setData promise @@ -243,8 +246,8 @@ export class Model< }; const prepareApi = this.#legacyPrepare - ? createLegacyExtensionApi(this) - : createExtensionApi(this); + ? createLegacyExtensionApi(this, options) + : createExtensionApi(this, options); const setDataPromise = Promise.resolve() .then(() => { checkIsNotPrevented(); diff --git a/src/main/widget.ts b/src/main/widget.ts index ffe5bdec..afa82691 100644 --- a/src/main/widget.ts +++ b/src/main/widget.ts @@ -245,6 +245,7 @@ export class Widget< await progressbar?.setState({ stage: 'prepare' }); await this.setData(data, context, { dataset, + setPrepareStepTitle: progressbar?.setStateStep.bind(progressbar), render: false }); diff --git a/src/views/controls/progress.css b/src/views/controls/progress.css index c5ebad6b..082b4594 100644 --- a/src/views/controls/progress.css +++ b/src/views/controls/progress.css @@ -32,3 +32,15 @@ /* transition: transform .2s; */ /* since Chrome (tested on 85) freezes transition during js loop */ background-color: var(--color, #1f7ec5); } + +.view-progress > .content.main-secondary { + display: flex; + white-space: nowrap; + gap: 1ex; +} +.view-progress > .content > .secondary { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + color: #888; +} diff --git a/src/views/controls/progress.js b/src/views/controls/progress.js index 615c9f7a..e599b36a 100644 --- a/src/views/controls/progress.js +++ b/src/views/controls/progress.js @@ -5,13 +5,16 @@ import usage from './progress.usage.js'; export default function(host) { host.view.define('progress', function(el, config, data, context) { const { content, progress, color } = config; - const progressEl = el.appendChild(createElement('div', { + + el.append(createElement('div', { class: 'progress', style: `--progress: ${Math.max(0, Math.min(1, Number(progress)))};--color: ${color || 'unset'};` })); if (content) { - const contentEl = el.insertBefore(createElement('div', { class: 'content' }), progressEl); + const contentEl = createElement('div', 'content'); + + el.prepend(contentEl); return host.view.render(contentEl, content, data, context); }