frontend: change config values

- does not apply changes to the backend
- allows only leaf's values changing, not creating nor deleting subtrees
diff --git a/frontend/config/config.component.css b/frontend/config/config.component.css
index c2336b0..6a10dab 100644
--- a/frontend/config/config.component.css
+++ b/frontend/config/config.component.css
@@ -92,4 +92,16 @@
 
 .item_action_expand:hover {
     color:green;
-}
\ No newline at end of file
+}
+
+.modifications-status {
+	position: fixed;
+	bottom: 2em;
+	right: 2em;
+	background-color: #fafad2;
+	border: 2px solid #e4e4a4;
+}
+.modifications-status-place {
+	margin-bottom: 2em;
+	margin-right: 2em;
+}
diff --git a/frontend/config/config.component.html b/frontend/config/config.component.html
index e1ea27f..7a1ef15 100644
--- a/frontend/config/config.component.html
+++ b/frontend/config/config.component.html
@@ -70,6 +70,14 @@
         <tree-view [treeData]="activeSession.data"></tree-view>
         <!--<pre>{{activeSession.data | json}}</pre>-->
     </div>
+	<div #modificationsStatus *ngIf="activeSession.modifications" class="modifications-status-place">&nbsp;
+		<div *ngIf="activeSession.modifications" class="modifications-status msg-rounded" [style.width.px]="modificationsStatus.offsetWidth">
+		Configuration data were changed. Do you wish to
+		<button (click)="applyChanges()">apply</button> /
+		<button (click)="cancelChanges()">cancel</button>
+		all changes?
+		</div>
+	</div>
     </ng-container>
 </div>
 
diff --git a/frontend/config/config.component.ts b/frontend/config/config.component.ts
index e1830f4..d13931f 100644
--- a/frontend/config/config.component.ts
+++ b/frontend/config/config.component.ts
@@ -15,7 +15,6 @@
     activeSession: Session;
     err_msg = "";
 
-    objectKeys = Object.keys;
     constructor(private sessionsService: SessionsService, private router: Router) {}
 
     addSession() {
@@ -155,7 +154,47 @@
             this.sessionsService.storeData();
         });
     }
+    
+    cancelChangesNode(node, recursion = true) {
+        
+        if (node['path'] in this.activeSession.modifications) {
+            node['dirty'] = false;
+            if (this.activeSession.modifications[node['path']]['type'] == 'change') {
+                node['value'] = this.activeSession.modifications[node['path']]['original'];
+            }
+            delete this.activeSession.modifications[node['path']]; 
+            if (!Object.keys(this.activeSession.modifications).length) {
+                delete this.activeSession.modifications;
+                return;
+            }
+        }
 
+        /* recursion */
+        if (recursion && 'children' in node) {
+            for (let child of node['children']) {
+                this.cancelChangesNode(child);
+                if (!this.activeSession.modifications) {
+                    return;
+                }
+            }
+        }
+    }
+    
+    cancelChanges() {
+        for (let iter of this.activeSession.data) {
+            this.cancelChangesNode(iter);
+            if (!this.activeSession.modifications) {
+                break;
+            }
+        }
+        this.sessionsService.storeData();
+    }
+
+    applyChanges() {
+        /* TODO */
+        this.cancelChanges();
+    }
+    
     ngOnInit(): void {
         this.sessionsService.checkSessions();
         this.activeSession = this.sessionsService.getActiveSession();
diff --git a/frontend/config/session.ts b/frontend/config/session.ts
index 9f56289..5bdda36 100644
--- a/frontend/config/session.ts
+++ b/frontend/config/session.ts
@@ -5,6 +5,7 @@
     public key: string,
     public device: Device,
     public data = null,
+    public modifications = null,
     public cpblts: string = "",
     public dataVisibility: string = 'none',
     public statusVisibility: boolean = true,
diff --git a/frontend/config/sessions.service.ts b/frontend/config/sessions.service.ts
index 09edecc..970f417 100644
--- a/frontend/config/sessions.service.ts
+++ b/frontend/config/sessions.service.ts
@@ -106,6 +106,27 @@
             .catch((err: Response | any) => Observable.throw(err));
     }
 
+    setDirty(node) {
+        let activeSession = this.getActiveSession();
+        if (!activeSession.modifications) {
+            return;
+        }
+        
+        if (node['path'] in activeSession.modifications) {
+            node['dirty'] = true;
+            if (activeSession.modifications[node['path']]['type'] == 'change') {
+                activeSession.modifications[node['path']]['original'] = node['value'];
+            }
+            node['value'] = activeSession.modifications[node['path']]['value']; 
+        }
+        /* recursion */
+        if ('children' in node) {
+            for (let child of node['children']) {
+                this.setDirty(child);
+            }
+        }
+    }
+    
     rpcGetSubtree(key: string, all: boolean, path: string = ""): Observable<string[]> {
         let params = new URLSearchParams();
         params.set('key', key);
@@ -115,7 +136,22 @@
         }
         let options = new RequestOptions({ search: params });
         return this.http.get('/netopeer/session/rpcGet', options)
