316 lines
11 KiB
Vue
316 lines
11 KiB
Vue
<script setup>
|
||
import { computed, reactive } from 'vue';
|
||
import Toggle from '@/components/Toggle.vue';
|
||
|
||
const props = defineProps({
|
||
field: { type: Object, required: true },
|
||
modelValue: { default: undefined },
|
||
// Options for multiselect fields (resolved by the page from optionsSource).
|
||
options: { type: Array, default: () => [] },
|
||
});
|
||
const emit = defineEmits(['update:modelValue']);
|
||
|
||
const val = computed({
|
||
get: () => props.modelValue,
|
||
set: (v) => emit('update:modelValue', v),
|
||
});
|
||
|
||
// Repeater helpers ---------------------------------------------------------
|
||
const rows = computed(() => (Array.isArray(props.modelValue) ? props.modelValue : []));
|
||
const addRow = () => emit('update:modelValue', [...rows.value, { find: '', replace: '' }]);
|
||
const updateRow = (i, key, v) => {
|
||
const next = rows.value.map((r, idx) => (idx === i ? { ...r, [key]: v } : r));
|
||
emit('update:modelValue', next);
|
||
};
|
||
const removeRow = (i) => emit('update:modelValue', rows.value.filter((_, idx) => idx !== i));
|
||
|
||
// Multiselect helpers -------------------------------------------------------
|
||
const selectedValues = computed(() =>
|
||
Array.isArray(props.modelValue) ? props.modelValue : []
|
||
);
|
||
const toggleValue = (value) => {
|
||
const next = selectedValues.value.includes(value)
|
||
? selectedValues.value.filter((v) => v !== value)
|
||
: [...selectedValues.value, value];
|
||
emit('update:modelValue', next);
|
||
};
|
||
|
||
// Remote sites helpers -------------------------------------------------------
|
||
const sites = computed(() => (Array.isArray(props.modelValue) ? props.modelValue : []));
|
||
const newSite = reactive({ url: '', username: '', app_password: '' });
|
||
const addSite = () => {
|
||
if (!newSite.url.trim()) return;
|
||
emit('update:modelValue', [
|
||
...sites.value,
|
||
{ url: newSite.url.trim(), username: newSite.username.trim(), app_password: newSite.app_password },
|
||
]);
|
||
newSite.url = '';
|
||
newSite.username = '';
|
||
newSite.app_password = '';
|
||
};
|
||
const removeSite = (i) => emit('update:modelValue', sites.value.filter((_, idx) => idx !== i));
|
||
|
||
// Media picker (image fields) ---------------------------------------------
|
||
const pickImage = () => {
|
||
const wp = window.wp;
|
||
if (!wp || !wp.media) {
|
||
// Fallback: prompt for a URL if the media library isn't available.
|
||
const url = window.prompt('Image URL');
|
||
if (url) emit('update:modelValue', url);
|
||
return;
|
||
}
|
||
const frame = wp.media({ title: props.field.label, multiple: false });
|
||
frame.on('select', () => {
|
||
const att = frame.state().get('selection').first().toJSON();
|
||
emit('update:modelValue', att.url);
|
||
});
|
||
frame.open();
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<div class="field">
|
||
<div class="field__info">
|
||
<div class="field__label">{{ field.label }}</div>
|
||
<p v-if="field.description" class="field__desc">{{ field.description }}</p>
|
||
</div>
|
||
|
||
<div class="field__control">
|
||
<!-- Text -->
|
||
<input
|
||
v-if="field.type === 'text'"
|
||
type="text"
|
||
class="field__input"
|
||
:placeholder="field.placeholder || ''"
|
||
:value="val || ''"
|
||
@input="val = $event.target.value"
|
||
/>
|
||
|
||
<!-- Toggle -->
|
||
<Toggle
|
||
v-else-if="field.type === 'toggle'"
|
||
:model-value="!!val"
|
||
@update:model-value="val = $event"
|
||
/>
|
||
|
||
<!-- Color -->
|
||
<div v-else-if="field.type === 'color'" class="field__color">
|
||
<input
|
||
type="color"
|
||
:value="val || field.default || '#4772fa'"
|
||
@input="val = $event.target.value"
|
||
/>
|
||
<span class="field__color-hex">{{ val || field.default || '#4772fa' }}</span>
|
||
</div>
|
||
|
||
<!-- Image / favicon -->
|
||
<button
|
||
v-else-if="field.type === 'image'"
|
||
type="button"
|
||
class="field__image"
|
||
@click="pickImage"
|
||
>
|
||
<img v-if="val" :src="val" alt="" />
|
||
<span v-else class="dashicons dashicons-format-image field__image-ph" />
|
||
<span class="field__image-label">{{ val ? 'Change' : (field.label + ' url') }}</span>
|
||
</button>
|
||
|
||
<!-- Multiselect (checkbox list) -->
|
||
<div v-else-if="field.type === 'multiselect'" class="field__multi">
|
||
<label v-for="opt in options" :key="opt.value" class="field__multi-item">
|
||
<input
|
||
type="checkbox"
|
||
:checked="selectedValues.includes(opt.value)"
|
||
@change="toggleValue(opt.value)"
|
||
/>
|
||
<span>{{ opt.label }}</span>
|
||
</label>
|
||
<p v-if="!options.length" class="field__multi-empty">No options available.</p>
|
||
</div>
|
||
|
||
<!-- Segmented No/Yes -->
|
||
<div v-else-if="field.type === 'segmented'" class="field__seg">
|
||
<button
|
||
type="button"
|
||
:class="{ 'field__seg--active': !val }"
|
||
@click="val = false"
|
||
>
|
||
No
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ 'field__seg--active': !!val }"
|
||
@click="val = true"
|
||
>
|
||
Yes
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Remote sites -->
|
||
<div v-else-if="field.type === 'remote-sites'" class="field__sites">
|
||
<div class="field__sites-add">
|
||
<input v-model="newSite.url" type="text" class="field__input" placeholder="Site URL (e.g., https://example.com)" />
|
||
<div class="field__sites-row">
|
||
<input v-model="newSite.username" type="text" class="field__input" placeholder="Username" />
|
||
<input v-model="newSite.app_password" type="password" class="field__input" placeholder="Application Password" />
|
||
<button type="button" class="field__newitem" @click="addSite">Add Site</button>
|
||
</div>
|
||
</div>
|
||
<div class="field__sites-list">
|
||
<div v-if="!sites.length" class="field__sites-empty">No remote sites configured yet</div>
|
||
<div v-for="(site, i) in sites" :key="i" class="field__sites-item">
|
||
<span class="field__sites-url">{{ site.url }}</span>
|
||
<span class="field__sites-user">{{ site.username }}</span>
|
||
<button type="button" class="field__repeater-remove" @click="removeSite(i)">×</button>
|
||
</div>
|
||
</div>
|
||
<p class="field__sites-help">
|
||
Application passwords can be created in WordPress under Users > Profile > Application Passwords
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Repeater -->
|
||
<div v-else-if="field.type === 'repeater'" class="field__repeater">
|
||
<div v-for="(row, i) in rows" :key="i" class="field__repeater-row">
|
||
<input
|
||
type="text"
|
||
class="field__input"
|
||
:placeholder="field.columns ? field.columns[0] : 'Find'"
|
||
:value="row.find"
|
||
@input="updateRow(i, 'find', $event.target.value)"
|
||
/>
|
||
<input
|
||
type="text"
|
||
class="field__input"
|
||
:placeholder="field.columns ? field.columns[1] : 'Replace'"
|
||
:value="row.replace"
|
||
@input="updateRow(i, 'replace', $event.target.value)"
|
||
/>
|
||
<button type="button" class="field__repeater-remove" @click="removeRow(i)">×</button>
|
||
</div>
|
||
<button type="button" class="field__newitem" @click="addRow">New item</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.field {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
gap: 32px;
|
||
padding: 28px 0;
|
||
border-bottom: 1px solid var(--aurora-border);
|
||
align-items: start;
|
||
}
|
||
.field__label { font-size: 0.95rem; font-weight: 600; color: var(--aurora-text); }
|
||
.field__desc { margin: 6px 0 0; font-size: 0.8125rem; color: var(--aurora-text-muted); line-height: 1.5; }
|
||
.field__control { display: flex; justify-content: flex-start; }
|
||
|
||
.field__input {
|
||
width: 100%;
|
||
background: var(--aurora-bg-subtle);
|
||
border: 1px solid var(--aurora-border);
|
||
color: var(--aurora-text);
|
||
border-radius: 8px;
|
||
padding: 9px 12px;
|
||
font-size: 0.875rem;
|
||
}
|
||
.field__input:focus { outline: none; border-color: var(--aurora-accent); }
|
||
|
||
.field__color { display: flex; align-items: center; gap: 10px; }
|
||
.field__color input[type='color'] {
|
||
width: 40px; height: 32px; border: 1px solid var(--aurora-border);
|
||
border-radius: 8px; background: none; cursor: pointer; padding: 2px;
|
||
}
|
||
.field__color-hex { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||
|
||
.field__image {
|
||
width: 160px; height: 120px;
|
||
border: 1px dashed var(--aurora-border);
|
||
border-radius: 12px;
|
||
background: var(--aurora-bg-subtle);
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
gap: 8px; cursor: pointer; color: var(--aurora-text-muted);
|
||
position: relative; overflow: hidden;
|
||
}
|
||
.field__image img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
|
||
.field__image-ph { font-size: 28px; width: 28px; height: 28px; }
|
||
.field__image-label {
|
||
position: absolute; bottom: 0; left: 0; right: 0;
|
||
background: var(--aurora-card-bg); padding: 6px; font-size: 0.75rem;
|
||
border-top: 1px solid var(--aurora-border);
|
||
}
|
||
|
||
.field__repeater { width: 100%; }
|
||
.field__repeater-row { display: flex; gap: 8px; margin-bottom: 8px; }
|
||
.field__repeater-remove {
|
||
flex-shrink: 0; width: 34px; border: 1px solid var(--aurora-border);
|
||
background: var(--aurora-bg-subtle); color: var(--aurora-text-muted);
|
||
border-radius: 8px; cursor: pointer; font-size: 1.1rem;
|
||
}
|
||
.field__newitem {
|
||
border: 1px solid var(--aurora-border);
|
||
background: var(--aurora-bg-subtle);
|
||
color: var(--aurora-text);
|
||
border-radius: 8px; padding: 8px 16px; font-size: 0.8125rem; cursor: pointer;
|
||
}
|
||
.field__newitem:hover { border-color: var(--aurora-accent); color: var(--aurora-accent); }
|
||
|
||
/* Multiselect */
|
||
.field__multi { display: flex; flex-direction: column; gap: 8px; width: 100%; }
|
||
.field__multi-item {
|
||
display: flex; align-items: center; gap: 8px;
|
||
font-size: 0.8125rem; color: var(--aurora-text); cursor: pointer;
|
||
}
|
||
.field__multi-item input { accent-color: var(--aurora-accent); }
|
||
.field__multi-empty { font-size: 0.8125rem; color: var(--aurora-text-muted); margin: 0; }
|
||
|
||
/* Segmented No/Yes */
|
||
.field__seg {
|
||
display: inline-flex;
|
||
background: var(--aurora-bg-subtle);
|
||
border: 1px solid var(--aurora-border);
|
||
border-radius: 9px;
|
||
padding: 3px;
|
||
gap: 2px;
|
||
}
|
||
.field__seg button {
|
||
border: none; background: none; cursor: pointer;
|
||
padding: 6px 26px; border-radius: 7px;
|
||
font-size: 0.8125rem; color: var(--aurora-text-muted);
|
||
}
|
||
.field__seg--active { background: var(--aurora-accent); color: #fff !important; }
|
||
|
||
/* Remote sites */
|
||
.field__sites { width: 100%; }
|
||
.field__sites-add .field__input { margin-bottom: 8px; }
|
||
.field__sites-row { display: flex; gap: 8px; }
|
||
.field__sites-row .field__input { margin-bottom: 0; }
|
||
.field__sites-row .field__newitem { flex-shrink: 0; white-space: nowrap; }
|
||
.field__sites-list {
|
||
margin-top: 12px;
|
||
border: 1px solid var(--aurora-border);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
.field__sites-empty {
|
||
padding: 18px; text-align: center;
|
||
font-size: 0.8125rem; color: var(--aurora-text-muted);
|
||
}
|
||
.field__sites-item {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--aurora-border);
|
||
font-size: 0.8125rem;
|
||
}
|
||
.field__sites-item:last-child { border-bottom: none; }
|
||
.field__sites-url { color: var(--aurora-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.field__sites-user { color: var(--aurora-text-muted); }
|
||
.field__sites-help { margin: 10px 0 0; font-size: 0.75rem; color: var(--aurora-text-muted); }
|
||
|
||
@media (max-width: 782px) {
|
||
.field { grid-template-columns: 1fr; gap: 12px; }
|
||
}
|
||
</style>
|