Introduced unspecified vc doc type and added 'ne' matchop to dates

Change-Id: I6f2cbfd0671ea896d10b65b4162dd5f130fe5b3b
diff --git a/Changes b/Changes
index a632a27..f1c0ddc 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.26 2018-03-19
+0.26 2018-04-06
         - Added meta data view.
         - Attach reference line to match bottom.
         - Separate match views and integrate relation menu
@@ -12,6 +12,11 @@
         - Protect login for csrf attacks.
         - Remember app state after login (issue #20).
         - Fixed DRuKoLA annotation assistant data.
+        - Fixed server error reporting.
+        - Introduced text type in VC creation.
+        - Removed "contains" and "containsnot" from
+          string type.
+        - Added "ne" operation to date meta type.
 
 0.25 2018-01-31
         - Make annotation helper configurable.
diff --git a/dev/demo/vcdemo.js b/dev/demo/vcdemo.js
index dfd6988..2dc5767 100644
--- a/dev/demo/vcdemo.js
+++ b/dev/demo/vcdemo.js
@@ -113,7 +113,7 @@
       ['title', 'string'],
       ['subTitle', 'string'],
       ['pubDate', 'date'],
-      ['author', 'string']
+      ['author', 'text']
     ]).fromJson(json);
 
     document.getElementById('vc').appendChild(vc.element());
diff --git a/dev/js/spec/vcSpec.js b/dev/js/spec/vcSpec.js
index 0f454c4..5433fb1 100644
--- a/dev/js/spec/vcSpec.js
+++ b/dev/js/spec/vcSpec.js
@@ -95,6 +95,7 @@
     var stringFactory = buildFactory(docClass, {
       "key"   : "author",
       "value" : "Max Birkendale",
+      "type"  : "type:string",
       "@type" : "koral:doc"
     });
 
@@ -102,6 +103,7 @@
     var textFactory = buildFactory(docClass, {
       "key"   : "author",
       "value" : "Birkendale",
+      "type"  : "type:string",
       "match" : "match:contains",
       "@type" : "koral:doc"
     });
@@ -251,7 +253,7 @@
 
       // Invalid matcher!
       doc = dateFactory.create({
-        "match" : "match:ne",
+        "match" : "match:contains",
       });
       expect(doc).toBeDefined();
       expect(doc.rewrites()).toBeDefined();
@@ -2122,6 +2124,8 @@
       expect(list.getElementsByTagName("LI")[0].innerText).toEqual('d');
       expect(list.getElementsByTagName("LI")[1].innerText).toEqual('e');
       expect(list.getElementsByTagName("LI")[2].innerText).toEqual('f');
+      // blur
+      document.body.click();
     });
 
     // Reinitialize to make tests stable
@@ -2132,25 +2136,103 @@
     ]).fromJson();
 
     it('should be clickable on key', function () {
-
+      // Click on unspecified
+      vc.element().firstChild.firstChild.click();
       // Click on "d"
       vc.element().firstChild.firstChild.getElementsByTagName("LI")[0].click();
       expect(vc.element().firstChild.firstChild.tagName).toEqual('SPAN');
       expect(vc.element().firstChild.firstChild.innerText).toEqual('d');
       expect(vc.element().firstChild.children[1].innerText).toEqual('eq');
       expect(vc.element().firstChild.children[1].getAttribute('data-type')).toEqual('text');
+      // blur
+      document.body.click();
     });
 