-            .map((resp: Response) => resp.json())
+            .map((resp: Response) => {
+                let result = resp.json();
+                //console.log(result);
+                if (result['success']) {
+                    if (path.length) {
+                        for (let iter of result['data']['children']) {
+                            this.setDirty(iter);
+                        }
+                    } else {
+                        for (let iter of result['data']) {
+                            this.setDirty(iter);
+                        }
+                    }
+                }
+                return result;
+            })
             .catch((err: Response | any) => Observable.throw(err));
     }
 
diff --git a/frontend/config/tree.component.css b/frontend/config/tree.component.css
index 0f98548..d511188 100644
--- a/frontend/config/tree.component.css
+++ b/frontend/config/tree.component.css
@@ -39,6 +39,10 @@
     display: inline-block;
 }
 
+.dirty {
+	background-color: #fafad2;
+}
+
 .status,
 .node_info {
     color: grey;
@@ -74,8 +78,7 @@
     margin-left: 1.5em;
 }
 
-.value,
-.value_standalone {
+.value {
     word-wrap: break-word;
 }
 
diff --git a/frontend/config/tree.component.html b/frontend/config/tree.component.html
index df773cc..1fc786c 100644
--- a/frontend/config/tree.component.html
+++ b/frontend/config/tree.component.html
@@ -4,7 +4,7 @@
 
         <!-- leaf -->
         <ng-container *ngSwitchCase="4">
-            <div #nodeContainer class="node">
+            <div #nodeContainer class="node" [class.dirty]="node['dirty']">
                 <ng-container *ngFor="let indent of indentation">
                     <img *ngIf="!indent" [style.height.px]="nodeContainer.offsetHeight" class="indentation" src="assets/netopeer/icons/tree_cont.svg"/>
                     <img *ngIf="indent" [style.height.px]="nodeContainer.offsetHeight" class="indentation" src="assets/netopeer/icons/tree_empty.svg"/>
@@ -18,32 +18,38 @@
                     <img class="icon" src="assets/netopeer/icons/key.svg" alt="key" title="list key"/>
                 </ng-container>
                 <ng-container *ngIf="node['info']['config'] && !node['info']['key']">
-                    <img *ngIf="!node['edit']" (click)="node['edit']=true" class="icon_action" src="assets/netopeer/icons/edit.svg"
+                    <img *ngIf="!node['edit']" class="icon_action" src="assets/netopeer/icons/edit.svg"
+                    	alt="edit" title="edit value" tabindex=0
+                    	(click)="startEditing(node, $event.target);" (keyup.enter)="startEditing(node, $event.target);" 
                         onmouseover="this.src='assets/netopeer/icons/edit_active.svg'"
-                        onmouseout="this.src='assets/netopeer/icons/edit.svg'" alt="edit" title="edit value"/>
+                        onmouseout="this.src='assets/netopeer/icons/edit.svg'"/>
                     <img *ngIf="node['edit']" class="icon" src="assets/netopeer/icons/edit.svg" alt="edit" title="editing value"/>
                 </ng-container>
                 <div>{{node['info']['name']}}</div> :
-                <div class="value" *ngIf="!node['edit']">{{node['value']}}</div>
+                <div class="value" *ngIf="!node['edit']" (click)="startEditing(node, $event.target);">{{node['value']}}</div>
                 <div class="value" *ngIf="node['edit']">{{node['info']['datatype']}} <span *ngIf="node['info']['datatype'] != node['info']['datatypebase']">({{node['info']['datatypebase']}})</span></div>
                 <div class="module_name">{{node['info']['module']}}</div>
             </div>
-            <div #nodeContainer class="node_edit" *ngIf="node['edit']">
+            <div #nodeContainer class="node_edit" [class.dirty]="node['dirty']" *ngIf="node['edit']">
                 <ng-container *ngFor="let indent of indentation">
                     <img *ngIf="!indent" class="indentation" src="assets/netopeer/icons/tree_cont.svg"/>
                     <img *ngIf="indent" class="indentation" src="assets/netopeer/icons/tree_empty.svg"/>
                 </ng-container>
                 <img *ngIf="node['last']" class="indentation" src="assets/netopeer/icons/tree_empty.svg"/>
                 <img *ngIf="!node['last']" class="indentation" src="assets/netopeer/icons/tree_cont.svg"/>
-                <img *ngIf="node['edit']" (click)="node['edit']=false" class="icon_action" src="assets/netopeer/icons/close.svg"
+                <img *ngIf="node['edit']" class="icon_action" src="assets/netopeer/icons/close.svg"
+                	alt="cancel" title="cancel editing" tabindex=0
+                	(click)="node['edit']=false" (keyup.enter)="node['edit']=false"
                     onmouseover="this.src='assets/netopeer/icons/close_active.svg'"
-                    onmouseout="this.src='assets/netopeer/icons/close.svg'" alt="cancel" title="cancel editing"/>
-                <img *ngIf="node['edit']" (click)="node['edit']=false" class="icon_action icon_hidden" src="assets/netopeer/icons/confirm.svg"
-                    id="{{node['path']}}_value_confirm"
+                    onmouseout="this.src='assets/netopeer/icons/close.svg'" />
+                <img *ngIf="node['edit']" class="icon_action icon_hidden" src="assets/netopeer/icons/confirm.svg"
+                    id="{{node['path']}}_value_confirm" alt="done" title="store changes" tabindex=0
+                    (click)="changeValue(node, $event.target)" (keyup.enter)="changeValue(node, $event.target)" 
                     onmouseover="this.src='assets/netopeer/icons/confirm_active.svg'"
-                    onmouseout="this.src='assets/netopeer/icons/confirm.svg'" alt="done" title="store changes" />
-                <input type="text" class="value_standalone" value="{{node['value']}}"
-                       (keyup)="checkValue(node, $event.target)" (change)="checkValue(node, $event.target)"/>
+                    onmouseout="this.src='assets/netopeer/icons/confirm.svg'"/>
+                <input type="text" class="value" value="{{node['value']}}" tabindex=0
+                    (keyup)="checkValue(node, $event.target)" (change)="checkValue(node, $event.target)"
+                    (keyup.enter)="changeValue(node, $event.target)" (keyup.escape)="node['edit']=false"/>
             </div>
         </ng-container>
 
diff --git a/frontend/config/tree.component.ts b/frontend/config/tree.component.ts
index f49462b..c61ae1a 100644
--- a/frontend/config/tree.component.ts
+++ b/frontend/config/tree.component.ts
@@ -1,4 +1,4 @@
-import {Component, Input, OnInit} from '@angular/core';
+import {Component, Input, OnInit, ChangeDetectorRef} from '@angular/core';
 
 import {Session} from './session';
 import {SessionsService} from './sessions.service';
@@ -14,11 +14,10 @@
     @Input() indentation;
     c = 1; i = 1;
     activeSession: Session;
-    objectKeys = Object.keys;
-    constructor(private sessionsService: SessionsService) {}
+    constructor(private sessionsService: SessionsService, private changeDetector: ChangeDetectorRef) {}
 
     ngOnInit(): void {
-        this.activeSession = this.sessionsService.getActiveSession(this.sessionsService.activeSession);
+        this.activeSession = this.sessionsService.getActiveSession();
     }
 
     inheritIndentation(node) {
@@ -36,6 +35,15 @@
         }
     }
 
