Merge "Style container menu"
diff --git a/Changes b/Changes
index 8dc9a86..c2c6e60 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.43 2021-07-02
+0.43 2021-07-26
         - New menu class that has an entry at the very end,
           similar to the input text prefix,
           that is always available (lerepp).
@@ -6,6 +6,16 @@
           response.
         - Added utility funcition to menu that gets all direct
           childNodes by a tag. Used in menu-style specs. (lerepp)
+<<<<<<< HEAD
+        - Remove 'X-Frame-Options' in favor of 'frame-ancestors'
+          as a CSP rule.
+        - Fix CSS compression for new SASS compiler.
+        - Support dynamic menu extensions.
+        - Dynamically extend buttongroup menus.
+=======
+        - Update to menu.js's removeItems function to only delete
+          direct childNodes aswell as a specification to test for it (lerepp)
+>>>>>>> Added the condition to removeItems, that the to be removedItem be direct child of the menus HTML element.
 
 0.42 2021-06-18
         - Added GitHub based CI for perl.
diff --git a/Gruntfile.js b/Gruntfile.js
index dd59ac3..51965ac 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -79,6 +79,7 @@
         options: {
           implementation: sass,
           style: 'compressed',
+          outputStyle: 'compressed',
           sourceMap: true
         },
         files: {
diff --git a/dev/demo/plugin-serverdemo.js b/dev/demo/plugin-serverdemo.js
index d606cff..9b05ce0 100644
--- a/dev/demo/plugin-serverdemo.js
+++ b/dev/demo/plugin-serverdemo.js
@@ -44,4 +44,14 @@
 }]; 
 
 
-require(['app/en','init']);
+define(['plugin/server', 'lib/domReady', 'app/en', 'init'], function(pluginClass, domReady) {
+    domReady(function (event) {
+        if (KorAP.Plugin === undefined) {
+            // Load Plugin Server first
+            KorAP.Plugin = pluginClass.create();
+            // Add services container to head
+            document.head.appendChild(KorAP.Plugin.element());
+        };
+        KorAP.Plugins.forEach(i => KorAP.Plugin.register(i));
+    });
+});
diff --git a/dev/js/spec/buttongroupSpec.js b/dev/js/spec/buttongroupSpec.js
index bf7eafb..3d018e0 100644
--- a/dev/js/spec/buttongroupSpec.js
+++ b/dev/js/spec/buttongroupSpec.js
@@ -119,7 +119,6 @@
       expect(btn.innerText).toEqual('Meta');
     });
 
-    
     it('should open lists', function () {
       var group = buttonGroupClass.create();
       expect(group.element().classList.contains('button-group')).toBeTruthy();
@@ -149,6 +148,56 @@
       document.body.removeChild(list.element());
     });
 
+    it('should add to lists', function () {
+      var group = buttonGroupClass.create();
+      expect(group.element().classList.contains('button-group')).toBeTruthy();
+
+      var list = group.addList('More', {'cls':['more']});
+
+      let x = 'empty';
+      list.add('Meta1', {'cls':['meta'], 'icon': 'metaicon'}, function (e) {
+        x = 'meta1';
+      });
+      list.add('Meta2', {'cls':['meta'], 'icon': 'metaicon'}, function (e) {
+        x = 'meta2'
+      });
+
+      var btn = group.element().firstChild;
+      expect(btn.tagName).toEqual('SPAN');
+      expect(btn.classList.contains('more')).toBeTruthy();
+      expect(btn.innerText).toEqual('More');
+
+      expect(list.element().classList.contains('visible')).toBeFalsy();
+      
+      // Click to show menu
+      btn.click();
+
+      expect(list.element().classList.contains('visible')).toBeTruthy();
+      expect(list.directElementChildrenByTagName("li")[0].innerHTML).toEqual('Meta1');
+      expect(list.directElementChildrenByTagName("li")[1].innerHTML).toEqual('Meta2');
+
+      expect(x).toEqual('empty');
+      list.directElementChildrenByTagName("li")[0].click();
+      expect(x).toEqual('meta1');
+
+      expect(list.element().classList.contains('visible')).toBeFalsy();
+
+      // Click to show menu
+      btn.click();
+
+      expect(list.element().classList.contains('visible')).toBeTruthy();
+      list.directElementChildrenByTagName("li")[1].click();
+      expect(x).toEqual('meta2');
+
+      expect(list.element().classList.contains('visible')).toBeFalsy();
+
+      expect(list.element().children[1].children[0].innerText).toEqual('Meta1--');
+      expect(list.element().children[1].children[1].innerText).toEqual('Meta2--');
+
+      document.body.removeChild(list.element());
+    });
+
+    
     it('should support toggle buttons', function () {
       var group = buttonGroupClass.create();
 
diff --git a/dev/js/spec/menuSpec.js b/dev/js/spec/menuSpec.js
index 5d115a8..883a71d 100644
--- a/dev/js/spec/menuSpec.js
+++ b/dev/js/spec/menuSpec.js
@@ -410,6 +410,21 @@
         menu = KorAP.HintMenu.create("cnx/", list);
         expect(menu.element().menu).toEqual(menu);
       });
