multiple images in the edit product
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -127,55 +127,77 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# --- Images gallery ------------------------------------------------------- #}
|
{# --- Images gallery ------------------------------------------------------- #}
|
||||||
{# The first image is the product's main image; the rest feed the storefront #}
|
{# Existing images are reorderable (drag) and removable; the kept set is #}
|
||||||
{# carousel. Existing images are reorderable (drag) and removable; the kept set #}
|
{# submitted in order as repeated `existing_images` ids. New uploads accumulate #}
|
||||||
{# is submitted in order as repeated `existing_images` ids, and newly chosen #}
|
{# across separate "Add images" clicks into a DataTransfer that backs the hidden #}
|
||||||
{# files (repeated `image` parts) are appended after them by the controller. #}
|
{# `image` input (a native file input would otherwise replace its selection on #}
|
||||||
|
{# every pick); the controller stores and appends them after the kept images. #}
|
||||||
<script id="images-data" type="application/json">{% if product %}{{ product.images | json_encode() | safe }}{% else %}[]{% endif %}</script>
|
<script id="images-data" type="application/json">{% if product %}{{ product.images | json_encode() | safe }}{% else %}[]{% endif %}</script>
|
||||||
<div class="space-y-2" x-data="imageGallery(JSON.parse(document.getElementById('images-data').textContent))">
|
<div class="space-y-2" x-data="{
|
||||||
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="images", lang=lang | default(value='sk')) }}</span>
|
images: JSON.parse(document.getElementById('images-data').textContent),
|
||||||
<p class="{{ sublabel }}">{{ t(key="gallery-hint", lang=lang | default(value='sk')) }}</p>
|
staged: [],
|
||||||
|
dt: new DataTransfer(),
|
||||||
<template x-if="images.length">
|
|
||||||
<div class="flex flex-wrap gap-3">
|
|
||||||
<template x-for="(im, i) in images" :key="im.id">
|
|
||||||
<div draggable="true"
|
|
||||||
@dragstart="dragIndex = i"
|
|
||||||
@dragover.prevent
|
|
||||||
@drop.prevent="onDrop(i)"
|
|
||||||
:class="dragIndex === i ? 'opacity-50' : ''"
|
|
||||||
class="group relative size-24 cursor-move overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
|
||||||
<input type="hidden" name="existing_images" :value="im.id">
|
|
||||||
<img :src="`/images/${im.image_id}`" alt="" class="size-full object-cover">
|
|
||||||
<span x-show="i === 0"
|
|
||||||
class="absolute left-1 top-1 rounded-radius bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-on-primary dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="main-image", lang=lang | default(value='sk')) }}</span>
|
|
||||||
<button type="button" @click="remove(i)"
|
|
||||||
class="absolute right-1 top-1 flex size-5 items-center justify-center rounded-full bg-surface/70 text-xs text-danger opacity-0 transition group-hover:opacity-100 dark:bg-surface-dark/70"
|
|
||||||
title="{{ t(key='delete', lang=lang | default(value='sk')) }}">✕</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<label for="image" class="block text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="add-images", lang=lang | default(value='sk')) }}</label>
|
|
||||||
{{ ui::file_input(name="image", id="image", accept="image/*", attrs='multiple') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function imageGallery(initial) {
|
|
||||||
return {
|
|
||||||
images: (initial || []).map(im => ({ id: im.id, image_id: im.image_id })),
|
|
||||||
dragIndex: null,
|
dragIndex: null,
|
||||||
onDrop(i) {
|
onDrop(i) {
|
||||||
if (this.dragIndex === null || this.dragIndex === i) { this.dragIndex = null; return; }
|
if (this.dragIndex === null || this.dragIndex === i) { this.dragIndex = null; return; }
|
||||||
const moved = this.images.splice(this.dragIndex, 1)[0];
|
this.images.splice(i, 0, this.images.splice(this.dragIndex, 1)[0]);
|
||||||
this.images.splice(i, 0, moved);
|
|
||||||
this.dragIndex = null;
|
this.dragIndex = null;
|
||||||
},
|
},
|
||||||
remove(i) { this.images.splice(i, 1); },
|
addFiles(e) {
|
||||||
};
|
for (const f of e.target.files) { this.dt.items.add(f); this.staged.push({ url: URL.createObjectURL(f) }); }
|
||||||
}
|
this.$refs.holder.files = this.dt.files;
|
||||||
</script>
|
e.target.value = '';
|
||||||
|
},
|
||||||
|
removeStaged(i) {
|
||||||
|
this.dt.items.remove(i);
|
||||||
|
URL.revokeObjectURL(this.staged[i].url);
|
||||||
|
this.staged.splice(i, 1);
|
||||||
|
this.$refs.holder.files = this.dt.files;
|
||||||
|
},
|
||||||
|
}">
|
||||||
|
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="images", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<p class="{{ sublabel }}">{{ t(key="gallery-hint", lang=lang | default(value='sk')) }}</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-3" x-show="images.length || staged.length">
|
||||||
|
<template x-for="(im, i) in images" :key="im.id">
|
||||||
|
<div draggable="true"
|
||||||
|
@dragstart="dragIndex = i"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="onDrop(i)"
|
||||||
|
:class="dragIndex === i ? 'opacity-50' : ''"
|
||||||
|
class="group relative size-24 cursor-move overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
||||||
|
<input type="hidden" name="existing_images" :value="im.id">
|
||||||
|
<img :src="`/images/${im.image_id}`" alt="" class="size-full object-cover">
|
||||||
|
<span x-show="i === 0"
|
||||||
|
class="absolute left-1 top-1 rounded-radius bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-on-primary dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="main-image", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<button type="button" @click="images.splice(i, 1)"
|
||||||
|
class="absolute right-1 top-1 flex size-5 items-center justify-center rounded-full bg-surface/70 text-xs text-danger opacity-0 transition group-hover:opacity-100 dark:bg-surface-dark/70"
|
||||||
|
title="{{ t(key='delete', lang=lang | default(value='sk')) }}">✕</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{# Newly staged uploads (not yet saved): previews + remove. #}
|
||||||
|
<template x-for="(f, i) in staged" :key="f.url">
|
||||||
|
<div class="group relative size-24 overflow-hidden rounded-radius border border-dashed border-outline dark:border-outline-dark">
|
||||||
|
<img :src="f.url" alt="" class="size-full object-cover">
|
||||||
|
<span x-show="images.length === 0 && i === 0"
|
||||||
|
class="absolute left-1 top-1 rounded-radius bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-on-primary dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="main-image", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<button type="button" @click="removeStaged(i)"
|
||||||
|
class="absolute right-1 top-1 flex size-5 items-center justify-center rounded-full bg-surface/70 text-xs text-danger opacity-0 transition group-hover:opacity-100 dark:bg-surface-dark/70"
|
||||||
|
title="{{ t(key='delete', lang=lang | default(value='sk')) }}">✕</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Hidden input carries the accumulated files on submit; the visible picker #}
|
||||||
|
{# only feeds addFiles() and is reset after each pick so selections stack. #}
|
||||||
|
<input type="file" name="image" multiple class="hidden" x-ref="holder">
|
||||||
|
<input type="file" accept="image/*" multiple class="hidden" x-ref="picker" @change="addFiles($event)">
|
||||||
|
<button type="button" @click="$refs.picker.click()"
|
||||||
|
class="rounded-radius border border-outline px-3 py-1.5 text-sm font-medium text-on-surface hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt/50">
|
||||||
|
+ {{ t(key="add-images", lang=lang | default(value='sk')) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
|
{{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user