From b2787f77f842a16df069227e74a2da54b8b36efb Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 29 Oct 2015 13:49:45 -0700 Subject: [PATCH] address standardization UI, part 1 --- FS/FS/Misc/Geo.pm | 2 +- httemplate/docs/license.html | 2 + httemplate/edit/cust_main.cgi | 16 +++-- httemplate/elements/freeside.css | 8 +++ httemplate/elements/jquery.deserialize.min.js | 8 +++ httemplate/elements/location.html | 86 +++++++++++++++++++++++++++ httemplate/elements/polyfill.js | 30 ++++++++++ httemplate/misc/address_standardize.cgi | 51 ++++++++++++++++ 8 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 httemplate/elements/jquery.deserialize.min.js create mode 100644 httemplate/elements/polyfill.js create mode 100644 httemplate/misc/address_standardize.cgi diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index 1aa593974..293748c7a 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -342,7 +342,7 @@ sub standardize_uscensus { die "Geocoding did not find a matching address.\n"; } else { warn Dumper($result) if $DEBUG; - die $result->error_message; + die $result->error_message."\n"; } } diff --git a/httemplate/docs/license.html b/httemplate/docs/license.html index 7e5bb1e3e..71643fcb3 100644 --- a/httemplate/docs/license.html +++ b/httemplate/docs/license.html @@ -130,6 +130,8 @@ and other contributors, licensed under the terms of the MIT license. Contains the Spectrum No Hassle jQuery Colorpicker by Brian Grinstead, licensed under the terms of the MIT license. +

+Contains jQuery.deserialize by Kyle Florence, licensed under the terms of the MIT license. diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi index effe84b96..25932019e 100755 --- a/httemplate/edit/cust_main.cgi +++ b/httemplate/edit/cust_main.cgi @@ -43,6 +43,7 @@ %#; padding-right:2px; vertical-align:top"> <% mt('Billing address') |h %> +

