loko/streetup/projects/templates/projects/projects_form.html
2026-07-22 14:48:40 +02:00

245 lines
No EOL
9.5 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block head %}
<!-- Mapbox GL Draw 1.5.0 (local) -->
<link rel="stylesheet" href="{% static 'common/mapbox-gl-draw.css' %}" />
<link href="https://cdn.maptiler.com/maptiler-geocoding-control/v2.1.4/style.css" rel="stylesheet">
{% endblock head%}
{% block content %}
<div class="container d-flex justify-content-center mt-5">
<div class="w-100" style="max-width: 700px;">
<h2 class="mb-4 text-center" style="color: var(--brand-color);">
{% if mode == "update" %}{% translate "Modifier le projet" %}{% else %}{% translate "Créer un nouveau projet" %}{% endif %}
</h2>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<div class="text-danger">
{{ field.errors }}
</div>
{% endif %}
</div>
{% endfor %}
<div id="map" style="height: 400px;" class="mb-3"></div>
<input type="hidden" name="geom_geojson" id="id_geom_geojson" value="{{ form.geom.value|default:'' }}">
<button type="submit" class="btn btn-primary">
{% if mode == "update" %}{% translate "Enregistrer les modifications" %}{% else %}{% translate "Créer le projet" %}{% endif %}
</button>
</form>
<div class="mt-3">
<small class="text-muted">
{% blocktranslate %}
Les champs avec une <span style="border-bottom: 1px dashed #6c757d;">bordure en pointillés</span> sont facultatifs.
{% endblocktranslate %}
</small>
</div>
</div>
</div>
{% endblock content%}
{% block script %}
<!-- Mapbox GL Draw 1.5.0 (local) -->
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
<script src="https://cdn.maptiler.com/maptiler-geocoding-control/v2.1.4/maplibregl.umd.js"></script>
<script src="{% static 'common/turf.min.js' %}"></script>
<script>
$(document).ready(function() {
function initCustomMultipleSelect(fieldId, placeholder, allSelectedMsg) {
const originalSelect = document.getElementById(fieldId);
if (!originalSelect) return;
// Copy original styles BEFORE hiding the element
const originalStyle = originalSelect.getAttribute('style') || '';
// Hide original select
originalSelect.style.display = 'none';
// Create wrapper container
const wrapper = document.createElement('div');
wrapper.className = 'custom-multiselect-wrapper';
originalSelect.parentNode.insertBefore(wrapper, originalSelect.nextSibling);
// Create container for selected items (badges)
const badgesContainer = document.createElement('div');
badgesContainer.className = 'd-flex flex-wrap gap-2 mb-2';
wrapper.appendChild(badgesContainer);
// Create the select dropdown for adding items
const adderSelect = document.createElement('select');
adderSelect.className = 'form-select';
// Copy original styles but strip out any display property
if (originalStyle) {
const cleanStyle = originalStyle.replace(/display\s*:\s*[^;]+;?/gi, '');
adderSelect.setAttribute('style', cleanStyle);
}
wrapper.appendChild(adderSelect);
// Function to rebuild options in the adder dropdown and the badge list
function updateWidget() {
badgesContainer.innerHTML = '';
adderSelect.innerHTML = '';
// Add placeholder option
const placeholderOpt = document.createElement('option');
placeholderOpt.value = '';
placeholderOpt.textContent = placeholder;
placeholderOpt.selected = true;
placeholderOpt.disabled = true;
adderSelect.appendChild(placeholderOpt);
let hasAvailableOptions = false;
Array.from(originalSelect.options).forEach(option => {
if (option.selected) {
// Create badge for selected item
const badge = document.createElement('span');
badge.className = 'badge bg-secondary d-inline-flex align-items-center gap-1 py-1.5 px-2.5';
badge.style.fontSize = '0.9em';
badge.style.fontWeight = 'normal';
badge.style.cursor = 'default';
badge.textContent = option.textContent;
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn-close btn-close-white ms-1';
removeBtn.style.fontSize = '0.55em';
removeBtn.style.padding = '0';
removeBtn.ariaLabel = 'Remove';
removeBtn.addEventListener('click', () => {
option.selected = false;
// Trigger change event on original select for any listener
originalSelect.dispatchEvent(new Event('change', { bubbles: true }));
updateWidget();
});
badge.appendChild(removeBtn);
badgesContainer.appendChild(badge);
} else {
// Add to adder dropdown
const opt = document.createElement('option');
opt.value = option.value;
opt.textContent = option.textContent;
adderSelect.appendChild(opt);
hasAvailableOptions = true;
}
});
// Hide or disable select dropdown if no options left
if (!hasAvailableOptions) {
const noOpt = document.createElement('option');
noOpt.value = '';
noOpt.textContent = allSelectedMsg;
noOpt.disabled = true;
adderSelect.appendChild(noOpt);
}
}
// Handle selection from dropdown
adderSelect.addEventListener('change', (e) => {
const val = e.target.value;
if (!val) return;
const optionToSelect = Array.from(originalSelect.options).find(opt => opt.value === val);
if (optionToSelect) {
optionToSelect.selected = true;
// Trigger change event on original select
originalSelect.dispatchEvent(new Event('change', { bubbles: true }));
updateWidget();
}
});
// Initial load
updateWidget();
}
initCustomMultipleSelect('id_tags', "{% translate 'Ajouter un tag...' %}", "{% translate 'Tous les tags sont sélectionnés' %}");
initCustomMultipleSelect('id_thematics', "{% translate 'Ajouter une thématique...' %}", "{% translate 'Toutes les thématiques sont sélectionnées' %}");
});
</script>
<script>
let map, draw;
function updateArea(e) {
if (!draw) return;
const data = draw.getAll();
const geom = JSON.stringify(data);
document.getElementById('id_geom_geojson').value = geom;
}
try {
map = new maplibregl.Map({
container: 'map', // ID de l'élément HTML
style: window.MAP_STYLE_LIGHT, // Style Maplibre
center: [4.3517, 50.8503], // Coordonnées par défaut (Bruxelles)
zoom: 11
});
MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas';
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib';
draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
polygon: true,
trash: true
}
});
map.addControl(draw);
map.on('draw.create', updateArea);
map.on('draw.delete', updateArea);
map.on('draw.update', updateArea);
const maptilerApiKey = (function(styleUrl) { if (!styleUrl) return ''; try { const url = new URL(styleUrl); return url.searchParams.get('key') || ''; } catch(e) { return ''; } })(window.MAP_STYLE_LIGHT);
if (maptilerApiKey) {
const gc = new maplibreglMaptilerGeocoder.GeocodingControl({
apiKey: maptilerApiKey,
country: ["BE"]
});
map.addControl(gc, "top-left");
}
map.on('load', () => {
{% if geom_geojson %}
const existingGeom = {{ geom_geojson|safe }};
const feature = {
type: "Feature",
geometry: existingGeom,
properties: {}
};
draw.add(feature);
const bbox = turf.bbox(feature);
map.fitBounds(bbox, { padding: 20 });
updateArea();
{% endif %}
});
} catch (e) {
console.error("Map initialization failed safely:", e);
}
</script>
{% endblock script %}