This commit is contained in:
+120
-12
@@ -152,6 +152,7 @@
|
|||||||
.editor-button,
|
.editor-button,
|
||||||
.editor-primary-button,
|
.editor-primary-button,
|
||||||
.editor-secondary-button,
|
.editor-secondary-button,
|
||||||
|
.editor-danger-button,
|
||||||
.editor-icon-button {
|
.editor-icon-button {
|
||||||
border-radius: .75rem;
|
border-radius: .75rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -216,6 +217,122 @@
|
|||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-media-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: .65rem;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(6.25rem, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-preview:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-preview-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(49, 93, 58, .22);
|
||||||
|
border-radius: .8rem;
|
||||||
|
min-width: 0;
|
||||||
|
padding: .35rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-preview-card[data-selected="true"] {
|
||||||
|
border-color: #315d3a;
|
||||||
|
box-shadow: 0 0 0 .12rem rgba(49, 93, 58, .18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-preview-select {
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #233326;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font: inherit;
|
||||||
|
gap: .35rem;
|
||||||
|
min-height: 5.2rem;
|
||||||
|
padding: .25rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-preview-select small {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-preview-image {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: .55rem;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-preview-audio {
|
||||||
|
align-items: center;
|
||||||
|
background: #eef5ef;
|
||||||
|
border-radius: .55rem;
|
||||||
|
display: flex;
|
||||||
|
font-size: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 4.1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-remove {
|
||||||
|
align-items: center;
|
||||||
|
background: #b42318;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
font-weight: 800;
|
||||||
|
height: 2.25rem;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
position: absolute;
|
||||||
|
right: .15rem;
|
||||||
|
top: .15rem;
|
||||||
|
width: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-media-remove[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-form-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-form-actions > button {
|
||||||
|
flex: 1 1 11rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-danger-button {
|
||||||
|
align-items: center;
|
||||||
|
background: #b42318;
|
||||||
|
border: 1px solid #b42318;
|
||||||
|
color: #fff;
|
||||||
|
display: inline-flex;
|
||||||
|
font-weight: 700;
|
||||||
|
gap: .55rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-danger-icon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-round-button {
|
.editor-round-button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #315d3a;
|
background: #315d3a;
|
||||||
@@ -236,6 +353,9 @@
|
|||||||
.editor-round-button:focus-visible,
|
.editor-round-button:focus-visible,
|
||||||
.editor-primary-button:focus-visible,
|
.editor-primary-button:focus-visible,
|
||||||
.editor-secondary-button:focus-visible,
|
.editor-secondary-button:focus-visible,
|
||||||
|
.editor-danger-button:focus-visible,
|
||||||
|
.editor-media-preview-select:focus-visible,
|
||||||
|
.editor-media-remove:focus-visible,
|
||||||
.editor-icon-button:focus-visible {
|
.editor-icon-button:focus-visible {
|
||||||
outline: .2rem solid #f2ca6b;
|
outline: .2rem solid #f2ca6b;
|
||||||
outline-offset: .15rem;
|
outline-offset: .15rem;
|
||||||
@@ -383,12 +503,6 @@
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-coordinate-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: .55rem;
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-secondary-button,
|
.editor-secondary-button,
|
||||||
.editor-icon-button {
|
.editor-icon-button {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -446,12 +560,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 42rem) {
|
|
||||||
.editor-coordinate-grid {
|
|
||||||
grid-template-columns: max-content minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 34rem) {
|
@media (max-width: 34rem) {
|
||||||
.editor-media-actions {
|
.editor-media-actions {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
+11
-10
@@ -391,15 +391,9 @@
|
|||||||
<label for="editor-poi-description">Text</label>
|
<label for="editor-poi-description">Text</label>
|
||||||
<textarea id="editor-poi-description" rows="6"></textarea>
|
<textarea id="editor-poi-description" rows="6"></textarea>
|
||||||
|
|
||||||
<div class="editor-coordinate-grid">
|
<input id="editor-poi-lat" type="hidden">
|
||||||
<label for="editor-poi-lat">Breite</label>
|
<input id="editor-poi-lon" type="hidden">
|
||||||
<input id="editor-poi-lat" type="number" inputmode="decimal" step="0.000001" required>
|
<input id="editor-poi-radius" type="hidden" value="25">
|
||||||
<label for="editor-poi-lon">Länge</label>
|
|
||||||
<input id="editor-poi-lon" type="number" inputmode="decimal" step="0.000001" required>
|
|
||||||
|
|
||||||
<label for="editor-poi-radius">Auslöseradius in Metern</label>
|
|
||||||
<input id="editor-poi-radius" type="number" inputmode="numeric" min="1" step="1" value="25">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-media-actions" aria-label="Medien zur Station hinzufügen">
|
<div class="editor-media-actions" aria-label="Medien zur Station hinzufügen">
|
||||||
<button id="editor-poi-audio-add" class="editor-media-button" type="button" title="Audio hinzufügen">
|
<button id="editor-poi-audio-add" class="editor-media-button" type="button" title="Audio hinzufügen">
|
||||||
@@ -420,9 +414,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<input id="editor-poi-audio" type="file" accept="audio/*" hidden>
|
<input id="editor-poi-audio" type="file" accept="audio/*" hidden>
|
||||||
<input id="editor-poi-images" type="file" accept="image/*" multiple hidden>
|
<input id="editor-poi-images" type="file" accept="image/*" multiple hidden>
|
||||||
|
<div id="editor-poi-media-preview" class="editor-media-preview" aria-label="Medienvorschau"></div>
|
||||||
<p id="editor-poi-media-status" class="editor-muted" aria-live="polite"></p>
|
<p id="editor-poi-media-status" class="editor-muted" aria-live="polite"></p>
|
||||||
|
|
||||||
<button id="editor-poi-save" class="editor-primary-button" type="submit">Station speichern</button>
|
<div class="editor-form-actions">
|
||||||
|
<button id="editor-poi-save" class="editor-primary-button" type="submit">Station speichern</button>
|
||||||
|
<button id="editor-poi-delete" class="editor-danger-button" type="button" hidden>
|
||||||
|
<span class="editor-danger-icon" aria-hidden="true">−</span>
|
||||||
|
<span>POI entfernen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+312
-32
@@ -13,6 +13,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
poiAudio: 'editor-poi-audio',
|
poiAudio: 'editor-poi-audio',
|
||||||
poiAudioAdd: 'editor-poi-audio-add',
|
poiAudioAdd: 'editor-poi-audio-add',
|
||||||
poiCancel: 'editor-poi-cancel',
|
poiCancel: 'editor-poi-cancel',
|
||||||
|
poiDelete: 'editor-poi-delete',
|
||||||
poiDescription: 'editor-poi-description',
|
poiDescription: 'editor-poi-description',
|
||||||
poiForm: 'editor-poi-form',
|
poiForm: 'editor-poi-form',
|
||||||
poiFormTitle: 'editor-poi-form-title',
|
poiFormTitle: 'editor-poi-form-title',
|
||||||
@@ -23,6 +24,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
poiLat: 'editor-poi-lat',
|
poiLat: 'editor-poi-lat',
|
||||||
poiList: 'editor-poi-list',
|
poiList: 'editor-poi-list',
|
||||||
poiLon: 'editor-poi-lon',
|
poiLon: 'editor-poi-lon',
|
||||||
|
poiMediaPreview: 'editor-poi-media-preview',
|
||||||
poiMediaStatus: 'editor-poi-media-status',
|
poiMediaStatus: 'editor-poi-media-status',
|
||||||
poiRadius: 'editor-poi-radius',
|
poiRadius: 'editor-poi-radius',
|
||||||
poiSave: 'editor-poi-save',
|
poiSave: 'editor-poi-save',
|
||||||
@@ -45,16 +47,24 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
|
|
||||||
const classes = Object.freeze({
|
const classes = Object.freeze({
|
||||||
button: 'editor-button',
|
button: 'editor-button',
|
||||||
coordinateGrid: 'editor-coordinate-grid',
|
|
||||||
dialog: 'editor-dialog',
|
dialog: 'editor-dialog',
|
||||||
dialogHeader: 'editor-dialog-header',
|
dialogHeader: 'editor-dialog-header',
|
||||||
|
dangerButton: 'editor-danger-button',
|
||||||
|
dangerIcon: 'editor-danger-icon',
|
||||||
dialogShell: 'editor-dialog-shell',
|
dialogShell: 'editor-dialog-shell',
|
||||||
|
formActions: 'editor-form-actions',
|
||||||
form: 'editor-form',
|
form: 'editor-form',
|
||||||
iconButton: 'editor-icon-button',
|
iconButton: 'editor-icon-button',
|
||||||
iconRow: 'editor-icon-row',
|
iconRow: 'editor-icon-row',
|
||||||
mediaActions: 'editor-media-actions',
|
mediaActions: 'editor-media-actions',
|
||||||
mediaButton: 'editor-media-button',
|
mediaButton: 'editor-media-button',
|
||||||
mediaIcon: 'editor-media-icon',
|
mediaIcon: 'editor-media-icon',
|
||||||
|
mediaPreview: 'editor-media-preview',
|
||||||
|
mediaPreviewAudio: 'editor-media-preview-audio',
|
||||||
|
mediaPreviewCard: 'editor-media-preview-card',
|
||||||
|
mediaPreviewImage: 'editor-media-preview-image',
|
||||||
|
mediaPreviewSelect: 'editor-media-preview-select',
|
||||||
|
mediaRemove: 'editor-media-remove',
|
||||||
muted: 'editor-muted',
|
muted: 'editor-muted',
|
||||||
poiItem: 'editor-poi-item',
|
poiItem: 'editor-poi-item',
|
||||||
poiList: 'editor-poi-list',
|
poiList: 'editor-poi-list',
|
||||||
@@ -75,8 +85,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
currentPoi: null,
|
currentPoi: null,
|
||||||
currentRoute: null,
|
currentRoute: null,
|
||||||
currentSource: null,
|
currentSource: null,
|
||||||
|
pendingAudioFile: null,
|
||||||
|
pendingImages: [],
|
||||||
pois: [],
|
pois: [],
|
||||||
routes: []
|
routes: [],
|
||||||
|
selectedMediaKey: null
|
||||||
};
|
};
|
||||||
|
|
||||||
function ready(callback) {
|
function ready(callback) {
|
||||||
@@ -261,6 +274,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
return handleResponse(response);
|
return handleResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendDelete(path) {
|
||||||
|
const response = await fetch(apiUrl(path), {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
return handleResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
function routeArray(payload) {
|
function routeArray(payload) {
|
||||||
if (Array.isArray(payload)) return payload;
|
if (Array.isArray(payload)) return payload;
|
||||||
if (Array.isArray(payload?.routes)) return payload.routes;
|
if (Array.isArray(payload?.routes)) return payload.routes;
|
||||||
@@ -455,22 +477,131 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetPendingMedia() {
|
||||||
|
state.pendingAudioFile = null;
|
||||||
|
state.pendingImages = [];
|
||||||
|
state.selectedMediaKey = null;
|
||||||
|
if (byId(ids.poiAudio)) byId(ids.poiAudio).value = '';
|
||||||
|
if (byId(ids.poiImages)) byId(ids.poiImages).value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function existingAudioUrl(poi) {
|
||||||
|
return poi?.audio?.dataUrl || poi?.audioUrl || poi?.audio_path || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function imagePreviewUrl(image) {
|
||||||
|
return image?.dataUrl || image?.url || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mediaPreviewCard({ key, kind, label, previewUrl = null, source, mediaId = '', mediaIndex = '' }) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = classes.mediaPreviewCard;
|
||||||
|
card.dataset.selected = String(state.selectedMediaKey === key);
|
||||||
|
|
||||||
|
const select = document.createElement('button');
|
||||||
|
select.className = classes.mediaPreviewSelect;
|
||||||
|
select.dataset.editorAction = 'select-media';
|
||||||
|
select.dataset.mediaKey = key;
|
||||||
|
select.type = 'button';
|
||||||
|
select.setAttribute('aria-expanded', String(state.selectedMediaKey === key));
|
||||||
|
select.setAttribute('aria-label', `${label}: Entfernen-Schaltfläche ${state.selectedMediaKey === key ? 'ausblenden' : 'anzeigen'}`);
|
||||||
|
|
||||||
|
if (kind === 'image' && previewUrl) {
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.className = classes.mediaPreviewImage;
|
||||||
|
image.src = previewUrl;
|
||||||
|
image.alt = '';
|
||||||
|
select.appendChild(image);
|
||||||
|
} else {
|
||||||
|
const audio = document.createElement('span');
|
||||||
|
audio.className = classes.mediaPreviewAudio;
|
||||||
|
audio.setAttribute('aria-hidden', 'true');
|
||||||
|
audio.textContent = '♪';
|
||||||
|
select.appendChild(audio);
|
||||||
|
}
|
||||||
|
appendText(select, 'small', label);
|
||||||
|
|
||||||
|
const remove = document.createElement('button');
|
||||||
|
remove.className = classes.mediaRemove;
|
||||||
|
remove.dataset.editorAction = 'remove-media';
|
||||||
|
remove.dataset.mediaId = text(mediaId);
|
||||||
|
remove.dataset.mediaIndex = text(mediaIndex);
|
||||||
|
remove.dataset.mediaKey = key;
|
||||||
|
remove.dataset.mediaKind = kind;
|
||||||
|
remove.dataset.mediaSource = source;
|
||||||
|
remove.hidden = state.selectedMediaKey !== key;
|
||||||
|
remove.type = 'button';
|
||||||
|
remove.textContent = '−';
|
||||||
|
remove.title = `${label} entfernen`;
|
||||||
|
remove.setAttribute('aria-label', `${label} entfernen`);
|
||||||
|
|
||||||
|
card.append(select, remove);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPoiMediaPreview(poi = state.currentPoi) {
|
||||||
|
const preview = byId(ids.poiMediaPreview);
|
||||||
|
if (!preview) return;
|
||||||
|
preview.textContent = '';
|
||||||
|
|
||||||
|
if (existingAudioUrl(poi)) {
|
||||||
|
preview.appendChild(mediaPreviewCard({
|
||||||
|
key: 'existing-audio',
|
||||||
|
kind: 'audio',
|
||||||
|
label: poi?.audio?.name || 'Audioaufnahme',
|
||||||
|
source: 'existing'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.pendingAudioFile) {
|
||||||
|
preview.appendChild(mediaPreviewCard({
|
||||||
|
key: 'pending-audio',
|
||||||
|
kind: 'audio',
|
||||||
|
label: state.pendingAudioFile.name || 'Neue Audioaufnahme',
|
||||||
|
source: 'pending'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = Array.isArray(poi?.images) ? poi.images : [];
|
||||||
|
images.forEach((image, index) => {
|
||||||
|
preview.appendChild(mediaPreviewCard({
|
||||||
|
key: `existing-image-${image.id ?? index}`,
|
||||||
|
kind: 'image',
|
||||||
|
label: image.name || image.caption || `Bild ${index + 1}`,
|
||||||
|
mediaId: image.id ?? '',
|
||||||
|
mediaIndex: index,
|
||||||
|
previewUrl: imagePreviewUrl(image),
|
||||||
|
source: 'existing'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
state.pendingImages.forEach((pending, index) => {
|
||||||
|
preview.appendChild(mediaPreviewCard({
|
||||||
|
key: pending.key,
|
||||||
|
kind: 'image',
|
||||||
|
label: pending.file?.name || `Neues Bild ${index + 1}`,
|
||||||
|
mediaIndex: index,
|
||||||
|
previewUrl: pending.preview?.dataUrl || null,
|
||||||
|
source: 'pending'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updatePoiMediaStatus(poi = state.currentPoi) {
|
function updatePoiMediaStatus(poi = state.currentPoi) {
|
||||||
const status = byId(ids.poiMediaStatus);
|
const status = byId(ids.poiMediaStatus);
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
const selectedAudio = byId(ids.poiAudio)?.files?.[0];
|
const existingAudio = existingAudioUrl(poi) ? 1 : 0;
|
||||||
const selectedImages = Array.from(byId(ids.poiImages)?.files || []);
|
|
||||||
const hasExistingAudio = Boolean(poi?.audio || poi?.audioUrl || poi?.audio_path);
|
|
||||||
const existingImages = Array.isArray(poi?.images) ? poi.images.length : 0;
|
const existingImages = Array.isArray(poi?.images) ? poi.images.length : 0;
|
||||||
|
const audioCount = existingAudio + (state.pendingAudioFile ? 1 : 0);
|
||||||
|
const imageCount = existingImages + state.pendingImages.length;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
|
||||||
if (selectedAudio) parts.push(`Audio ausgewählt: ${selectedAudio.name || 'Audiodatei'}`);
|
if (audioCount) parts.push(`${audioCount} Audio${audioCount === 1 ? 'aufnahme' : 'aufnahmen'}`);
|
||||||
else if (hasExistingAudio) parts.push('Audio vorhanden');
|
if (imageCount) parts.push(`${imageCount} Bild${imageCount === 1 ? '' : 'er'}`);
|
||||||
|
|
||||||
if (selectedImages.length) parts.push(`${selectedImages.length} Bild${selectedImages.length === 1 ? '' : 'er'} ausgewählt`);
|
status.textContent = parts.length
|
||||||
else if (existingImages) parts.push(`${existingImages} Bild${existingImages === 1 ? '' : 'er'} vorhanden`);
|
? `${parts.join(' · ')}. Vorschau antippen, um die Entfernen-Schaltfläche einzublenden.`
|
||||||
|
: 'Noch keine Medien vorhanden.';
|
||||||
status.textContent = parts.length ? parts.join(' · ') : 'Noch keine neuen Medien ausgewählt.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFilePicker(inputId) {
|
function openFilePicker(inputId) {
|
||||||
@@ -479,17 +610,41 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fillPoiForm(poi) {
|
function fillPoiForm(poi) {
|
||||||
|
resetPendingMedia();
|
||||||
byId(ids.poiId).value = poiId(poi);
|
byId(ids.poiId).value = poiId(poi);
|
||||||
byId(ids.poiTitle).value = poiTitle(poi) === 'Unbenannte Station' ? '' : poiTitle(poi);
|
byId(ids.poiTitle).value = poiTitle(poi) === 'Unbenannte Station' ? '' : poiTitle(poi);
|
||||||
byId(ids.poiDescription).value = text(poi?.description || poi?.text);
|
byId(ids.poiDescription).value = text(poi?.description || poi?.text);
|
||||||
byId(ids.poiLat).value = text(poi?.lat ?? poi?.latitude);
|
byId(ids.poiLat).value = text(poi?.lat ?? poi?.latitude);
|
||||||
byId(ids.poiLon).value = text(poi?.lon ?? poi?.longitude);
|
byId(ids.poiLon).value = text(poi?.lon ?? poi?.longitude);
|
||||||
byId(ids.poiRadius).value = text(poi?.triggerRadiusM ?? poi?.radiusM ?? poi?.radius ?? 25);
|
byId(ids.poiRadius).value = text(poi?.triggerRadiusM ?? poi?.radiusM ?? poi?.radius ?? 25);
|
||||||
byId(ids.poiAudio).value = '';
|
setHidden(byId(ids.poiDelete), !poiId(poi));
|
||||||
byId(ids.poiImages).value = '';
|
renderPoiMediaPreview(poi);
|
||||||
updatePoiMediaStatus(poi);
|
updatePoiMediaStatus(poi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAudioSelection() {
|
||||||
|
state.pendingAudioFile = byId(ids.poiAudio)?.files?.[0] || null;
|
||||||
|
state.selectedMediaKey = null;
|
||||||
|
if (byId(ids.poiAudio)) byId(ids.poiAudio).value = '';
|
||||||
|
renderPoiMediaPreview();
|
||||||
|
updatePoiMediaStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImageSelection() {
|
||||||
|
const files = Array.from(byId(ids.poiImages)?.files || []);
|
||||||
|
for (const file of files) {
|
||||||
|
state.pendingImages.push({
|
||||||
|
file,
|
||||||
|
key: `pending-image-${ns.LocalRoutes.createPoiId()}`,
|
||||||
|
preview: await ns.LocalRoutes.fileToDataUrl(file)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.selectedMediaKey = null;
|
||||||
|
if (byId(ids.poiImages)) byId(ids.poiImages).value = '';
|
||||||
|
renderPoiMediaPreview();
|
||||||
|
updatePoiMediaStatus();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRoute(route) {
|
async function loadRoute(route) {
|
||||||
const id = routeId(route);
|
const id = routeId(route);
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -602,12 +757,20 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
}
|
}
|
||||||
|
|
||||||
function newPoi() {
|
function newPoi() {
|
||||||
state.currentPoi = null;
|
|
||||||
const position = currentPosition();
|
const position = currentPosition();
|
||||||
|
const lat = Number(position?.lat);
|
||||||
|
const lon = Number(position?.lon);
|
||||||
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||||
|
setStatus('Für einen neuen POI muss zuerst eine aktuelle GPS-Position verfügbar sein.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accuracy = Number(position?.accuracy);
|
||||||
|
state.currentPoi = null;
|
||||||
fillPoiForm({
|
fillPoiForm({
|
||||||
lat: position?.lat ?? '',
|
lat,
|
||||||
lon: position?.lon ?? '',
|
lon,
|
||||||
triggerRadiusM: 25
|
triggerRadiusM: Number.isFinite(accuracy) && accuracy > 0 ? accuracy : 25
|
||||||
});
|
});
|
||||||
byId(ids.poiFormTitle).textContent = state.currentSource === 'local' ? 'GPX-POI hinzufügen' : 'Station hinzufügen';
|
byId(ids.poiFormTitle).textContent = state.currentSource === 'local' ? 'GPX-POI hinzufügen' : 'Station hinzufügen';
|
||||||
showPoiView();
|
showPoiView();
|
||||||
@@ -621,15 +784,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function mediaFromLocalForm(existingPoi, poiIdentifier) {
|
async function mediaFromLocalForm(existingPoi, poiIdentifier) {
|
||||||
const audioFile = byId(ids.poiAudio)?.files?.[0];
|
const audio = state.pendingAudioFile
|
||||||
const imageFiles = Array.from(byId(ids.poiImages)?.files || []);
|
? await ns.LocalRoutes.fileToDataUrl(state.pendingAudioFile)
|
||||||
const audio = audioFile ? await ns.LocalRoutes.fileToDataUrl(audioFile) : existingPoi?.audio || null;
|
: existingPoi?.audio || null;
|
||||||
const images = [...(existingPoi?.images || [])];
|
const images = [...(existingPoi?.images || [])];
|
||||||
for (const imageFile of imageFiles) {
|
for (const pending of state.pendingImages) {
|
||||||
const image = await ns.LocalRoutes.fileToDataUrl(imageFile);
|
const image = pending.preview || await ns.LocalRoutes.fileToDataUrl(pending.file);
|
||||||
images.push({
|
images.push({
|
||||||
...image,
|
...image,
|
||||||
id: `${poiIdentifier}-image-${Date.now()}-${images.length}`,
|
id: `${poiIdentifier}-image-${ns.LocalRoutes.createPoiId()}`,
|
||||||
caption: ''
|
caption: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -639,6 +802,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
async function persistLocalCurrentRoute() {
|
async function persistLocalCurrentRoute() {
|
||||||
const id = routeId(state.currentRoute);
|
const id = routeId(state.currentRoute);
|
||||||
if (!id) throw new Error('Speichere zuerst die Strecke.');
|
if (!id) throw new Error('Speichere zuerst die Strecke.');
|
||||||
|
const currentPoiId = poiId(state.currentPoi);
|
||||||
const saved = ns.LocalRoutes.save({
|
const saved = ns.LocalRoutes.save({
|
||||||
id,
|
id,
|
||||||
gpx: state.currentRoute.gpx,
|
gpx: state.currentRoute.gpx,
|
||||||
@@ -647,6 +811,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
});
|
});
|
||||||
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
|
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
|
||||||
state.pois = saved.pois;
|
state.pois = saved.pois;
|
||||||
|
state.currentPoi = currentPoiId
|
||||||
|
? state.pois.find(poi => poiId(poi) === currentPoiId) || null
|
||||||
|
: null;
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,19 +840,20 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
await loadRoute({ id: routeId(state.currentRoute), _source: 'local' });
|
await loadRoute({ id: routeId(state.currentRoute), _source: 'local' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadOne(path, fieldName, file) {
|
async function uploadOne(path, fieldName, file, method = 'POST') {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append(fieldName, file);
|
data.append(fieldName, file);
|
||||||
return sendForm(path, 'POST', data);
|
return sendForm(path, method, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadPoiMedia(currentRouteId, currentPoiId) {
|
async function uploadPoiMedia(currentRouteId, currentPoiId) {
|
||||||
const audio = byId(ids.poiAudio)?.files?.[0];
|
|
||||||
const images = Array.from(byId(ids.poiImages)?.files || []);
|
|
||||||
const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
|
const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
|
||||||
if (audio) await uploadOne(`${basePath}/audio`, 'audio', audio);
|
if (state.pendingAudioFile) {
|
||||||
for (const image of images) {
|
const method = existingAudioUrl(state.currentPoi) ? 'PUT' : 'POST';
|
||||||
await uploadOne(`${basePath}/pictures`, 'picture', image);
|
await uploadOne(`${basePath}/audio`, 'audio', state.pendingAudioFile, method);
|
||||||
|
}
|
||||||
|
for (const pending of state.pendingImages) {
|
||||||
|
await uploadOne(`${basePath}/pictures`, 'picture', pending.file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,6 +880,116 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
else await saveServerPoi(values);
|
else await saveServerPoi(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateCurrentPoi(updatedPoi) {
|
||||||
|
const identifier = poiId(updatedPoi);
|
||||||
|
const index = state.pois.findIndex(poi => poiId(poi) === identifier);
|
||||||
|
if (index >= 0) state.pois[index] = updatedPoi;
|
||||||
|
state.currentPoi = updatedPoi;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removePendingMedia(kind, mediaIndex) {
|
||||||
|
if (kind === 'audio') {
|
||||||
|
state.pendingAudioFile = null;
|
||||||
|
} else {
|
||||||
|
const index = Number.parseInt(mediaIndex, 10);
|
||||||
|
if (Number.isInteger(index)) state.pendingImages.splice(index, 1);
|
||||||
|
}
|
||||||
|
state.selectedMediaKey = null;
|
||||||
|
renderPoiMediaPreview();
|
||||||
|
updatePoiMediaStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeExistingLocalMedia(kind, mediaIndex) {
|
||||||
|
if (!state.currentPoi) return;
|
||||||
|
const updated = { ...state.currentPoi };
|
||||||
|
if (kind === 'audio') {
|
||||||
|
updated.audio = null;
|
||||||
|
} else {
|
||||||
|
const index = Number.parseInt(mediaIndex, 10);
|
||||||
|
updated.images = [...(state.currentPoi.images || [])];
|
||||||
|
if (Number.isInteger(index)) updated.images.splice(index, 1);
|
||||||
|
}
|
||||||
|
updateCurrentPoi(updated);
|
||||||
|
await persistLocalCurrentRoute();
|
||||||
|
state.selectedMediaKey = null;
|
||||||
|
renderPoiMediaPreview();
|
||||||
|
updatePoiMediaStatus();
|
||||||
|
setStatus(kind === 'audio' ? 'Audioaufnahme entfernt.' : 'Bild entfernt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeExistingServerMedia(kind, mediaId) {
|
||||||
|
const currentRouteId = routeId(state.currentRoute);
|
||||||
|
const currentPoiId = poiId(state.currentPoi);
|
||||||
|
if (!currentRouteId || !currentPoiId) throw new Error('Der POI ist noch nicht gespeichert.');
|
||||||
|
const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
|
||||||
|
const updated = { ...state.currentPoi };
|
||||||
|
|
||||||
|
if (kind === 'audio') {
|
||||||
|
await sendDelete(`${basePath}/audio`);
|
||||||
|
updated.audio = null;
|
||||||
|
updated.audioUrl = null;
|
||||||
|
updated.audio_path = null;
|
||||||
|
} else {
|
||||||
|
if (!mediaId) throw new Error('Das Bild besitzt keine Server-ID und kann nicht entfernt werden.');
|
||||||
|
await sendDelete(`${basePath}/pictures/${encodeURIComponent(mediaId)}`);
|
||||||
|
updated.images = (state.currentPoi.images || []).filter(image => text(image.id) !== text(mediaId));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentPoi(updated);
|
||||||
|
state.selectedMediaKey = null;
|
||||||
|
renderPoiMediaPreview();
|
||||||
|
updatePoiMediaStatus();
|
||||||
|
setStatus(kind === 'audio' ? 'Audioaufnahme entfernt.' : 'Bild entfernt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMedia(control) {
|
||||||
|
const kind = control.dataset.mediaKind;
|
||||||
|
const source = control.dataset.mediaSource;
|
||||||
|
if (source === 'pending') {
|
||||||
|
await removePendingMedia(kind, control.dataset.mediaIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.currentSource === 'local') {
|
||||||
|
await removeExistingLocalMedia(kind, control.dataset.mediaIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await removeExistingServerMedia(kind, control.dataset.mediaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mediaPreviewAction(event) {
|
||||||
|
const control = event.target.closest('[data-editor-action]');
|
||||||
|
const action = control?.dataset.editorAction;
|
||||||
|
if (action === 'select-media') {
|
||||||
|
const key = control.dataset.mediaKey;
|
||||||
|
state.selectedMediaKey = state.selectedMediaKey === key ? null : key;
|
||||||
|
renderPoiMediaPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'remove-media') await removeMedia(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCurrentPoi() {
|
||||||
|
const currentRouteId = routeId(state.currentRoute);
|
||||||
|
const currentPoiId = poiId(state.currentPoi);
|
||||||
|
if (!currentRouteId || !currentPoiId) return;
|
||||||
|
|
||||||
|
if (state.currentSource === 'local') {
|
||||||
|
state.pois = state.pois
|
||||||
|
.filter(poi => poiId(poi) !== currentPoiId)
|
||||||
|
.map((poi, sequence) => ({ ...poi, sequence }));
|
||||||
|
state.currentPoi = null;
|
||||||
|
await persistLocalCurrentRoute();
|
||||||
|
renderPoiList();
|
||||||
|
showRouteView();
|
||||||
|
setStatus('POI entfernt und lokale Strecke aktualisiert.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendDelete(`/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`);
|
||||||
|
await loadRoute({ id: currentRouteId, _source: 'server' });
|
||||||
|
setStatus('POI entfernt.');
|
||||||
|
}
|
||||||
|
|
||||||
function reorderPois(index, offset) {
|
function reorderPois(index, offset) {
|
||||||
const target = index + offset;
|
const target = index + offset;
|
||||||
if (target < 0 || target >= state.pois.length) return false;
|
if (target < 0 || target >= state.pois.length) return false;
|
||||||
@@ -797,8 +1075,10 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
activate(byId(ids.addPoi), newPoi);
|
activate(byId(ids.addPoi), newPoi);
|
||||||
activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio));
|
activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio));
|
||||||
activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages));
|
activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages));
|
||||||
byId(ids.poiAudio)?.addEventListener('change', () => updatePoiMediaStatus());
|
byId(ids.poiAudio)?.addEventListener('change', handleAudioSelection);
|
||||||
byId(ids.poiImages)?.addEventListener('change', () => updatePoiMediaStatus());
|
byId(ids.poiImages)?.addEventListener('change', () => guarded(handleImageSelection));
|
||||||
|
activate(byId(ids.poiMediaPreview), event => guarded(() => mediaPreviewAction(event)));
|
||||||
|
activate(byId(ids.poiDelete), () => guarded(deleteCurrentPoi));
|
||||||
activate(byId(ids.poiCancel), showRouteView);
|
activate(byId(ids.poiCancel), showRouteView);
|
||||||
activate(byId(ids.routeList), event => guarded(() => routeListAction(event)));
|
activate(byId(ids.routeList), event => guarded(() => routeListAction(event)));
|
||||||
activate(byId(ids.poiList), poiListAction);
|
activate(byId(ids.poiList), poiListAction);
|
||||||
|
|||||||
+61
-9
@@ -28,6 +28,12 @@ test('editor markup and source describe local draft creation instead of immediat
|
|||||||
assert.match(html, /id="editor-poi-images-add"[^>]+type="button"/);
|
assert.match(html, /id="editor-poi-images-add"[^>]+type="button"/);
|
||||||
assert.match(html, /id="editor-poi-audio"[^>]+accept="audio\/\*"[^>]+hidden/);
|
assert.match(html, /id="editor-poi-audio"[^>]+accept="audio\/\*"[^>]+hidden/);
|
||||||
assert.match(html, /id="editor-poi-images"[^>]+accept="image\/\*"[^>]+multiple[^>]+hidden/);
|
assert.match(html, /id="editor-poi-images"[^>]+accept="image\/\*"[^>]+multiple[^>]+hidden/);
|
||||||
|
assert.match(html, /id="editor-poi-lat"[^>]+type="hidden"/);
|
||||||
|
assert.match(html, /id="editor-poi-lon"[^>]+type="hidden"/);
|
||||||
|
assert.match(html, /id="editor-poi-radius"[^>]+type="hidden"/);
|
||||||
|
assert.doesNotMatch(html, /id="editor-poi-(?:lat|lon)"[^>]+step=/);
|
||||||
|
assert.match(html, /id="editor-poi-media-preview"/);
|
||||||
|
assert.match(html, /id="editor-poi-delete"[^>]+editor-danger-button/);
|
||||||
assert.doesNotMatch(html, /accept="video\//);
|
assert.doesNotMatch(html, /accept="video\//);
|
||||||
assert.match(html, /ausschließlich in diesem Browser/i);
|
assert.match(html, /ausschließlich in diesem Browser/i);
|
||||||
|
|
||||||
@@ -35,6 +41,9 @@ test('editor markup and source describe local draft creation instead of immediat
|
|||||||
assert.match(css, /\.editor-poi-list/);
|
assert.match(css, /\.editor-poi-list/);
|
||||||
assert.match(css, /\.editor-route-group/);
|
assert.match(css, /\.editor-route-group/);
|
||||||
assert.match(css, /\.editor-media-button/);
|
assert.match(css, /\.editor-media-button/);
|
||||||
|
assert.match(css, /\.editor-media-preview/);
|
||||||
|
assert.match(css, /\.editor-media-remove/);
|
||||||
|
assert.match(css, /\.editor-danger-button/);
|
||||||
|
|
||||||
assert.match(ui, /readJson\(['"]\/routes\?activatedOnly=true['"]\)/);
|
assert.match(ui, /readJson\(['"]\/routes\?activatedOnly=true['"]\)/);
|
||||||
assert.match(ui, /ns\.LocalRoutes\.save\(/);
|
assert.match(ui, /ns\.LocalRoutes\.save\(/);
|
||||||
@@ -51,6 +60,8 @@ test('editor markup and source describe local draft creation instead of immediat
|
|||||||
assert.match(ui, /\/pictures/);
|
assert.match(ui, /\/pictures/);
|
||||||
assert.match(ui, /'picture'/);
|
assert.match(ui, /'picture'/);
|
||||||
assert.match(ui, /'audio'/);
|
assert.match(ui, /'audio'/);
|
||||||
|
assert.match(ui, /method:\s*'DELETE'/);
|
||||||
|
assert.match(ui, /position\?\.accuracy/);
|
||||||
});
|
});
|
||||||
|
|
||||||
class FakeElement extends EventTarget {
|
class FakeElement extends EventTarget {
|
||||||
@@ -167,6 +178,7 @@ function buildEditorContext() {
|
|||||||
'editor-poi-audio',
|
'editor-poi-audio',
|
||||||
'editor-poi-audio-add',
|
'editor-poi-audio-add',
|
||||||
'editor-poi-cancel',
|
'editor-poi-cancel',
|
||||||
|
'editor-poi-delete',
|
||||||
'editor-poi-description',
|
'editor-poi-description',
|
||||||
'editor-poi-form',
|
'editor-poi-form',
|
||||||
'editor-poi-form-title',
|
'editor-poi-form-title',
|
||||||
@@ -177,6 +189,7 @@ function buildEditorContext() {
|
|||||||
'editor-poi-lat',
|
'editor-poi-lat',
|
||||||
'editor-poi-list',
|
'editor-poi-list',
|
||||||
'editor-poi-lon',
|
'editor-poi-lon',
|
||||||
|
'editor-poi-media-preview',
|
||||||
'editor-poi-media-status',
|
'editor-poi-media-status',
|
||||||
'editor-poi-radius',
|
'editor-poi-radius',
|
||||||
'editor-poi-title',
|
'editor-poi-title',
|
||||||
@@ -228,7 +241,10 @@ function buildEditorContext() {
|
|||||||
}
|
}
|
||||||
if (url === '/api/routes/1/pois' && method === 'POST') return jsonResponse({ poi: { id: 12, title: 'Neue Serverstation' } });
|
if (url === '/api/routes/1/pois' && method === 'POST') return jsonResponse({ poi: { id: 12, title: 'Neue Serverstation' } });
|
||||||
if (/^\/api\/routes\/1\/pois\/\d+$/.test(url) && method === 'PUT') return jsonResponse({ poi: { id: Number(url.split('/').pop()), title: 'Station' } });
|
if (/^\/api\/routes\/1\/pois\/\d+$/.test(url) && method === 'PUT') return jsonResponse({ poi: { id: Number(url.split('/').pop()), title: 'Station' } });
|
||||||
if (/^\/api\/routes\/1\/pois\/\d+\/(audio|pictures)$/.test(url) && method === 'POST') return jsonResponse({ ok: true });
|
if (/^\/api\/routes\/1\/pois\/\d+\/(audio|pictures)$/.test(url) && (method === 'POST' || method === 'PUT')) return jsonResponse({ ok: true });
|
||||||
|
if (/^\/api\/routes\/1\/pois\/\d+\/audio$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||||
|
if (/^\/api\/routes\/1\/pois\/\d+\/pictures\/\d+$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||||
|
if (/^\/api\/routes\/1\/pois\/\d+$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||||
return { ok: false, status: 404, text: async () => `unexpected ${method} ${url}` };
|
return { ok: false, status: 404, text: async () => `unexpected ${method} ${url}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +286,7 @@ function buildEditorContext() {
|
|||||||
globalThis: null,
|
globalThis: null,
|
||||||
structuredClone,
|
structuredClone,
|
||||||
Wegwichtel: {
|
Wegwichtel: {
|
||||||
App: { currentPosition: () => ({ lat: 50.5, lon: 10.5 }) },
|
App: { currentPosition: () => ({ lat: 50.123456789, lon: 10.987654321, accuracy: 4.321 }) },
|
||||||
Config: { apiBase: '/api/' },
|
Config: { apiBase: '/api/' },
|
||||||
LocalRoutes
|
LocalRoutes
|
||||||
}
|
}
|
||||||
@@ -311,27 +327,63 @@ test('public editor workflow saves new route and GPX POI locally without POSTing
|
|||||||
elements.get('editor-poi-images-add').dispatchEvent(pointerEvent());
|
elements.get('editor-poi-images-add').dispatchEvent(pointerEvent());
|
||||||
assert.equal(elements.get('editor-poi-audio').clickCount, 1);
|
assert.equal(elements.get('editor-poi-audio').clickCount, 1);
|
||||||
assert.equal(elements.get('editor-poi-images').clickCount, 1);
|
assert.equal(elements.get('editor-poi-images').clickCount, 1);
|
||||||
|
assert.equal(elements.get('editor-poi-lat').value, '50.123456789');
|
||||||
|
assert.equal(elements.get('editor-poi-lon').value, '10.987654321');
|
||||||
|
assert.equal(elements.get('editor-poi-radius').value, '4.321');
|
||||||
elements.get('editor-poi-title').value = 'Aussicht';
|
elements.get('editor-poi-title').value = 'Aussicht';
|
||||||
elements.get('editor-poi-description').value = 'Text zur Station';
|
elements.get('editor-poi-description').value = 'Text zur Station';
|
||||||
elements.get('editor-poi-lat').value = '50.51';
|
|
||||||
elements.get('editor-poi-lon').value = '10.52';
|
|
||||||
elements.get('editor-poi-radius').value = '35';
|
|
||||||
elements.get('editor-poi-audio').files = [{ name: 'aussicht.mp3', size: 5, type: 'audio/mpeg' }];
|
elements.get('editor-poi-audio').files = [{ name: 'aussicht.mp3', size: 5, type: 'audio/mpeg' }];
|
||||||
|
elements.get('editor-poi-audio').dispatchEvent(new Event('change'));
|
||||||
elements.get('editor-poi-images').files = [{ name: 'aussicht.jpg', size: 7, type: 'image/jpeg' }];
|
elements.get('editor-poi-images').files = [{ name: 'aussicht.jpg', size: 7, type: 'image/jpeg' }];
|
||||||
|
elements.get('editor-poi-images').dispatchEvent(new Event('change'));
|
||||||
|
await tick();
|
||||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||||
await tick();
|
await tick();
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
const local = packages.get('local-new');
|
let local = packages.get('local-new');
|
||||||
assert.equal(local.pois.length, 1);
|
assert.equal(local.pois.length, 1);
|
||||||
assert.equal(local.pois[0].title, 'Aussicht');
|
assert.equal(local.pois[0].title, 'Aussicht');
|
||||||
assert.equal(local.pois[0].lat, 50.51);
|
assert.equal(local.pois[0].lat, 50.123456789);
|
||||||
assert.equal(local.pois[0].lon, 10.52);
|
assert.equal(local.pois[0].lon, 10.987654321);
|
||||||
assert.equal(local.pois[0].triggerRadiusM, 35);
|
assert.equal(local.pois[0].triggerRadiusM, 4.321);
|
||||||
assert.match(local.pois[0].audio.dataUrl, /^data:audio\/mpeg/);
|
assert.match(local.pois[0].audio.dataUrl, /^data:audio\/mpeg/);
|
||||||
assert.equal(local.pois[0].images.length, 1);
|
assert.equal(local.pois[0].images.length, 1);
|
||||||
assert.match(local.pois[0].images[0].dataUrl, /^data:image\/jpeg/);
|
assert.match(local.pois[0].images[0].dataUrl, /^data:image\/jpeg/);
|
||||||
|
|
||||||
|
const poiList = elements.get('editor-poi-list');
|
||||||
|
const poiItem = poiList.children.find(child => child.dataset.poiId);
|
||||||
|
const poiEditButton = poiItem.children[1].children[2];
|
||||||
|
poiList.dispatchEvent(pointerEvent(poiEditButton));
|
||||||
|
const mediaPreview = elements.get('editor-poi-media-preview');
|
||||||
|
assert.equal(mediaPreview.children.length, 2);
|
||||||
|
|
||||||
|
let imageCard = mediaPreview.children.find(card => card.children[1]?.dataset.mediaKind === 'image');
|
||||||
|
mediaPreview.dispatchEvent(pointerEvent(imageCard.children[0]));
|
||||||
|
imageCard = mediaPreview.children.find(card => card.children[1]?.dataset.mediaKind === 'image');
|
||||||
|
assert.equal(imageCard.children[1].hidden, false);
|
||||||
|
mediaPreview.dispatchEvent(pointerEvent(imageCard.children[1]));
|
||||||
|
await tick();
|
||||||
|
await tick();
|
||||||
|
local = packages.get('local-new');
|
||||||
|
assert.equal(local.pois[0].images.length, 0);
|
||||||
|
|
||||||
|
let audioCard = elements.get('editor-poi-media-preview').children.find(card => card.children[1]?.dataset.mediaKind === 'audio');
|
||||||
|
elements.get('editor-poi-media-preview').dispatchEvent(pointerEvent(audioCard.children[0]));
|
||||||
|
audioCard = elements.get('editor-poi-media-preview').children.find(card => card.children[1]?.dataset.mediaKind === 'audio');
|
||||||
|
elements.get('editor-poi-media-preview').dispatchEvent(pointerEvent(audioCard.children[1]));
|
||||||
|
await tick();
|
||||||
|
await tick();
|
||||||
|
local = packages.get('local-new');
|
||||||
|
assert.equal(local.pois[0].audio, null);
|
||||||
|
|
||||||
|
assert.equal(elements.get('editor-poi-delete').hidden, false);
|
||||||
|
elements.get('editor-poi-delete').dispatchEvent(pointerEvent());
|
||||||
|
await tick();
|
||||||
|
await tick();
|
||||||
|
local = packages.get('local-new');
|
||||||
|
assert.equal(local.pois.length, 0);
|
||||||
|
|
||||||
context.Wegwichtel.EditorUi.close();
|
context.Wegwichtel.EditorUi.close();
|
||||||
assert.equal(elements.get('editor-dialog').open, false);
|
assert.equal(elements.get('editor-dialog').open, false);
|
||||||
assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'false');
|
assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'false');
|
||||||
|
|||||||
Reference in New Issue
Block a user