Support for integer values in Kalamar

Change-Id: I13994db8006fbfc213621ec46095a53600f22a0d
diff --git a/dev/js/spec/corpusByMatchSpec.js b/dev/js/spec/corpusByMatchSpec.js
index cdf941f..21e6560 100644
--- a/dev/js/spec/corpusByMatchSpec.js
+++ b/dev/js/spec/corpusByMatchSpec.js
@@ -100,9 +100,11 @@
       cbm.add("pubDate", "2018-11-20", "type:date");
       expect(cbm.toQuery()).toEqual('author = "Peter" & pubDate in 2018-11-20');
 
+      cbm.add("KED.nToks", 200, "type:integer");
+
       cbm.toVcBuilder();
 
-      expect(KorAP.vc.toQuery()).toEqual('(title = "Hello World!" | foo = "bar") & author = "Peter" & pubDate in 2018-11-20');
+      expect(KorAP.vc.toQuery()).toEqual('(title = "Hello World!" | foo = "bar") & author = "Peter" & pubDate in 2018-11-20 & KED.nToks = 200');
     })
   });
 });
diff --git a/dev/js/spec/vcSpec.js b/dev/js/spec/vcSpec.js
index bfdf9fa..656a827 100644
--- a/dev/js/spec/vcSpec.js
+++ b/dev/js/spec/vcSpec.js
@@ -12,6 +12,7 @@
   'vc/operators',
   'vc/rewrite',
   'vc/stringval',
+  'vc/intval',
   'vc/fragment'
 ], function (vcClass,
              docClass,
@@ -23,6 +24,7 @@
              operatorsClass,
              rewriteClass,
              stringValClass,
+             intValClass,
              fragmentClass) {
 
   KorAP._vcKeyMenu = undefined;
@@ -132,6 +134,14 @@
       "@type" : "koral:doc"
     });
 
+    var integerFactory = buildFactory(docClass, {
+      "key"   : "KED.nToks",
+      "type"  : "type:integer",
+      "match" : "match:eq",
+      "value" : "200",
+      "@type" : "koral:doc"
+    });
+
     // Create example factories
     var regexFactory = buildFactory(docClass, {
       "key"   : "title",
@@ -281,6 +291,15 @@
       expect(doc.matchop()).toEqual('eq');
     });
 
+    it('should deserialize JSON-LD integer', function () {
+      doc = integerFactory.create({});
+
+      expect(doc.matchop()).toEqual('eq');
+      expect(doc.key()).toEqual("KED.nToks");
+      expect(doc.type()).toEqual("integer");
+      expect(doc.value()).toEqual("200");
+    });
+
     it('should be serializale to JSON', function () {
 
       // Empty doc
@@ -381,6 +400,33 @@
         value : "2014"
       });
       expect(doc.toQuery()).toEqual('pubDate in 2014');
+
+      doc = integerFactory.create();
+      expect(doc.toQuery()).toEqual('KED.nToks = 200');
+
+      doc = integerFactory.create({
+        value : "100"
+      });
+      expect(doc.toQuery()).toEqual('KED.nToks = 100');
+
+      doc = integerFactory.create({
+        value : "100",
+        match : "match:geq"
+      });
+      expect(doc.toQuery()).toEqual('KED.nToks >= 100');
+
+      doc = integerFactory.create({
+        value : "100",
+        match : "match:leq"
+      });
+      expect(doc.toQuery()).toEqual('KED.nToks <= 100');
+
+      // Check for numeric values
+      doc = integerFactory.create({
+        value : 100,
+      });
+      expect(doc.toQuery()).toEqual('KED.nToks = 100');
+
     });
   });
 
@@ -571,7 +617,15 @@
                 "match": 'match:ne',
                 "value": '[a]?bar',
                 "type": 'type:regex'
+              },
+              {
+                "@type": 'koral:doc',
+                "key": 'KED.nToks',
+                "match": 'match:leq',
+                "value": '300',
+                "type": 'type:integer'
               }
+
             ]
           }
         ]
@@ -579,7 +633,8 @@
       expect(docGroup.toQuery()).toEqual(
         'author = "Max Birkendale" | ' +
           '(pubDate since 2014-05-12 & ' +
-          'pubDate until 2014-12-05 & foo != /[a]?bar/)'
+          'pubDate until 2014-12-05 & foo != /[a]?bar/ & ' +
+          'KED.nToks <= 300)'
       );
 
 
