Add points of interest
Do a bunch of refactoring in the process. Current display on homepage is a little shonky but that can be improved when there is something using it.
This commit is contained in:
@ -13,6 +13,36 @@
|
||||
background: red;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* UTILITY CLASSES
|
||||
*/
|
||||
|
||||
/* Colours */
|
||||
.ojuso-red { color: #ea4639; }
|
||||
.ojuso-green { color: #4ac95d; }
|
||||
.ojuso-yellow { color: #f9db3c; }
|
||||
.ojuso-blue { color: #008ad5; }
|
||||
|
||||
|
||||
/*
|
||||
* LAYOUT
|
||||
*/
|
||||
|
||||
/* Narrow content area wrapper */
|
||||
.content--narrow {
|
||||
max-width: 50em;
|
||||
margin: auto;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
/* Subheading, designed to sit on a paragraph after h1 */
|
||||
.subheading {
|
||||
font-size: 120%;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
|
||||
/* Page footer */
|
||||
|
||||
.footer {
|
||||
@ -42,8 +72,128 @@
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/* Colour utility classes */
|
||||
.ojuso-red { color: #ea4639; }
|
||||
.ojuso-green { color: #4ac95d; }
|
||||
.ojuso-yellow { color: #f9db3c; }
|
||||
.ojuso-blue { color: #008ad5; }
|
||||
|
||||
|
||||
/*
|
||||
* FORMS
|
||||
*/
|
||||
|
||||
/* Map widget - Minimum zoom help text */
|
||||
.minzoomhelptext {
|
||||
font-size: 14px;
|
||||
background-color: rgba(255, 255, 255, 80%);
|
||||
padding: 2px 10px;
|
||||
border: 1px solid #ccc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/* Case study draft functionality */
|
||||
.savebutton {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
padding: 8px 15px;
|
||||
z-index: 9999;
|
||||
|
||||
background-color: hsla(111, 25%, 84%, 1);
|
||||
border-top-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
.savebutton--icon { margin-left: 15px; margin-right: 2px; }
|
||||
.savebutton--icon-failed { color: #d9534f; }
|
||||
.savebutton--details { font-style: italic; }
|
||||
|
||||
.draftprompt { display: none; }
|
||||
.draftprompt--delete { margin-left: 10px; }
|
||||
.draftprompt--details { margin-left: 10px; font-weight: bold; }
|
||||
|
||||
|
||||
/* File upload widget styles */
|
||||
.filewidget--input { position: absolute; left: -1000px; }
|
||||
.filewidget--list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.filewidget--file {
|
||||
border: 1px solid #aaa;
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
width: 34em;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.filewidget--file--icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.filewidget--file--actions {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.filewidget--file--actions a {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filewidget--file--actions a:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* HOME PAGE
|
||||
*/
|
||||
|
||||
/* This is the popup that you see when you first arrive on the site */
|
||||
.hello {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-image: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHZpZXdCb3g9IjEwMCA1IDIwMCAyNTUiPg0KICA8cGF0aCBkPSJNMjM4IDEyN2g1M2wtMjYtMTUtMjcgMTUiIGZpbGw9InJnYmEoMjUyLCAyMTEsIDI5LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yOTQgMTIybC0yOS01MS0yOSA1MSAyOS0xNyAyOSAxNyIgZmlsbD0icmdiYSgxMDIsIDE5OCwgODcsIDAuNSkiLz4NCiAgPHBhdGggZD0iTTIyOCAxMjRsMzItNTZoLTY0bDMyIDU2IiBmaWxsPSJyZ2JhKDIyMSwgNjgsIDU4LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0xODYgNjNMMTUzIDdsLTMyIDU2aDY1bTExMSAxOTNsLTMyLTU2LTMyIDU2aDY0IiBmaWxsPSJyZ2JhKDEwMiwgMTk4LCA4NywgMC41KSIvPg0KICA8cGF0aCBkPSJNMjIzIDEyN2wtMzItNTYtMzMgNTZoNjUiIGZpbGw9InJnYmEoMjUyLCAyMTEsIDI5LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yNjAgMTkxbC0zMi01Ni0zMiA1Nmg2NCIgZmlsbD0icmdiYSgyMjEsIDY4LCA1OCwgMC41KSIvPg0KICA8cGF0aCBkPSJNMTUzIDEyNGwzMy01NmgtNjVsMzIgNTYiIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yMjggMjUzbDMyLTU2aC02NGwzMiA1NiIgZmlsbD0icmdiYSgyNTIsIDIxMSwgMjksIDAuNSkiLz4NCiAgDQogIA0KICA8cGF0aCBkPSJNMTg2IDE5MWwtMzMtNTUtMzIgNTVoNjUiIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQogIA0KICANCiAgPHBhdGggZD0iTTExOSAxODRsMjctNDYtMjcgMTV2MzEiIGZpbGw9InJnYmEoMTAyLCAxOTgsIDg3LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0xOTMgMTg0bDI3LTQ2LTI3IDE1djMxIiBmaWxsPSJyZ2JhKDM3LCAxNDAsIDIxMiwgMC41KSIvPg0KICA8cGF0aCBkPSJNMjE3IDEzM2gtNTlsMzAgNTF2LTM0bDI5LTE3IiBmaWxsPSJyZ2JhKDEwMiwgMTk4LCA4NywgMC41KSIvPg0KICA8cGF0aCBkPSJNMTg4IDIwNWwtMjcgNDYgMjctMTZ2LTMwIiBmaWxsPSJyZ2JhKDM3LCAxNDAsIDIxMiwgMC41KSIvPg0KICA8cGF0aCBkPSJNMTY0IDI1Nmg1OWwtMzAtNTF2MzRsLTI5IDE3IiBmaWxsPSJyZ2JhKDIyMSwgNjgsIDU4LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yMjUgMTJsLTI2IDQ2IDI2LTE2VjEyIiBmaWxsPSJyZ2JhKDI1MiwgMjExLCAyOSwgMC41KSIvPg0KICA8cGF0aCBkPSJNMjAxIDYzaDU5bC0yOS01MXYzNGwtMzAgMTciIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQo8L3N2Zz4=');
|
||||
background-position: center;
|
||||
background-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
.hello--container {
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hello--text {
|
||||
background-color: white;
|
||||
margin-top: 5em;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding: 1em;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
/* Top margin on small screns should be same as the rest */
|
||||
@media (max-width: 768px) {
|
||||
.hello--text {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.hello--logo {
|
||||
width: 200px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hello--logo, .hello--hide {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
243
assets/js/form_casestudy_drafts.js
Normal file
243
assets/js/form_casestudy_drafts.js
Normal file
@ -0,0 +1,243 @@
|
||||
"use strict";
|
||||
|
||||
//
|
||||
// UI ELEMENTS
|
||||
//
|
||||
|
||||
class SaveButton {
|
||||
constructor(div) {
|
||||
this.root = div
|
||||
this.root.className = "savebutton"
|
||||
this.root.style.display = "block"
|
||||
|
||||
this.element = {
|
||||
root: this.root,
|
||||
button: this.root.querySelector('.savebutton--button'),
|
||||
icon: this.root.querySelector('.savebutton--icon'),
|
||||
details: this.root.querySelector('.savebutton--details')
|
||||
}
|
||||
|
||||
this.switchStateInitial()
|
||||
}
|
||||
|
||||
changeState(data) {
|
||||
var data = data || {}
|
||||
this.element.button.innerText = data.buttonText || "Save"
|
||||
this.element.button.disabled = data.buttonClickable === false ? true : false
|
||||
this.element.icon.className = 'savebutton--icon ' + (data.iconClasses || "")
|
||||
this.element.details.innerText = data.detailsText || ""
|
||||
}
|
||||
|
||||
switchStateInitial() {
|
||||
this.changeState({
|
||||
buttonClickable: false
|
||||
})
|
||||
}
|
||||
|
||||
switchStateUnsaved() {
|
||||
this.changeState({
|
||||
detailsText: django.gettext("You have unsaved changes. Click here to save a draft, which you can access next time you are here.")
|
||||
})
|
||||
}
|
||||
|
||||
switchStateSaving() {
|
||||
this.changeState({
|
||||
buttonText: django.gettext("Saving..."),
|
||||
iconClasses: "fa fa-spinner fa-spin"
|
||||
})
|
||||
}
|
||||
|
||||
switchStateSaveSuccess() {
|
||||
this.changeState({
|
||||
buttonText: django.gettext("Saved"),
|
||||
detailsText: django.gettext("Saved successfully."),
|
||||
iconClasses: "fa fa-check",
|
||||
buttonClickable: false
|
||||
})
|
||||
}
|
||||
|
||||
switchStateSaveFailed(reason = "") {
|
||||
this.changeState({
|
||||
detailsText: django.gettext("Save failed! ") + reason,
|
||||
iconClasses: "fa fa-exclamation-triangle savebutton--icon-failed"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DraftPrompt {
|
||||
constructor(opts) {
|
||||
this.restoreDraft = opts.restoreFn
|
||||
this.deleteDraft = opts.deleteFn
|
||||
|
||||
this.root = opts.root
|
||||
this.element = {
|
||||
root: this.root,
|
||||
restore: this.root.querySelector('.draftprompt--restore'),
|
||||
delete: this.root.querySelector('.draftprompt--delete'),
|
||||
details: this.root.querySelector('.draftprompt--details')
|
||||
}
|
||||
|
||||
// Restore should restore, then hide the prompt
|
||||
this.element.restore.addEventListener('click', () => {
|
||||
this.restoreDraft()
|
||||
this.switchStateHidden()
|
||||
})
|
||||
|
||||
// Delete button will delete the draft
|
||||
this.element.delete.addEventListener('click', () => {
|
||||
if (window.confirm(django.gettext('Are you sure you want to delete your draft?'))) {
|
||||
this.switchStateDeleting()
|
||||
this.deleteDraft().then(ok => ok ?
|
||||
this.switchStateHidden() :
|
||||
this.switchStateDeleteFailed()
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
this.switchStateShown()
|
||||
}
|
||||
|
||||
changeState(data) {
|
||||
var data = data || {}
|
||||
this.element.root.style.display = data.visible ? "block" : "none"
|
||||
this.element.details.innerHTML = data.detailsText || ""
|
||||
}
|
||||
|
||||
switchStateHidden() {
|
||||
this.changeState({
|
||||
visible: false
|
||||
})
|
||||
}
|
||||
|
||||
switchStateShown() {
|
||||
this.changeState({
|
||||
visible: true
|
||||
})
|
||||
}
|
||||
|
||||
switchStateDeleting() {
|
||||
this.changeState({
|
||||
visible: true,
|
||||
details: '<i classs="fa fa-spinner fa-spin"></i>'
|
||||
})
|
||||
}
|
||||
|
||||
switchStateDeleteFailed() {
|
||||
this.changeState({
|
||||
details: django.gettext("Delete failed!")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// API UTILITIES
|
||||
//
|
||||
|
||||
function apiGetDraft() {
|
||||
return fetch('/en-gb/case-study/draft', {
|
||||
method: 'GET',
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function apiPutDraft(formSaver) {
|
||||
if (!formSaver) {
|
||||
throw new Error("apiPutDraft: parameter not provided")
|
||||
}
|
||||
|
||||
return fetch('/en-gb/case-study/draft', {
|
||||
method: 'PUT',
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ version: 1, data: formSaver.serialise() })
|
||||
})
|
||||
}
|
||||
|
||||
function apiDeleteDraft() {
|
||||
return fetch('/en-gb/case-study/draft', {
|
||||
method: 'DELETE',
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// GNARLY BITS TYING API & UI STUFF TOGETHER
|
||||
//
|
||||
|
||||
function showDraftPrompt(formSaver, serialisedForm) {
|
||||
let prompt = new DraftPrompt({
|
||||
root: document.querySelector('.draftprompt'),
|
||||
restoreFn: () => formSaver.deserialise(serialisedForm),
|
||||
deleteFn: () => apiDeleteDraft().then(response => response.ok)
|
||||
})
|
||||
}
|
||||
|
||||
function initDrafts() {
|
||||
const formSaver = new FormSaver({
|
||||
formId: 'case-study-form',
|
||||
except: [ 'csrfmiddlewaretoken' ]
|
||||
})
|
||||
|
||||
// Use whether the form has errors as a proxy for whether the server has
|
||||
// returned us data in the form. In this case, don't show a draft.
|
||||
const formHasErrors = document.getElementById('form_errors').value === 'true' ? true : false
|
||||
if (!formHasErrors) {
|
||||
// Get the previous draft, if any
|
||||
apiGetDraft()
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
// Handle the case where we didn't get a response
|
||||
if (!json) return
|
||||
|
||||
if (json.version !== 1) {
|
||||
throw new Error("Bad JSON response version")
|
||||
}
|
||||
|
||||
showDraftPrompt(formSaver, json.data.form)
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
}
|
||||
|
||||
// Init the button controller
|
||||
var button = new SaveButton(document.querySelector('.savebutton'))
|
||||
|
||||
// Dirty forms set up
|
||||
$('#case-study-form').dirtyForms();
|
||||
$('#case-study-form').on('dirty.dirtyforms', ev => {
|
||||
button.switchStateUnsaved()
|
||||
});
|
||||
|
||||
// Save button
|
||||
button.element.button.addEventListener('click', evt => {
|
||||
button.switchStateSaving()
|
||||
|
||||
apiPutDraft(formSaver).then(response => {
|
||||
if (response.ok) {
|
||||
button.switchStateSaveSuccess();
|
||||
$('#case-study-form').dirtyForms('setClean');
|
||||
document.querySelector('.draftprompt').style.display = "none"
|
||||
} else {
|
||||
button.switchStateSaveFailed();
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
button.switchStateSaveFailed();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/snikch/jquery.dirtyforms
|
72
assets/js/map_minzoom.js
Normal file
72
assets/js/map_minzoom.js
Normal file
@ -0,0 +1,72 @@
|
||||
ZoomHelpText = L.Control.extend({
|
||||
options: {
|
||||
position: 'bottomleft'
|
||||
},
|
||||
onAdd: function (map) {
|
||||
return L.DomUtil.create('div', 'minzoomhelptext')
|
||||
},
|
||||
setContent: function (content) {
|
||||
this.getContainer().innerHTML = content
|
||||
}
|
||||
})
|
||||
|
||||
// See GeometryField source (static/leaflet/leaflet.forms.js) to override more stuff...
|
||||
MinimumZoomField = L.GeometryField.extend({
|
||||
zoomLevelTooLow: function() {
|
||||
if (this._controlsShown !== false) {
|
||||
this._map.removeControl(this._drawControl)
|
||||
this._controlsShown = false
|
||||
}
|
||||
|
||||
this._zoomHelpText.setContent(
|
||||
django.gettext("Please zoom in further to place a marker on the map.")
|
||||
)
|
||||
},
|
||||
|
||||
zoomLevelOk: function() {
|
||||
if (this._controlsShown !== true) {
|
||||
this._map.addControl(this._drawControl)
|
||||
this._controlsShown = true
|
||||
}
|
||||
|
||||
this._zoomHelpText.setContent(
|
||||
django.gettext("Please use the marker tool on the left to select a location.")
|
||||
)
|
||||
},
|
||||
|
||||
addTo: function (map) {
|
||||
// super()
|
||||
L.GeometryField.prototype.addTo.call(this, map)
|
||||
this._controlsShown = true
|
||||
this._map = map
|
||||
|
||||
// Add a help text control
|
||||
this._zoomHelpText = new ZoomHelpText().addTo(map)
|
||||
|
||||
// Only allow editing past a certain zoom level (see #56)
|
||||
// Remove the edit controls
|
||||
this.zoomLevelTooLow()
|
||||
|
||||
// Enable or disable depending on zoom level
|
||||
map.addEventListener('zoomend', evt => {
|
||||
if (map.getZoom() >= 13) {
|
||||
this.zoomLevelOk()
|
||||
} else {
|
||||
this.zoomLevelTooLow()
|
||||
}
|
||||
})
|
||||
|
||||
// Respond to underlying text field changing
|
||||
const textarea = document.getElementById(this.options.fieldid)
|
||||
textarea.addEventListener('change', evt => {
|
||||
this.load()
|
||||
})
|
||||
|
||||
const triggerChange = evt => {
|
||||
document.getElementById('case-study-form').dispatchEvent(new Event('dirty'))
|
||||
}
|
||||
map.on(L.Draw.Event.CREATED, triggerChange)
|
||||
map.on(L.Draw.Event.EDITED, triggerChange)
|
||||
map.on(L.Draw.Event.DELETED, triggerChange)
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user