svc_hardware MAC address input format, #16266
authorMark Wells <mark@freeside.biz>
Wed, 14 Mar 2012 20:44:00 +0000 (13:44 -0700)
committerMark Wells <mark@freeside.biz>
Wed, 14 Mar 2012 20:44:00 +0000 (13:44 -0700)
FS/FS/svc_hardware.pm
httemplate/docs/credits.html
httemplate/docs/license.html
httemplate/edit/svc_broadband.cgi
httemplate/edit/svc_hardware.cgi
httemplate/elements/masked_input_1.1.js [new file with mode: 0644]
httemplate/elements/tr-input-mac_addr.html [new file with mode: 0644]
httemplate/elements/tr-input-mask.html [new file with mode: 0644]
httemplate/view/svc_broadband.cgi
httemplate/view/svc_hardware.cgi

index b4eb8cc..22e6275 100644 (file)
@@ -164,7 +164,7 @@ sub check {
   if ( $conf->exists('svc_hardware-check_mac_addr') ) {
     $hw_addr = uc($hw_addr);
     $hw_addr =~ /^[0-9A-F]{12}$/ 
-      or return "Illegal (MAC address) ".$self->getfield('hw_addr');
+      or return "Illegal (MAC address) '".$self->getfield('hw_addr')."'";
   }
   $self->setfield('hw_addr', $hw_addr);
 
index 7b3b0b9..9bb1dec 100644 (file)
@@ -61,6 +61,7 @@ Joe Camadine<BR>
 Chris Cappuccio<BR>
 Rebecca Cardennis<BR>
 Shane Chrisp<BR>
+Kendall Conrad<BR>
 Luke Crawford<BR>
 Brad Dameron<BR>
 Dave Denney<BR>
index e0e40b7..fab8cd0 100644 (file)
@@ -112,6 +112,11 @@ terms as Perl (GPL/Artistic).
 Contains code derived from HTML::GoogleMapsV3 by David Peters, licensed under
 the same terms as Perl (GPL/Artistic).
 
+<P>
+Contains the Masked Input JavaScript library by Kendall Conrad, licensed under
+a <a href="http://creativecommons.org/licenses/by-sa/3.0/us/">Creative Commons 
+Attribution-ShareAlike 3.0 United States</a> license.
+
 <!-- artwork -->
 
 <P>
index b07c725..b266928 100644 (file)
@@ -103,7 +103,8 @@ my @fields = (
   qw( description speed_down speed_up ),
   { field=>'sectornum', type=>'select-tower_sector', },
   { field=>'routernum', type=>'select-router_block_ip' },
-    qw( mac_addr latitude longitude altitude vlan_profile 
+  { field=>'mac_addr' , type=>'input-mac_addr' },
+    qw( latitude longitude altitude vlan_profile 
     performance_profile authkey plan_id )
 );
 
index dcf83de..d9cd4cd 100644 (file)
@@ -26,7 +26,8 @@ my @fields = (
   },
   {
     field => 'hw_addr',
-    type  => 'text',
+    type  => $conf->exists('svc_hardware-check_mac_addr') ? 
+              'input-mac_addr' : 'text',
     label => 'Hardware address',
   },
   {
diff --git a/httemplate/elements/masked_input_1.1.js b/httemplate/elements/masked_input_1.1.js
new file mode 100644 (file)
index 0000000..05efa77
--- /dev/null
@@ -0,0 +1,195 @@
+/***********************************************************************
+                       Masked Input version 1.1
+************************************************************************
+Author: Kendall Conrad
+Home page: http://www.angelwatt.com/coding/masked_input.php
+Created:  2008-12-16
+Modified: 2010-04-14
+Description:
+License: This work is licensed under a Creative Commons Attribution-Share Alike
+  3.0 United States License http://creativecommons.org/licenses/by-sa/3.0/us/
+
+Argument pieces:
+- elm:        [req] text input node to apply the mask on
+- format:     [req] string format for the mask
+- allowed:    [opt, '0123456789'] string with chars allowed to be typed
+- sep:        [opt, '\/:-'] string of char(s) used as separators in mask
+- typeon:     [opt, '_YMDhms'] string of chars in mask that can be typed on
+- onbadkey:   [opt, null] function to run when user types a unallowed key
+- badkeywait: [opt, 0] used with onbadkey. Indicates how long (in ms) to lock
+  text input for onbadkey function to run
+***********************************************************************/
+function MaskedInput(args)
+{
+  if (args['elm'] === null || args['format'] === null) { return false; }
+  var el     = args['elm'],
+    format   = args['format'],
+    allowed  = args['allowed']    || '0123456789',
+    sep      = args['separator']  || '\/:-',
+    open     = args['typeon']     || '_YMDhms',
+    onbadkey = args['onbadkey']   || function(){},
+    badwait  = args['badkeywait'] || 0;
+  
+  var locked = false, hold = 0;
+  el.value = format;
+  // Assign events
+  el.onkeydown  = KeyHandlerDown;  //
+  el.onkeypress = KeyHandlerPress; // add event handlers to element
+  el.onkeyup    = KeyHandlerUp;    //
+
+  function GetKey(code)
+  {
+    code = code || window.event, ch = '';
+    var keyCode = code.which, evt = code.type;
+    if (keyCode == null) { keyCode = code.keyCode; }
+    if (keyCode === null) { return ''; } // no key, no play
+    // deal with special keys
+    switch (keyCode) {
+    case 8:  ch = 'bksp'; break;
+    case 46: // handle del and . both being 46
+      ch = (evt == 'keydown') ? 'del' : '.'; break;
+    case 16: ch = 'shift'; break;//shift
+    case 0:/*CRAP*/ case 9:/*TAB*/ case 13:/*ENTER*/
+      ch = 'etc'; break;
+    case 37: case 38: case 39: case 40: // arrow keys
+      ch = (!code.shiftKey &&
+           (code.charCode != 39 && code.charCode !== undefined)) ?
+        'etc' : String.fromCharCode(keyCode);
+      break;
+    // default to thinking it's a character or digit
+    default: ch = String.fromCharCode(keyCode);
+    }
+    return ch;
+  }
+  function KeyHandlerDown(e)
+  {
+    e = e || event;
+    if (locked) { return false; }
+    var key = GetKey(e);
+    if (el.value == '') { el.value = format; SetTextCursor(el,0); }
+    // Only do update for bksp del
+    if (key == 'bksp' || key == 'del') { Update(key); return false; }
+    else if (key == 'etc' || key == 'shift') { return true; }
+    else { return true; }    
+  }
+  function KeyHandlerPress(e)
+  {
+    e = e || event;
+    if (locked) { return false; }
+    var key = GetKey(e);
+    // Check if modifier key is being pressed; command
+    if (key=='etc' || e.metaKey || e.ctrlKey || e.altKey) { return true; }
+    if (key != 'bksp' && key != 'del' && key != 'etc' && key != 'shift') {
+      if (!GoodOnes(key)) { return false; }
+      return Update(key);
+    }
+    else { return false; }
+  }
+  function KeyHandlerUp(e) { hold = 0; }
+  function Update(key)
+  {
+    var p = GetTextCursor(el), c = el.value, val = '';
+    // Handle keys now
+    switch (true) {
+    case (allowed.indexOf(key) != -1):
+      if (++p > format.length) { return false; } // if text csor at end
+      // Handle cases where user places csor before separator
+      while (sep.indexOf(c.charAt(p-1)) != -1 && p <= format.length) { p++; }
+      val = c.substr(0, p-1) + key + c.substr(p);
+      // Move csor up a spot if next char is a separator char
+      if (allowed.indexOf(c.charAt(p)) == -1
+          && open.indexOf(c.charAt(p)) == -1) { p++; }
+      break;
+    case (key=='bksp'): // backspace
+      if (--p < 0) return false; // at start of field
+      // If previous char is a separator, move a little more
+      while (allowed.indexOf(c.charAt(p)) == -1
+             && open.indexOf(c.charAt(p)) == -1
+             && p > 1) { p--; }
+      val = c.substr(0, p) + format.substr(p,1) + c.substr(p+1);
+      break;
+    case (key=='del'): // forward delete
+      if (p >= c.length) { return false; } // at end of field
+      // If next char is a separator and not the end of the text field
+      while (sep.indexOf(c.charAt(p)) != -1
+             && c.charAt(p) != '') { p++; }
+      val = c.substr(0, p) + format.substr(p,1) + c.substr(p+1);
+      p++; // Move position forward
+      break;
+    case (key=='etc'): return true; // Catch other allowed chars
+    default: return false;   // Ignore the rest
+    }
+    el.value = '';        // blank it first (Firefox issue)
+    el.value = val;       // put updated value back in
+    SetTextCursor(el, p); // Set the text cursor
+    return false;
+  }
+  function GetTextCursor(node)
+  {
+    try {
+      if (node.selectionStart >= 0) { return node.selectionStart; }
+      else if (document.selection) {// IE
+        var ntxt = node.value; // getting starting text
+        var rng = document.selection.createRange();
+        rng.text = '|%|';
+        var start = node.value.indexOf('|%|');
+        rng.moveStart('character', -3);
+        rng.text = '';
+        // put starting text back in,
+        // fixes issue if all text was highlighted
+        node.value = ntxt;
+        return start;
+      } return -1;
+    } catch(e) { return false; }
+  }
+  function SetTextCursor(node, pos)
+  {
+    try {
+      if (node.selectionStart) {
+        node.focus();
+        node.setSelectionRange(pos,pos);
+      }
+      else if (node.createTextRange) { // IE
+        var rng = node.createTextRange();
+        rng.move('character', pos);
+        rng.select();
+      }
+    } catch(e) { return false; }
+  }
+  function GoodOnes(k)
+  {
+    if (allowed.indexOf(k) == -1 && k!='bksp' && k!='del' && k!='etc') {
+      var p = GetTextCursor(el); // Need to ensure cursor position not lost
+      locked = true; onbadkey();
+      // Hold lock long enough for onbadkey function to run
+      setTimeout(function(){locked=false; SetTextCursor(el,p);}, badwait);
+      return false;
+    } return true;
+  }
+  function resetField() {
+    el.value = format;
+  }
+  function setAllowed(a) {
+    allowed = a;
+    resetField();
+  }
+  function setFormat(f) {
+    format = f;
+    resetField();
+  }
+  function setSeparator(s) {
+    sep = s;
+    resetField();
+  }
+  function setTypeon(t) {
+    open = t;
+    resetField();
+  }
+  return {
+    resetField:resetField,
+    setAllowed:setAllowed,
+    setFormat:setFormat,
+    setSeparator:setSeparator,
+    setTypeon:setTypeon
+  }
+}
diff --git a/httemplate/elements/tr-input-mac_addr.html b/httemplate/elements/tr-input-mac_addr.html
new file mode 100644 (file)
index 0000000..d768d4e
--- /dev/null
@@ -0,0 +1,11 @@
+<& /elements/tr-input-mask.html, 
+  format => '__:__:__:__:__:__',
+  allowed => '0123456789ABCDEFabcdef',
+  %opt,
+&>
+<%init>
+my %opt = @_;
+my $value = length($opt{curr_value}) ? $opt{curr_value} : $opt{value};
+$value =~ s/\W//g;
+$opt{curr_value} = join(':', $value =~ /../g);
+</%init>
diff --git a/httemplate/elements/tr-input-mask.html b/httemplate/elements/tr-input-mask.html
new file mode 100644 (file)
index 0000000..33725b9
--- /dev/null
@@ -0,0 +1,41 @@
+% if ( !$init ) {
+<script type="text/javascript" src="<%$p%>elements/masked_input_1.1.js">
+</script>
+% $init++;
+% }
+<& /elements/tr-input-text.html, id => $id, @_ &>
+<script type="text/javascript">
+MaskedInput({
+  elm: document.getElementById('<%$id%>'),
+  format: '<% $opt{format} %>',
+  <% $opt{allowed} ? "allowed: '$opt{allowed}'," : '' %>
+  <% $opt{typeon}  ? "typeon:  '$opt{typeon}',"  : '' %>
+});
+document.getElementById('<%$id%>').value = <% $value |js_string %>;
+</script>
+<%shared>
+my $init = 0;
+</%shared>
+<%init>
+my %opt = @_;
+# must have a DOM id
+my $id = $opt{id} || sprintf('input%04d',int(rand(10000)));
+my $value = length($opt{curr_value}) ? $opt{curr_value} : $opt{value} || '';
+</%init>
+<%doc>
+Set up a text input field with input masking.
+
+<& /elements/tr-input-mask.html,
+  format    => '____-__-__',
+  #typeon   => '_YMDhms',    # which characters in the format represent blanks
+  #allowed  => '0123456789', # characters allowed in the blanks
+  ... all other options as for tr-input-text.html
+&>
+
+Note that the value sent on form submission will contain the mask 
+separators, and if value/curr_value is passed, it should also be 
+formatted to fit the mask.
+
+Uses masked_input_1.1.js by Kendall Conrad, available under a Creative Commons
+Attribution-ShareAlike license.
+</%doc>
index af2c575..131582f 100644 (file)
@@ -31,7 +31,7 @@ my @fields = (
   'speed_up',
   { field => 'ip_addr', value => \&ip_addr },
   { field => 'sectornum', value => \&sectornum },
-  'mac_addr',
+  { field => 'mac_addr', value => \&mac_addr },
   #'latitude',
   #'longitude',
   { field => 'coordinates', value => \&coordinates },
@@ -66,6 +66,11 @@ sub ip_addr {
   $out;
 }
 
+sub mac_addr {
+  my $svc = shift;
+  join(':', $svc->mac_addr =~ /../g);
+}
+
 sub usergroup {
   my $svc = shift;
   my $usergroup = $svc->usergroup;
index 1d88235..725358c 100644 (file)
@@ -6,6 +6,7 @@
 %>
 <%init>
 
+my $conf = new FS::Conf;
 my $fields = FS::svc_hardware->table_info->{'fields'};
 my %labels = map { $_ =>  ( ref($fields->{$_})
                              ? $fields->{$_}{'label'}
@@ -24,5 +25,22 @@ my $note =   { field => 'note',
                type  => 'text',
                value => sub { encode_entities($_[0]->note) }
              };
-my @fields = ($model, qw( serial hw_addr ip_addr smartcard ), $status, $note );
+my $hw_addr ={ field => 'hw_addr',
+               type  => 'text',
+               value => sub { 
+                my $hw_addr = $_[0]->hw_addr;
+                $conf->exists('svc_hardware-check_mac_addr') ?
+                  join(':', $hw_addr =~ /../g) : $hw_addr
+                },
+              };
+
+my @fields = (
+  $model,
+  'serial',
+  $hw_addr,
+  'ip_addr',
+  'smartcard',
+  $status,
+  $note,
+);
 </%init>