ES6: Port state to ES6

Change-Id: I8f58774ba468ebc47a4a6a3ad923d1b55f2bdae0
diff --git a/dev/js/src-lib/state/state.js b/dev/js/src-lib/state/state.js
new file mode 100644
index 0000000..dfc72d4
--- /dev/null
+++ b/dev/js/src-lib/state/state.js
@@ -0,0 +1,126 @@
+/**
+ * Create a state object, that can have a single value
+ * (mostly boolean) and multiple objects associated to it.
+ * Whenever the state changes, all objects are informed
+ * by their setState() method of the value change.
+ *
+ * @author Nils Diewald
+ */
+/*
+ * TODO:
+ *   Require names for states, that should be quite short, so they
+ *   can easily be serialized and kept between page turns (via cookie
+ *   and/or query param)
+ */
+
+export default class State {
+
+  /**
+   * Constructor
+   */
+  constructor (values) {
+      this._init(values);
+  }
+
+
+  // Initialize
+  _init (values) {
+      const t = this;
+      t._assoc = [];
+      if (values == undefined) {
+        t.values = [false,true];
+      }
+      else if (Array.isArray(values)) {
+        t.values = values;
+      }
+      else {
+        t.values = [values];
+      }
+      return t;
+  }
+
+
+  /**
+   * Associate the state with some objects.
+   */
+  associate (obj) {
+
+      // Check if the object has a setState() method
+      if (obj.hasOwnProperty("setState")) {
+
+        this._assoc.push(obj);
+        if (this.value != undefined) {
+          obj.setState(this.value);
+        };
+      } else {
+        console.log("Object " + obj + " has no setState() method");
+      }
+  }
+
+
+  /**
+   * Set the state to a certain value.
+   * This will set the state to all associated objects as well.
+   */
+  set (value) {
+      if (value != this.value) {
+        this.value = value;
+        this._assoc.forEach(i => i.setState(value));
+      };
+  }
+
+
+  /**
+   * Set the state to a default value.
+   * This will only be set, if no other value is set yet.
+   */
+  setIfNotYet (value) {
+      if (this.value == undefined) {
+        this.set(value);
+      };
+  }
+
+
+  /**
+   * Get the state value
+   */
+  get () {
+      if (this.value == undefined) {
+        this.value = this.values[0];
+      };
+
+      return this.value;
+  }
+
+
+  /**
+   * Get the number of associated objects
+   */
+  associates () {
+      return this._assoc.length;
+  }
+
+
+  /**
+   * Clear all associated objects
+   */
+  clear () {
+      return this._assoc = [];
+  }
+
+
+  /**
+   * Roll to the next value.
+   * This may be used for toggling.
+   */
+  roll () {
+      let next = 0;
+      for (let i = 0; i < this.values.length - 1; i++) {
+        if (this.get() == this.values[i]) {
+          next = i+1;
+          break;
+        };
+      };
+      this.set(this.values[next]);
+  }
+}
diff --git a/dev/js/src-lib/state/state/manager.js b/dev/js/src-lib/state/state/manager.js
new file mode 100644
index 0000000..140c660
--- /dev/null
+++ b/dev/js/src-lib/state/state/manager.js
@@ -0,0 +1,100 @@
+/**
+ * Create a state manager object, that can deserialize and
+ * serialize states of associated states.
+ * At the moment this requires an element for serialization,
+ * but it may very well serialize in a cookie.
+ *
+ * @author Nils Diewald
+ */
+
+import stateClass from '../state.js';
+
+export default class Manager {
+  // Create new state amanger.
+  // Expects an object with a value
+  // to contain the serialization of all states.
+  constructor (element) {
+      this._init(element);
+  }
+
+
+  // Initialize state manager
+  _init (element) {
+      this._e = element;
+      this._states = {};
+      this._defaults = {};
+      this._parse(element.value);
+
+      return this;
+  }
+
+
+  // Parse a value and populate states
+  _parse (value) {
+      if (value == null || value == undefined || value == '')
+        return;
+
+      this._states = JSON.parse('{' + value + '}');
+  }
+
+  // Return element
+  element () {
+      return this._e;
+  }
+
+  // Return the string representation of all states
+  toString () {
+
+      if (this._states.size === 0)
+        return undefined;
+      
+      return JSON.stringify(this._states).slice(1,-1);
+  }
+
+
+  // Update the query component for states
+  _update () {
+      this._e.setAttribute("value", this.toString());
+  }
+
+
+  // Create new state that is automatically associated
+  // with the state manager
+  newState (name, values, defValue) {
+
+      const t = this;
+      let s = new stateClass(values);
+
+      if (this._states[name] != undefined) {
+        if (values.includes(this._states[name]))
+          s.setIfNotYet(this._states[name]);
+      };
+
+      // Set default value
+      // TODO: It would be better to make this part
+      // of the state and serialize correctly using TOJSON()
+      if (defValue !== undefined) {
+        s.setIfNotYet(defValue);
+        t._defaults[name] = defValue;
+      };
+      
+      // Associate with dummy object
+      s.associate({
+        setState (value) {
+          if (t._defaults[name] !== undefined && t._defaults[name] == value) {
+            delete t._states[name];
+          } else {
+            t._states[name] = value;
+          };
+          t._update();
+        }
+      });
+      
+      // Load state
+      if (t._states[name] !== undefined) {
+        s.set(t._states[name]);
+      };
+
+      return s;
+  }
+}
diff --git a/dev/js/src-lib/state/test/stateSpec.js b/dev/js/src-lib/state/test/stateSpec.js
new file mode 100644
index 0000000..10963b5
--- /dev/null
+++ b/dev/js/src-lib/state/test/stateSpec.js
@@ -0,0 +1,249 @@
+import { describe, it, expect } from 'vitest';
+import stateClass from '../state.js';
+import stateManagerClass from '../state/manager.js';
+
+  describe('KorAP.State', function () {
+    it('should be initializable', function () {
+      let s = new stateClass();
+      expect(s.get()).toBeFalsy();
+
+      s = new stateClass(true);
+      expect(s.get()).toBeTruthy();
+    });
+
+    it('should be settable and gettable', function () {
+      let s = new stateClass();
+      expect(s.get()).toBeFalsy();
+      s.set(true);
+      expect(s.get()).toBeTruthy();
+    });
+
+    it('should accept a default value', function () {
+      let s = new stateClass([true, false]);
+      expect(s.get()).toBeTruthy();
+      s.set(false);
+      expect(s.get()).toBeFalsy();
+
+      s = new stateClass([true, false]);
+      s.setIfNotYet(false);
+      expect(s.get()).toBeFalsy();
+
+      s.setIfNotYet(true);
+      expect(s.get()).toBeFalsy();
+    });
+    
+    it('should be associatable', function () {
+      let s = new stateClass();
+
+      // Create
+      let obj1 = {
+        x : false,
+        setState : function (value) {
+          this.x = value;
+        }
+      };
+
+      // Create
+      let obj2 = {
+        x : true,
+        setState : function (value) {
+          this.x = value;
+        }
+      };
+
+      expect(s.associates()).toEqual(0);
+      expect(s.get()).toBeFalsy();
+      expect(obj1.x).toBeFalsy();
+      expect(obj2.x).toBeTruthy();
+
+      // Associate object with state
+      s.associate(obj1);
+      expect(s.associates()).toEqual(1);
+      s.associate(obj2);
+      expect(s.associates()).toEqual(2);
+
+      expect(s.get()).toBeFalsy();
+      expect(obj1.x).toBeFalsy();
+      expect(obj2.x).toBeFalsy();
+
+      s.set(true);
+
+      expect(s.get()).toBeTruthy();
+      expect(obj1.x).toBeTruthy();
+      expect(obj2.x).toBeTruthy();
+    });
+
+    it('should be clearable', function () {
+      let s = new stateClass();
+
+      // Create
+      let obj1 = {
+        x : false,
+        setState : function (value) {
+          this.x = value;
+        }
+      };
+
+      // Create
+      let obj2 = {
+        x : true,
+        setState : function (value) {
+          this.x = value;
+        }
+      };
+
+      expect(s.associates()).toEqual(0);
+      expect(s.get()).toBeFalsy();
+      expect(obj1.x).toBeFalsy();
+      expect(obj2.x).toBeTruthy();
+
+      // Associate object with state
+      s.associate(obj1);
+      expect(s.associates()).toEqual(1);
+      s.associate(obj2);
+      expect(s.associates()).toEqual(2);
+
+      expect(s.get()).toBeFalsy();
+      expect(obj1.x).toBeFalsy();
+      expect(obj2.x).toBeFalsy();
+
+      s.clear();
+
+      s.set(true);
+      expect(s.get()).toBeTruthy();
+      expect(obj1.x).toBeFalsy();
+      expect(obj2.x).toBeFalsy();
+
+      s.set(false);
+      expect(s.get()).toBeFalsy();
+      expect(obj1.x).toBeFalsy();
+      expect(obj2.x).toBeFalsy();
+    });
+
+    it('should roll', function () {
+      let s = new stateClass(['der','alte','mann']);
+
+      expect(s.get()).toEqual('der');
+      s.roll();
+      expect(s.get()).toEqual('alte');
+      s.roll();
+      expect(s.get()).toEqual('mann');
+      s.roll();
+      expect(s.get()).toEqual('der');
+      s.roll();
+      expect(s.get()).toEqual('alte');
+
+      s.set('alte');
+      expect(s.get()).toEqual('alte');
+      s.roll();
+      expect(s.get()).toEqual('mann');
+    });
+  });
+
+  describe('KorAP.State.Manager', function () {
+
+    it('should be initializable', function () {
+
+      const el = document.createElement('input');
+      let sm = new stateManagerClass(el);
+      expect(sm).toBeTruthy();
+
+      expect(sm.toString()).toEqual("");
+    });
+
+
+    it('should be extensible', function () {
+
+      const el = document.createElement('input');
+      const sm = new stateManagerClass(el);
+      expect(sm).toBeTruthy();
+
+      const s1 = sm.newState('test', [1,2,3]);
+      
+      expect(sm.toString()).toEqual("");
+
+      s1.set(2);
+
+      expect(sm.toString()).toEqual("\"test\":2");
+
+      s1.set(3);
+
+      expect(sm.toString()).toEqual("\"test\":3");
+
+      const s2 = sm.newState('glemm', [true,false]);
+
+      let serial = JSON.parse('{' + sm.toString() + '}');   
+      expect(serial["test"]).toEqual(3);
+      expect(serial["glemm"]).toBeUndefined();
+
+      s2.set(false);
+
+      serial = JSON.parse('{' + sm.toString() + '}');   
+      expect(serial["test"]).toEqual(3);
+      expect(serial["glemm"]).toEqual(false);
+    });
+
+    it('should serialize correctly', function () {
+      const el = document.createElement('input');
+      const sm = new stateManagerClass(el);
+      expect(sm).toBeTruthy();
+
+      const s1 = sm.newState('x', [1,2,3]);
+      
+      expect(sm.toString()).toEqual("");
+
+      s1.set(2);
+
+      expect(sm.toString()).toEqual("\"x\":2");
+
+      const s2 = sm.newState('y', [true,false]);
+      s2.set(false)
+
+      const s3 = sm.newState('z', ['a','b','c']);
+      s3.set('b')
+
+      expect(sm.toString().indexOf("\"x\":2")).not.toEqual(-1);
+      expect(sm.toString().indexOf("\"y\":false")).not.toEqual(-1);
+      expect(sm.toString().indexOf("\"z\":\"b\"")).not.toEqual(-1);
+      expect(sm.toString().indexOf("\"a\":\"d\"")).toEqual(-1);
+    });
+    
+    it('should accept and not serialize default values', function () {
+      const el = document.createElement('input');
+      const sm = new stateManagerClass(el);
+      expect(sm).toBeTruthy();
+
+      const s1 = sm.newState('test', [1,2,3], 1);
+      
+      expect(sm.toString()).toEqual("");
+
+      s1.set(2);
+
+      expect(sm.toString()).toEqual("\"test\":2");
+
+      s1.set(3);
+
+      expect(sm.toString()).toEqual("\"test\":3");
+
+      s1.set(1);
+
+      expect(sm.toString()).toEqual("");
+
+      s1.set(2);
+
+      expect(sm.toString()).toEqual("\"test\":2");
+    });
+
+    it('should load stored states', function () {
+      const el = document.createElement('input');
+      el.setAttribute("value","\"test\":2");
+      const sm = new stateManagerClass(el);
+      expect(sm).toBeTruthy();
+
+      const s1 = sm.newState('test', [1,2,3], 1);
+
+      expect(s1.get()).toEqual(2);
+      
+      expect(sm.toString()).toEqual("\"test\":2");
+    });
+  });