+      
+      it('should only remove direct descendants with removeItems', function () {
+        var menu = KorAP.HintMenu.create("cnx/", list);
+        var newUL = document.createElement("ul");
+        var newLI = document.createElement("li");
+        newUL.appendChild(newLI);
+        //This is a very constructed example, but this actually happens within containerMenu
+        menu.element().appendChild(newUL);
+        expect(menu.element().childNodes[3].nodeName).toEqual("UL");
+        expect(menu.element().childNodes[3].childNodes[0]).toEqual(newLI);
+        menu.prefix("a"); //to call show
+        expect(menu.element().childNodes[3].nodeName).toEqual("UL");
+        expect(menu.element().childNodes[3].childNodes[0]).toEqual(newLI);
+        
+      });
 
 
       it('should be visible', function () {
@@ -1491,6 +1506,43 @@
       });
 
       xit('should scroll to a chosen value after prefixing, if the chosen value is live');
+
+      it('should be expendable', function () {
+        var menu = menuClass.create([]);
+        let entryData = 'empty';
+        menu.readItems([
+          ['a', '', function () { entryData = 'a' }],
+          ['bb', '', function () { entryData = 'bb' }],
+          ['ccc', '', function () { entryData = 'ccc' }],
+        ]);
+
+        expect(menu.limit(3).show(3)).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' a');
+        expect(menu.shownItem(1).lcField()).toEqual(' bb');
+        expect(menu.shownItem(2).lcField()).toEqual(' ccc');
+        expect(entryData).toEqual('empty');
+        menu.shownItem(1).element().click();
+        expect(entryData).toEqual('bb');
+        expect(menu.lengthField().element().innerText).toEqual("a--bb--ccc--")
+        expect(menu.slider().length()).toEqual(3);
+
+        let obj = menu.itemClass().create(
+          ['dddd','',function () { entryData = 'dddd'} ]
+        );
+        menu.append(obj)
+
+        expect(menu.limit(2).show(1)).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' a');
+        expect(menu.shownItem(1).lcField()).toEqual(' bb');
+        menu.next();
+        expect(menu.shownItem(1).lcField()).toEqual(' ccc');
+        menu.next();
+        expect(menu.shownItem(1).lcField()).toEqual(' dddd');
+        menu.next();
+        expect(menu.shownItem(0).lcField()).toEqual(' a');
+        expect(menu.lengthField().element().innerText).toEqual("a--bb--ccc--dddd--")
+        expect(menu.slider().length()).toEqual(4);
+      });
     });
 
     describe('KorAP.Prefix', function () {
diff --git a/dev/js/src/buttongroup/menu.js b/dev/js/src/buttongroup/menu.js
index 7a4f8df..32ba0eb 100644
--- a/dev/js/src/buttongroup/menu.js
+++ b/dev/js/src/buttongroup/menu.js
@@ -9,12 +9,7 @@
 
     /**
      * Create new menu object.
-     * Pass the panel object
-     * and the item parameters.
-     *
-     * @param panel The panel object
-     * @param params The match menu items
-     *   as an array of arrays.
+     * Pass the list of items and the itemClass.
      */
     create : function (list, itemClass) {
       const obj = Object.create(menuClass)
@@ -26,7 +21,7 @@
 
       // This is only domspecific
       e.addEventListener('blur', function (e) {
-	      this.menu.hide();
+	this.menu.hide();
       });
 
       e.classList.add('button-group-list');
@@ -39,9 +34,10 @@
 
 
     /**
-     * The panel object of the menu.
+     * The panel object of the menu,
+     * in case the menu was spawned by a panel.
      */
-    panel :function (panelVar) {
+    panel : function (panelVar) {
       if (panelVar !== undefined)
         this._panel = panelVar;
 
@@ -72,6 +68,30 @@
       window.addEventListener('scroll', this._onscroll);
     },
 
+
+    /**
+     * Add button in order
+     * 
+     * Returns the button element
+     */
+    add : function (title, data, cb) {
+
+      let that = this;
+
+      const cbWrap = function (e) {
+
+        // Call callback
+        let obj = that._bind || this;
+        obj.button = b;
+        cb.apply(obj)
+      };
+
+      // TODO:
+      //   support classes, data-icon and state in itemClass!
+      const b = this.itemClass().create([title, title, cbWrap]);
+      this.append(b);
+      return b;
+    },
     
     // Overwrite onHide method
     onHide : function () {
diff --git a/dev/js/src/menu.js b/dev/js/src/menu.js
index e26d19b..4e93390 100644
--- a/dev/js/src/menu.js
+++ b/dev/js/src/menu.js
@@ -177,6 +177,21 @@
       t.offset = 0;
       t.position = 0;
     },
+
+    // Append item to list
+    append : function (item) {
+      const t = this;
+      // This is cyclic!
+      item["_menu"] = t;
+      t._list = undefined;
+      t.removeItems();
+      t._items.push(item);
+      t._lengthField.add([item.content().data]);
+      t._slider.length(t.liveLength()).reInit();
+      t._firstActive = false;
+      t.offset = 0;
+      t.position = 0;
+    },
     
     // Initialize the item list
     _initList : function () {
@@ -622,10 +637,12 @@
      * Delete all visible items from the menu element
      */
     
-     removeItems : function () {
+    removeItems : function () {
       const liElements=this._el.getElementsByTagName("LI");
-      while (liElements.length>0){
-        this._el.removeChild(liElements[0]);
+      for (let ii = liElements.length-1; ii >= 0; ii-- ) {
+        if (liElements[ii].parentNode === this._el){
+          this._el.removeChild(liElements[ii]);
+        };
       };
      },
       
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 4f619a6..fd8fe69 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -137,7 +137,6 @@
     before_dispatch => sub {
       my $h = shift->res->headers;
       $h->header('X-Content-Type-Options' => 'nosniff');
-      $h->header('X-Frame-Options' => 'sameorigin');
       $h->header('X-XSS-Protection' => '1; mode=block');
       $h->header(
         'Access-Control-Allow-Methods' =>
@@ -197,6 +196,7 @@
     'script-src'  => ['self','sha256-VGXK99kFz+zmAQ0kxgleFrBWZgybFAPOl3GQtS7FQkI='],
     'connect-src' => 'self',
     'frame-src'   => '*',
+    'frame-ancestors' => 'self',
     'media-src'   => 'none',
     'object-src'  => 'self',
     'font-src'    => 'self',
diff --git a/t/page.t b/t/page.t
index 9146c0f..0be13c8 100644
--- a/t/page.t
+++ b/t/page.t
@@ -29,11 +29,11 @@
   ->header_like('Content-Security-Policy', qr!media-src 'none';!)
   ->header_like('Content-Security-Policy', qr!object-src 'self';!)
   ->header_like('Content-Security-Policy', qr!nonce-!)
+  ->header_like('Content-Security-Policy', qr!frame-ancestors 'self';!)
   ->content_like(qr/<script nonce/)
   ->content_like(qr/document\.body\.classList\.remove\(\'no-js\'\);/)
   ->header_is('X-Content-Type-Options', 'nosniff')
   ->header_is('Access-Control-Allow-Methods','GET, POST, OPTIONS')
-  ->header_is('X-Frame-Options', 'sameorigin')
   ->header_is('X-XSS-Protection', '1; mode=block')
   ;