diff options
Diffstat (limited to 'www/js')
-rw-r--r-- | www/js/app.js | 84 | ||||
-rw-r--r-- | www/js/config.js-example | 45 | ||||
-rw-r--r-- | www/js/files.js | 14 | ||||
-rw-r--r-- | www/js/map-OpenLayers.js | 10 | ||||
-rw-r--r-- | www/js/models/draft.js | 26 | ||||
-rw-r--r-- | www/js/models/report.js | 122 | ||||
-rw-r--r-- | www/js/router.js | 2 | ||||
-rw-r--r-- | www/js/strings.en.js | 5 | ||||
-rw-r--r-- | www/js/strings.es.js | 5 | ||||
-rw-r--r-- | www/js/views/around.js | 31 | ||||
-rw-r--r-- | www/js/views/details.js | 35 | ||||
-rw-r--r-- | www/js/views/details_extra.js | 8 | ||||
-rw-r--r-- | www/js/views/fms.js | 30 | ||||
-rw-r--r-- | www/js/views/home.js | 5 | ||||
-rw-r--r-- | www/js/views/login.js | 28 | ||||
-rw-r--r-- | www/js/views/offline.js | 93 | ||||
-rw-r--r-- | www/js/views/photo.js | 67 | ||||
-rw-r--r-- | www/js/views/submit.js | 110 |
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); + } } }) }); |