aboutsummaryrefslogtreecommitdiffstats
path: root/www/js
diff options
context:
space:
mode:
Diffstat (limited to 'www/js')
-rw-r--r--www/js/app.js84
-rw-r--r--www/js/config.js-example45
-rw-r--r--www/js/files.js14
-rw-r--r--www/js/map-OpenLayers.js10
-rw-r--r--www/js/models/draft.js26
-rw-r--r--www/js/models/report.js122
-rw-r--r--www/js/router.js2
-rw-r--r--www/js/strings.en.js5
-rw-r--r--www/js/strings.es.js5
-rw-r--r--www/js/views/around.js31
-rw-r--r--www/js/views/details.js35
-rw-r--r--www/js/views/details_extra.js8
-rw-r--r--www/js/views/fms.js30
-rw-r--r--www/js/views/home.js5
-rw-r--r--www/js/views/login.js28
-rw-r--r--www/js/views/offline.js93
-rw-r--r--www/js/views/photo.js67
-rw-r--r--www/js/views/submit.js110
18 files changed, 529 insertions, 191 deletions
diff --git a/www/js/app.js b/www/js/app.js
index 282e921..8b7432c 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -100,6 +100,11 @@ var tpl = {
printDebug: function(msg) {
if ( CONFIG.DEBUG ) {
console.log(msg);
+
+ // Some messages get logged before we've had a chance to
+ // attach the debugger, so keep them all for later reference.
+ FMS.debug_messages = FMS.debug_messages || [];
+ FMS.debug_messages.push(msg);
}
},
@@ -113,21 +118,28 @@ var tpl = {
},
checkLoggedInStatus: function() {
+ var p = $.Deferred();
+
if ( FMS.isOffline ) {
+ p.resolve();
} else {
$.ajax( {
url: CONFIG.FMS_URL + '/auth/ajax/check_auth',
type: 'GET',
dataType: 'json',
timeout: 30000,
- success: function( data, status ) {
- FMS.isLoggedIn = 1;
- },
- error: function() {
- FMS.isLoggedIn = 0;
- }
- } );
+ })
+ .done(function() {
+ FMS.isLoggedIn = 1;
+ p.resolve();
+ })
+ .fail(function() {
+ FMS.isLoggedIn = 0;
+ p.resolve();
+ })
}
+
+ return p;
},
saveCurrentDraft: function(force) {
@@ -151,12 +163,12 @@ var tpl = {
removeDraft: function(draftID, removePhoto) {
var draft = FMS.allDrafts.get(draftID);
- var uri = draft.get('file');
+ var files = draft.get('files');
FMS.allDrafts.remove(draft);
draft.destroy();
- if ( removePhoto && uri ) {
- return FMS.files.deleteURI( uri );
+ if ( removePhoto && files.length ) {
+ return FMS.files.deleteURIs( files );
}
var p = $.Deferred();
p.resolve();
@@ -266,6 +278,13 @@ var tpl = {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(false);
}
$('#load-screen').height( $(window).height() );
+
+ // Rough-and-ready iPhone X detection so CSS can stop things
+ // obscuring the home indicator at the bottom of the screen.
+ if (window.screen.width == 375 && window.screen.height == 812) {
+ $("body").addClass("iphone-x");
+ }
+
FMS.initialized = 1;
if ( navigator && navigator.splashscreen ) {
navigator.splashscreen.hide();
@@ -283,11 +302,7 @@ var tpl = {
if ( typeof device !== 'undefined' && device.platform === 'iOS' ) {
var model = parseInt(device.model.replace('iPhone',''), 10);
FMS.iPhoneModel = model;
-
- // fix overlap of status bar in ios7
- if (parseFloat(window.device.version) >= 7.0) {
- $('body').addClass('ios7');
- }
+ $('body').addClass('ios');
}
_.extend(FMS, {
@@ -306,9 +321,6 @@ var tpl = {
}
FMS.windowHeight = $(window).height();
- if ( $('body').hasClass('ios7') ) {
- FMS.windowHeight -= 20;
- }
if ( localStorage.usedBefore ) {
FMS.usedBefore = 1;
@@ -327,23 +339,37 @@ var tpl = {
$(document).on('ajaxStart', function() { $.mobile.loading('show'); } );
$(document).on('ajaxStop', function() { $.mobile.loading('hide'); } );
- $('#display-help').on('vclick', function(e) { FMS.helpShow(e); } );
+ $('#display-help').on('vclick', function(e) {
+ // Avoid a problem with input cursors being visible through
+ // the help layer on Web View, by unfocusing the element
+ if (device.platform === 'iOS') {
+ $('input').blur();
+ }
+
+ FMS.helpShow(e);
+ });
+
$('#dismiss').on('vclick', function(e) { FMS.helpHide(e); } );
FMS.allDrafts.comparator = function(a,b) { var a_date = a.get('created'), b_date = b.get('created'); return a_date === b_date ? 0 : a_date < b_date ? 1 : -1; };
FMS.allDrafts.fetch();
FMS.checkOnlineStatus();
FMS.loadCurrentDraft();
- FMS.checkLoggedInStatus();
- FMS.setupHelp();
-
- Backbone.history.start();
- if ( navigator && navigator.splashscreen ) {
- navigator.splashscreen.hide();
- } else {
- $('#load-screen').hide();
- }
- $('#display-help').show();
+ FMS.checkLoggedInStatus().done(function() {
+ if (!CONFIG.HELP_DISABLED) {
+ FMS.setupHelp();
+ }
+
+ Backbone.history.start();
+ if ( navigator && navigator.splashscreen ) {
+ navigator.splashscreen.hide();
+ } else {
+ $('#load-screen').hide();
+ }
+ if (!CONFIG.HELP_DISABLED) {
+ $('#display-help').show();
+ }
+ });
});
}
});
diff --git a/www/js/config.js-example b/www/js/config.js-example
index 2fceda7..1f1eef1 100644
--- a/www/js/config.js-example
+++ b/www/js/config.js-example
@@ -43,7 +43,50 @@ var CONFIG = {
image_svg: 'images/pin.svg',
background_svg: 'images/pin_shadow.svg'
}
- }
+ },
+
+ // Set this to true if you want to disable the help button on the right hand
+ // side of the screen. NB you'll also need to hide #display-help and #help
+ // elements in your CSS.
+ HELP_DISABLED: false,
+
+ // Set this to true if the user must provide at least one photo when making
+ // a report. If this is true the 'skip' button on the photo page is removed
+ // and 'next' doesn't appear until at least one photo is attached.
+ PHOTO_REQUIRED: false,
+
+ // The maximum number of photos the user can attach to a report.
+ MAX_PHOTOS: 3,
+
+ // If this is true then the user must login as the first step after
+ // installing the app, and before making any reports.
+ LOGIN_REQUIRED: false,
+
+ // The ratio of the data bounds to the viewport bounds (in each dimension).
+ // See http://dev.openlayers.org/releases/OpenLayers-2.13.1/doc/apidocs/files/OpenLayers/Strategy/BBOX-js.html
+ MAP_LOADING_RATIO: 2,
+
+ // If the user is logged in and this setting is true, the 'Your details'
+ // page is skipped and the report is sent immediately after the report
+ // details have been entered.
+ SKIP_CONFIRM_REPORT: false,
+
+ // You can optionally enforce a minimum password length if the user is
+ // registering an account when submitting a report. This should match the
+ // same minimum length required by your FixMyStreet server.
+ // Set this to 0 if you wish to disable this check. NB: If the check is
+ // active on the server the user's password may still be rejected if it's
+ // too short.
+ PASSWORD_MIN_LENGTH: 6,
+
+ // FMS provides a mechanism for rejecting passwords that are too common.
+ // Set this flag to true if the password should be checked against the
+ // server when a user registers an account via the app.
+ // NB: If this flag is false here but the check is active on the FMS server,
+ // common passwords will still be rejected at the point the report is sent
+ // from the app to the server - which may be a large POST if the report has
+ // photos attached.
+ PASSWORD_CHECK_COMMON: true
};
diff --git a/www/js/files.js b/www/js/files.js
index eea38c3..0725323 100644
--- a/www/js/files.js
+++ b/www/js/files.js
@@ -57,6 +57,16 @@
},
+ deleteURIs: function(uris) {
+ console.log("deleteURIs", uris);
+ var deferred = $.Deferred();
+ deferred.resolve();
+ uris.forEach(function(uri) {
+ deferred = deferred.then(FMS.files.deleteURI(uri));
+ });
+ return deferred;
+ },
+
// Delete a file from the filesystem
deleteFile: function (path) {
@@ -124,7 +134,7 @@
function moveFile (src, dest, newName) {
- FMS.printDebug( 'moveing file ' + src.fullPath + ' to ' + dest.fullPath );
+ FMS.printDebug( 'moving file ' + src.fullPath + ' to ' + dest.fullPath );
var move = $.Deferred();
@@ -166,7 +176,7 @@
var file = $.Deferred();
- window.resolveLocalFileSystemURI( uri, file.resolve, file.reject);
+ window.resolveLocalFileSystemURL( uri, file.resolve, file.reject);
return file.promise();
}
diff --git a/www/js/map-OpenLayers.js b/www/js/map-OpenLayers.js
index d458641..1de220d 100644
--- a/www/js/map-OpenLayers.js
+++ b/www/js/map-OpenLayers.js
@@ -128,7 +128,7 @@ function fixmystreet_onload() {
fixmystreet.map.addLayer(fixmystreet.report_location);
if (fixmystreet.page == 'around') {
- fixmystreet.bbox_strategy = new OpenLayers.Strategy.BBOX({ ratio: 1 });
+ fixmystreet.bbox_strategy = new OpenLayers.Strategy.BBOX({ ratio: CONFIG.MAP_LOADING_RATIO });
pin_layer_options.strategies = [ fixmystreet.bbox_strategy ];
pin_layer_options.protocol = new OpenLayers.Protocol.HTTP({
url: CONFIG.FMS_URL + '/ajax',
@@ -202,13 +202,9 @@ OpenLayers.Map.prototype.getCurrentSize = function() {
function show_map(event) {
if (typeof fixmystreet !== 'undefined' && fixmystreet.page == 'around') {
// Immediately go full screen map if on around page
- var mapTop = 0;
- if ( $('body').hasClass('ios7') ) {
- mapTop = 20;
- }
$('#map_box').css({
position: 'fixed',
- top: mapTop, left: 0, right: 0, bottom: 0,
+ top: 0, left: 0, right: 0, bottom: 0,
height: FMS.windowHeight,
margin: 0
});
@@ -408,7 +404,7 @@ OpenLayers.Control.ActionAfterDrag = OpenLayers.Class(OpenLayers.Control, {
if ( $('#confirm-map').css('display') == 'block' ) {
$('#reposition').show();
} else {
- $('#relocate').show();
+ $('#relocate, #hidepins').removeClass('nodisplay');
$('#front-howto').hide();
}
}
diff --git a/www/js/models/draft.js b/www/js/models/draft.js
index 56d8111..e517f1b 100644
--- a/www/js/models/draft.js
+++ b/www/js/models/draft.js
@@ -3,17 +3,19 @@
Draft: Backbone.Model.extend({
localStorage: new Backbone.LocalStorage(CONFIG.NAMESPACE + '-drafts'),
- defaults: {
- lat: 0,
- lon: 0,
- title: '',
- details: '',
- may_show_name: '',
- category: '',
- phone: '',
- pc: '',
- file: '',
- created: moment.utc()
+ defaults: function() {
+ return {
+ lat: 0,
+ lon: 0,
+ title: '',
+ details: '',
+ may_show_name: '',
+ category: '',
+ phone: '',
+ pc: '',
+ files: [],
+ created: moment.utc()
+ };
},
description: function() {
@@ -33,7 +35,7 @@
this.get('title') ||
this.get('details') ||
this.get('category') ||
- this.get('file')
+ this.get('files').length
) {
return true;
}
diff --git a/www/js/models/report.js b/www/js/models/report.js
index 1a72e73..6926980 100644
--- a/www/js/models/report.js
+++ b/www/js/models/report.js
@@ -3,16 +3,18 @@
Report: Backbone.Model.extend({
urlRoot: CONFIG.FMS_URL + '/report/ajax',
- defaults: {
- lat: 0,
- lon: 0,
- title: '',
- details: '',
- may_show_name: '',
- category: '',
- phone: '',
- pc: '',
- file: ''
+ defaults: function() {
+ return {
+ lat: 0,
+ lon: 0,
+ title: '',
+ details: '',
+ may_show_name: '',
+ category: '',
+ phone: '',
+ pc: '',
+ files: []
+ };
},
sync: function(method, model, options) {
@@ -51,6 +53,74 @@
return false;
},
+ _readFileAsBase64String: function(file, success, error) {
+ return this._readFileAsBinaryString(file, function(data) {
+ var b64 = btoa(data);
+ success(b64);
+ }, error);
+ },
+
+ _readFileAsBinaryString: function(file, success, error) {
+ var reader = new FileReader();
+ reader.onloadend = function() {
+ success(this.result);
+ };
+ reader.onerror = error;
+ return reader.readAsBinaryString(file);
+ },
+
+ _getParamName: function(field, encoding, length) {
+ // The FileTransfer plugin technically only supports a single
+ // file in each upload. However, we can force other files to
+ // be added with a little workaround.
+ // FileTransfer allows extra parameters to be sent with the
+ // HTTP POST request, each of which is its own part of the
+ // multipart-encoded request.
+ // For a part to be treated as a file by the backend we need
+ // to provide a 'filename' value in the Content-Disposition
+ // header. The FileTransfer code doesn't escape the names of
+ // extra POST parameters[0][1], so we can take advantage of this
+ // and essentially inject our own header lines and filename
+ // value with a carefully-crafted HTTP POST field name that's
+ // passed to FileTransfer.upload.
+ // FIXME: This is basically a hack, and needs a better
+ // solution at some point.
+ // [0]: https://github.com/apache/cordova-plugin-file-transfer/blob/49c21f951f51381d887646b38823222ed11c60c1/src/ios/CDVFileTransfer.m#L208
+ // [1]: https://github.com/apache/cordova-plugin-file-transfer/blob/49c21f951f51381d887646b38823222ed11c60c1/src/android/FileTransfer.java#L369
+ var name = field + '"; filename="' + field + '.jpg"\r\n';
+ name += "Content-Type: image/jpeg\r\n";
+ name += "Content-Transfer-Encoding: " + encoding + "\r\n";
+ name += "Content-Length: " + length + "\r\n";
+ name += 'X-Ignore-This-Header: "'; // to close the open quotes
+ return name;
+ },
+
+ _addExtraPhotos: function(files, options, success, error) {
+ var photos = [];
+ for (var i = 0; i < files.length; i++) {
+ var uri = files[i];
+ photos.push({field: "photo"+(i+2), uri: uri});
+ }
+ this._addNextExtraPhoto(photos, options, success, error);
+ },
+
+ _addNextExtraPhoto: function(photos, options, success, error) {
+ var photo = photos.shift();
+ if (photo === undefined) {
+ success();
+ return;
+ }
+ var self = this;
+ resolveLocalFileSystemURL(photo.uri, function(fileentry) {
+ fileentry.file(function(file) {
+ self._readFileAsBase64String(file, function(data) {
+ options.params[self._getParamName(photo.field, "base64", data.length)] = data;
+ self._addNextExtraPhoto(photos, options, success, error);
+ }, error);
+ }, error);
+ }, error);
+ },
+
post: function(model,options) {
var params = {
@@ -86,7 +156,7 @@
}
var that = this;
- if ( model.get('file') && model.get('file') !== '' ) {
+ if ( model.get('files') && model.get('files').length > 0 ) {
var fileUploadSuccess = function(r) {
FMS.uploading = false;
$.mobile.loading('hide');
@@ -122,7 +192,8 @@
}
};
- fileURI = model.get('file');
+ var files = model.get('files').slice();
+ fileURI = files.shift();
var fileOptions = new FileUploadOptions();
fileOptions.fileKey="photo";
@@ -169,14 +240,25 @@
uploadPcnt++;
}
};
- $.mobile.loading('show', {
- text: FMS.strings.photo_loading,
- textVisible: true,
- html: '<span class="ui-icon ui-icon-loading"></span><h1>' + FMS.strings.photo_loading + '</h1><span id="progress"></span>'
- });
- window.setTimeout( checkUpload, 15000 );
- FMS.uploading = true;
- ft.upload(fileURI, CONFIG.FMS_URL + "/report/new/mobile", fileUploadSuccess, fileUploadFail, fileOptions);
+
+ // If file2 or file3 have been set on this model we need to
+ // add the photos to the file upload request manually
+ // as FileTransfer only supports a single file upload.
+ that._addExtraPhotos(
+ files,
+ fileOptions,
+ function() {
+ $.mobile.loading('show', {
+ text: FMS.strings.photo_loading,
+ textVisible: true,
+ html: '<span class="ui-icon ui-icon-loading"></span><h1>' + FMS.strings.photo_loading + '</h1><span id="progress"></span>'
+ });
+ window.setTimeout( checkUpload, 15000 );
+ FMS.uploading = true;
+ ft.upload(fileURI, CONFIG.FMS_URL + "/report/new/mobile", fileUploadSuccess, fileUploadFail, fileOptions);
+ },
+ fileUploadFail
+ );
};
setupChecker();
} else {
diff --git a/www/js/router.js b/www/js/router.js
index c691abb..2a23708 100644
--- a/www/js/router.js
+++ b/www/js/router.js
@@ -159,7 +159,7 @@
// any transitions as they just add visual distraction to no end
// likewise displaying the offline page
var options = { changeHash: false };
- if ( !this.currentView || this.currentView.id == 'front-page' || view.id == 'offline' ) {
+ if ( !this.currentView || this.currentView.id == 'front-page' || view.id == 'offline' || view.id === this.currentView.id) {
options.transition = 'none';
}
if ( this.reverse ) {
diff --git a/www/js/strings.en.js b/www/js/strings.en.js
index 1dabd30..bf67b14 100644
--- a/www/js/strings.en.js
+++ b/www/js/strings.en.js
@@ -17,7 +17,10 @@
required: 'Please enter your email',
email: 'Please enter a valid email'
},
- password: 'Please enter a password'
+ password: {
+ required: 'Please enter a password',
+ short: 'Please enter a password at least %d characters long'
+ }
},
strings: {
next: 'Next',
diff --git a/www/js/strings.es.js b/www/js/strings.es.js
index 9c8b950..5b2481b 100644
--- a/www/js/strings.es.js
+++ b/www/js/strings.es.js
@@ -17,7 +17,10 @@
required: 'Por favor, introduzca su email',
email: 'Por favor, introduzca un email válido'
},
- password: 'Por favor, introduzca la contraseña'
+ password: {
+ required: 'Por favor, introduzca la contraseña',
+ short: 'Please enter a password at least %d characters long'
+ }
},
strings: {
next: 'Siguiente',
diff --git a/www/js/views/around.js b/www/js/views/around.js
index f0b8692..4afc5bb 100644
--- a/www/js/views/around.js
+++ b/www/js/views/around.js
@@ -15,6 +15,7 @@
'vclick .ui-input-clear': 'clearSearchErrors',
'blur #pc': 'clearSearchErrors',
'vclick #relocate': 'centerMapOnPosition',
+ 'vclick #hidepins': 'toggleMarkersVisibility',
'vclick #cancel': 'onClickCancel',
'vclick #confirm-map': 'onClickReport',
'vclick #mark-here': 'onClickMark',
@@ -45,7 +46,7 @@
$('#view-my-reports').hide();
$('#login-options').hide();
$('#postcodeForm').hide();
- $('#relocate').hide();
+ $('#relocate, #hidepins').addClass("nodisplay");
$('#cancel').hide();
$('#map_box').removeClass('background-map');
this.fixPageHeight();
@@ -101,7 +102,7 @@
},
gotLocation: function( info ) {
- $('#relocate').show();
+ $('#relocate, #hidepins').removeClass("nodisplay");
this.finishedLocating();
this.listenTo(FMS.locator, 'gps_current_position', this.positionUpdate);
@@ -121,7 +122,7 @@
positionUpdate: function( info ) {
if ( $('#front-howto').is(':hidden') ) {
- $('#relocate').show();
+ $('#relocate, #hidepins').removeClass("nodisplay");
}
FMS.currentPosition = info.coordinates;
var centre = this.projectCoords( info.coordinates );
@@ -182,7 +183,7 @@
msg = FMS.strings.location_problem;
}
if ( !fixmystreet.map ) {
- $('#relocate').hide();
+ $('#relocate, #hidepins').addClass("nodisplay");
$('#mark-here').hide();
// if we are going to display the help then we don't want to focus on
// the search box as it will show through the help
@@ -197,7 +198,7 @@
},
displayHelpIfFirstTime: function() {
- if ( !FMS.usedBefore ) {
+ if ( !FMS.usedBefore && !CONFIG.HELP_DISABLED ) {
FMS.helpShow();
}
},
@@ -267,7 +268,6 @@
onClickCancel: function(e) {
e.preventDefault();
- fixmystreet.markers.removeAllFeatures();
fixmystreet_activate_drag();
// force pins to be refetched and displayed
fixmystreet.bbox_strategy.update({force: true});
@@ -352,7 +352,7 @@
},
goAddress: function(e) {
- $('#relocate').show();
+ $('#relocate, #hidepins').removeClass("nodisplay");
$('#front-howto').html('').hide();
var t = $(e.target);
var lat = t.attr('data-lat');
@@ -371,7 +371,7 @@
$('#pc').attr('placeholder', msg).addClass('error');;
} else {
$('#front-howto').html(msg);
- $('#relocate').hide();
+ $('#relocate, #hidepins').addClass("nodisplay");
$('#front-howto').show();
}
},
@@ -380,7 +380,7 @@
$('#pc').attr('placeholder', this.origPcPlaceholder).removeClass('error');;
if ( fixmystreet.map ) {
$('#front-howto').hide();
- $('#relocate').show();
+ $('#relocate, #hidepins').removeClass("nodisplay");
}
},
@@ -400,7 +400,7 @@
}
$('#front-howto').html('<p>' + FMS.strings.multiple_matches + '</p><ul data-role="listview" data-inset="true">' + multiple + '</ul>');
$('.ui-page').trigger('create');
- $('#relocate').hide();
+ $('#relocate, #hidepins').addClass("nodisplay");
$('#front-howto').show();
} else {
this.searchError( FMS.strings.location_problem );
@@ -410,9 +410,7 @@
pauseMap: function() {
this.stopListening(FMS.locator);
FMS.locator.stopTracking();
- if ( FMS.iPhoneModel > 3 ) {
- $('#map_box').addClass('background-map');
- }
+ $('#map_box').addClass('background-map');
$('#map_box').off('touchend');
if ( fixmystreet.map ) {
fixmystreet.nav.deactivate();
@@ -442,7 +440,7 @@
e.preventDefault();
if ( !fixmystreet.map ) {
this.$('#mark-here').hide();
- this.$('#relocate').hide();
+ this.$('#relocate, #hidepins').addClass("nodisplay");
$('#front-howto').html('<p>' + FMS.strings.locate_dismissed + '</p>');
$('#front-howto').show();
}
@@ -497,6 +495,11 @@
);
return centre;
+ },
+
+ toggleMarkersVisibility: function(e) {
+ e.preventDefault();
+ fixmystreet.markers.setVisibility(!fixmystreet.markers.getVisibility());
}
})
});
diff --git a/www/js/views/details.js b/www/js/views/details.js
index 9f03d58..84b6ac2 100644
--- a/www/js/views/details.js
+++ b/www/js/views/details.js
@@ -18,6 +18,16 @@
'blur input': 'updateCurrentReport'
},
+ initialize: function() {
+ var that = this;
+ window.addEventListener('native.keyboardshow', function(e) {
+ that.fixDetailTextAreaHeight(e.keyboardHeight);
+ });
+ window.addEventListener('native.keyboardhide', function(e) {
+ that.fixDetailTextAreaHeight();
+ });
+ },
+
afterRender: function() {
this.$('#form_category').attr('data-role', 'none');
@@ -28,13 +38,18 @@
},
- beforeDisplay: function() {
- this.fixPageHeight();
+ beforeDisplay: function(extra) {
+ this.fixDetailTextAreaHeight();
+ },
+
+ fixDetailTextAreaHeight: function(extra) {
+ extra = extra || 0;
+ this.fixPageHeight(extra);
var header = this.$("div[data-role='header']:visible"),
detail = this.$('#form_detail'),
top = detail.position().top,
viewHeight = $(window).height(),
- contentHeight = viewHeight - header.outerHeight() + 15;
+ contentHeight = viewHeight - header.outerHeight() + 15 - extra;
detail.height( contentHeight - top );
},
@@ -87,8 +102,18 @@
timeout: 30000,
success: function( data, status ) {
if ( data && data.category_extra && data.category_extra.length > 0 ) {
- that.model.set('category_extras', data.category_extra);
- that.navigate('details_extra');
+ // Some categories have only hidden fields - in that case we
+ // don't want to navigate to the details_extra view.
+ var all_hidden = data.category_extra_json.reduce(function(accumulator, field) {
+ return accumulator && (field.automated === "hidden_field");
+ }, true);
+
+ if (all_hidden) {
+ that.navigate( that.next );
+ } else {
+ that.model.set('category_extras', data.category_extra);
+ that.navigate('details_extra');
+ }
} else {
that.navigate( that.next );
}
diff --git a/www/js/views/details_extra.js b/www/js/views/details_extra.js
index 160ff11..e5f63ba 100644
--- a/www/js/views/details_extra.js
+++ b/www/js/views/details_extra.js
@@ -19,15 +19,19 @@
afterRender: function() {
this.populateFields();
+ this.enableScrolling();
},
- onClickButtonPrev: function() {
+ onClickButtonPrev: function(e) {
+ e.preventDefault();
+ this.disableScrolling();
this.model.set('hasExtras', 0);
this.updateCurrentReport();
this.navigate( this.prev, true );
},
onClickButtonNext: function() {
+ this.disableScrolling();
this.clearValidationErrors();
var valid = 1;
var that = this;
@@ -49,6 +53,8 @@
this.clearValidationErrors();
this.updateCurrentReport();
this.navigate( this.next );
+ } else {
+ this.enableScrolling();
}
},
diff --git a/www/js/views/fms.js b/www/js/views/fms.js
index 6ff569b..ae1174f 100644
--- a/www/js/views/fms.js
+++ b/www/js/views/fms.js
@@ -2,7 +2,7 @@
_.extend( FMS, {
FMSView: Backbone.View.extend({
tag: 'div',
- bottomMargin: 20,
+ bottomMargin: 0,
contentSelector: '[data-role="content"]',
events: {
@@ -45,12 +45,21 @@
return this;
},
- fixPageHeight: function() {
+ fixPageHeight: function(extra) {
+ extra = extra || 0;
var header = this.$("div[data-role='header']:visible"),
content = this.$(this.contentSelector),
top = content.position().top,
viewHeight = $(window).height(),
- contentHeight = FMS.windowHeight - header.outerHeight() - this.bottomMargin;
+ contentHeight = FMS.windowHeight - header.outerHeight() - this.bottomMargin - extra;
+
+ if ($("body").hasClass("iphone-x")) {
+ var body = $("body").get(0);
+ var inset = window.getComputedStyle(body).getPropertyValue("--safe-area-inset-bottom");
+ // We want the pixel value, not the CSS string
+ inset = parseInt(inset.replace(/[^\d]*/g, ''));
+ contentHeight -= inset;
+ }
this.setHeight( content, contentHeight - top );
},
@@ -107,7 +116,20 @@
$('.form-error').removeClass('form-error');
},
- destroy: function() { FMS.printDebug('destory for ' + this.id); this._destroy(); this.remove(); },
+ disableScrolling: function() {
+ if ( typeof cordova !== 'undefined' ) {
+ cordova.plugins.Keyboard.disableScroll(true);
+ $('body').scrollTop(0);
+ }
+ },
+
+ enableScrolling: function() {
+ if ( typeof cordova !== 'undefined' ) {
+ cordova.plugins.Keyboard.disableScroll(false);
+ }
+ },
+
+ destroy: function() { FMS.printDebug('destroy for ' + this.id); this._destroy(); this.remove(); },
_destroy: function() {}
})
diff --git a/www/js/views/home.js b/www/js/views/home.js
index 998af1c..d82b874 100644
--- a/www/js/views/home.js
+++ b/www/js/views/home.js
@@ -24,11 +24,14 @@
afterDisplay: function() {
$('#load-screen').hide();
+
if ( FMS.isOffline ) {
this.navigate( 'offline' );
+ } else if ( !FMS.isLoggedIn && CONFIG.LOGIN_REQUIRED ) {
+ this.navigate( 'login' );
} else if ( FMS.currentDraft && (
FMS.currentDraft.get('title') || FMS.currentDraft.get('lat') ||
- FMS.currentDraft.get('details') || FMS.currentDraft.get('file') )
+ FMS.currentDraft.get('details') || FMS.currentDraft.get('files').length > 0 )
) {
this.navigate( 'existing' );
} else {
diff --git a/www/js/views/login.js b/www/js/views/login.js
index c0f16ba..9225c18 100644
--- a/www/js/views/login.js
+++ b/www/js/views/login.js
@@ -40,8 +40,7 @@
that.model.set('name', data.name);
that.model.save();
FMS.isLoggedIn = 1;
- that.$('#password_row').hide();
- that.$('#success_row').show();
+ that.rerender();
} else {
that.validationError('signinForm', FMS.strings.login_details_error);
}
@@ -65,11 +64,7 @@
FMS.isLoggedIn = 0;
that.model.set('password', '');
that.model.save();
- that.$('#form_email').val('');
- that.$('#form_password').val('');
- that.$('#success_row').hide();
- that.$('#signed_in_row').hide();
- that.$('#password_row').show();
+ that.rerender();
},
error: function() {
that.validationError('err', FMS.strings.logout_error);
@@ -83,7 +78,7 @@
if ( !$('#form_password').val() ) {
isValid = 0;
- this.validationError('form_password', FMS.validationStrings.password );
+ this.validationError('form_password', FMS.validationStrings.password.required);
}
var email = $('#form_email').val();
@@ -102,6 +97,23 @@
}
return isValid;
+ },
+
+ beforeDisplay: function() {
+ this.fixPageHeight();
+ if ( !FMS.isLoggedIn && CONFIG.LOGIN_REQUIRED ) {
+ this.$("#reports-next-btn").hide();
+ }
+ },
+
+ rerender: function() {
+ // Simply calling this.render() breaks the DOM in a weird and
+ // interesting way - somehow the main view element is duplicated
+ // instead of replaced and none of the event handlers are
+ // hooked up so you end up with a blank screen.
+ // This is a convenience wrapper around the correct router call
+ // which works around the problem.
+ FMS.router.login();
}
})
});
diff --git a/www/js/views/offline.js b/www/js/views/offline.js
index ac007d1..3ce94a9 100644
--- a/www/js/views/offline.js
+++ b/www/js/views/offline.js
@@ -9,17 +9,18 @@
events: {
'pagehide': 'destroy',
- 'pagebeforeshow': 'beforeShow',
+ 'pagebeforeshow': 'beforeDisplay',
'pageshow': 'afterDisplay',
'vclick .ui-btn-left': 'onClickButtonPrev',
'vclick .ui-btn-right': 'onClickButtonNext',
'vclick #id_photo_button': 'takePhoto',
'vclick #id_existing': 'addPhoto',
- 'vclick #id_del_photo_button': 'deletePhoto',
+ 'vclick .del_photo_button': 'deletePhoto',
'vclick #locate': 'onClickLocate',
'vclick #locate_cancel': 'onClickCancel',
'blur input': 'toggleNextButton',
- 'blur textarea': 'toggleNextButton'
+ 'blur textarea': 'blurTextArea',
+ 'focus textarea': 'focusTextArea'
},
_back: function() {
@@ -30,7 +31,7 @@
var hasContent = false;
if ( $('#form_title').val() || $('#form_detail').val() ||
- this.model.get('lat') || this.model.get('file') ) {
+ this.model.get('lat') || this.model.get('files').length > 0 ) {
hasContent = true;
}
@@ -39,11 +40,22 @@
afterDisplay: function() {
$('body')[0].scrollTop = 0;
- $('div[data-role="content"]').show();
- },
- beforeShow: function() {
- $('div[data-role="content"]').hide();
+ // The height of the photos container needs to be adjusted
+ // depending on the number of photos - if the max number of
+ // photos have already been added then the 'add photo' UI isn't
+ // shown so we should use all the vertical space for the
+ // thumbnails.
+ var wrapperHeight = $(".ui-content").height();
+ wrapperHeight -= $(".ui-content .notopmargin").outerHeight(true);
+ wrapperHeight -= $(".ui-content #locate_result").outerHeight(true);
+ wrapperHeight -= $(".ui-content .inputcard").outerHeight(true);
+ wrapperHeight -= $(".ui-content #add_photo").outerHeight(true);
+ $(".photo-wrapper").height(wrapperHeight);
+ },
+
+ beforeDisplay: function() {
+ this.fixPageHeight();
this.toggleNextButton();
},
@@ -55,6 +67,15 @@
}
},
+ focusTextArea: function() {
+ $("textarea#form_detail").get(0).rows = 7;
+ },
+
+ blurTextArea: function(e) {
+ $("textarea#form_detail").get(0).rows = 2;
+ this.toggleNextButton();
+ },
+
failedLocation: function(details) {
this.finishedLocating();
this.locateCount = 21;
@@ -73,32 +94,47 @@
takePhoto: function() {
var that = this;
+ $.mobile.loading('show');
navigator.camera.getPicture( function(imgURI) { that.addPhotoSuccess(imgURI); }, function(error) { that.addPhotoFail(error); }, { saveToPhotoAlbum: true, quality: 49, destinationType: Camera.DestinationType.FILE_URI, sourceType: navigator.camera.PictureSourceType.CAMERA, correctOrientation: true });
},
addPhoto: function() {
var that = this;
+ $.mobile.loading('show');
navigator.camera.getPicture( function(imgURI) { that.addPhotoSuccess(imgURI); }, function(error) { that.addPhotoFail(error); }, { saveToPhotoAlbum: false, quality: 49, destinationType: Camera.DestinationType.FILE_URI, sourceType: navigator.camera.PictureSourceType.PHOTOLIBRARY, correctOrientation: true });
},
addPhotoSuccess: function(imgURI) {
- var move = FMS.files.moveURI( imgURI );
+ var move;
+ // on iOS the photos go into a temp folder in the apps own filespace so we
+ // can move them, and indeed have to as the tmp space is cleaned out by the OS
+ // so draft reports might have their images removed. on android you access the
+ // images where they are stored on the filesystem so if you move, and then delete
+ // them, you are moving and deleting the only copy of them which is likely to be
+ // surprising and unwelcome so we copy them instead.
+ var fileName = CONFIG.NAMESPACE + '_' + this.model.cid + '_' + moment().unix() + '.jpg';
+ if ( FMS.isAndroid ) {
+ move = FMS.files.copyURI( imgURI, fileName );
+ } else {
+ move = FMS.files.moveURI( imgURI, fileName );
+ }
var that = this;
move.done( function( file ) {
- $('#photo').attr('src', file.toURL());
- that.model.set('file', file.toURL());
- FMS.saveCurrentDraft();
+ var files = that.model.get('files');
+ files.push(file.toURL());
+ that.model.set('files', files);
+ that.updateCurrentReport();
- $('#photo-next-btn .ui-btn-text').text(FMS.strings.next);
- $('#display_photo').show();
- $('#add_photo').hide();
+ $.mobile.loading('hide');
+ that.rerender();
});
move.fail( function() { that.addPhotoFail(); } );
},
addPhotoFail: function(message) {
+ $.mobile.loading('hide');
if ( message != 'no image selected' &&
message != 'Selection cancelled.' &&
message != 'Camera cancelled.' ) {
@@ -106,18 +142,22 @@
}
},
- deletePhoto: function() {
- var that = this;
- var del = FMS.files.deleteURI( this.model.get('file') );
+ deletePhoto: function(e) {
+ e.preventDefault();
+ $.mobile.loading('show');
+ var files = this.model.get('files');
+ var index = parseInt($(e.target).data('fileIndex'));
+ var deleted_file = files.splice(index, 1)[0];
+ var del = FMS.files.deleteURI( deleted_file );
+
+ var that = this;
del.done( function() {
- that.model.set('file', '');
- FMS.saveCurrentDraft();
- $('#photo').attr('src', '');
+ that.model.set('files', files);
+ that.updateCurrentReport();
- $('#photo-next-btn .ui-btn-text').text(FMS.strings.skip);
- $('#display_photo').hide();
- $('#add_photo').show();
+ $.mobile.loading('hide');
+ that.rerender();
});
},
@@ -154,7 +194,12 @@
this.model.set('title', $('#form_title').val());
this.model.set('details', $('#form_detail').val());
FMS.saveCurrentDraft();
+ },
+
+ rerender: function() {
+ FMS.router.offline();
}
+
})
});
})(FMS, Backbone, _, $);
diff --git a/www/js/views/photo.js b/www/js/views/photo.js
index 485b74c..0b0c5e9 100644
--- a/www/js/views/photo.js
+++ b/www/js/views/photo.js
@@ -14,17 +14,23 @@
'vclick .ui-btn-right': 'onClickButtonNext',
'vclick #id_photo_button': 'takePhoto',
'vclick #id_existing': 'addPhoto',
- 'vclick #id_del_photo_button': 'deletePhoto'
+ 'vclick .del_photo_button': 'deletePhoto'
},
beforeDisplay: function() {
this.fixPageHeight();
- this.$('#id_del_photo_button').hide();
- if ( this.model.get('file') ) {
- $('#id_photo_button').parents('.ui-btn').hide();
- $('#id_existing').parents('.ui-btn').hide();
- window.setTimeout( function() { $('#id_del_photo_button').show(); }, 250 );
- }
+ },
+
+ afterDisplay: function() {
+ // The height of the photos container needs to be adjusted
+ // depending on the number of photos - if the max number of
+ // photos have already been added then the 'add photo' UI isn't
+ // shown so we should use all the vertical space for the
+ // thumbnails.
+ var wrapperHeight = $(".ui-content").height();
+ wrapperHeight -= $(".ui-content h2").outerHeight(true);
+ wrapperHeight -= $(".ui-content .bottom-btn").outerHeight(true)
+ $(".photo-wrapper").height(wrapperHeight);
},
getOptions: function(isFromAlbum) {
@@ -52,7 +58,7 @@
takePhoto: function(e) {
e.preventDefault();
$.mobile.loading('show');
- $('#photo').hide();
+ $('.photo-wrapper .photo img').hide();
var that = this;
var options = this.getOptions();
@@ -63,7 +69,7 @@
addPhoto: function(e) {
e.preventDefault();
$.mobile.loading('show');
- $('#photo').hide();
+ $('.photo-wrapper .photo img').hide();
var that = this;
var options = this.getOptions(true);
navigator.camera.getPicture( function(imgURI) { that.addPhotoSuccess(imgURI); }, function(error) { that.addPhotoFail(error); }, options);
@@ -86,25 +92,19 @@
var that = this;
move.done( function( file ) {
- $('#nophoto_title').hide();
- $('#photo_title').html(FMS.strings.photo_added).show();
- $('#photo').attr('src', file.toURL()).addClass('small').removeClass('placeholder');
- that.model.set('file', file.toURL());
+ var files = that.model.get('files');
+ files.push(file.toURL());
+ that.model.set('files', files);
FMS.saveCurrentDraft();
-
- $('#photo-next-btn .ui-btn-text').text(FMS.strings.next);
- $('#id_photo_button').parents('.ui-btn').hide();
- $('#id_existing').parents('.ui-btn').hide();
- $('#photo').show();
- window.setTimeout(function() { $('#id_del_photo_button').show() }, 500);
- window.setTimeout(function() { $.mobile.loading('hide') }, 100);
+ $.mobile.loading('hide');
+ that.rerender();
});
move.fail( function() { that.addPhotoFail(); } );
},
addPhotoFail: function(message) {
- $('#photo').show();
+ $('.photo-wrapper .photo img').show();
$.mobile.loading('hide');
if ( message != 'no image selected' &&
message != 'Selection cancelled.' &&
@@ -115,22 +115,25 @@
deletePhoto: function(e) {
e.preventDefault();
- var that = this;
- var del = FMS.files.deleteURI( this.model.get('file') );
+ var files = this.model.get('files');
+ var index = parseInt($(e.target).data('fileIndex'));
+ var deleted_file = files.splice(index, 1)[0];
+ var del = FMS.files.deleteURI( deleted_file );
+
+ var that = this;
del.done( function() {
- $('#photo_title').hide();
- $('#nophoto_title').show();
- $('#id_del_photo_button').hide();
- that.model.set('file', '');
+ that.model.set('files', files);
FMS.saveCurrentDraft(true);
- $('#photo').attr('src', 'images/placeholder-photo.png').addClass('placeholder').removeClass('small');
-
- $('#photo-next-btn .ui-btn-text').text(FMS.strings.skip);
- $('#id_photo_button').parents('.ui-btn').show();
- $('#id_existing').parents('.ui-btn').show();
+ that.rerender();
});
+ },
+ rerender: function() {
+ // Simply calling this.render() breaks the DOM in a weird and
+ // interesting way, so this is a convenience wrapper around
+ // the correct router call.
+ FMS.router.photo();
}
})
});
diff --git a/www/js/views/submit.js b/www/js/views/submit.js
index 6a7c946..4a92fe3 100644
--- a/www/js/views/submit.js
+++ b/www/js/views/submit.js
@@ -86,7 +86,7 @@
if ( !this._handleInvalid( model, err, options ) ) {
var errors = err.errors;
var errorList = '<ul><li class="plain">' + FMS.strings.invalid_report + '</li>';
- var validErrors = [ 'password', 'category', 'name' ];
+ var validErrors = [ 'password', 'password_register', 'category', 'name' ];
for ( var k in errors ) {
if ( validErrors.indexOf(k) >= 0 || errors[k].match(/required/) ) {
if ( k === 'password' ) {
@@ -128,6 +128,22 @@
}
},
+ validateUserTitle: function() {
+ if ( this.model.get('titles_list') && this.model.get('titles_list').length > 0 ) {
+ if ( $('#form_title').val() === '' ) {
+ this.validationError('form_title', FMS.strings.required);
+ return false;
+ }
+ }
+ return true;
+ },
+
+ setUserTitle: function() {
+ if ( this.model.get('titles_list') && this.model.get('titles_list').length > 0 ) {
+ FMS.currentUser.set('title', $('#form_title').val());
+ }
+ },
+
beforeSubmit: function() {},
afterSubmit: function() {},
@@ -145,7 +161,8 @@
(function (FMS, Backbone, _, $) {
_.extend( FMS, {
SubmitInitialPageView: FMS.SubmitView.extend({
- onClickButtonPrev: function() {
+ onClickButtonPrev: function(e) {
+ e.preventDefault();
if ( this.model.get('hasExtras') == 1 ) {
this.navigate( 'details_extra', true );
} else {
@@ -264,11 +281,8 @@
}
}
- if ( this.model.get('titles_list') && this.model.get('titles_list').length > 0 ) {
- if ( $('#form_title').val() === '' ) {
- this.validationError('form_title', FMS.strings.required);
- isValid = 0;
- }
+ if (!this.validateUserTitle()) {
+ isValid = 0;
}
return isValid;
@@ -282,10 +296,7 @@
this.model.set('may_show_name', $('#form_may_show_name').is(':checked'));
FMS.currentUser.set('name', $('#form_name').val());
FMS.currentUser.set('may_show_name', $('#form_may_show_name').is(':checked'));
-
- if ( this.model.get('titles_list') && this.model.get('titles_list').length > 0 ) {
- FMS.currentUser.set('title', $('#form_title').val());
- }
+ this.setUserTitle();
if ( FMS.currentUser ) {
FMS.currentUser.save();
@@ -329,20 +340,21 @@
},
validate: function() {
+ this.clearValidationErrors();
var isValid = 1;
if ( !$('#form_password').val() ) {
isValid = 0;
- this.validationError('form_password', FMS.validationStrings.password );
+ this.validationError('form_password', FMS.validationStrings.password.required );
+ } else if ( CONFIG.PASSWORD_MIN_LENGTH && $('#form_password').val().length < CONFIG.PASSWORD_MIN_LENGTH ) {
+ isValid = 0;
+ var msg = FMS.validationStrings.password.short.replace('%d', CONFIG.PASSWORD_MIN_LENGTH);
+ this.validationError('form_password', msg);
}
- if ( $('#form_name').val() && this.model.get('titles_list') && this.model.get('titles_list').length > 0 ) {
- if ( $('#form_title').val() === '' ) {
- this.validationError('form_title', FMS.strings.required);
- isValid = 0;
- }
+ if ($('#form_name').val() && !this.validateUserTitle()) {
+ isValid = 0;
}
-
return isValid;
},
@@ -357,9 +369,7 @@
this.model.set('may_show_name', $('#form_may_show_name').is(':checked'));
FMS.currentUser.set('name', $('#form_name').val());
FMS.currentUser.set('may_show_name', $('#form_may_show_name').is(':checked'));
- if ( this.model.get('titles_list') && this.model.get('titles_list').length > 0 ) {
- FMS.currentUser.set('title', $('#form_title').val());
- }
+ this.setUserTitle();
FMS.currentUser.save();
} else {
// if this is set then we are registering a password
@@ -408,14 +418,43 @@
onClickContinue: function(e) {
e.preventDefault();
- if ( this.validate() ) {
- $('#continue').focus();
- if ( ! this.model.get('submit_clicked') ) {
- this.model.set('submit_clicked', 'submit_sign_in');
+ if (this.validate()) {
+ // The password may be long enough, but is it going to be
+ // accepted by the server? Check before proceeding.
+ if (CONFIG.PASSWORD_CHECK_COMMON) {
+ var that = this;
+ $.post(
+ CONFIG.FMS_URL + "/auth/common_password",
+ { password_register: $('#form_password').val() },
+ null,
+ 'json'
+ )
+ .done(function(result) {
+ if (result === true) {
+ that.savePasswordAndContinue();
+ } else {
+ that.validationError('form_password', result);
+ }
+ })
+ .fail(function() {
+ // If this failed for whatever reason (e.g. network
+ // error etc), don't worry about it as it'll be
+ // resubmitted with the report.
+ that.savePasswordAndContinue();
+ });
+ } else {
+ this.savePasswordAndContinue();
}
- FMS.currentUser.set('password', $('#form_password').val());
- this.navigate( this.next );
}
+ },
+
+ savePasswordAndContinue: function() {
+ $('#continue').focus();
+ if ( ! this.model.get('submit_clicked') ) {
+ this.model.set('submit_clicked', 'submit_sign_in');
+ }
+ FMS.currentUser.set('password', $('#form_password').val());
+ this.navigate( this.next );
}
})
});
@@ -462,18 +501,33 @@
this.model.set('submit_clicked', 'submit_register');
FMS.currentUser.set('name', $('#form_name').val());
FMS.currentUser.set('may_show_name', $('#form_may_show_name').is(':checked'));
+ this.setUserTitle();
},
onReportError: function(model, err, options) {
// TODO: this is a temporary measure which should be replaced by a more
// sensible login mechanism
if ( err.check_name ) {
- this.onClickSubmit();
+ this.doSubmit();
} else {
if ( err.errors && err.errors.password ) {
this.validationError('form_password', err.errors.password );
}
}
+ },
+
+ afterRender: function() {
+ console.log("SubmitConfirmView.afterRender");
+ if (CONFIG.SKIP_CONFIRM_REPORT) {
+ var that = this;
+ setTimeout(function() {
+ // This needs to be in a setTimeout call otherwise
+ // the app gets stuck on an empty "Your Details" page.
+ // This is something to do with the way Backbone routes
+ // between views, I believe.
+ that.doSubmit();
+ }, 10);
+ }
}
})
});