import { Scope } from "./core/reactive.ts"; import { type State, type Derived } from "./core/handle.ts"; import { type AjaxOptions, type AjaxPromise } from "./compat.ts"; import { resource, action, type Resource, type Action } from "./resource.ts"; import { component, componentAttr, mount, unmount, type ComponentSetup } from "./component.ts"; import { type FieldRule, type FormApi, type FormOptions } from "./safe-form.ts"; import { bind, type StateBag, type BindOptions } from "./directives.ts"; export type { State, Derived, Resource, Action, ComponentSetup, FieldRule, FormApi, FormOptions, StateBag, BindOptions, AjaxOptions, AjaxPromise, }; /** "@total" → '[data-brg="total"]' / "@list li" → '[data-brg="list"] li' */ export declare function resolveSelector(selector: string): string; export declare class Bridgey { els: HTMLElement[]; /** 何で引いたか(警告メッセージに出すため)。 */ selector: string | null; /** どこから引いたか(後から足された要素を拾い直すため)。 */ roots: HTMLElement[]; constructor(input?: unknown, selector?: string | null, roots?: HTMLElement[]); get length(): number; get el(): HTMLElement | null; /** * 束縛しようとしたのに0件だった場合に警告する。 * 命令的操作(addClass 等)の0件は無害なことも多いので、そちらでは鳴らさない。 */ _requireElements(method: string): boolean; /** 各要素に effect を張り、要素の片付け単位に登録する。 */ _bind(method: string, run: (el: HTMLElement) => void): this; each(fn: (el: HTMLElement, index: number) => void): this; find(selector: string): Bridgey; closest(selector: string): Bridgey; parent(): Bridgey; children(selector?: string): Bridgey; first(): Bridgey; last(): Bridgey; is(selector: string): boolean; /** * テキスト。引数は「値 → 加工 → 形」の順に足していく。 * * .text(count) → "3" * .text(count, "件") → "3件" 後ろに付く * .text(count, "残り{}件") → "残り3件" {} の位置に入る * .text(count, n => n * 1.1) → "3.3" 加工だけ * .text(count, n => n * 1.1, "円") → "3.3円" 加工してから後ろに付ける * .text(count, n => n * 1.1, "税込{}円") → "税込3.3円" 加工してから {} に入れる * .text(total, $$.money, "円") → "23,800円" 3桁区切り * * これがあるので `() => \`${…}円\`` を書かなくて済む。 */ text(): string; text(value: unknown, format?: TextFormat, template?: string): this; html(): string; html(value: unknown): this; /** * フォーム値。jQuery と完全に同じ振る舞い(普通の文字列を返す)。 * $$("#q").val() … 文字列。=== "" も期待通り動く * $$("#q").state("") … この入力欄に双方向で繋がった state を作る * * 【設計メモ】以前は .val().state("") と書けるよう String のサブクラスを返していたが、 * `.val() === ""` が false になる罠を作るのでやめた。詰まったときに原因が分からない * 種類のバグは、書き味の良さと引き換えにしてはいけない。 */ val(): string; val(value: unknown): this; attr(name: string): string | null; attr(name: string, value: unknown): this; removeAttr(name: string): this; prop(name: string): unknown; prop(name: string, value: unknown): this; css(name: string): string; css(name: string, value: unknown): this; addClass(names: string): this; removeClass(names: string): this; /** * class の付け外し。真偽の state を渡せば追従する。 * $$("@box").toggleClass("is-open", open) ← fadeIn/slideDown を持たない理由がこれ */ toggleClass(names: string, force?: unknown): this; hasClass(name: string): boolean; /** * 排他的な状態を data-mode 属性に出す。CSS 側は [data-mode="…"] で受ける。 * $$("@form").mode(() => sending() ? "sending" : error() ? "error" : "ready"); * →
... CSS: [data-mode="sending"] .spinner { display:block } * * falsy(false / null / undefined / "") なら属性そのものを外す。 * 第2引数で別名にできる: mode(value, "phase") → data-phase */ mode(value: unknown, attribute?: string): this; /** * 真偽で属性の有無を切り替える。CSS 側は [data-open] で受ける。 * $$("@panel").toggleAttr("data-open", isOpen); * * .attr("data-open", false) だと文字列 "false" が入ってしまい * [data-open] セレクタに当たり続ける(= 閉じたのに開いて見える)。その罠を避けるための API。 */ toggleAttr(name: string, condition?: unknown): this; /** 選択中( .active の正しい表現 )。 aria-selected + [data-selected] */ selected(condition?: unknown): boolean | this; /** 開いている(アコーディオン・ドロップダウンのトリガー)。 aria-expanded は false も明示する */ expanded(condition?: unknown): boolean | this; /** 入力エラー。 aria-invalid + [data-invalid] */ invalid(condition?: unknown): boolean | this; /** 読み込み中。 aria-busy + [data-busy] ($$.resource の loading をそのまま渡せる) */ busy(condition?: unknown): boolean | this; /** * 操作不可。ネイティブに disabled があれば本物の disabled を使い、 * 無ければ aria-disabled で伝える(a や div をボタンにしている現場のため)。 * $$("@save").disabled(save.running); // 二重送信対策がこれ1行 */ disabled(condition?: unknown): boolean | this; /** UI 状態の共通処理(静的な値でも state でも同じ書き味にする)。 */ _uiState(method: string, condition: unknown, apply: (el: HTMLElement, on: boolean) => void): this; /** * CSS カスタムプロパティに値を流す。見た目の決定権は CSS に残る。 * $$("@bar").vars({ progress: () => `${percent()}%` }); * → style="--progress: 60%" CSS: .bar__fill { width: var(--progress) } * * $$("@card").vars({ "--x": () => `${x()}px`, "--y": () => `${y()}px` }); */ vars(map: Record): this; data(key: string): string | undefined; data(key: string, value: unknown): this; append(content: unknown): this; empty(): this; remove(): this; /** * 一覧を描く。行は使い回すので、入力中の文字・スクロール・フォーカスが飛ばない。 * * ① テンプレートを HTML に置く(推奨) * * * * $$("#list").repeat(todos, { * key: (t) => t.id, * tpl: "#row-tpl", * render: ($row, t) => $row.find(".title").text(t.title), * }); * * ② サーバーが既に出力している一覧を乗っ取る(HTML を1文字も変えない) * $$("#list").repeat(rows, { from: "li", key: r => r.id, render: … }); * * ③ 簡単な行なら文字列で(短縮形) * $$("#list").repeat(todos, (t) => `
  • ${t.title}
  • `); */ repeat(items: unknown, options: RepeatOptions | RowHtml): this; /** * 条件表示({#if} の代わり)。 * $$("@error").when(hasError) … display の出し入れ * $$("@modal").when(open, { detach: true }) … 偽なら DOM から外す */ when(condition: unknown, options?: { detach?: boolean; }): this; /** * この要素に結びついた state を作る。 * const count = $$("@count").state(0); … 側面は自動推論 * const name = $$("@name").state("", "value"); … 明示 * const done = $$("@chk").state(false, "checked"); * const src = $$("@img").state(url, "attr:src"); * const n = $$("@count").state(); … DOM の現在値を初期値にする * * value / checked は双方向(ユーザー入力で state が動く)。それ以外は state → DOM の一方向。 */ state(initial?: T, aspect?: string): State; /** * ラジオ/チェックボックス群をまとめて1つの state にする。 * const plan = $$("@plan").group(); … ラジオ群 → 選ばれている value * const options = $$("@option").group(); … チェックボックス群 → value の配列 * * 【なぜ要るか】 * jQuery 現場では選択状態が「input:checked」と「.active クラス」で二重管理され、 * 初期化処理と更新処理が別に書かれてズレる(=選べない/数値計算が合わない)。 * 選択を1つの state にすれば、見た目も金額もそこから導出できる。 */ group(): State; /** * 同じセレクタの要素が**後から DOM に足されたとき**にも同じ束縛を掛ける。 * * 【なぜ要るか】 * jQuery 現場で最も多い事故が「Ajax や JS で足した行にだけハンドラが付いていない」。 * 数値計算が合わない・チェックが効かない、しかも**例外は出ない**ので気付けない。 * 引いた時点の要素だけを見ていると、bridgey でも同じ事故を起こしてしまう。 * * $$(要素) のように直接渡された場合はセレクタが無いので、拾い直しはしない。 */ _watchAdded(onAdd: (el: HTMLElement) => void): void; /** リスナを張り、要素の片付け単位に解除を登録する。 */ _listen(el: HTMLElement, type: string, listener: () => void): void; /** input/change を state に流し込む(双方向の片側)。 */ _syncFromInput(handle: State, prop: string): void; /** * on("click", fn) 直接 * on("click", "@row", fn) 委譲(後から追加された要素にも効く) * on("click input", fn) 複数 */ on(events: string, selectorOrHandler: string | Listener, maybeHandler?: Listener): this; /** off() 全解除 / off("click") 種別 / off("click", fn) 個別 */ off(events?: string, handler?: Listener): this; /** * フォームの検証を状態にする。 * * const f = $$("@signup").form(); // HTML の required / type=email を読む * const f = $$("@signup").form({ // JS 側でルールを足す(HTMLの分と合成される) * company: { required: true, when: () => plan() === "corp" }, * confirm: { same: "password" }, * }); * * $$("@submit").disabled(f.submitting); // 既定では未入力で無効化しない(CV優先) * f.submit(async (values) => { … }); // 二重送信は自動で防がれる * * 見えていないフィールドは検証されないので、「押せないのに理由が出ない」が起きない。 */ form(rules?: Record, options?: FormOptions): FormApi; /** * 登録済みの部品を明示的に適用する(HTML に data-component を書けない場合の逃げ道)。 * $$("@tabs").component("tabs", { start: 2 }); */ component(name: string, props?: Record): this; /** この要素に張った購読・リスナを全部解放する(要素は残す)。 */ dispose(): this; } type Listener = (this: HTMLElement, event: Event) => void; export type RowHtml = (item: T, index: number) => string; export interface RepeatOptions { /** 行を見分ける値。id など一意なものを渡す(省略すると位置=indexになる)。 */ key?: (item: T, index: number) => unknown; /** 雛形にする