Consolidate Soma input runtimes

active-uix
dev 5 months ago
parent 6df2f062ad
commit 959439f5fa

@ -10,7 +10,7 @@ import { EDITABLE_LANGS } from './langs';
import { editableMorfo } from '../../../morfo/components/editable'; import { editableMorfo } from '../../../morfo/components/editable';
const attrs = createAttrs(editableMorfo); const attrs = createAttrs(editableMorfo);
import type { SomaRuntimePart } from '../../runtime.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
export type ActivationMode = 'focus' | 'click' | 'dblclick' | 'none'; export type ActivationMode = 'focus' | 'click' | 'dblclick' | 'none';
@ -41,6 +41,8 @@ interface EditableOpts
export class EditableProvider { export class EditableProvider {
readonly opts: EditableOpts; readonly opts: EditableOpts;
readonly runtimePart: SomaRuntimePart; readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
readonly runtime: SomaRuntime;
static readonly ctx = context<EditableProvider>('Editable'); static readonly ctx = context<EditableProvider>('Editable');
static get(): EditableProvider | undefined { static get(): EditableProvider | undefined {
return this.ctx.getOr(undefined) as EditableProvider | undefined; return this.ctx.getOr(undefined) as EditableProvider | undefined;
@ -53,8 +55,6 @@ export class EditableProvider {
return new EditableProvider(opts); return new EditableProvider(opts);
} }
readonly soma = Soma.get();
// Draft value (in-progress edit) // Draft value (in-progress edit)
draft = $state(''); draft = $state('');
inputRef = state<HTMLElement | null>(null); inputRef = state<HTMLElement | null>(null);
@ -62,9 +62,14 @@ export class EditableProvider {
private constructor(opts: EditableOpts) { private constructor(opts: EditableOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require() this.soma = Soma.require();
.runtime(editableMorfo, {}) this.runtime = this.soma.runtime(editableMorfo, {});
.part('provider', { id: opts.id, ref: opts.ref, owner: this, context: EditableProvider.ctx }); this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: EditableProvider.ctx
});
this.draft = opts.value.current; this.draft = opts.value.current;
@ -149,10 +154,12 @@ export class EditableAreaProvider {
private constructor(opts: EditableAreaOpts) { private constructor(opts: EditableAreaOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(editableMorfo, {})
.part('area', { id: opts.id, ref: opts.ref, owner: this });
this.provider = EditableProvider.require(); this.provider = EditableProvider.require();
this.runtimePart = this.provider.runtime.part('area', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly props = $derived.by(() => readonly props = $derived.by(() =>
@ -180,10 +187,12 @@ export class EditablePreviewProvider {
private constructor(opts: EditablePreviewOpts) { private constructor(opts: EditablePreviewOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(editableMorfo, {})
.part('preview', { id: opts.id, ref: opts.ref, owner: this });
this.provider = EditableProvider.require(); this.provider = EditableProvider.require();
this.runtimePart = this.provider.runtime.part('preview', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly displayValue = $derived.by( readonly displayValue = $derived.by(
@ -244,9 +253,8 @@ export class EditableInputProvider {
private constructor(opts: EditableInputOpts) { private constructor(opts: EditableInputOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require() this.provider = EditableProvider.require();
.runtime(editableMorfo, {}) this.runtimePart = this.provider.runtime.part('input', {
.part('input', {
id: opts.id, id: opts.id,
ref: opts.ref, ref: opts.ref,
owner: this, owner: this,
@ -254,7 +262,6 @@ export class EditableInputProvider {
this.provider.inputRef.current = el; this.provider.inputRef.current = el;
} }
}); });
this.provider = EditableProvider.require();
} }
readonly oninput = (e: Event) => { readonly oninput = (e: Event) => {
@ -335,10 +342,12 @@ export class EditableEditTriggerProvider {
private constructor(opts: EditableEditTriggerOpts) { private constructor(opts: EditableEditTriggerOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(editableMorfo, {})
.part('edit-trigger', { id: opts.id, ref: opts.ref, owner: this });
this.provider = EditableProvider.require(); this.provider = EditableProvider.require();
this.runtimePart = this.provider.runtime.part('edit-trigger', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => { readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
@ -350,7 +359,7 @@ export class EditableEditTriggerProvider {
...this.runtimePart.props, ...this.runtimePart.props,
type: 'button' as const, type: 'button' as const,
hidden: this.provider.isEditing || undefined, hidden: this.provider.isEditing || undefined,
'aria-label': this.provider.soma?.langs.ts(EDITABLE_LANGS.EDIT), 'aria-label': this.provider.soma.langs.ts(EDITABLE_LANGS.EDIT),
'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current), 'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current),
'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current), 'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current),
disabled: disabled:
@ -375,10 +384,12 @@ export class EditableSubmitTriggerProvider {
private constructor(opts: EditableSubmitTriggerOpts) { private constructor(opts: EditableSubmitTriggerOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(editableMorfo, {})
.part('submit-trigger', { id: opts.id, ref: opts.ref, owner: this });
this.provider = EditableProvider.require(); this.provider = EditableProvider.require();
this.runtimePart = this.provider.runtime.part('submit-trigger', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => { readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
@ -390,7 +401,7 @@ export class EditableSubmitTriggerProvider {
...this.runtimePart.props, ...this.runtimePart.props,
type: 'button' as const, type: 'button' as const,
hidden: !this.provider.isEditing || undefined, hidden: !this.provider.isEditing || undefined,
'aria-label': this.provider.soma?.langs.ts(EDITABLE_LANGS.SUBMIT), 'aria-label': this.provider.soma.langs.ts(EDITABLE_LANGS.SUBMIT),
'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current), 'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current),
'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current), 'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current),
disabled: disabled:
@ -415,10 +426,12 @@ export class EditableCancelTriggerProvider {
private constructor(opts: EditableCancelTriggerOpts) { private constructor(opts: EditableCancelTriggerOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(editableMorfo, {})
.part('cancel-trigger', { id: opts.id, ref: opts.ref, owner: this });
this.provider = EditableProvider.require(); this.provider = EditableProvider.require();
this.runtimePart = this.provider.runtime.part('cancel-trigger', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => { readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
@ -430,7 +443,7 @@ export class EditableCancelTriggerProvider {
...this.runtimePart.props, ...this.runtimePart.props,
type: 'button' as const, type: 'button' as const,
hidden: !this.provider.isEditing || undefined, hidden: !this.provider.isEditing || undefined,
'aria-label': this.provider.soma?.langs.ts(EDITABLE_LANGS.CANCEL), 'aria-label': this.provider.soma.langs.ts(EDITABLE_LANGS.CANCEL),
'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current), 'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current),
'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current), 'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current),
disabled: this.provider.opts.disabled.current || undefined, disabled: this.provider.opts.disabled.current || undefined,

@ -16,7 +16,7 @@ import { RATING_GROUP_LANGS } from './langs';
import type { RatingItemDescriptor, RatingItemState } from './types'; import type { RatingItemDescriptor, RatingItemState } from './types';
import { ratingGroupMorfo } from '../../../morfo/components/rating-group'; import { ratingGroupMorfo } from '../../../morfo/components/rating-group';
import type { SomaRuntimePart } from '../../runtime.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
// ── Helpers ──────────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────────
@ -56,6 +56,8 @@ interface RatingGroupOpts
export class RatingGroupProvider { export class RatingGroupProvider {
readonly opts: RatingGroupOpts; readonly opts: RatingGroupOpts;
readonly runtimePart: SomaRuntimePart; readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
readonly runtime: SomaRuntime;
static readonly ctx = context<RatingGroupProvider>('RatingGroup'); static readonly ctx = context<RatingGroupProvider>('RatingGroup');
static get(): RatingGroupProvider | undefined { static get(): RatingGroupProvider | undefined {
return this.ctx.getOr(undefined) as RatingGroupProvider | undefined; return this.ctx.getOr(undefined) as RatingGroupProvider | undefined;
@ -68,7 +70,6 @@ export class RatingGroupProvider {
return new RatingGroupProvider(opts); return new RatingGroupProvider(opts);
} }
readonly soma = Soma.get();
readonly field = FieldProvider.get(); readonly field = FieldProvider.get();
hoverValue = $state<number | null>(null); hoverValue = $state<number | null>(null);
@ -76,9 +77,9 @@ export class RatingGroupProvider {
private constructor(opts: RatingGroupOpts) { private constructor(opts: RatingGroupOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require() this.soma = Soma.require();
.runtime(ratingGroupMorfo, {}) this.runtime = this.soma.runtime(ratingGroupMorfo, {});
.part('provider', { this.runtimePart = this.runtime.part('provider', {
id: opts.id, id: opts.id,
ref: opts.ref, ref: opts.ref,
owner: this, owner: this,
@ -121,7 +122,7 @@ export class RatingGroupProvider {
}); });
readonly resolvedAriaLabel: Active<string | undefined> = readableActive( readonly resolvedAriaLabel: Active<string | undefined> = readableActive(
() => this.opts.ariaLabel.current || this.soma?.langs.ts(RATING_GROUP_LANGS.LABEL) || undefined () => this.opts.ariaLabel.current || this.soma.langs.ts(RATING_GROUP_LANGS.LABEL) || undefined
); );
// ── Mutations ──────────────────────────────────────────────────────────── // ── Mutations ────────────────────────────────────────────────────────────
@ -298,10 +299,12 @@ export class RatingGroupItemProvider {
private constructor(opts: RatingGroupItemOpts) { private constructor(opts: RatingGroupItemOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(ratingGroupMorfo, {})
.part('item', { id: opts.id, ref: opts.ref, owner: this });
this.provider = RatingGroupProvider.require(); this.provider = RatingGroupProvider.require();
this.runtimePart = this.provider.runtime.part('item', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly isDisabled = $derived.by(() => this.opts.disabled.current || this.provider.isDisabled); readonly isDisabled = $derived.by(() => this.opts.disabled.current || this.provider.isDisabled);

@ -20,7 +20,7 @@ import { FieldProvider } from '../field/field-provider.svelte';
import { SEARCH_FIELD_LANGS } from './langs'; import { SEARCH_FIELD_LANGS } from './langs';
import { searchFieldMorfo } from '../../../morfo/components/search-field'; import { searchFieldMorfo } from '../../../morfo/components/search-field';
import type { SomaRuntimePart } from '../../runtime.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
// ── Root provider ────────────────────────────────────────────────────────── // ── Root provider ──────────────────────────────────────────────────────────
@ -47,6 +47,8 @@ interface SearchFieldOpts
export class SearchFieldProvider { export class SearchFieldProvider {
readonly opts: SearchFieldOpts; readonly opts: SearchFieldOpts;
readonly runtimePart: SomaRuntimePart; readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
readonly runtime: SomaRuntime;
static readonly ctx = context<SearchFieldProvider>('SearchField'); static readonly ctx = context<SearchFieldProvider>('SearchField');
static get(): SearchFieldProvider | undefined { static get(): SearchFieldProvider | undefined {
return this.ctx.getOr(undefined) as SearchFieldProvider | undefined; return this.ctx.getOr(undefined) as SearchFieldProvider | undefined;
@ -59,7 +61,6 @@ export class SearchFieldProvider {
return new SearchFieldProvider(opts); return new SearchFieldProvider(opts);
} }
readonly soma = Soma.get();
readonly field = FieldProvider.get(); readonly field = FieldProvider.get();
inputRef = state<HTMLInputElement | null>(null); inputRef = state<HTMLInputElement | null>(null);
@ -67,9 +68,9 @@ export class SearchFieldProvider {
private constructor(opts: SearchFieldOpts) { private constructor(opts: SearchFieldOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require() this.soma = Soma.require();
.runtime(searchFieldMorfo, {}) this.runtime = this.soma.runtime(searchFieldMorfo, {});
.part('provider', { this.runtimePart = this.runtime.part('provider', {
id: opts.id, id: opts.id,
ref: opts.ref, ref: opts.ref,
owner: this, owner: this,
@ -104,9 +105,7 @@ export class SearchFieldProvider {
// If a Field.Label is present (labelId registered), it takes over via // If a Field.Label is present (labelId registered), it takes over via
// aria-labelledby — no need for aria-label on the input. // aria-labelledby — no need for aria-label on the input.
if (this.field?.labelId.current) return undefined; if (this.field?.labelId.current) return undefined;
return ( return this.opts.ariaLabel.current || this.soma.langs.ts(SEARCH_FIELD_LANGS.LABEL) || undefined;
this.opts.ariaLabel.current || this.soma?.langs.ts(SEARCH_FIELD_LANGS.LABEL) || undefined
);
}); });
setInputRef(el: HTMLInputElement | null) { setInputRef(el: HTMLInputElement | null) {
@ -205,9 +204,8 @@ export class SearchFieldInputProvider {
private constructor(opts: SearchFieldInputOpts) { private constructor(opts: SearchFieldInputOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require() this.provider = SearchFieldProvider.require();
.runtime(searchFieldMorfo, {}) this.runtimePart = this.provider.runtime.part('input', {
.part('input', {
id: opts.id, id: opts.id,
ref: opts.ref, ref: opts.ref,
owner: this, owner: this,
@ -215,7 +213,6 @@ export class SearchFieldInputProvider {
this.provider.setInputRef(el as HTMLInputElement | null); this.provider.setInputRef(el as HTMLInputElement | null);
} }
}); });
this.provider = SearchFieldProvider.require();
} }
readonly props = $derived.by(() => readonly props = $derived.by(() =>
@ -264,10 +261,12 @@ export class SearchFieldClearTriggerProvider {
private constructor(opts: SearchFieldClearTriggerOpts) { private constructor(opts: SearchFieldClearTriggerOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(searchFieldMorfo, {})
.part('clear-trigger', { id: opts.id, ref: opts.ref, owner: this });
this.provider = SearchFieldProvider.require(); this.provider = SearchFieldProvider.require();
this.runtimePart = this.provider.runtime.part('clear-trigger', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => { readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
@ -277,7 +276,7 @@ export class SearchFieldClearTriggerProvider {
readonly resolvedAriaLabel: Active<string | undefined> = readableActive( readonly resolvedAriaLabel: Active<string | undefined> = readableActive(
() => () =>
this.opts.ariaLabel.current || this.opts.ariaLabel.current ||
this.provider.soma?.langs.ts(SEARCH_FIELD_LANGS.CLEAR) || this.provider.soma.langs.ts(SEARCH_FIELD_LANGS.CLEAR) ||
undefined undefined
); );

@ -15,7 +15,7 @@ import { TAG_GROUP_LANGS } from './langs';
import type { TagGroupSelectionMode } from './types'; import type { TagGroupSelectionMode } from './types';
import { tagGroupMorfo } from '../../../morfo/components/tag-group'; import { tagGroupMorfo } from '../../../morfo/components/tag-group';
import type { SomaRuntimePart } from '../../runtime.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
const attrs = createAttrs(tagGroupMorfo); const attrs = createAttrs(tagGroupMorfo);
@ -38,6 +38,8 @@ interface TagGroupOpts
export class TagGroupProvider { export class TagGroupProvider {
readonly opts: TagGroupOpts; readonly opts: TagGroupOpts;
readonly runtimePart: SomaRuntimePart; readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
readonly runtime: SomaRuntime;
static readonly ctx = context<TagGroupProvider>('TagGroup'); static readonly ctx = context<TagGroupProvider>('TagGroup');
static get(): TagGroupProvider | undefined { static get(): TagGroupProvider | undefined {
return this.ctx.getOr(undefined) as TagGroupProvider | undefined; return this.ctx.getOr(undefined) as TagGroupProvider | undefined;
@ -50,25 +52,28 @@ export class TagGroupProvider {
return new TagGroupProvider(opts); return new TagGroupProvider(opts);
} }
readonly soma = Soma.get();
/** Registered by `TagGroup.Label` on mount. */ /** Registered by `TagGroup.Label` on mount. */
labelId = state(''); labelId = state('');
private constructor(opts: TagGroupOpts) { private constructor(opts: TagGroupOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require() this.soma = Soma.require();
.runtime(tagGroupMorfo, {}) this.runtime = this.soma.runtime(tagGroupMorfo, {});
.part('provider', { id: opts.id, ref: opts.ref, owner: this, context: TagGroupProvider.ctx }); this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: TagGroupProvider.ctx
});
} }
readonly resolvedDir: Active<Direction> = readableActive( readonly resolvedDir: Active<Direction> = readableActive(
() => this.soma?.prefs.getDir() ?? 'ltr' () => this.soma.prefs.getDir() ?? 'ltr'
); );
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(() => { readonly resolvedAriaLabel: Active<string | undefined> = readableActive(() => {
if (this.opts.ariaLabelledby.current || this.labelId.current) return undefined; if (this.opts.ariaLabelledby.current || this.labelId.current) return undefined;
return this.opts.ariaLabel.current || this.soma?.langs.ts(TAG_GROUP_LANGS.LABEL) || undefined; return this.opts.ariaLabel.current || this.soma.langs.ts(TAG_GROUP_LANGS.LABEL) || undefined;
}); });
readonly resolvedAriaLabelledby = $derived.by( readonly resolvedAriaLabelledby = $derived.by(
@ -246,10 +251,12 @@ export class TagGroupLabelProvider {
private constructor(opts: TagGroupLabelOpts) { private constructor(opts: TagGroupLabelOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(tagGroupMorfo, {})
.part('label', { id: opts.id, ref: opts.ref, owner: this });
this.provider = TagGroupProvider.require(); this.provider = TagGroupProvider.require();
this.runtimePart = this.provider.runtime.part('label', {
id: opts.id,
ref: opts.ref,
owner: this
});
// A30: direct assignment in constructor, not $effect. // A30: direct assignment in constructor, not $effect.
this.provider.labelId.current = opts.id.current; this.provider.labelId.current = opts.id.current;
} }
@ -291,10 +298,13 @@ export class TagGroupItemProvider {
private constructor(opts: TagGroupItemOpts) { private constructor(opts: TagGroupItemOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(tagGroupMorfo, {})
.part('item', { id: opts.id, ref: opts.ref, owner: this, context: TagGroupItemProvider.ctx });
this.provider = TagGroupProvider.require(); this.provider = TagGroupProvider.require();
this.runtimePart = this.provider.runtime.part('item', {
id: opts.id,
ref: opts.ref,
owner: this,
context: TagGroupItemProvider.ctx
});
} }
readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current));
@ -370,10 +380,12 @@ export class TagGroupLinkProvider {
// the root's selector `[${attrs.item}]` picks it up too — we add a // the root's selector `[${attrs.item}]` picks it up too — we add a
// DOM-query bridge via `data-tag-group-item` + 'data-tag-group-link'. // DOM-query bridge via `data-tag-group-item` + 'data-tag-group-link'.
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(tagGroupMorfo, {})
.part('link', { id: opts.id, ref: opts.ref, owner: this });
this.provider = TagGroupProvider.require(); this.provider = TagGroupProvider.require();
this.runtimePart = this.provider.runtime.part('link', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current));
@ -428,17 +440,19 @@ export class TagGroupRemoveButtonProvider {
private constructor(opts: TagGroupRemoveButtonOpts) { private constructor(opts: TagGroupRemoveButtonOpts) {
this.opts = opts; this.opts = opts;
this.runtimePart = Soma.require()
.runtime(tagGroupMorfo, {})
.part('remove-button', { id: opts.id, ref: opts.ref, owner: this });
this.provider = TagGroupProvider.require(); this.provider = TagGroupProvider.require();
this.item = TagGroupItemProvider.require(); this.item = TagGroupItemProvider.require();
this.runtimePart = this.provider.runtime.part('remove-button', {
id: opts.id,
ref: opts.ref,
owner: this
});
} }
readonly resolvedAriaLabel = $derived.by(() => { readonly resolvedAriaLabel = $derived.by(() => {
if (this.opts.ariaLabel.current) return this.opts.ariaLabel.current; if (this.opts.ariaLabel.current) return this.opts.ariaLabel.current;
return ( return (
this.provider.soma?.langs.t(TAG_GROUP_LANGS.REMOVE, { this.provider.soma.langs.t(TAG_GROUP_LANGS.REMOVE, {
tag: this.item.opts.value.current tag: this.item.opts.value.current
}) || 'Remove' }) || 'Remove'
); );

Loading…
Cancel
Save

Powered by TurnKey Linux.