<& cust_main/before_bill_location.html, $cust_main &> <& /elements/location.html, @@ -54,6 +55,7 @@ &> <& cust_main/after_bill_location.html, $cust_main &>
+
@@ -68,7 +70,7 @@ VALUE="Y" <% $has_ship_address ? '' : 'CHECKED' %> ><% mt('same as billing address') |h %> -
+
<& cust_main/before_ship_location.html, $cust_main &> <& /elements/location.html, @@ -91,7 +93,7 @@ % }
-
+ @@ -99,16 +101,20 @@ function samechanged(what) { if ( what.checked ) { - $('#div_ship_location').slideUp(); + $('#ship_location').slideUp(); } else { - $('#div_ship_location').slideDown(); + $('#ship_location').slideDown(); } } % if ( ! $has_ship_address ) { - $('#div_ship_location').hide(); + $('#ship_location').hide(); % } +$().ready( function() { + window.bill_location = new Location($('fieldset#bill_location')); +}); + <& cust_main/contacts_new.html, 'cust_main'=>$cust_main, &> diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css index dbd27cbaa..5eb8f720d 100644 --- a/httemplate/elements/freeside.css +++ b/httemplate/elements/freeside.css @@ -335,3 +335,11 @@ div.package-marker-change_from { border-left: solid #bbffbb 30px; display: inline-block; } + +/* elements/location.html and co. */ +fieldset.location { + padding: 0px; + margin: 0px; + border: none; +} + diff --git a/httemplate/elements/jquery.deserialize.min.js b/httemplate/elements/jquery.deserialize.min.js new file mode 100644 index 000000000..7054ea4bb --- /dev/null +++ b/httemplate/elements/jquery.deserialize.min.js @@ -0,0 +1,8 @@ +/** + * @author Kyle Florence + * @website https://github.com/kflorence/jquery-deserialize/ + * @version 1.2.1 + * + * Dual licensed under the MIT and GPLv2 licenses. + */ +(function(i,b){var f=Array.prototype.push,a=/^(?:radio|checkbox)$/i,e=/\+/g,d=/^(?:option|select-one|select-multiple)$/i,g=/^(?:button|color|date|datetime|datetime-local|email|hidden|month|number|password|range|reset|search|submit|tel|text|textarea|time|url|week)$/i;function c(j){return j.map(function(){return this.elements?i.makeArray(this.elements):this}).filter(":input").get()}function h(j){var k,l={};i.each(j,function(n,m){k=l[m.name];l[m.name]=k===b?m:(i.isArray(k)?k.concat(m):[k,m])});return l}i.fn.deserialize=function(A,l){var y,n,q=c(this),t=[];if(!A||!q.length){return this}if(i.isArray(A)){t=A}else{if(i.isPlainObject(A)){var B,w;for(B in A){i.isArray(w=A[B])?f.apply(t,i.map(w,function(j){return{name:B,value:j}})):f.call(t,{name:B,value:w})}}else{if(typeof A==="string"){var v;A=A.split("&");for(y=0,n=A.length;y + + + % if ( $opt{'alt_format'} ) { @@ -324,6 +327,89 @@ Example: } + +function Location(fieldset) { + if ( typeof fieldset == 'String' ) { + fieldset = $('#' + fieldset); + } + this.fieldset = $(fieldset); + var errorbox = document.createElement('DIV'); + errorbox.className = 'error'; + fieldset.append(errorbox); // after the + $(errorbox).position({ + my: 'left', + at: 'right+20px', + of: fieldset + }); + this.errorbox = $(errorbox); // so we can find it + + var img_tick = $(''); + var img_wait = $(''); + + // get/set the serialized (URL parameter string) contents of the form fields + this.value = function(newvalue) { + if (newvalue) { + try { + this.fieldset.deserialize(newvalue); + this.errorbox.empty(); + if ( newvalue['error'] ) { + this.errorbox.text(newvalue['error']); + } else { + this.errorbox.append(img_tick); + } + } catch(err) { + console.log("Couldn't parse returned data:\n" + newvalue); + // show an error also + } + } + return this.fieldset.serialize(); + }; + + // send a standardization request and do something with the result + this.standardize = function(callback) { + this.errorbox.empty(); + this.errorbox.append(img_wait); + $.ajax({ + type: 'POST', + url: '<% $fsurl %>misc/address_standardize.cgi', + success: callback, + data: this.value() + }); + }; + + // check if required fields are filled, and if so, standardize + var standardize_if_ready = function() { + var loc = this; + var ready = true; + var required_fields = this.fieldset.find(':data(required)'); + for ( var i = 0; ready && i < required_fields.length; i++ ) { + if ( required_fields[i].prop('value').length == 0 ) { + ready = false; + } + } + + if ( ready ) { + // pass the "value" method, prebound to the location object + this.standardize( this.value.bind(loc) ); + } + }; + + // event handler; the Location object is passed in event.data + var location_change_timer; + var location_changed = function( ev ) { + if ( location_change_timer ) { + window.clearTimeout(location_change_timer); + } + location_change_timer = window.setTimeout( + standardize_if_ready.bind(ev.data), + 2000 + ); + }; + + fieldset.find('input').on('change', this, location_changed); + fieldset.find('select').on('change', this, location_changed); +} + <%init> diff --git a/httemplate/elements/polyfill.js b/httemplate/elements/polyfill.js new file mode 100644 index 000000000..5e08a9933 --- /dev/null +++ b/httemplate/elements/polyfill.js @@ -0,0 +1,30 @@ +// Function.bind(), not supported in IE8 +// polyfill from Mozilla Developer Network + +if (!Function.prototype.bind) { + Function.prototype.bind = function(oThis) { + if (typeof this !== 'function') { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function() {}, + fBound = function() { + return fToBind.apply(this instanceof fNOP + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + if (this.prototype) { + // native functions don't have a prototype + fNOP.prototype = this.prototype; + } + fBound.prototype = new fNOP(); + + return fBound; + }; +} diff --git a/httemplate/misc/address_standardize.cgi b/httemplate/misc/address_standardize.cgi new file mode 100644 index 000000000..d9ba55097 --- /dev/null +++ b/httemplate/misc/address_standardize.cgi @@ -0,0 +1,51 @@ +<% encode_json($return) %>\ +<%init> + +local $SIG{__DIE__}; #disable Mason error trap + +my $DEBUG = 0; + +my $conf = new FS::Conf; + +# figure out the prefix +my $pre; +foreach my $name ($cgi->param) { + if ($name =~ /^(\w*)address1$/) { + $pre = $1; + last; + } +} +die "no address1 field in location" if !defined($pre); + +# gather relevant fields +my %old = ( map { $_ => scalar($cgi->param($pre . $_)) } + qw( company address1 address2 city state zip country ) +); + +my $cache = eval { FS::GeocodeCache->standardize(\%old) }; +$cache->set_coord; +# don't do set_censustract here, though censustract may be set by now + +# give the fields their prefixed names back +# except always name the error string 'error' +my $error = delete($cache->{'error'}) || ''; +my %new = ( + 'changed' => 0, + 'error' => $error, + map { $pre.$_, $cache->get($_) } keys %$cache +); + +foreach ( qw(address1 address2 city state zip country) ) { + if ( $new{$pre.$_} ne $old{$pre.$_} ) { + $new{changed} = 1; + last; + } +} + +# refold this to make it acceptable to jquery +#my $return = [ map { { name => $_, value => $new{$_} } } keys %new ]; +my $return = \%new; +warn "result:\n".encode_json($return) if $DEBUG; + +$r->content_type('application/json'); + -- 2.11.0