Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 16 additions & 44 deletions assets/js/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -14064,47 +14064,19 @@ ${end.comment}` : end.comment;
function shell() {
return window.wp?.os ?? null;
}
const BUTTON_ID = "allterrain-forms/preview";
const PREVIEW_WINDOW_ID = "allterrain-forms-preview";
function registerPreviewButton(source) {
const os = shell();
if (!os?.registerTitleBarButton) {
return () => {
};
}
const register = () => {
try {
os.registerTitleBarButton({
id: BUTTON_ID,
label: "Preview this form",
icon: "dashicons-visibility",
placement: "right",
// Just before the shell's own Related button, so the builder's
// eye lands where every other window's eye is.
order: 90,
// Only the builder window. The predicate is called against a live
// `Window`, and a throw counts as "does not match" — so a shell
// whose `Window` shape differs simply does not show the button
// rather than erroring on every repaint.
match: (window2) => {
const id2 = window2?.id ?? window2?.config?.id ?? "";
return id2 === "allterrain-forms" || id2.startsWith("allterrain-forms#");
},
onClick: () => void openPreview(source),
owner: "allterrain-forms-builder"
});
} catch {
}
};
if (os.ready) {
os.ready(register);
} else {
register();
}
const SOURCES_KEY = "allTerrainFormsPreviewSources";
function sources() {
const host = window;
return host[SOURCES_KEY] ?? (host[SOURCES_KEY] = []);
}
function providePreviewSource(source) {
sources().push(source);
return () => {
try {
os.unregisterTitleBarButton?.(BUTTON_ID);
} catch {
const list = sources();
const index = list.lastIndexOf(source);
if (index !== -1) {
list.splice(index, 1);
}
};
}
Expand Down Expand Up @@ -17989,9 +17961,9 @@ ${end.comment}` : end.comment;
}
const sections = conditionSections(schema2);
const target = sections.find((item) => item.key === to);
const sources = sections.filter((item) => item.logic.rules.length && target && canCopyCondition(item, target, schema2));
const source = sources.find((item) => item.key === from);
const sourcePicker = select(from, [{ value: "", label: "Choose a section…" }, ...sources.map((item) => ({ value: item.key, label: item.label }))], (value) => {
const sources2 = sections.filter((item) => item.logic.rules.length && target && canCopyCondition(item, target, schema2));
const source = sources2.find((item) => item.key === from);
const sourcePicker = select(from, [{ value: "", label: "Choose a section…" }, ...sources2.map((item) => ({ value: item.key, label: item.label }))], (value) => {
from = value;
paint();
dialog.querySelector('[aria-label="Copy from"]')?.focus();
Expand All @@ -18012,7 +17984,7 @@ ${end.comment}` : end.comment;
el("h2", { text: "Copy condition" }),
row("Copy from", sourcePicker),
el("p", { class: "atfb-hint", text: `Apply to ${destination?.label ?? "this section"}.` }),
el("p", { class: "atfb-condition-copy__preview", attrs: { "aria-live": "polite" }, text: source ? `${source.logic.enabled ? "Enabled" : "Disabled"} · ${source.logic.action === "hide" ? "Hide" : "Show"} · Match ${source.logic.match}: ` + source.logic.rules.map((rule) => tokensToText(ruleTokens(rule, schema2.fields))).join(source.logic.match === "all" ? " and " : " or ") : sources.length ? "Choose the section whose condition you want to reuse." : "No conditions are available to copy here yet." }),
el("p", { class: "atfb-condition-copy__preview", attrs: { "aria-live": "polite" }, text: source ? `${source.logic.enabled ? "Enabled" : "Disabled"} · ${source.logic.action === "hide" ? "Hide" : "Show"} · Match ${source.logic.match}: ` + source.logic.rules.map((rule) => tokensToText(ruleTokens(rule, schema2.fields))).join(source.logic.match === "all" ? " and " : " or ") : sources2.length ? "Choose the section whose condition you want to reuse." : "No conditions are available to copy here yet." }),
el("p", { class: "atfb-hint", text: destination?.logic.rules.length ? "This replaces the destination’s entire condition. You can edit the copy independently." : "Copies every rule, all/any matching, show/hide and enabled state. You can edit the copy independently." }),
el("div", { class: "atfb-modal__actions", children: [button("Cancel", close), apply] })
);
Expand Down Expand Up @@ -19285,7 +19257,7 @@ ${end.comment}` : end.comment;
}
this.teardowns.push(watchShellDragVisuals([FIELD_PAYLOAD_TYPE]));
this.teardowns.push(
registerPreviewButton({
providePreviewSource({
current: () => this.form ? { id: this.form.id, title: this.form.title, previewUrl: this.form.previewUrl } : null,
isDirty: () => this.dirty,
save: () => this.save(true)
Expand Down
12 changes: 6 additions & 6 deletions assets/js/builder.min.js

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions assets/js/titlebar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
(function() {
"use strict";
function shell() {
return window.wp?.os ?? null;
}
const BUTTON_ID = "allterrain-forms/preview";
const PREVIEW_WINDOW_ID = "allterrain-forms-preview";
const SOURCES_KEY = "allTerrainFormsPreviewSources";
function sources() {
const host = window;
return host[SOURCES_KEY] ?? (host[SOURCES_KEY] = []);
}
function registerPreviewButton() {
const os = shell();
if (!os?.registerTitleBarButton) {
return () => {
};
}
const register = () => {
try {
os.registerTitleBarButton({
id: BUTTON_ID,
label: "Preview this form",
icon: "dashicons-visibility",
placement: "right",
// Just before the shell's own Related button, so the builder's
// eye lands where every other window's eye is.
order: 90,
// Only the builder window. The predicate is called against a live
// `Window`, and a throw counts as "does not match" — so a shell
// whose `Window` shape differs simply does not show the button
// rather than erroring on every repaint.
match: (window2) => {
const id = window2?.id ?? window2?.config?.id ?? "";
return id === "allterrain-forms" || id.startsWith("allterrain-forms#");
},
onClick: () => {
const list = sources();
const source = list[list.length - 1];
if (source) {
void openPreview(source);
}
},
owner: "allterrain-forms-titlebar"
});
} catch {
}
};
if (os.ready) {
os.ready(register);
} else {
register();
}
return () => {
try {
os.unregisterTitleBarButton?.(BUTTON_ID);
} catch {
}
};
}
async function openPreview(source) {
if (source.isDirty()) {
await source.save();
}
const form = source.current();
if (!form) {
return;
}
openPreviewWindow(form.id, form.title, form.previewUrl);
}
function openPreviewWindow(formId, title, url) {
const os = shell();
if (!os?.windowManager?.open) {
window.open(url, "_blank", "noopener");
return;
}
os.windowManager.open({
id: `${PREVIEW_WINDOW_ID}-${formId}`,
baseId: PREVIEW_WINDOW_ID,
url,
title: `Preview: ${title}`,
icon: "dashicons-visibility"
});
}
registerPreviewButton();
})();
1 change: 1 addition & 0 deletions assets/js/titlebar.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin/deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ if ( existsSync( target ) && ! existsSync( join( target, OWNERSHIP_MARKER ) ) )
for ( const required of [
'assets/js/form.min.js',
'assets/js/builder.min.js',
'assets/js/titlebar.min.js',
'assets/js/entries.min.js',
'assets/js/widget.min.js',
] ) {
Expand Down
1 change: 1 addition & 0 deletions bin/package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function fail( message ) {
const required = [
'assets/js/form.min.js',
'assets/js/builder.min.js',
'assets/js/titlebar.min.js',
'assets/js/entries.min.js',
'assets/js/dock.min.js',
'assets/js/widget.min.js',
Expand Down
10 changes: 10 additions & 0 deletions includes/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ function alltfo_register_assets() {
true
);

// The title-bar provider. Like the dock tile it runs at every boot, so it
// holds only the preview button and never the builder.
wp_register_script(
'allterrain-forms-titlebar',
ALLTFO_URL . "assets/js/titlebar{$suffix}.js",
array(),
alltfo_asset_version( "assets/js/titlebar{$suffix}.js" ),
true
);

wp_register_script(
'allterrain-forms-widget',
ALLTFO_URL . "assets/js/widget{$suffix}.js",
Expand Down
4 changes: 3 additions & 1 deletion includes/openstation.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,10 @@ function alltfo_register_shell_surfaces() {
// *script* here is what makes the button paint for a session that was
// already open when this plugin was activated -- without it, the button only
// appears after a reload, which is exactly when nobody is looking for it.
// The shell runs this script at every boot, so it is the small titlebar
// bundle rather than the builder, which loads with its window.
if ( $has_builder && alltfo_shell_has( 'register_titlebar_button_script' ) ) {
alltfo_shell_call( 'register_titlebar_button_script', 'allterrain-forms-builder' );
alltfo_shell_call( 'register_titlebar_button_script', 'allterrain-forms-titlebar' );
}

if ( $has_builder && alltfo_shell_has( 'register_icon' ) ) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
"build": "npm run build:bundles && npm run deploy",
"build:builder": "ATF_TARGET=builder vite build --mode development && ATF_TARGET=builder vite build --mode production",
"build:analytics": "ATF_TARGET=analytics vite build --mode development && ATF_TARGET=analytics vite build --mode production",
"build:bundles": "npm run build:form && npm run build:builder && npm run build:entries && npm run build:widget && npm run build:dock && npm run build:analytics",
"build:bundles": "npm run build:form && npm run build:builder && npm run build:entries && npm run build:widget && npm run build:dock && npm run build:titlebar && npm run build:analytics",
"build:dock": "ATF_TARGET=dock vite build --mode development && ATF_TARGET=dock vite build --mode production",
"build:entries": "ATF_TARGET=entries vite build --mode development && ATF_TARGET=entries vite build --mode production",
"build:form": "ATF_TARGET=form vite build --mode development && ATF_TARGET=form vite build --mode production",
"build:titlebar": "ATF_TARGET=titlebar vite build --mode development && ATF_TARGET=titlebar vite build --mode production",
"build:widget": "ATF_TARGET=widget vite build --mode development && ATF_TARGET=widget vite build --mode production",
"check": "npm run typecheck && npm run test && npm run build",
"deploy": "node bin/deploy.mjs",
Expand Down
10 changes: 5 additions & 5 deletions src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { formulaInput, openFormulaEditor } from './formula-editor';
import { openConditionCopy } from './condition-copy';
import { compileRecipe, describeRecipe, openValidationEditor, parseRecipe } from './validation-editor';
import { VALIDATION_GROUPS, VALIDATION_PRESETS, validationPreset } from './shared/validation';
import { openPreview, refreshPreview, registerPreviewButton } from './preview-button';
import { openPreview, providePreviewSource, refreshPreview } from './preview-button';
import { formIdentity, setIdentity } from './relations';

/**
Expand Down Expand Up @@ -661,11 +661,11 @@ export class Builder {
this.teardowns.push( watchShellDragVisuals( [ FIELD_PAYLOAD_TYPE ] ) );

// The eye in the window's title bar, matching the shell's own
// editor-preview convention. Registered with a view onto this builder
// rather than a snapshot, so the button always previews whichever form
// is open now.
// editor-preview convention. The button is registered by the small
// titlebar bundle; this hands it a view onto this builder rather than a
// snapshot, so it always previews whichever form is open now.
this.teardowns.push(
registerPreviewButton( {
providePreviewSource( {
current: () =>
this.form
? { id: this.form.id, title: this.form.title, previewUrl: this.form.previewUrl }
Expand Down
53 changes: 47 additions & 6 deletions src/preview-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* change one thing, and open it again.
*
* All of it degrades. Without a shell there is no title bar to put a button in,
* so `register()` returns a teardown that does nothing and the builder's own
* so `registerPreviewButton()` returns a teardown that does nothing and the builder's own
* Preview button opens the same URL in a tab.
*/

Expand Down Expand Up @@ -49,13 +49,47 @@ export interface PreviewSource {
save(): Promise< void >;
}

/** Where builders publish their sources, shared across bundles on `window`. */
const SOURCES_KEY = 'allTerrainFormsPreviewSources';

function sources(): PreviewSource[] {
const host = window as unknown as Record< string, PreviewSource[] | undefined >;

return ( host[ SOURCES_KEY ] ??= [] );
}

/**
* Tells the title-bar button which form to preview.
*
* The button itself lives in the small `titlebar` bundle, which the shell runs
* at boot so the eye can paint before any builder has loaded. The builder, when
* it does load, hands over a view onto itself here. The most recent builder
* wins, which is what a single registered button meant before the split.
*
* Returns a teardown that withdraws this source.
*/
export function providePreviewSource( source: PreviewSource ): () => void {
sources().push( source );

return () => {
const list = sources();
const index = list.lastIndexOf( source );

if ( index !== -1 ) {
list.splice( index, 1 );
}
};
}

/**
* Adds the eye to the builder window's title bar.
*
* Returns a teardown. Safe to call with no shell present — it registers nothing
* and the teardown is a no-op.
* Called once, from the `titlebar` bundle. The click previews whichever form
* the most recently loaded builder has open, and does nothing before a builder
* has loaded. Returns a teardown. Safe to call with no shell present: it
* registers nothing and the teardown is a no-op.
*/
export function registerPreviewButton( source: PreviewSource ): () => void {
export function registerPreviewButton(): () => void {
const os = shell() as ( ShellApi & TitleBarApi ) | null;

if ( ! os?.registerTitleBarButton ) {
Expand All @@ -81,8 +115,15 @@ export function registerPreviewButton( source: PreviewSource ): () => void {

return id === 'allterrain-forms' || id.startsWith( 'allterrain-forms#' );
},
onClick: () => void openPreview( source ),
owner: 'allterrain-forms-builder',
onClick: () => {
const list = sources();
const source = list[ list.length - 1 ];

if ( source ) {
void openPreview( source );
}
},
owner: 'allterrain-forms-titlebar',
} );
} catch {
// `registerTitleBarButton` throws a RegistrationError on a shell
Expand Down
12 changes: 12 additions & 0 deletions src/titlebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* The title-bar provider.
*
* OpenStation runs every title-bar provider script at boot, so the button can
* paint on a window restored from the last session. That makes this the one
* builder-related script every station load pays for, so it carries only the
* preview button and nothing of the builder itself.
*/

import { registerPreviewButton } from './preview-button';

registerPreviewButton();
Loading
Loading