Mixins / foldkit-mixins-ui

WindowList

namespace · packages/mixins-ui/src/windowList.ts:30

export * as WindowList from './windowList.js'

A virtual list that scrolls with the window: it draws the rows in view of the document's viewport, and spacers as tall as the rest, so a list of thousands below a header and above a footer costs what thirty rows do, while the page keeps its own scrolling. Nothing in it scrolls: the browser restores the document's offset on back and reload, a sticky header above it sticks, and the page's keys (Space, End) move it.

Track (on root) -> Placed { top, height } -> update -> viewport
                -> Measured { rows }      -> update -> heights
view: windowOf(model, keys) -> start spacer, rows [start, end), end spacer

top is how far the viewport's top is past the list's, so content above the list, or growing above it, is read every time and never assumed. A row's height is its border box, measured once drawn; until then it is estimatedHeight, or with unmeasured: 'mean' the mean of the rows measured. A row is drawn display: flow-root, so space rows with padding, not margins. @foldkit/ui's VirtualList is the list in a box of its own.

A server's page can open the list at a row (init({ from })): its window starts there, and the rows above it take no room until Track places the list, which gives them their room and scrolls the window by as much, so the row stays where the reader saw it. A page without scripts draws that window in the page's flow (Recipes.WindowList hides the spacers) and pages by links (pageOf).

In the guides

Members

WindowList.init

function · packages/mixins-ui/src/windowList.ts:115

function init(config: InitConfig): Model

A list with nothing measured. Throws on an estimate that is not above 0, or an overscan or a from that is not a whole number.

WindowList.InitConfig

interface · packages/mixins-ui/src/windowList.ts:80

interface InitConfig {
  readonly id: string
  readonly estimatedHeight: number
  readonly unmeasured?: Model['unmeasured']
  readonly overscan?: number
  readonly viewportHeight?: number
  readonly from?: number
}

WindowList.keep

function · packages/mixins-ui/src/windowList.ts:189

function keep(model: Model, keys: ReadonlyArray<string>): Model

The Model without the heights of rows no longer in keys, its 'mean' taken again over those kept; the same Model when none left.

WindowList.Message

const · packages/mixins-ui/src/windowList.ts:73

const Message: MessageUnion<{
  readonly Placed: {
    readonly top: Schema.Number
    readonly height: Schema.Number
  }
  readonly Measured: {
    readonly rows: Schema.$Array<
      Schema.Struct<{
        readonly key: Schema.String
        readonly height: Schema.Number
      }>
    >
  }
}>

WindowList.Model

const · packages/mixins-ui/src/windowList.ts:56

const Model: Schema.Struct<{
  readonly id: Schema.String
  readonly estimatedHeight: Schema.Number
  readonly unmeasured: Schema.Literals<readonly ['estimate', 'mean']>
  readonly overscan: Schema.Number
  readonly heights: Schema.$Record<Schema.String, Schema.Number>
  readonly viewport: Schema.Struct<{
    readonly top: Schema.Number
    readonly height: Schema.Number
  }>
  readonly from: Schema.Number
}>

WindowList.Page

interface · packages/mixins-ui/src/windowList.ts:247

interface Page {
  readonly start: number
  readonly end: number
  readonly previous: Option.Option<number>
  readonly next: Option.Option<number>
}

The rows drawn and the rows to open next, for links that page a list without scripts (?from=): previous is the first row whose window reaches this one's start, next this one's end; none at either end of the list.

WindowList.pageOf

function · packages/mixins-ui/src/windowList.ts:255

function pageOf(model: Model, keys: ReadonlyArray<string>): Page

The page windowOf draws, and the rows the pages either side of it open at.

WindowList.resolveTrack

function · packages/mixins-ui/src/windowList.ts:364

function resolveTrack<Outer>(
  onMessage: (message: Message) => Outer,
  reading: Message,
): Scene.MountResolver<Outer>

Track resolved with reading, for a Scene test of a page that draws the list: the reading goes through the view's own onMessage, as it does when the Mount reports, so the page's update receives its own Message. Scene.Mount.resolveAll(WindowList.resolveTrack(onMessage, WindowList.Message.Placed({ top: 0, height: 900 }))).

WindowList.Track

const · packages/mixins-ui/src/windowList.ts:338

const Track: Mount.MountDefinitionNoArgs<
  'WindowList.Track',
  | { readonly _tag: 'Placed'; readonly top: number; readonly height: number }
  | {
      readonly _tag: 'Measured'
      readonly rows: readonly {
        readonly key: string
        readonly height: number
      }[]
    }
>

The list's one Mount, on its root: where the viewport lies over the list (WindowViewport, as Placed) and the heights of the rows drawn (MeasureRows, as Measured). A failure in either ends both, reported as this Mount's. Before either reads, it gives the rows above a list opened at a row their room. A Scene test resolves it with resolveTrack.

WindowList.update

function · packages/mixins-ui/src/windowList.ts:157

function update(
  model: Model,
  message: Message,
): {
  readonly model: Model
}

The list after message; the same Model when nothing changes.

WindowList.view

function · packages/mixins-ui/src/windowList.ts:370

function view<Item, Outer>(
  options: WindowListView<Item, Outer>,
  h: HtmlBuilder<Outer>,
): Html

The list: the rows in view of the window, between spacers as tall as the rest.

WindowList.WindowListSlots

const · packages/mixins-ui/src/windowList.ts:48

const WindowListSlots: Slots.Contract<{
  readonly root: Slot.UnnamedSlot<
    Capability.Capability<
      'Container',
      readonly [
        Capability.Capability<
          'Interactive',
          readonly [Capability.Capability<'Base', readonly []>]
        >,
      ]
    >,
    readonly Slot.EventName[],
    readonly Slot.AttributeName[],
    readonly Slot.RequirementName[],
    boolean
  >
  readonly spacer: Slot.UnnamedSlot<
    Capability.Capability<
      'Container',
      readonly [
        Capability.Capability<
          'Interactive',
          readonly [Capability.Capability<'Base', readonly []>]
        >,
      ]
    >,
    readonly Slot.EventName[],
    readonly Slot.AttributeName[],
    readonly Slot.RequirementName[],
    boolean
  >
  readonly row: Slot.UnnamedSlot<
    Capability.Capability<
      'Container',
      readonly [
        Capability.Capability<
          'Interactive',
          readonly [Capability.Capability<'Base', readonly []>]
        >,
      ]
    >,
    readonly Slot.EventName[],
    readonly Slot.AttributeName[],
    readonly Slot.RequirementName[],
    boolean
  >
}>

WindowList.WindowListView

type · packages/mixins-ui/src/windowList.ts:273

type WindowListView<Item, Outer> = Readonly<{
  model: Model
  items: ReadonlyArray<Item>
  itemToKey: (item: Item, index: number) => string
  itemToView: (item: Item, index: number) => Html
  onMessage: (message: Message) => NoInfer<Outer>
  label?: string | undefined
  style?: NamedStyle<typeof WindowListSlots> | undefined
  mixins?: MixinList<Outer> | undefined
}>

WindowList.windowOf

function · packages/mixins-ui/src/windowList.ts:227

function windowOf(model: Model, keys: ReadonlyArray<string>): VirtualWindow

The rows drawn, [start, end): those the viewport overlaps, and overscan more at each end; until a list opened at a row is placed, the rows from that one. Keep keys between calls: the offsets are cached by its identity.