Files
aurora-wordpress-admin/app/src/pages/settings/components/SettingsField.vue
T

316 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 &gt; Profile &gt; 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>