-    it('should be clickable on operation', function () {
+    it('should be clickable on operation for text', function () {
+      // Click on "d" (or unspecified)
+      vc.element().firstChild.firstChild.click();
+
+      // Choose "d"
+      vc.element().firstChild.firstChild.getElementsByTagName("LI")[0].click();
+
+      // Click on matchop
       vc.element().firstChild.children[1].click();
+
       expect(vc.element().firstChild.children[1].tagName).toEqual('UL');
+
       var ul = vc.element().firstChild.children[1];
       expect(ul.getElementsByTagName('li')[0].innerText).toEqual("eq");
       expect(ul.getElementsByTagName('li')[1].innerText).toEqual("ne");
       expect(ul.getElementsByTagName('li')[2].innerText).toEqual("contains");
       expect(ul.getElementsByTagName('li')[3].innerText).toEqual("containsnot");
       expect(ul.getElementsByTagName('li')[4]).toBeUndefined();
+
+      // Choose "contains"
+      ul.getElementsByTagName('li')[2].click();
+      expect(vc.element().firstChild.children[1].tagName).toEqual("SPAN");
+      expect(vc.element().firstChild.children[1].innerText).toEqual("contains");
+      // blur
+      document.body.click();
     })
+
+    it('should be clickable on operation for string', function () {
+      // Click on "d" (or unspecified)
+      vc.element().firstChild.firstChild.click();
+
+      // Choose "e"
+      vc.element().firstChild.firstChild.getElementsByTagName("LI")[1].click();
+
+      // As a consequence the matchoperator may no longer
+      // be valid and needs to be re-evaluated
+      var fc = vc.element().firstChild;
+      expect(fc.firstChild.tagName).toEqual('SPAN');
+      expect(fc.firstChild.innerText).toEqual('e');
+      expect(fc.children[1].innerText).toEqual('eq');
+      expect(fc.children[1].getAttribute('data-type')).toEqual('string');
+
+      vc.element().firstChild.children[1].click();
+
+      expect(vc.element().firstChild.children[1].tagName).toEqual('UL');
+
+      var ul = vc.element().firstChild.children[1];
+      expect(ul.getElementsByTagName('li')[0].innerText).toEqual("eq");
+      expect(ul.getElementsByTagName('li')[1].innerText).toEqual("ne");
+      expect(ul.getElementsByTagName('li')[2]).toBeUndefined();
+
+      // Choose "ne"
+      ul.getElementsByTagName('li')[1].click();
+      expect(vc.element().firstChild.children[1].tagName).toEqual("SPAN");
+      expect(vc.element().firstChild.children[1].innerText).toEqual("ne");
+      // blur
+      document.body.click();
+    });
+
+    it('should be clickable on operation for date', function () {
+
+      // Replay matchop check - so it's guaranteed that "ne" is chosen
+      // Click on "e" (or unspecified)
+      vc.element().firstChild.firstChild.click();
+      // Rechoose "e"
+      vc.element().firstChild.firstChild.getElementsByTagName("LI")[1].click();
+      // Click on matchop
+      vc.element().firstChild.children[1].click();
+      // Choose "ne"
+      vc.element().firstChild.children[1].getElementsByTagName('li')[1].click();
+      expect(vc.element().firstChild.children[1].innerText).toEqual("ne");
+
+      // Click on "e"
+      vc.element().firstChild.firstChild.click();
+      // Choose "f"
+      vc.element().firstChild.firstChild.getElementsByTagName("LI")[2].click();
+      
+      // The matchoperator should still be "ne" as this is valid for dates as well (now)
+      var fc = vc.element().firstChild;
+      expect(fc.firstChild.tagName).toEqual('SPAN');
+      expect(fc.firstChild.innerText).toEqual('f');
+      expect(fc.children[1].innerText).toEqual('ne');
+      // blur
+      document.body.click();
+    });
   });
 
   
