camper/web/static/camper.js

273 lines
8.6 KiB
JavaScript

/**
* SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
* SPDX-License-Identifier: AGPL-3.0-only
*/
function ready(fn) {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
ready(function () {
const snackBar = Object.assign(document.body.appendChild(document.createElement('section')), {
id: 'snackbar',
});
const errorMessage = snackBar.appendChild(document.createElement('div'));
errorMessage.setAttribute('role', 'alert');
const openClass = 'open';
const toasts = [];
let timeoutId = null;
function showError(message) {
toasts.push(message);
popUp();
}
function popUp() {
if (toasts.length === 0) {
return;
}
if (errorMessage.classList.contains(openClass)) {
dismiss();
return;
}
if (errorMessage.innerText !== "") {
// it will show after remove calls popUp again.
return;
}
errorMessage.innerText = toasts[0];
errorMessage.classList.add(openClass);
timeoutId = setTimeout(dismiss, 4000);
}
function dismiss() {
if (!errorMessage.classList.contains(openClass)) {
// already dismissed
return;
}
errorMessage.classList.remove(openClass);
clearTimeout(timeoutId);
timeoutId = setTimeout(remove, 350);
}
function remove() {
clearTimeout(timeoutId);
toasts.splice(0, 1);
errorMessage.innerText = "";
popUp();
}
document.body.addEventListener('htmx:error', function (evt) {
const errorInfo = evt.detail.errorInfo;
const error = errorInfo.xhr && errorInfo.xhr.responseText || errorInfo.error;
showError(error);
});
})
ready(function () {
const textareas = document.querySelectorAll('textarea.html');
if (textareas.length > 0) {
const language = document.documentElement.getAttribute('lang');
const csrfHeader = JSON.parse(document.querySelector('meta[name="csrf-header"]').getAttribute('content'));
const script = document.head.appendChild(Object.assign(document.createElement('script'), {
src: '/static/ckeditor5@40.2.0/ckeditor.js',
}));
if (language !== 'en') {
document.head.appendChild(Object.assign(document.createElement('script'), {
src: '/static/ckeditor5@40.2.0/translations/' + language + '.js',
}));
}
const editorConfig = {
language,
image: {
toolbar: [
'imageTextAlternative',
'toggleImageCaption',
'|',
'imageStyle:inline',
'imageStyle:wrapText',
'imageStyle:breakText',
'|',
'resizeImage',
],
},
simpleUpload: {
uploadUrl: '/admin/media',
headers: Object.assign(csrfHeader, {
Accept: 'application/vnd.ckeditor+json',
}),
},
};
script.addEventListener('load', function () {
for (const textarea of textareas) {
const canvas = document.createElement('div');
textarea.parentNode.insertBefore(canvas, textarea.nextSibling);
textarea.style.display = 'none';
ClassicEditor
.create(canvas, editorConfig)
.then(editor => {
const xml = document.createElement('div');
const serializer = new XMLSerializer();
editor.setData(textarea.value);
editor.ui.focusTracker.on('change:isFocused', (event, name, focused) => {
if (!focused) {
xml.innerHTML = editor.getData();
let text = serializer.serializeToString(xml);
text = text.replace('&nbsp;', '&#xA0;');
text = text.substring(text.indexOf('>') + 1, text.lastIndexOf('<'));
textarea.value = text;
}
});
})
.catch(error => {
console.error(error);
});
}
});
}
})
export function camperUploadForm(el) {
const progress = el.querySelector('progress');
htmx.on(el, 'drop', function (evt) {
evt.preventDefault();
[...evt.dataTransfer.items].forEach(function (i) {
console.log(i);
i.getAsString(console.log)
});
});
htmx.on(el, 'dragover', function (evt) {
evt.preventDefault();
});
htmx.on(el, 'dragleave', function (evt) {
evt.preventDefault();
});
htmx.on(el, 'htmx:xhr:progress', function (evt) {
if (progress && evt.detail.lengthComputable) {
progress.setAttribute('value', evt.detail.loaded / evt.detail.total * 100);
}
});
}
export function setupCampgroundMap(map) {
if (!map) {
return;
}
const prefix = "cp_";
for (const campsite of Array.from(map.querySelectorAll(`[id^="${prefix}"]`))) {
const label = campsite.id.substring(prefix.length);
if (!label) {
continue;
}
const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
link.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '/admin/campsites/' + label);
link.append(...campsite.childNodes);
campsite.appendChild(link);
}
}
export function setupIconInput(icon) {
if (!icon) {
return;
}
const input = icon.querySelector('input[type="hidden"]')
if (!input) {
return;
}
const buttons = Array.from(icon.querySelectorAll('button[data-icon-name]'));
const updateValue = function (iconName) {
input.value = iconName;
for (const button of buttons) {
button.setAttribute('aria-pressed', button.dataset.iconName === iconName);
}
}
for (const button of buttons) {
button.addEventListener('click', function (e) {
updateValue(e.target.dataset.iconName);
})
}
updateValue(input.value);
}
export function setupCalendar(calendar) {
const startDate = calendar.querySelector('input[name="start_date"]');
const endDate = calendar.querySelector('input[name="end_date"]');
const days = Array.from(calendar.querySelectorAll('time'));
const dialog = calendar.querySelector('dialog');
const clear = function () {
startDate.value = endDate.value = "";
days.forEach((e) => e.removeAttribute('aria-checked'));
}
dialog.addEventListener('close', clear);
dialog.querySelector('button').addEventListener('click', function (e) {
e.preventDefault();
dialog.close();
});
const selectDate = function (e) {
e.preventDefault();
const date = e.currentTarget.dateTime;
if (!date) {
return;
}
if (!startDate.value) {
startDate.value = date;
e.currentTarget.setAttribute('aria-checked', true);
return;
} else if (startDate.value > date) {
endDate.value = startDate.value;
startDate.value = date;
} else {
endDate.value = date;
}
for (const day of days) {
if (day.dateTime >= startDate.value && day.dateTime <= endDate.value) {
day.setAttribute('aria-checked', true);
} else {
day.removeAttribute('aria-checked');
}
}
dialog.showModal();
}
for (const day of days) {
day.addEventListener('click', selectDate);
}
clear();
}
htmx.onLoad((target) => {
if (target.tagName === 'DIALOG') {
target.showModal();
}
})
htmx.onLoad((content) => {
const sortables = Array.from(content.querySelectorAll('.sortable table tbody'));
for (const sortable of sortables) {
const sortableInstance = new Sortable(sortable, {
animation: 150,
draggable: '>tr',
handle: '.handle',
onMove: (evt) => evt.related.className.indexOf('htmx-indicator') === -1,
onEnd: function () {
this.option('disabled', true);
},
});
sortable.addEventListener('htmx:afterSwap', function () {
sortableInstance.option('disabled', false);
});
}
})