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");
+ });
+ });