luci-base: form.js: allow nesting Grid and Table sections
authorJo-Philipp Wich <jo@mein.io>
Sun, 21 Nov 2021 22:29:24 +0000 (23:29 +0100)
committerJo-Philipp Wich <jo@mein.io>
Mon, 22 Nov 2021 08:43:29 +0000 (09:43 +0100)
Implement a simple view stack within the modal overlay to allow the modal
edit dialog of a table or grid section to render another table or grid
section which recursively opens another modal edit dialog.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/htdocs/luci-static/resources/form.js

index cf09a1da905d2f0628752a26b4ae8e11bdf1ed4f..2be5bbce86c0679ab5417da8b066021764c49a6b 100644 (file)
@@ -2972,15 +2972,44 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
 
        /** @private */
        handleModalCancel: function(modalMap, ev) {
-               return Promise.resolve(ui.hideModal());
+               var prevNode = this.getPreviousModalMap();
+
+               if (prevNode) {
+                       var heading = prevNode.parentNode.querySelector('h4');
+
+                       prevNode.classList.add('flash');
+                       prevNode.classList.remove('hidden');
+                       prevNode.parentNode.removeChild(prevNode.nextElementSibling);
+
+                       heading.removeChild(heading.lastElementChild);
+
+                       if (!this.getPreviousModalMap())
+                               prevNode.parentNode
+                                       .querySelector('div.right > button')
+                                       .firstChild.data = _('Dismiss');
+               }
+               else {
+                       ui.hideModal();
+               }
+
+               return Promise.resolve();
        },
 
        /** @private */
        handleModalSave: function(modalMap, ev) {
-               return modalMap.save(null, true)
-                       .then(L.bind(this.map.load, this.map))
-                       .then(L.bind(this.map.reset, this.map))
-                       .then(ui.hideModal)
+               var mapNode = this.getActiveModalMap(),
+                   activeMap = dom.findClassInstance(mapNode),
+                   saveTasks = activeMap.save(null, true);
+
+               while (activeMap.parent) {
+                       activeMap = activeMap.parent;
+                       saveTasks = saveTasks
+                               .then(L.bind(activeMap.load, activeMap))
+                               .then(L.bind(activeMap.reset, activeMap));
+               }
+
+               return saveTasks
+                       .then(L.bind(this.handleModalCancel, this, modalMap, ev))
                        .catch(function() {});
        },
 
@@ -3013,6 +3042,19 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
 
        },
 
+       /** @private */
+       getActiveModalMap: function() {
+               return document.querySelector('body.modal-overlay-active > #modal_overlay > .modal.cbi-modal > .cbi-map:not(.hidden)');
+       },
+
+       /** @private */
+       getPreviousModalMap: function() {
+               var mapNode = this.getActiveModalMap(),
+                   prevNode = mapNode ? mapNode.previousElementSibling : null;
+
+               return (prevNode && prevNode.matches('.cbi-map.hidden')) ? prevNode : null;
+       },
+
        /** @private */
        renderMoreOptionsModal: function(section_id, ev) {
                var parent = this.map,
@@ -3061,20 +3103,37 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                }
 
                return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
-                       ui.showModal(title, [
-                               nodes,
-                               E('div', { 'class': 'right' }, [
-                                       E('button', {
-                                               'class': 'cbi-button',
-                                               'click': ui.createHandlerFn(this, 'handleModalCancel', m)
-                                       }, [ _('Dismiss') ]), ' ',
-                                       E('button', {
-                                               'class': 'cbi-button cbi-button-positive important',
-                                               'click': ui.createHandlerFn(this, 'handleModalSave', m),
-                                               'disabled': m.readonly || null
-                                       }, [ _('Save') ])
-                               ])
-                       ], 'cbi-modal');
+                       var modalMap = this.getActiveModalMap();
+
+                       if (modalMap) {
+                               modalMap.parentNode
+                                       .querySelector('h4')
+                                       .appendChild(E('span', title ? ' ยป ' + title : ''));
+
+                               modalMap.parentNode
+                                       .querySelector('div.right > button')
+                                       .firstChild.data = _('Back');
+
+                               modalMap.classList.add('hidden');
+                               modalMap.parentNode.insertBefore(nodes, modalMap.nextElementSibling);
+                               nodes.classList.add('flash');
+                       }
+                       else {
+                               ui.showModal(title, [
+                                       nodes,
+                                       E('div', { 'class': 'right' }, [
+                                               E('button', {
+                                                       'class': 'cbi-button',
+                                                       'click': ui.createHandlerFn(this, 'handleModalCancel', m)
+                                               }, [ _('Dismiss') ]), ' ',
+                                               E('button', {
+                                                       'class': 'cbi-button cbi-button-positive important',
+                                                       'click': ui.createHandlerFn(this, 'handleModalSave', m),
+                                                       'disabled': m.readonly || null
+                                               }, [ _('Save') ])
+                                       ])
+                               ], 'cbi-modal');
+                       }
                }, this)).catch(L.error);
        }
 });
@@ -3155,25 +3214,33 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro
        /** @private */
        handleAdd: function(ev, name) {
                var config_name = this.uciconfig || this.map.config,
-                   section_id = this.map.data.add(config_name, this.sectiontype, name);
+                   section_id = this.map.data.add(config_name, this.sectiontype, name),
+                   mapNode = this.getPreviousModalMap(),
+                   prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
+
+               prevMap.addedSection = section_id;
 
-               this.addedSection = section_id;
                return this.renderMoreOptionsModal(section_id);
        },
 
        /** @private */
        handleModalSave: function(/* ... */) {
+               var mapNode = this.getPreviousModalMap(),
+                   prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
+
                return this.super('handleModalSave', arguments)
-                       .then(L.bind(function() { this.addedSection = null }, this));
+                       .then(function() { delete prevMap.addedSection });
        },
 
        /** @private */
        handleModalCancel: function(/* ... */) {
-               var config_name = this.uciconfig || this.map.config;
+               var config_name = this.uciconfig || this.map.config,
+                   mapNode = this.getPreviousModalMap(),
+                   prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map;
 
-               if (this.addedSection != null) {
-                       this.map.data.remove(config_name, this.addedSection);
-                       this.addedSection = null;
+               if (prevMap.addedSection != null) {
+                       this.map.data.remove(config_name, prevMap.addedSection);
+                       delete prevMap.addedSection;
                }
 
                return this.super('handleModalCancel', arguments);