@@ -590,7 +645,8 @@
       expect(docGroup.incomplete()).toBeFalsy();
       expect(docGroup.toQuery()).toEqual(
         '(pubDate since 2014-05-12 & ' +
-          'pubDate until 2014-12-05 & foo != /[a]?bar/)'
+          'pubDate until 2014-12-05 & foo != /[a]?bar/ & ' +
+          'KED.nToks <= 300)'
       );
     });
   });
@@ -2820,6 +2876,7 @@
       var sv = stringValClass.create('der');
       expect(sv.element().nodeName).toBe('DIV');
       expect(sv.element().firstChild.nodeName).toBe('INPUT');
+      expect(sv.element().firstChild.getAttribute('type')).toBeNull();
       expect(sv.element().firstChild.value).toBe('der');
     });
 
@@ -2862,6 +2919,50 @@
 
   });
 
+
+  describe('KorAP.VC.intValue', function () {
+    it('should be initializable', function () {
+      var iv = intValClass.create();
+      expect(iv.value()).toBe(0);
+
+      iv = intValClass.create('400');
+      expect(iv.value()).toBe(400);
+
+      iv = intValClass.create(400);
+      expect(iv.value()).toBe(400);
+
+      iv = intValClass.create('Baum');
+      expect(iv.value()).toBe(0);
+    });
+
+    it('should be modifiable', function () {
+      var iv = intValClass.create();
+      expect(iv.value()).toBe(0);
+
+      expect(iv.value('33')).toBe(33);
+      expect(iv.value()).toBe(33);
+    });
+
+    it('should have an element', function () {
+      var iv = intValClass.create(22);
+      expect(iv.element().nodeName).toBe('DIV');
+      expect(iv.element().firstChild.nodeName).toBe('INPUT');
+      expect(iv.element().firstChild.getAttribute('type')).toBe('number');
+      expect(iv.element().firstChild.value).toBe('22');
+    });
+
+    it('should be storable', function () {
+      var iv = intValClass.create();
+      var count = 1;
+      iv.store = function (value) {
+        expect(value).toBe(80);
+      };
+      iv.value('80');
+      iv.element().lastChild.click();
+    });
+  });
+
+  
   describe('KorAP.VC.Menu', function () {
 
     var vc;
@@ -2889,7 +2990,8 @@
       vc = vcClass.create([
         ['d', 'text'],
         ['e', 'string'],
-        ['f', 'date']
+        ['f', 'date'],
+        ['g', 'integer']
       ]).fromJson();
       expect(vc.builder().firstChild.classList.contains('unspecified')).toBeTruthy();
       expect(vc.builder().firstChild.firstChild.tagName).toEqual('SPAN');
@@ -2903,6 +3005,7 @@
       expect(list.getElementsByTagName("LI")[1].innerText).toEqual('d');
       expect(list.getElementsByTagName("LI")[2].innerText).toEqual('e');
       expect(list.getElementsByTagName("LI")[3].innerText).toEqual('f');
+      expect(list.getElementsByTagName("LI")[4].innerText).toEqual('g');
       // blur
       document.body.click();
     });
@@ -3022,6 +3125,33 @@
       document.body.click();
     });
 
