Initial commit: plantilla base PHP para webs Acai CMS
This commit is contained in:
723
template/estandar/css/builder-acaivueselect-v2.vue
Normal file
723
template/estandar/css/builder-acaivueselect-v2.vue
Normal file
@@ -0,0 +1,723 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- MODO 1: TOGGLE SEGMENTADO (exactamente 2 opciones) -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<div v-if="displayMode === 'toggle'"
|
||||
ref="toggleContainer"
|
||||
class="acai-toggle relative inline-flex items-center rounded-full cursor-pointer select-none"
|
||||
role="switch"
|
||||
:aria-checked="isToggleOn"
|
||||
:aria-label="fieldConfig ? fieldConfig.label || field : field"
|
||||
tabindex="0"
|
||||
@click="toggleSwitch"
|
||||
@keydown.enter.prevent="toggleSwitch"
|
||||
@keydown.space.prevent="toggleSwitch">
|
||||
|
||||
<!-- Pill deslizante -->
|
||||
<span ref="togglePill" class="acai-toggle-pill absolute rounded-full"></span>
|
||||
|
||||
<!-- Textos -->
|
||||
<span v-for="(opt, idx) in allRealOptions" :key="'toggle-opt-' + idx"
|
||||
:ref="'toggleOpt' + idx"
|
||||
class="acai-toggle-option relative flex items-center justify-center"
|
||||
:class="getToggleOptionClass(idx)">
|
||||
<div v-if="getToggleIcon(idx)" class="toggle-icon flex-shrink-0" style="margin-right: 6px;" v-html="getToggleIcon(idx)"></div>
|
||||
<div class="font-light leading-none">{{ opt.label }}</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- MODO 2: COLOR SELECT -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<div v-else-if="displayMode === 'color'" v-click-outside="closeDropdown">
|
||||
<div class="p-3 w-full bg-gray-200 border-gray-600 border-2 rounded-lg shadow cursor-pointer flex items-center justify-between transition-colors duration-200"
|
||||
:style="{
|
||||
backgroundColor: currentColor || '#e5e7eb',
|
||||
color: currentTextColor,
|
||||
fontWeight: currentColor ? '600' : '400'
|
||||
}"
|
||||
@click="open = !open">
|
||||
<span>{{ currentLabel || 'Seleccionar color...' }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon w-6 h-6 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': open }"
|
||||
:style="{ color: currentTextColor }"
|
||||
width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div v-show="open" class="absolute z-50 w-full mt-1 border-2 border-gray-600 rounded-lg shadow-lg overflow-hidden bg-white">
|
||||
<div v-for="(opt, idx) in realOptions"
|
||||
:key="idx"
|
||||
class="px-3 py-2 cursor-pointer flex items-center transition-all duration-150 border-b border-gray-200 last:border-b-0"
|
||||
:style="getColorOptionStyle(opt.value)"
|
||||
@mouseenter="hovered = opt.value"
|
||||
@mouseleave="hovered = null"
|
||||
@click="selectOption(opt.value)">
|
||||
<span class="w-6 h-6 rounded border border-gray-300 mr-3 flex-shrink-0 transition-transform duration-150"
|
||||
:style="{
|
||||
backgroundColor: getSwatchColor(opt.value),
|
||||
transform: hovered === opt.value ? 'scale(1.2)' : 'scale(1)',
|
||||
borderColor: hovered === opt.value ? '#9ca3af' : '#d1d5db'
|
||||
}"></span>
|
||||
<span>{{ opt.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- MODO 3: SELECT NORMAL -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<div v-else>
|
||||
<vue-select
|
||||
:multiple="multiple"
|
||||
:class="['border-2 border-gray-600 rounded-lg', { 'multiple': multiple }]"
|
||||
v-model="selected_values"
|
||||
:options="tableRecords"
|
||||
:label="labelKey"
|
||||
:reduce="(option) => option[valueKey]"
|
||||
:clearable="false"
|
||||
:filterable="isFilterable"
|
||||
:loading="loading"
|
||||
@input="onInput"
|
||||
@search="onSearch"
|
||||
placeholder="Seleccionar..."
|
||||
>
|
||||
<template #no-options="{ search }">
|
||||
<span v-if="search">No se encontraron resultados para "<b>{{ search }}</b>"</span>
|
||||
<span v-else>No hay opciones disponibles</span>
|
||||
</template>
|
||||
<template #list-footer v-if="isPaginated && totalRecords > limit">
|
||||
<li class="pagination flex justify-between px-3 py-2 border-t border-gray-300 bg-gray-100">
|
||||
<button class="text-sm px-2 py-1 rounded" :class="hasPrevPage ? 'text-blue-600 hover:bg-blue-50 cursor-pointer' : 'text-gray-400 cursor-not-allowed'" :disabled="!hasPrevPage" @click.prevent="offset -= limit">← Anterior</button>
|
||||
<span class="text-xs text-gray-500 self-center">{{ offset + 1 }}-{{ Math.min(offset + limit, totalRecords) }} de {{ totalRecords }}</span>
|
||||
<button class="text-sm px-2 py-1 rounded" :class="hasNextPage ? 'text-blue-600 hover:bg-blue-50 cursor-pointer' : 'text-gray-400 cursor-not-allowed'" :disabled="!hasNextPage" @click.prevent="offset += limit">Siguiente →</button>
|
||||
</li>
|
||||
</template>
|
||||
</vue-select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
components: {
|
||||
"vue-select": VueSelect.VueSelect,
|
||||
},
|
||||
directives: {
|
||||
'click-outside': {
|
||||
bind: function(el, binding) {
|
||||
el._clickOutside = function(event) {
|
||||
if (!(el === event.target || el.contains(event.target))) {
|
||||
binding.value();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', el._clickOutside);
|
||||
},
|
||||
unbind: function(el) {
|
||||
if (el._clickOutside) {
|
||||
document.removeEventListener('click', el._clickOutside);
|
||||
delete el._clickOutside;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
builder: { type: Object, required: true },
|
||||
data: { type: Object, required: true },
|
||||
field: { type: String, required: true },
|
||||
toggleIcons: { type: Object, default: null },
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
selected_values: '',
|
||||
currentLabel: '',
|
||||
loading: false,
|
||||
tableRecords: [],
|
||||
search: '',
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
cmsColors: null,
|
||||
loadingColors: false,
|
||||
currentColor: '',
|
||||
currentTextColor: '#111827',
|
||||
open: false,
|
||||
hovered: null,
|
||||
_visibilityObserver: null,
|
||||
// ── Mapa completo de colores con nombre → hex ──
|
||||
// Incluye: main colors (resueltos via CMS), nombres ES/EN,
|
||||
// y paleta Tailwind v3/v4 con variantes de nombre personalizadas
|
||||
colorNameMap: {
|
||||
// ── Main colors (se sobreescriben con valores reales del CMS) ──
|
||||
'main color': '#6366f1',
|
||||
'main color light': '#818cf8',
|
||||
'main color dark': '#4f46e5',
|
||||
// ── Blanco / Negro ──
|
||||
'blanco': '#ffffff',
|
||||
'white': '#ffffff',
|
||||
'negro': '#000000',
|
||||
'black': '#000000',
|
||||
// ── Grises (gray - Tailwind) ──
|
||||
'gris': '#6b7280',
|
||||
'gray': '#6b7280',
|
||||
'gris claro': '#9ca3af',
|
||||
'gris light': '#9ca3af',
|
||||
'light gray': '#9ca3af',
|
||||
'gris oscuro': '#374151',
|
||||
'gris dark': '#374151',
|
||||
'dark gray': '#374151',
|
||||
'gris muy claro': '#d1d5db',
|
||||
'gris muy oscuro': '#1f2937',
|
||||
// ── Grises cálidos (neutral - Tailwind) ──
|
||||
'gris calido': '#737373',
|
||||
'gris calido light': '#a3a3a3',
|
||||
'gris calido dark': '#525252',
|
||||
'gris calido muy claro': '#d4d4d4',
|
||||
'gris calido muy oscuro': '#262626',
|
||||
'warm gray': '#737373',
|
||||
// ── Grises neutros (zinc - Tailwind) ──
|
||||
'gris neutro': '#71717a',
|
||||
'gris neutro light': '#a1a1aa',
|
||||
'gris neutro dark': '#52525b',
|
||||
'zinc': '#71717a',
|
||||
// ── Grises fríos (slate - Tailwind) ──
|
||||
'gris frio': '#64748b',
|
||||
'gris frio light': '#94a3b8',
|
||||
'gris frio dark': '#475569',
|
||||
'slate': '#64748b',
|
||||
// ── Grises piedra (stone - Tailwind) ──
|
||||
'gris piedra': '#78716c',
|
||||
'gris piedra light': '#a8a29e',
|
||||
'gris piedra dark': '#57534e',
|
||||
'stone': '#78716c',
|
||||
// ── Rojos ──
|
||||
'rojo': '#ef4444',
|
||||
'rojo claro': '#f87171',
|
||||
'rojo oscuro': '#dc2626',
|
||||
'red': '#ef4444',
|
||||
// ── Naranjas ──
|
||||
'naranja': '#f97316',
|
||||
'naranja claro': '#fb923c',
|
||||
'naranja oscuro': '#ea580c',
|
||||
'orange': '#f97316',
|
||||
// ── Amarillos ──
|
||||
'amarillo': '#eab308',
|
||||
'amarillo claro': '#facc15',
|
||||
'amarillo oscuro': '#ca8a04',
|
||||
'yellow': '#eab308',
|
||||
// ── Verdes ──
|
||||
'verde': '#22c55e',
|
||||
'verde claro': '#4ade80',
|
||||
'verde oscuro': '#16a34a',
|
||||
'green': '#22c55e',
|
||||
// ── Azules ──
|
||||
'azul': '#3b82f6',
|
||||
'azul claro': '#60a5fa',
|
||||
'azul oscuro': '#2563eb',
|
||||
'blue': '#3b82f6',
|
||||
// ── Índigos ──
|
||||
'indigo': '#6366f1',
|
||||
'indigo claro': '#818cf8',
|
||||
'indigo oscuro': '#4f46e5',
|
||||
// ── Violetas / Morados ──
|
||||
'violeta': '#8b5cf6',
|
||||
'morado': '#8b5cf6',
|
||||
'violet': '#8b5cf6',
|
||||
'purple': '#a855f7',
|
||||
// ── Rosas ──
|
||||
'rosa': '#ec4899',
|
||||
'rosa claro': '#f472b6',
|
||||
'rosa oscuro': '#db2777',
|
||||
'pink': '#ec4899',
|
||||
// ── Fucsias ──
|
||||
'fucsia': '#d946ef',
|
||||
'fuchsia': '#d946ef',
|
||||
// ── Cianes / Turquesas ──
|
||||
'cian': '#06b6d4',
|
||||
'cyan': '#06b6d4',
|
||||
'turquesa': '#14b8a6',
|
||||
'teal': '#14b8a6',
|
||||
// ── Esmeraldas ──
|
||||
'esmeralda': '#10b981',
|
||||
'emerald': '#10b981',
|
||||
// ── Limas ──
|
||||
'lima': '#84cc16',
|
||||
'lime': '#84cc16',
|
||||
// ── Ámbar ──
|
||||
'ambar': '#f59e0b',
|
||||
'amber': '#f59e0b',
|
||||
// ── Cielo ──
|
||||
'cielo': '#0ea5e9',
|
||||
'sky': '#0ea5e9',
|
||||
// ── Transparente ──
|
||||
'transparente': 'transparent',
|
||||
'transparent': 'transparent',
|
||||
}
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.initFieldValue();
|
||||
this.loadOptions();
|
||||
},
|
||||
mounted: function() {
|
||||
var self = this;
|
||||
this.$nextTick(function() {
|
||||
self.positionPill(false);
|
||||
// ── IntersectionObserver ──
|
||||
// Reposiciona la pill cuando el toggle pasa de oculto a visible
|
||||
// (resuelve el problema de toggles en tabs inactivos con offsetWidth = 0)
|
||||
if (self.$refs.toggleContainer) {
|
||||
self._visibilityObserver = new IntersectionObserver(function(entries) {
|
||||
if (entries[0].isIntersecting) {
|
||||
self.positionPill(false);
|
||||
}
|
||||
}, { threshold: 0.1 });
|
||||
self._visibilityObserver.observe(self.$refs.toggleContainer);
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy: function() {
|
||||
if (this._visibilityObserver) {
|
||||
this._visibilityObserver.disconnect();
|
||||
this._visibilityObserver = null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldConfig: function() { return this.builder.vars[this.field]; },
|
||||
fieldOptions: function() { return this.fieldConfig.options.builder_custom; },
|
||||
multiple: function() { return !!this.fieldConfig.multi; },
|
||||
tableName: function() { return this.fieldOptions.tableName || null; },
|
||||
querySQL: function() { return this.fieldOptions.query || null; },
|
||||
isRemote: function() { return !!(this.tableName || this.querySQL); },
|
||||
isPaginated: function() { return this.isRemote; },
|
||||
isFilterable: function() { return this.tableRecords.length > 10; },
|
||||
realOptions: function() {
|
||||
if (this.isRemote) return [];
|
||||
var internalKeys = ['tableName', 'fieldLabel', 'fieldValue', 'query'];
|
||||
var opts = [];
|
||||
var entries = Object.entries(this.fieldOptions);
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
if (internalKeys.indexOf(entries[i][0]) === -1) {
|
||||
opts.push({ value: entries[i][0], label: entries[i][1] });
|
||||
}
|
||||
}
|
||||
var emptyIdx = -1;
|
||||
for (var j = 0; j < opts.length; j++) {
|
||||
if (opts[j].value === '') { emptyIdx = j; break; }
|
||||
}
|
||||
if (emptyIdx > 0) {
|
||||
var emptyOpt = opts.splice(emptyIdx, 1)[0];
|
||||
opts.unshift(emptyOpt);
|
||||
}
|
||||
return opts;
|
||||
},
|
||||
allRealOptions: function() { return this.realOptions; },
|
||||
displayMode: function() {
|
||||
if (this.isRemote || this.multiple) return 'select';
|
||||
var opts = this.allRealOptions;
|
||||
if (opts.length >= 2 && this.isColorOptions(opts)) return 'color';
|
||||
if (opts.length === 2) return 'toggle';
|
||||
return 'select';
|
||||
},
|
||||
isToggleOn: function() {
|
||||
if (this.allRealOptions.length < 2) return false;
|
||||
var currentVal = this.data[this.field].newValues.builder_custom.value;
|
||||
return currentVal === this.allRealOptions[1].value;
|
||||
},
|
||||
labelKey: function() {
|
||||
if (this.querySQL) return this.tableRecords.length > 0 ? Object.keys(this.tableRecords[0])[1] || 'label' : 'label';
|
||||
if (this.tableName) return this.fieldOptions.fieldLabel || 'label';
|
||||
return 'label';
|
||||
},
|
||||
valueKey: function() {
|
||||
if (this.querySQL) return this.tableRecords.length > 0 ? Object.keys(this.tableRecords[0])[0] || 'value' : 'value';
|
||||
if (this.tableName) return this.fieldOptions.fieldValue || 'value';
|
||||
return 'value';
|
||||
},
|
||||
totalRecords: function() { return this.tableRecords.length; },
|
||||
hasNextPage: function() { return (this.offset + this.limit) < this.totalRecords; },
|
||||
hasPrevPage: function() { return this.offset > 0; },
|
||||
},
|
||||
methods: {
|
||||
|
||||
initFieldValue: function() {
|
||||
var fieldData = this.data[this.field];
|
||||
if (typeof fieldData.newValues.builder_custom.value === 'undefined') {
|
||||
Vue.set(fieldData.newValues.builder_custom, 'value', '');
|
||||
}
|
||||
},
|
||||
|
||||
loadOptions: function() {
|
||||
if (this.fieldConfig.type !== 'list') return;
|
||||
if (this.tableName) { this.loadFromTable(); }
|
||||
else if (this.querySQL) { this.loadFromQuery(); }
|
||||
else { this.loadManualOptions(); }
|
||||
this.syncSelectedValue();
|
||||
this.updateCurrentLabel();
|
||||
if (this.displayMode === 'color') { this.loadColors(); }
|
||||
},
|
||||
|
||||
loadFromTable: function() {
|
||||
var self = this;
|
||||
this.loading = true;
|
||||
Rest.get(this.tableName)
|
||||
.then(function(r) { self.tableRecords = self.prependEmpty(r.data || []); })
|
||||
.catch(function(e) { console.error('[acai-vue-selectv2] Error:', e); self.tableRecords = []; })
|
||||
.finally(function() { self.loading = false; self.syncSelectedValue(); });
|
||||
},
|
||||
|
||||
loadFromQuery: function() {
|
||||
var self = this;
|
||||
this.loading = true;
|
||||
Rest.query(this.querySQL)
|
||||
.then(function(r) {
|
||||
if (!r || !r.data || !r.data[0]) { self.tableRecords = []; return; }
|
||||
self.tableRecords = self.prependEmpty(r.data);
|
||||
})
|
||||
.catch(function(e) { console.error('[acai-vue-selectv2] Error:', e); self.tableRecords = []; })
|
||||
.finally(function() { self.loading = false; self.syncSelectedValue(); });
|
||||
},
|
||||
|
||||
loadManualOptions: function() {
|
||||
var internalKeys = ['tableName', 'fieldLabel', 'fieldValue', 'query'];
|
||||
var entries = Object.entries(this.fieldOptions);
|
||||
this.tableRecords = [];
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
if (internalKeys.indexOf(entries[i][0]) === -1) {
|
||||
this.tableRecords.push({ label: entries[i][1], value: entries[i][0] });
|
||||
}
|
||||
}
|
||||
if (!this.multiple) {
|
||||
this.tableRecords.unshift({ label: '(Sin valor asignado)', value: '' });
|
||||
}
|
||||
},
|
||||
|
||||
prependEmpty: function(records) {
|
||||
if (this.multiple || !records.length) return records;
|
||||
var empty = {};
|
||||
empty[this.labelKey] = '(Sin valor asignado)';
|
||||
empty[this.valueKey] = '';
|
||||
return [empty].concat(records);
|
||||
},
|
||||
|
||||
syncSelectedValue: function() {
|
||||
var currentValue = this.data[this.field].newValues.builder_custom.value;
|
||||
this.selected_values = this.multiple
|
||||
? this.parseMultiValue(currentValue)
|
||||
: (currentValue != null ? currentValue : '');
|
||||
},
|
||||
|
||||
parseMultiValue: function(value) {
|
||||
if (!value || String(value).trim() === '') return [];
|
||||
return String(value).trim().split("\t").filter(function(v) { return v !== ''; });
|
||||
},
|
||||
|
||||
updateCurrentLabel: function() {
|
||||
var val = this.data[this.field].newValues.builder_custom.value;
|
||||
this.currentLabel = this.fieldOptions[val] || '';
|
||||
},
|
||||
|
||||
saveField: function(value) {
|
||||
this.data[this.field].newValues.builder_custom.value = value;
|
||||
this.data[this.field].value = value;
|
||||
this.updateCurrentLabel();
|
||||
if (this.displayMode === 'color') { this.updateCurrentColor(); }
|
||||
this.$emit("save-data");
|
||||
},
|
||||
|
||||
// ══════════════════════════════════════
|
||||
// MODO TOGGLE
|
||||
// ══════════════════════════════════════
|
||||
|
||||
positionPill: function(animate) {
|
||||
var pill = this.$refs.togglePill;
|
||||
if (!pill) return;
|
||||
|
||||
var container = this.$refs.toggleContainer;
|
||||
if (!container) return;
|
||||
|
||||
var opt0Ref = this.$refs['toggleOpt0'];
|
||||
var opt1Ref = this.$refs['toggleOpt1'];
|
||||
var opt0 = Array.isArray(opt0Ref) ? opt0Ref[0] : opt0Ref;
|
||||
var opt1 = Array.isArray(opt1Ref) ? opt1Ref[0] : opt1Ref;
|
||||
if (!opt0 || !opt1) return;
|
||||
|
||||
var clientW = container.clientWidth;
|
||||
var clientH = container.clientHeight;
|
||||
|
||||
// Si el contenedor no es visible aún, salir sin posicionar
|
||||
// (el IntersectionObserver lo reintentará cuando sea visible)
|
||||
if (clientW === 0 || clientH === 0) return;
|
||||
|
||||
var activeIdx = this.isToggleOn ? 1 : 0;
|
||||
var hasIcon = this.getToggleIcon(activeIdx) !== null;
|
||||
var extra = hasIcon ? 4 : 0;
|
||||
|
||||
var pillLeft, pillWidth;
|
||||
|
||||
if (this.isToggleOn) {
|
||||
pillWidth = opt1.offsetWidth + extra;
|
||||
pillLeft = clientW - pillWidth;
|
||||
if (pillLeft < 0) pillLeft = 0;
|
||||
} else {
|
||||
pillWidth = opt0.offsetWidth + extra;
|
||||
pillLeft = 0;
|
||||
if (pillWidth > clientW) pillWidth = clientW;
|
||||
}
|
||||
|
||||
pill.style.width = pillWidth + 'px';
|
||||
pill.style.height = clientH + 'px';
|
||||
pill.style.top = '0px';
|
||||
|
||||
if (animate) {
|
||||
pill.style.transition = 'left 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
} else {
|
||||
pill.style.transition = 'none';
|
||||
}
|
||||
|
||||
pill.style.left = pillLeft + 'px';
|
||||
},
|
||||
|
||||
toggleSwitch: function() {
|
||||
var opts = this.allRealOptions;
|
||||
if (opts.length < 2) return;
|
||||
var newValue = this.isToggleOn ? opts[0].value : opts[1].value;
|
||||
this.saveField(newValue);
|
||||
var self = this;
|
||||
this.$nextTick(function() {
|
||||
self.positionPill(true);
|
||||
});
|
||||
},
|
||||
|
||||
getToggleOptionClass: function(idx) {
|
||||
var isActive = (idx === 0 && !this.isToggleOn) || (idx === 1 && this.isToggleOn);
|
||||
return isActive ? 'acai-toggle-option--active' : 'acai-toggle-option--inactive';
|
||||
},
|
||||
|
||||
getToggleIcon: function(index) {
|
||||
if (!this.toggleIcons) return null;
|
||||
var opt = this.allRealOptions[index];
|
||||
if (!opt) return null;
|
||||
return this.toggleIcons[opt.value] || this.toggleIcons[index] || null;
|
||||
},
|
||||
|
||||
// ══════════════════════════════════════
|
||||
// MODO COLOR
|
||||
// ══════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Determina si un conjunto de opciones representa colores.
|
||||
* Detecta: colorNameMap keys, hex (#xxx/#xxxxxx), rgb/rgba, hsl/hsla.
|
||||
* Activa modo color si al menos la mitad de las opciones son colores.
|
||||
*/
|
||||
isColorOptions: function(opts) {
|
||||
var hexRegex = /^#[0-9a-f]{3,8}$/i;
|
||||
var rgbRegex = /^rgba?\s*\(/i;
|
||||
var hslRegex = /^hsla?\s*\(/i;
|
||||
var map = this.colorNameMap;
|
||||
var colorCount = 0;
|
||||
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
var l = opts[i].label.toLowerCase().trim();
|
||||
// Vacío = opción por defecto, la contamos como color
|
||||
if (l === '') { colorCount++; continue; }
|
||||
// Buscar en el mapa de nombres
|
||||
if (map[l] !== undefined) { colorCount++; continue; }
|
||||
// Hex
|
||||
if (hexRegex.test(l)) { colorCount++; continue; }
|
||||
// RGB / RGBA
|
||||
if (rgbRegex.test(l)) { colorCount++; continue; }
|
||||
// HSL / HSLA
|
||||
if (hslRegex.test(l)) { colorCount++; continue; }
|
||||
}
|
||||
return colorCount >= Math.ceil(opts.length / 2);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resuelve el hex real de una label de color.
|
||||
* Prioridad: CMS colors (main) > colorNameMap > hex/rgb directo > fallback gris.
|
||||
*/
|
||||
resolveColorHex: function(label) {
|
||||
var l = (typeof label === 'string') ? label.toLowerCase().trim() : '';
|
||||
if (!l) return '';
|
||||
|
||||
// 1) Hex directo
|
||||
if (/^#[0-9a-f]{3,8}$/i.test(l)) return l;
|
||||
|
||||
// 2) RGB directo → convertir a hex
|
||||
var rgbMatch = l.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
var r = parseInt(rgbMatch[1]), g = parseInt(rgbMatch[2]), b = parseInt(rgbMatch[3]);
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
// 3) Mapa de nombres
|
||||
if (this.colorNameMap[l] !== undefined) return this.colorNameMap[l];
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Carga los colores reales del CMS (main color, light, dark)
|
||||
* y construye el mapa cmsColors con hex reales para cada opción.
|
||||
*/
|
||||
loadColors: function() {
|
||||
var self = this;
|
||||
this.loadingColors = true;
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/admin.php?menu=configuracion_tienda&action=edit&num=1', true);
|
||||
xhr.onload = function() {
|
||||
var doc = new DOMParser().parseFromString(xhr.responseText, 'text/html');
|
||||
var principal = doc.querySelector('input[name="color_principal"]');
|
||||
var claro = doc.querySelector('input[name="color_principal_claro"]');
|
||||
var oscuro = doc.querySelector('input[name="color_principal_oscuro"]');
|
||||
|
||||
// Sobreescribir main colors en el mapa con valores reales del CMS
|
||||
if (principal && principal.value) self.colorNameMap['main color'] = principal.value;
|
||||
if (claro && claro.value) self.colorNameMap['main color light'] = claro.value;
|
||||
if (oscuro && oscuro.value) self.colorNameMap['main color dark'] = oscuro.value;
|
||||
|
||||
// Construir mapa de valor → hex para cada opción
|
||||
self.cmsColors = {};
|
||||
var entries = Object.entries(self.fieldOptions);
|
||||
var internalKeys = ['tableName', 'fieldLabel', 'fieldValue', 'query'];
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var val = entries[i][0], label = entries[i][1];
|
||||
if (internalKeys.indexOf(val) !== -1) continue;
|
||||
var resolved = self.resolveColorHex(label);
|
||||
if (resolved) {
|
||||
self.cmsColors[val] = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
self.updateCurrentColor();
|
||||
self.loadingColors = false;
|
||||
};
|
||||
xhr.onerror = function() { console.error('[acai-vue-selectv2] Error de red'); self.loadingColors = false; };
|
||||
xhr.send();
|
||||
} catch (e) { console.error('[acai-vue-selectv2] Error:', e); this.loadingColors = false; }
|
||||
},
|
||||
|
||||
updateCurrentColor: function() {
|
||||
if (!this.cmsColors) { this.currentColor = ''; this.currentTextColor = '#111827'; return; }
|
||||
var val = this.data[this.field].newValues.builder_custom.value;
|
||||
if (this.cmsColors[val] !== undefined) {
|
||||
this.currentColor = this.cmsColors[val];
|
||||
this.currentTextColor = this.isLightColor(this.currentColor) ? '#111827' : '#ffffff';
|
||||
} else { this.currentColor = ''; this.currentTextColor = '#111827'; }
|
||||
},
|
||||
|
||||
isLightColor: function(hex) {
|
||||
if (!hex || hex === 'transparent') return true;
|
||||
var clean = hex.replace('#', '');
|
||||
var full = clean.length === 3 ? clean[0]+clean[0]+clean[1]+clean[1]+clean[2]+clean[2] : clean;
|
||||
var r = parseInt(full.substring(0, 2), 16);
|
||||
var g = parseInt(full.substring(2, 4), 16);
|
||||
var b = parseInt(full.substring(4, 6), 16);
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.5;
|
||||
},
|
||||
|
||||
getSwatchColor: function(value) {
|
||||
return (this.cmsColors && this.cmsColors[value] !== undefined) ? this.cmsColors[value] : '#e5e7eb';
|
||||
},
|
||||
|
||||
selectOption: function(value) { this.open = false; this.saveField(value); },
|
||||
closeDropdown: function() { this.open = false; },
|
||||
|
||||
getColorOptionStyle: function(value) {
|
||||
var currentVal = this.data[this.field].newValues.builder_custom.value;
|
||||
var isSelected = value === currentVal;
|
||||
var isHovered = this.hovered === value;
|
||||
return {
|
||||
backgroundColor: isSelected ? '#e5e7eb' : (isHovered ? '#f3f4f6' : '#ffffff'),
|
||||
color: '#111827',
|
||||
fontWeight: isSelected ? '600' : '400',
|
||||
transition: 'all 0.2s ease',
|
||||
};
|
||||
},
|
||||
|
||||
onInput: function() {
|
||||
var value = this.multiple
|
||||
? (this.selected_values.length === 0 ? '' : ['', ...this.selected_values, ''].join("\t"))
|
||||
: (this.selected_values || '');
|
||||
this.saveField(value);
|
||||
},
|
||||
|
||||
onSearch: function(query) { this.search = query; this.offset = 0; },
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler: function() {
|
||||
this.updateCurrentLabel();
|
||||
if (this.displayMode === 'color') { this.updateCurrentColor(); }
|
||||
},
|
||||
deep: true,
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ── Select normal ── */
|
||||
.vs__dropdown-toggle { background: #edf2f7; padding: 10px; }
|
||||
.multiple .vs__selected {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgb(113 128 150);
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
/* ── Color dropdown ── */
|
||||
.rotate-180 { transform: rotate(180deg); }
|
||||
|
||||
/* ══════════════════════════════════════════════ */
|
||||
/* TOGGLE */
|
||||
/* ══════════════════════════════════════════════ */
|
||||
.acai-toggle {
|
||||
height: 42px;
|
||||
background-color: #d5dbe1;
|
||||
border: 2px solid #4b5563;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.acai-toggle-pill {
|
||||
background-color: #4a5568;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,.2), 0 1px 3px 3px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.acai-toggle-option {
|
||||
z-index: 10;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0;
|
||||
padding: 0 14px;
|
||||
transition: color 0.25s ease;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.acai-toggle-option--active {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.acai-toggle-option--inactive {
|
||||
color: #4a5568;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.acai-toggle:hover {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* ── Toggle icons ── */
|
||||
.toggle-icon { display: inline-flex; align-items: center; line-height: 0; }
|
||||
.toggle-icon svg { width: 20px; height: 20px; stroke: currentColor; fill: none; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user