+    startEditing(node, target) {        
+        let parent = target.parentElement;
+
+        node['edit'] = true;
+        this.changeDetector.detectChanges();
+
+        parent.nextElementSibling.lastElementChild.focus();        
+    }
+    
     checkValue(node, target) {
         let confirm = target.previousElementSibling;
         this.sessionsService.checkValue(this.activeSession.key, node['path'], target.value).subscribe(result => {
@@ -48,6 +56,52 @@
             }
         });
     }
+    
+    changeValue(node, target) {
+        let input;
+        if (target.classList.contains('value')) {
+            if (target.classList.contains('invalid')) {
+                return;
+            }
+            input = target;            
+        } else {
+            input = target.nextElementSibling;
+        }
+
+        if (!this.activeSession.modifications) {
+            this.activeSession.modifications = {};
+        }
+        if (!(node['path'] in this.activeSession.modifications)) {
+            /* new record */
+            if (node['value'] == input['value']) {
+                /* no change to the original value */
+                return;
+            }
+            this.activeSession.modifications[node['path']] = {};
+            this.activeSession.modifications[node['path']]['type'] = 'change';
+            this.activeSession.modifications[node['path']]['original'] = node['value'];
+            this.activeSession.modifications[node['path']]['value'] = input.value;  
+            node['dirty'] = true;          
+        } else if (this.activeSession.modifications[node['path']]['type'] == 'change' &&
+                   this.activeSession.modifications[node['path']]['original'] == input['value']) {
+            /* change to the original value, remove the change record */
+            delete this.activeSession.modifications[node['path']];
+            node['dirty'] = false;
+            
+            if (!Object.keys(this.activeSession.modifications).length) {
+                delete this.activeSession.modifications;
+            }
+        } else {
+            /* another change of existing change record */
+            this.activeSession.modifications[node['path']]['value'] = input.value;
+            node['dirty'] = true;
+        }
+        console.log('Modifications list: ' + this.activeSession.modifications);
+        
+        node['value'] = input.value;
+        node['edit'] = false;
+        this.sessionsService.storeData();
+    }
 
     expandable(node): boolean {
         if (node['info']['type'] == 1 || /* container */
@@ -79,7 +133,7 @@
         }
         return node['hasHiddenChild'];
     }
-
+    
     collapse(node) {
         delete node['children'];
         this.activeSession.dataVisibility = 'mixed';