+    it('should be clickable on operation for integer', function () {
+
+      vc.builder().firstChild.firstChild.click();// Choose "g"
+      vc.builder().firstChild.firstChild.getElementsByTagName("LI")[4].click()
+      // Click on "g" (or unspecified)
+      vc.builder().firstChild.firstChild.click();
+      // Rechoose "g"
+      vc.builder().firstChild.firstChild.getElementsByTagName("LI")[4].click();
+      // Click on matchop
+      vc.builder().firstChild.children[1].click();
+      // Choose "geq"
+      vc.builder().firstChild.children[1].getElementsByTagName('li')[2].click();
+      expect(vc.builder().firstChild.children[1].innerText).toEqual("geq");
+
+      // Click on "e"
+      vc.builder().firstChild.firstChild.click();
+      // Choose "f"
+      vc.builder().firstChild.firstChild.getElementsByTagName("LI")[3].click();
+
+      // The matchoperator should still be "geq" as this is valid for dates as well (now)
+      var fc = vc.builder().firstChild;
+      expect(fc.firstChild.tagName).toEqual('SPAN');
+      expect(fc.firstChild.innerText).toEqual('f');
+      expect(fc.children[1].innerText).toEqual('geq');
+      // blur
+      document.body.click();
+    });
 
     // Check json deserialization
     it('should be initializable', function () {
diff --git a/dev/js/src/match/corpusByMatch.js b/dev/js/src/match/corpusByMatch.js
index cc2ae2f..4b8ec73 100644
--- a/dev/js/src/match/corpusByMatch.js
+++ b/dev/js/src/match/corpusByMatch.js
@@ -99,7 +99,7 @@
       if (target.tagName === 'DD') {
         type = target.getAttribute("data-type");
         key  = target.previousElementSibling.innerText;
-        value = target.innerText;
+        value = (type == "type:integer" ? Number(target.innerText) : target.innerText);
       }
 
       // Meta information is in a list
diff --git a/dev/js/src/vc.js b/dev/js/src/vc.js
index 2c068ee..9d91a62 100644
--- a/dev/js/src/vc.js
+++ b/dev/js/src/vc.js
@@ -74,6 +74,7 @@
   KorAP._validUnspecMatchRE = new RegExp(
     "^(?:eq|ne|contains(?:not)?|excludes)$");
   KorAP._validStringMatchRE = new RegExp("^(?:eq|ne)$");
+  KorAP._validIntegerMatchRE = new RegExp("^(?:[gl]?eq|ne)$");
   KorAP._validTextMatchRE = KorAP._validUnspecMatchRE;
   KorAP._validTextOnlyMatchRE = new RegExp(
     "^(?:contains(?:not)?|excludes)$");
@@ -111,6 +112,12 @@
     'regex' : menuClass.create([
       [ 'eq', null ],
       [ 'ne', null ]
+    ]),
+    'integer' : menuClass.create([
+      [ 'eq', null ],
+      [ 'ne', null ],
+      [ 'geq', null ],
+      [ 'leq', null ]
     ])
   };
 
diff --git a/dev/js/src/vc/doc.js b/dev/js/src/vc/doc.js
index 37b0e0b..d504642 100644
--- a/dev/js/src/vc/doc.js
+++ b/dev/js/src/vc/doc.js
@@ -7,9 +7,10 @@
   'vc/jsonld',
   'vc/rewritelist',
   'vc/stringval',
+  'vc/intval',
   'vc/docgroupref',
   'util'
-], function (jsonldClass, rewriteListClass, stringValClass, docGroupRefClass) {
+], function (jsonldClass, rewriteListClass, stringValClass, intValClass, docGroupRefClass) {
 
   /*
    * TODO:
@@ -219,8 +220,14 @@
         return;
       };
 
-      if (json["value"] === undefined ||
-          typeof json["value"] != 'string') {
+      if (json["value"] === undefined
+          ||
+          (typeof json["value"] != 'string'
+
+           && !(json["type"] != undefined &&
+                json["type"] == "type:integer" &&
+                typeof json["value"] == 'number')
+          )) {
         KorAP.log(805, "Value is invalid");
         return;
       };
@@ -305,6 +312,24 @@
           t.value(json["value"]);
         }
 
+        // Key is integer
+        else if (json["type"] == "type:integer") {
+          t.type("integer");
+
+          // Check match type
+          if (!KorAP._validIntegerMatchRE.test(t.matchop())) {
+            KorAP.log(802, errstr802);
+
+            // Rewrite method
+            t.matchop('eq');
+            rewrite = 'modification';
+          };
+
+          // Set string value
+          t.value(json["value"]);
+        }
+
+        
         // Key is a date
         else if (json["type"] === "type:date") {
           t.type("date");
@@ -450,6 +475,8 @@
             ||
             (t._type === 'text' && KorAP._validTextMatchRE.test(m))
             ||
+            (t._type === 'integer' && KorAP._validIntegerMatchRE.test(m))
+            ||
             (t._type === 'date' && KorAP._validDateMatchRE.test(m))
         ) {
           t._matchop = m;
@@ -557,32 +584,49 @@
 
         dp.input().focus();
       }
-
+   
       else {
-        const regex = this.type() === 'regex' ? true : false;
-        const str = stringValClass.create(this.value(), regex);
-        const strElem = str.element();
 
-        str.store = function (value, regex) {
-          that.value(value);
-          if (regex === true)
-            that.type('regex');
-          else
-            that.type('string');
+        let vcVal;
+        
+        if (this.type() === 'integer') {
+          vcVal = intValClass.create(this.value());
+
+          vcVal.store = function (value) {
+            that.value(value);
+            that.type('integer');
+            that._el.removeChild(
+              this._el
+            );
+            that.update();
+          };
+        }
+
+        else {
+          const regex = this.type() === 'regex' ? true : false;
+          vcVal = stringValClass.create(this.value(), regex);
+
+          vcVal.store = function (value, regex) {
+            that.value(value);
+            if (regex === true)
+              that.type('regex');
+            else
+              that.type('string');
           
-          that._el.removeChild(
-            this._el
-          );
-          that.update();
+            that._el.removeChild(
+              this._el
+            );
+            that.update();
+          };
         };
 
         // Insert element
         this._el.insertBefore(
-          strElem,
+          vcVal.element(),
           this._valueE
         );
 
-        str.focus();
+        vcVal.focus();
       };
     },
 
@@ -666,10 +710,10 @@
         string += '!~';
         break;
       case "geq":
-        string += 'since';
+        string += (this.type() == 'date') ? 'since' : '>=';
         break;
       case "leq":
-        string += 'until';
+        string += (this.type() == 'date') ? 'until' : '<=';
         break;
       default:
         string += (this.type() == 'date') ? 'in' : '=';
@@ -681,6 +725,7 @@
       // Add value
       switch (this.type()) {
       case "date":
+      case "integer":
         return string + this.value();
       case "regex":
         return string + '/' + this.value().escapeRegex() + '/';
diff --git a/dev/js/src/vc/fragment.js b/dev/js/src/vc/fragment.js
index 72bd80e..e5b03a0 100644
--- a/dev/js/src/vc/fragment.js
+++ b/dev/js/src/vc/fragment.js
@@ -115,7 +115,7 @@
           doc.key(item[0]);
           doc.matchop("eq");
           doc.value(item[1]);
-          doc.type(item[2] === "date" ? "date" : "string");
+          doc.type(item[2] === "date" ? "date" : (item[2] === "integer" ? "integer" : "string"));
           return doc;
         }
       );
diff --git a/dev/js/src/vc/intval.js b/dev/js/src/vc/intval.js
new file mode 100644
index 0000000..55a5829
--- /dev/null
+++ b/dev/js/src/vc/intval.js
@@ -0,0 +1,144 @@
+/**
+ * Add integer values to the virtual corpus
+ */
+"use strict";
+
+define(['util'], function () {
+
+  return {
+    /**
+     * Create new integer value helper.
+     */
+    create : function () {
+      const a = arguments;
+      let value = 0;
+
+      // Set value
+      if (a.length >= 1) {
+        if (a[0] !== undefined)
+          value = a[0];
+      };
+
+      return Object.create(this)._init(value);
+    },
+    
+
+    // Initialize the integer value
+    _init : function (value) {
+      this.value(value);
+      return this;
+    },
+
+    /**
+     * Get or set the integer value.
+     */
+    value : function (val) {
+      if (arguments.length === 1) {
+
+        if (typeof val != "number")
+          val = parseInt(val);
+
+        if (isNaN(val))
+          val = 0;
+        
+        this._value = val;
+        this._update();
+      };
+      return this._value;
+    },
+
+
+    // Update dom element
+    _update : function () {
+      if (this._el === undefined)
+        return;
+      
+      this._value = this._input.value;
+    },
+    
+
+    /**
+     * Store the integer value.
+     * This method should be overwritten.
+     * The method receives the value.
+     */
+    store : function (v) {},
+
+
+    /**
+     * Put focus on element
+     */
+    focus : function () {
+      this._el.children[0].focus();
+    },
+
+
+    /**
+     * Get the associated dom element.
+     */
+    element : function () {
+      if (this._el !== undefined)
+        return this._el;
+
+      // Create element
+      const e = this._el = document.createElement('div');
+      e.setAttribute('tabindex', 0);
+      e.style.outline = 0;
+
+      const cl = e.classList;
+      cl.add('value');
+      
+      // Add input field
+      this._input = e.addE('input');
+      this._input.setAttribute("type", "number");
+
+      if (this.value() !== undefined) {
+        this._input.value = this.value();
+      };
+
+      // If the focus is not on the text field anymore,
+      // delegate focus to
+      this._input.addEventListener(
+        'blur',
+        function (ev) {
+          const t = this;
+          if (!t._inField) {
+	          t.value(t._input.value);
+            t.store(t.value());
+          };
+          ev.halt();
+        }.bind(this)
+      );
+
+      // Workaround to check the click is in the field
+      e.addEventListener(
+        'mousedown',
+        function () {
+          this._inField = true;
+        }.bind(this)
+      );
+
+      e.addEventListener(
+        'mouseup',
+        function () {
+          this._inField = false;
+          this._input.focus();
+        }.bind(this)
+      );
+
+      this._input.addEventListener(
+        'keypress',
+        function (ev) {
+          const t = this;
+	        if (ev.keyCode == 13) {
+	          t.value(t._input.value);
+	          t.store(t.value());
+            return false;
+	        };
+        }.bind(this)
+      );
+
+      return e;
+    }
+  };
+});