diff options
author | Mark Wells <mark@freeside.biz> | 2015-10-29 13:49:45 -0700 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2015-10-29 13:49:45 -0700 |
commit | b2787f77f842a16df069227e74a2da54b8b36efb (patch) | |
tree | 5d857b719ed8209fc413cf65e673e2118f3feb81 | |
parent | 07b346c23e24d7fb368935b964c091421517dbc0 (diff) |
address standardization UI, part 1
-rw-r--r-- | FS/FS/Misc/Geo.pm | 2 | ||||
-rw-r--r-- | httemplate/docs/license.html | 2 | ||||
-rwxr-xr-x | httemplate/edit/cust_main.cgi | 16 | ||||
-rw-r--r-- | httemplate/elements/freeside.css | 8 | ||||
-rw-r--r-- | httemplate/elements/jquery.deserialize.min.js | 8 | ||||
-rw-r--r-- | httemplate/elements/location.html | 86 | ||||
-rw-r--r-- | httemplate/elements/polyfill.js | 30 | ||||
-rw-r--r-- | httemplate/misc/address_standardize.cgi | 51 |
8 files changed, 197 insertions, 6 deletions
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. +<P> +Contains <a href="https://github.com/kflorence/jquery-deserialize/">jQuery.deserialize</a> by Kyle Florence, licensed under the terms of the MIT license. <!-- artwork --> 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 @@ <TD> %#; padding-right:2px; vertical-align:top"> <FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT> + <FIELDSET ID="bill_location" CLASS="location"> <TABLE CLASS="fsinnerbox" WIDTH="100%"> <& cust_main/before_bill_location.html, $cust_main &> <& /elements/location.html, @@ -54,6 +55,7 @@ &> <& cust_main/after_bill_location.html, $cust_main &> </TABLE> + </FIELDSET> </TD> </TR> <TR><TD STYLE="height:14px"></TD></TR> @@ -68,7 +70,7 @@ VALUE="Y" <% $has_ship_address ? '' : 'CHECKED' %> ><% mt('same as billing address') |h %> - <DIV ID="div_ship_location"> + <FIELDSET ID="ship_location" CLASS="location"> <TABLE WIDTH="100%" CLASS="fsinnerbox"> <& cust_main/before_ship_location.html, $cust_main &> <& /elements/location.html, @@ -91,7 +93,7 @@ </TR> % } </TABLE> - </DIV> + </FIELDSET> </TD> </TR></TABLE> @@ -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')); +}); + </SCRIPT> <& 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 <kyle[dot]florence[at]gmail[dot]com> + * @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<n;y++){v=A[y].split("=");f.call(t,{name:decodeURIComponent(v[0]),value:decodeURIComponent(v[1].replace(e,"%20"))})}}}}if(!(n=t.length)){return this}var u,k,x,z,C,o,m,w,p=i.noop,s=i.noop,r={};l=l||{};q=h(q);if(i.isFunction(l)){s=l}else{p=i.isFunction(l.change)?l.change:p;s=i.isFunction(l.complete)?l.complete:s}for(y=0;y<n;y++){u=t[y];C=u.name;w=u.value;if(!(k=q[C])){continue}m=(z=k.length)?k[0]:k;m=(m.type||m.nodeName).toLowerCase();o=null;if(g.test(m)){if(z){x=r[C];k=k[r[C]=(x==b)?0:++x]}p.call(k,(k.value=w))}else{if(a.test(m)){o="checked"}else{if(d.test(m)){o="selected"}}}if(o){if(!z){k=[k];z=1}for(x=0;x<z;x++){u=k[x];if(u.value==w){p.call(u,(u[o]=true)&&w)}}}}s.call(this);return this}})(jQuery); diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html index b5f0a963c..d4c0f14d4 100644 --- a/httemplate/elements/location.html +++ b/httemplate/elements/location.html @@ -19,6 +19,9 @@ Example: </%doc> +<SCRIPT SRC="<% $fsurl %>elements/jquery.deserialize.min.js"></SCRIPT> +<SCRIPT SRC="<% $fsurl %>elements/polyfill.js"></SCRIPT> + % if ( $opt{'alt_format'} ) { <TR> @@ -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 <table> + $(errorbox).position({ + my: 'left', + at: 'right+20px', + of: fieldset + }); + this.errorbox = $(errorbox); // so we can find it + + var img_tick = $('<IMG SRC="http://localhost/freeside/images/tick.png">'); + var img_wait = $('<IMG SRC="http://localhost/freeside/images/wait-orange.gif">'); + + // 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); +} + </SCRIPT> <%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'); +</%init> |