Separate settings component style helpers

This commit is contained in:
vorotamoroz
2026-08-26 05:49:37 +00:00
parent 783fbb8f23
commit edeac6f7e2
11 changed files with 119 additions and 80 deletions
@@ -24,7 +24,8 @@ import {
type AllBooleanItemKey, type AllBooleanItemKey,
} from "./settingConstants.ts"; } from "./settingConstants.ts";
import { $msg } from "@/common/translation"; import { $msg } from "@/common/translation";
import { setButtonDestructiveState, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts"; import { wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
import { setButtonDestructiveState } from "./settingComponentStyles.ts";
export class LiveSyncSetting extends Setting { export class LiveSyncSetting extends Setting {
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent; autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
@@ -24,7 +24,8 @@ import {
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts"; import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts"; import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
import { setButtonDestructiveState, type PageFunctions } from "./SettingPane.ts"; import type { PageFunctions } from "./SettingPane.ts";
import { setButtonDestructiveState } from "./settingComponentStyles.ts";
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
import { import {
chooseAndCopyFileDatabaseInfo, chooseAndCopyFileDatabaseInfo,
@@ -10,7 +10,8 @@ import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator"; import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import { setButtonDestructiveState, visibleOnly, type PageFunctions } from "./SettingPane"; import { visibleOnly, type PageFunctions } from "./SettingPane";
import { setButtonDestructiveState } from "./settingComponentStyles.ts";
export function paneMaintenance( export function paneMaintenance(
this: ObsidianLiveSyncSettingTab, this: ObsidianLiveSyncSettingTab,
paneEl: HTMLElement, paneEl: HTMLElement,
@@ -9,7 +9,8 @@ import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
import type { PageFunctions } from "./SettingPane.ts"; import type { PageFunctions } from "./SettingPane.ts";
import { markSettingRowWithSubsequentButtons, markSubsequentButton, visibleOnly } from "./SettingPane.ts"; import { visibleOnly } from "./SettingPane.ts";
import { setButtonAdditionalActionState, setSettingAdditionalActionsState } from "./settingComponentStyles.ts";
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser"; import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser";
import { ExtraSuffixIndexedDB } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { ExtraSuffixIndexedDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { migrateDatabases } from "./settingUtils.ts"; import { migrateDatabases } from "./settingUtils.ts";
@@ -177,7 +178,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch"); new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch");
}); });
void addPanel(paneEl, "Remediation").then((paneEl) => { void addPanel(paneEl, "Remediation").then((paneEl) => {
const setting = markSettingRowWithSubsequentButtons(new Setting(paneEl)); const setting = setSettingAdditionalActionsState(new Setting(paneEl));
const dateEl = setting.controlEl.createSpan(); const dateEl = setting.controlEl.createSpan();
setting setting
.addText((text) => { .addText((text) => {
@@ -216,7 +217,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
.setAuto("maxMTimeForReflectEvents") .setAuto("maxMTimeForReflectEvents")
.addApplyButton(["maxMTimeForReflectEvents"]); .addApplyButton(["maxMTimeForReflectEvents"]);
if (setting.applyButtonComponent) { if (setting.applyButtonComponent) {
markSubsequentButton(setting.applyButtonComponent); setButtonAdditionalActionState(setting.applyButtonComponent);
} }
this.addOnSaved("maxMTimeForReflectEvents", async (key) => { this.addOnSaved("maxMTimeForReflectEvents", async (key) => {
@@ -12,15 +12,15 @@ const remediationHarness = vi.hoisted(() => {
onChange: vi.fn(), onChange: vi.fn(),
setValue: vi.fn(), setValue: vi.fn(),
}; };
const addButtonClass = vi.fn(); const setButtonClassState = vi.fn();
const setClass = vi.fn(); const setSettingClassState = vi.fn();
return { return {
addButtonClass,
createSpan, createSpan,
dateElement, dateElement,
inputEl, inputEl,
setClass, setButtonClassState,
setSettingClassState,
textComponent, textComponent,
}; };
}); });
@@ -29,7 +29,14 @@ vi.mock("./LiveSyncSetting.ts", () => ({
LiveSyncSetting: class LiveSyncSetting { LiveSyncSetting: class LiveSyncSetting {
applyButtonComponent = { applyButtonComponent = {
buttonEl: { buttonEl: {
addClass: remediationHarness.addButtonClass, classList: {
toggle: remediationHarness.setButtonClassState,
},
},
};
settingEl = {
classList: {
toggle: remediationHarness.setSettingClassState,
}, },
}; };
controlEl = { controlEl = {
@@ -45,11 +52,6 @@ vi.mock("./LiveSyncSetting.ts", () => ({
return this; return this;
} }
setClass(value: string): this {
remediationHarness.setClass(value);
return this;
}
addApplyButton(): this { addApplyButton(): this {
return this; return this;
} }
@@ -107,7 +109,10 @@ describe("panePatches remediation setting", () => {
expect(createSpan).not.toHaveBeenCalled(); expect(createSpan).not.toHaveBeenCalled();
expect(remediationHarness.createSpan).toHaveBeenCalledOnce(); expect(remediationHarness.createSpan).toHaveBeenCalledOnce();
expect(remediationHarness.dateElement.textContent).toBe("No limit configured"); expect(remediationHarness.dateElement.textContent).toBe("No limit configured");
expect(remediationHarness.setClass).toHaveBeenCalledWith("sls-setting-row-with-subsequent-buttons"); expect(remediationHarness.setSettingClassState).toHaveBeenCalledWith(
expect(remediationHarness.addButtonClass).toHaveBeenCalledWith("sls-setting-subsequent-button"); "sls-setting-with-additional-actions",
true
);
expect(remediationHarness.setButtonClassState).toHaveBeenCalledWith("sls-setting-additional-action", true);
}); });
}); });
@@ -12,11 +12,11 @@ import { $msg } from "@/common/translation";
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
import { import {
markSettingRowWithSubsequentButtons, setButtonAdditionalActionState,
markSubsequentButton,
setButtonDestructiveState, setButtonDestructiveState,
type PageFunctions, setSettingAdditionalActionsState,
} from "./SettingPane.ts"; } from "./settingComponentStyles.ts";
import type { PageFunctions } from "./SettingPane.ts";
// import { visibleOnly } from "./SettingPane.ts"; // import { visibleOnly } from "./SettingPane.ts";
import InfoPanel from "./InfoPanel.svelte"; import InfoPanel from "./InfoPanel.svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
@@ -110,10 +110,10 @@ export function paneRemoteConfig(
void addPanel(paneEl, "E2EE Configuration", () => {}).then((paneEl) => { void addPanel(paneEl, "E2EE Configuration", () => {}).then((paneEl) => {
const infoPanel = new SveltePanel(InfoPanel, paneEl, E2EESummaryWritable); const infoPanel = new SveltePanel(InfoPanel, paneEl, E2EESummaryWritable);
this.lifetimeComponent.register(() => infoPanel.destroy()); this.lifetimeComponent.register(() => infoPanel.destroy());
const setupButton = markSettingRowWithSubsequentButtons(new Setting(paneEl).setName("Configure E2EE")); const setupButton = setSettingAdditionalActionsState(new Setting(paneEl).setName("Configure E2EE"));
setupButton setupButton
.addButton((button) => .addButton((button) =>
setButtonDestructiveState(markSubsequentButton(button)) setButtonDestructiveState(button)
.onClick(async () => { .onClick(async () => {
const setupManager = this.core.getModule(SetupManager); const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings); const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
@@ -123,7 +123,7 @@ export function paneRemoteConfig(
.setButtonText("Configure") .setButtonText("Configure")
) )
.addButton((button) => .addButton((button) =>
setButtonDestructiveState(markSubsequentButton(button)) setButtonDestructiveState(setButtonAdditionalActionState(button))
.onClick(async () => { .onClick(async () => {
const setupManager = this.core.getModule(SetupManager); const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings); const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
@@ -23,6 +23,13 @@ vi.mock("@/common/translation", () => ({
vi.mock("./LiveSyncSetting.ts", () => ({ vi.mock("./LiveSyncSetting.ts", () => ({
LiveSyncSetting: class { LiveSyncSetting: class {
nameEl = { addClass: vi.fn(), appendText: vi.fn() }; nameEl = { addClass: vi.fn(), appendText: vi.fn() };
settingEl = {
classList: {
toggle: (value: string, enabled: boolean) => {
if (enabled) runtime.settingClasses.push(value);
},
},
};
setName() { setName() {
return this; return this;
@@ -32,16 +39,17 @@ vi.mock("./LiveSyncSetting.ts", () => ({
return this; return this;
} }
setClass(value: string) {
runtime.settingClasses.push(value);
return this;
}
addButton(callback: (button: unknown) => void) { addButton(callback: (button: unknown) => void) {
const button = { const button = {
buttonEl: { buttonEl: {
addClass: (value: string) => runtime.buttonClasses.push(value), classList: {
classList: { toggle: vi.fn() }, toggle: (value: string, enabled: boolean) => {
if (enabled) runtime.buttonClasses.push(value);
},
},
},
setDestructive() {
return this;
}, },
onClick() { onClick() {
return this; return this;
@@ -133,8 +141,8 @@ describe("paneRemoteConfig", () => {
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never); paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
await vi.waitFor(() => expect(runtime.panels).toHaveLength(1)); await vi.waitFor(() => expect(runtime.panels).toHaveLength(1));
expect(runtime.settingClasses).toContain("sls-setting-row-with-subsequent-buttons"); expect(runtime.settingClasses).toContain("sls-setting-with-additional-actions");
expect(runtime.buttonClasses).toEqual(["sls-setting-subsequent-button", "sls-setting-subsequent-button"]); expect(runtime.buttonClasses).toEqual(["sls-setting-additional-action"]);
lifetimeComponent.unload(); lifetimeComponent.unload();
@@ -6,7 +6,6 @@ import {
type ConfigLevel, type ConfigLevel,
} from "@vrtmrz/livesync-commonlib/compat/common/types"; } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { AllSettingItemKey, AllSettings } from "./settingConstants"; import type { AllSettingItemKey, AllSettings } from "./settingConstants";
import type { ButtonComponent, Setting } from "@/deps.ts";
export const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => { export const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => {
return () => ({ return () => ({
@@ -39,37 +38,6 @@ export function setStyle(el: HTMLElement, styleHead: string, condition: () => bo
} }
} }
/**
* Applies destructive-action styling without requiring Obsidian 1.13 at
* runtime. Older supported versions used the `mod-warning` class for the same
* presentation.
*/
export function setButtonDestructiveState(button: ButtonComponent, isDestructive = true): ButtonComponent {
const compatibleButton = button as unknown as {
setDestructive?: () => ButtonComponent;
removeDestructive?: () => ButtonComponent;
};
const updateNativeStyle = isDestructive ? compatibleButton.setDestructive : compatibleButton.removeDestructive;
if (typeof updateNativeStyle === "function") {
updateNativeStyle.call(button);
} else {
button.buttonEl.classList.toggle("mod-warning", isDestructive);
}
return button;
}
/** Marks a setting row whose selected action buttons may wrap onto separate lines. */
export function markSettingRowWithSubsequentButtons<T extends Setting>(setting: T): T {
setting.setClass("sls-setting-row-with-subsequent-buttons");
return setting;
}
/** Marks an action button which may wrap onto a later line in its setting row. */
export function markSubsequentButton(button: ButtonComponent): ButtonComponent {
button.buttonEl.addClass("sls-setting-subsequent-button");
return button;
}
export function visibleOnly(cond: () => boolean): OnUpdateFunc { export function visibleOnly(cond: () => boolean): OnUpdateFunc {
return () => ({ return () => ({
visibility: cond(), visibility: cond(),
@@ -0,0 +1,35 @@
import type { ButtonComponent, Setting } from "@/deps.ts";
const SETTING_WITH_ADDITIONAL_ACTIONS_CLASS = "sls-setting-with-additional-actions";
const ADDITIONAL_ACTION_CLASS = "sls-setting-additional-action";
/**
* Applies destructive-action styling without requiring Obsidian 1.13 at
* runtime. Older supported versions used the `mod-warning` class for the same
* presentation.
*/
export function setButtonDestructiveState<T extends ButtonComponent>(button: T, isDestructive = true): T {
const compatibleButton = button as unknown as {
setDestructive?: () => ButtonComponent;
removeDestructive?: () => ButtonComponent;
};
const updateNativeStyle = isDestructive ? compatibleButton.setDestructive : compatibleButton.removeDestructive;
if (typeof updateNativeStyle === "function") {
updateNativeStyle.call(button);
} else {
button.buttonEl.classList.toggle("mod-warning", isDestructive);
}
return button;
}
/** Sets whether a setting row contains actions which may move onto a later line. */
export function setSettingAdditionalActionsState<T extends Setting>(setting: T, hasAdditionalActions = true): T {
setting.settingEl.classList.toggle(SETTING_WITH_ADDITIONAL_ACTIONS_CLASS, hasAdditionalActions);
return setting;
}
/** Sets whether a button is an additional action which may move onto a later line. */
export function setButtonAdditionalActionState<T extends ButtonComponent>(button: T, isAdditionalAction = true): T {
button.buttonEl.classList.toggle(ADDITIONAL_ACTION_CLASS, isAdditionalAction);
return button;
}
@@ -1,6 +1,10 @@
import type { ButtonComponent, Setting } from "@/deps.ts"; import type { ButtonComponent, Setting } from "@/deps.ts";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { markSettingRowWithSubsequentButtons, markSubsequentButton, setButtonDestructiveState } from "./SettingPane.ts"; import {
setButtonAdditionalActionState,
setButtonDestructiveState,
setSettingAdditionalActionsState,
} from "./settingComponentStyles.ts";
type CompatibleButton = ButtonComponent & { type CompatibleButton = ButtonComponent & {
setDestructive?: () => ButtonComponent; setDestructive?: () => ButtonComponent;
@@ -10,7 +14,6 @@ type CompatibleButton = ButtonComponent & {
function createButton(overrides: Partial<CompatibleButton> = {}): CompatibleButton { function createButton(overrides: Partial<CompatibleButton> = {}): CompatibleButton {
return { return {
buttonEl: { buttonEl: {
addClass: vi.fn(),
classList: { classList: {
toggle: vi.fn(), toggle: vi.fn(),
}, },
@@ -21,25 +24,41 @@ function createButton(overrides: Partial<CompatibleButton> = {}): CompatibleButt
function createSetting(): Setting { function createSetting(): Setting {
return { return {
setClass: vi.fn().mockReturnThis(), settingEl: {
classList: {
toggle: vi.fn(),
},
},
} as unknown as Setting; } as unknown as Setting;
} }
describe("markSettingRowWithSubsequentButtons", () => { describe("setSettingAdditionalActionsState", () => {
it("marks only the supplied setting row as containing subsequent actions", () => { it("sets whether the supplied setting row contains additional actions", () => {
const setting = createSetting(); const setting = createSetting();
expect(markSettingRowWithSubsequentButtons(setting)).toBe(setting); expect(setSettingAdditionalActionsState(setting, true)).toBe(setting);
expect(setting.setClass).toHaveBeenCalledWith("sls-setting-row-with-subsequent-buttons"); expect(setSettingAdditionalActionsState(setting, false)).toBe(setting);
expect(setting.settingEl.classList.toggle).toHaveBeenNthCalledWith(
1,
"sls-setting-with-additional-actions",
true
);
expect(setting.settingEl.classList.toggle).toHaveBeenNthCalledWith(
2,
"sls-setting-with-additional-actions",
false
);
}); });
}); });
describe("markSubsequentButton", () => { describe("setButtonAdditionalActionState", () => {
it("marks only the supplied button as a subsequent action", () => { it("sets whether the supplied button is an additional action", () => {
const button = createButton(); const button = createButton();
expect(markSubsequentButton(button)).toBe(button); expect(setButtonAdditionalActionState(button, true)).toBe(button);
expect(button.buttonEl.addClass).toHaveBeenCalledWith("sls-setting-subsequent-button"); expect(setButtonAdditionalActionState(button, false)).toBe(button);
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(1, "sls-setting-additional-action", true);
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(2, "sls-setting-additional-action", false);
}); });
}); });
+3 -3
View File
@@ -547,17 +547,17 @@ body.is-mobile .sls-setting button {
white-space: normal; white-space: normal;
} }
body.is-mobile .sls-setting-row-with-subsequent-buttons { body.is-mobile .sls-setting-with-additional-actions {
flex-wrap: wrap; flex-wrap: wrap;
} }
body.is-mobile .sls-setting-row-with-subsequent-buttons .setting-item-control { body.is-mobile .sls-setting-with-additional-actions .setting-item-control {
min-width: 0; min-width: 0;
flex: 1 1 100%; flex: 1 1 100%;
flex-wrap: wrap; flex-wrap: wrap;
} }
body.is-mobile .sls-setting .sls-setting-subsequent-button { body.is-mobile .sls-setting .sls-setting-additional-action {
flex: 1 1 12rem; flex: 1 1 12rem;
} }