diff --git a/dev/js/src/datepicker.js b/dev/js/src/datepicker.js
index f11db3c..89a9a68 100644
--- a/dev/js/src/datepicker.js
+++ b/dev/js/src/datepicker.js
@@ -7,7 +7,7 @@
 define(['util'], function () {
   "use strict";
 
-  KorAP._validDateMatchRE   = new RegExp("^[lg]?eq$");
+  KorAP._validDateMatchRE   = new RegExp("^(?:[lg]?eq|ne)$");
   KorAP._validDateRE        = new RegExp("^(?:\\d{4})(?:-\\d\\d(?:-\\d\\d)?)?$");
 
   /*
diff --git a/dev/js/src/vc.js b/dev/js/src/vc.js
index 18c621a..9aef8d5 100644
--- a/dev/js/src/vc.js
+++ b/dev/js/src/vc.js
@@ -56,9 +56,12 @@
 ], function (unspecDocClass, docClass, docGroupClass, menuClass, dpClass) {
   "use strict";
 
-  // ???
-  KorAP._validStringMatchRE = new RegExp("^(?:eq|ne|contains(?:not)?|excludes)$");
+  KorAP._validUnspecMatchRE = new RegExp("^(?:eq|ne|contains(?:not)?|excludes)$");
+  KorAP._validStringMatchRE = new RegExp("^(?:eq|ne)$");
+  KorAP._validTextMatchRE = KorAP._validUnspecMatchRE;
+  KorAP._validTextOnlyMatchRE = new RegExp("^(?:contains(?:not)?|excludes)$");
   KorAP._overrideStyles     = false;
+  // KorAP._validDateMatchRE is defined in datepicker.js!
 
   const loc = KorAP.Locale;
 
@@ -79,6 +82,7 @@
 	  ]),
 	  'date' : 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 f173de8..3df0f70 100644
--- a/dev/js/src/vc/doc.js
+++ b/dev/js/src/vc/doc.js
@@ -10,6 +10,7 @@
 ], function (jsonldClass, rewriteListClass, stringValClass) {
 
 
+  const errstr802 = "Match type is not supported by value type";
   const loc = KorAP.Locale;
   loc.EMPTY = loc.EMPTY || '⋯';
 
@@ -54,7 +55,7 @@
       // Set ref - TODO: Cleanup!
       e.refTo = this;
 
-      // Check if there is a change
+      // Check if there is a change in the underlying data
       if (this.__changed) {
 
         // Was rewritten
@@ -193,22 +194,58 @@
         // Set match operation
         if (json["match"] !== undefined) {
           if (typeof json["match"] === 'string') {
+
             this.matchop(json["match"]);
           }
           else {
-            KorAP.log(802, "Match type is not supported by value type");
+            KorAP.log(802, errstr802);
             return;
           };
         };
+        
+        // Type is unspecified
+        if (json["type"] === undefined) {
 
-        // Key is a string
-        if (json["type"] === undefined ||
-            json["type"] == "type:string") {
+          // TODO:
+          // First check the VC list if the field is known
+
+          // Check match type
+          if (!KorAP._validUnspecMatchRE.test(this.matchop())) {
+            KorAP.log(802, errstr802);
+
+            // Rewrite method
+            this.matchop('eq');
+            rewrite = 'modification';
+          };
+
+          // Set string value
+          this.value(json["value"]);
+        }
+
+        // Field is string type
+        else if (json["type"] == "type:string") {
           this.type("string");
 
           // Check match type
           if (!KorAP._validStringMatchRE.test(this.matchop())) {
-            KorAP.log(802, "Match type is not supported by value type");
+            KorAP.log(802, errstr802);
+
+            // Rewrite method
+            this.matchop('eq');
+            rewrite = 'modification';
+          };
+
+          // Set string value
+          this.value(json["value"]);
+        }
+
+        // Field is specified
+        else if (json["type"] == "type:text") {
+          this.type("text");
+
+          // Check match type
+          if (!KorAP._validTextMatchRE.test(this.matchop())) {
+            KorAP.log(802, errstr802);
 
             // Rewrite method
             this.matchop('eq');
@@ -227,7 +264,7 @@
               KorAP._validDateRE.test(json["value"])) {
 
             if (!KorAP._validDateMatchRE.test(this.matchop())) {
-              KorAP.log(802, "Match type is not supported by value type");
+              KorAP.log(802, errstr802);
 
               // Rewrite method
               this.matchop('eq');
@@ -253,7 +290,7 @@
             var check = new RegExp(json["value"]);
 
             if (!KorAP._validStringMatchRE.test(this.matchop())) {
-              KorAP.log(802, "Match type is not supported by value type");
+              KorAP.log(802, errstr802);
 
               // Rewrite method
               this.matchop('eq');
@@ -336,16 +373,20 @@
      * Get or set the match operator
      */
     matchop : function (match) {
+
       if (arguments.length === 1) {
         var m = match.replace(/^match:/, '');
+
         if (
-          (this._type === undefined)
+          (this._type == undefined) // && KorAP._validUnspecMatchRE.test(m))
             ||
             (
               (this._type === 'string' || this._type === 'regex') &&
                 KorAP._validStringMatchRE.test(m)
             )
             ||
+            (this._type === 'text' && KorAP._validTextMatchRE.test(m))
+            ||
             (this._type === 'date' && KorAP._validDateMatchRE.test(m))
         ) {
           this._matchop = m;
@@ -355,7 +396,7 @@
         };
 
         this._changed();
-        return this;
+        return this
       };
       return this._matchop || "eq";
     },
@@ -499,7 +540,10 @@
       this._rewrites = rewriteListClass.create(value);
     },
 
-    // Remove rewrite marker when the data changes
+    // Mark the underlying data as being changed.
+    // This is important for rerendering the dom.
+    // This will also remove rewrite markers, when the data
+    // change happened by the user
     _changed : function () {
       this.__changed = true;
 
diff --git a/package.json b/package.json
index 451ae0d..f18858d 100755
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "Kalamar",
   "description": "Mojolicious-based Frontend for KorAP",
   "license" : "BSD-2-Clause",
-  "version": "0.26.2",
+  "version": "0.26.3",
   "repository" : {
     "type": "git",
     "url": "https://github.com/KorAP/Kalamar.git"