From c2e06aca0a20b20481b2d06fdc76cf58b293120b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Jul 2022 15:48:15 +0200 Subject: [PATCH] TreeRenderer: Use Icinga Web's collapsible implementation now resolves #254 --- library/Businessprocess/Renderer/Renderer.php | 2 +- .../Businessprocess/Renderer/TreeRenderer.php | 43 +++++++--- public/css/module.less | 78 +++++++++---------- public/js/module.js | 50 ++++++------ 4 files changed, 95 insertions(+), 78 deletions(-) diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index 58e1c4d..94a9667 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -265,7 +265,7 @@ abstract class Renderer extends HtmlDocument */ public function getId(Node $node, $path) { - return md5((empty($path) ? '' : implode(';', $path)) . $node->getName()); + return 'businessprocess-' . md5((empty($path) ? '' : implode(';', $path)) . $node->getName()); } public function setPath(array $path) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index a34ac35..add4334 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -2,19 +2,24 @@ namespace Icinga\Module\Businessprocess\Renderer; +use Icinga\Application\Version; use Icinga\Date\DateFormatter; use Icinga\Module\Businessprocess\BpConfig; use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\ImportedNode; use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\Web\Form\CsrfToken; -use Icinga\Module\Icingadb\Model\State; +use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Web\Widget\Icon; use ipl\Web\Widget\StateBall; class TreeRenderer extends Renderer { + const NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE = '2.11.2'; + public function assemble() { $bp = $this->config; @@ -32,7 +37,6 @@ class TreeRenderer extends Renderer 'put' => 'function:rowPutAllowed' ]), 'data-sortable-invert-swap' => 'true', - 'data-is-root-config' => $this->wantsRootNodes() ? 'true' : 'false', 'data-csrf-token' => CsrfToken::generate() ], $this->renderBp($bp) @@ -42,6 +46,10 @@ class TreeRenderer extends Renderer 'data-action-url', $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl() ); + + if (version_compare(Version::VERSION, self::NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE, '<')) { + $tree->getAttributes()->add('data-is-root-config', true); + } } else { $nodeName = $this->parent instanceof ImportedNode ? $this->parent->getNodeName() @@ -192,27 +200,35 @@ class TreeRenderer extends Renderer $attributes->add('class', 'node'); } - $div = Html::tag('div'); - $li->add($div); + $details = new HtmlElement('details', Attributes::create(['open' => true])); + $summary = new HtmlElement('summary'); + if (version_compare(Version::VERSION, self::NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE, '>=')) { + $details->getAttributes()->add('class', 'collapsible'); + $summary->getAttributes()->add('class', 'collapsible-control'); // Helps JS, improves performance a bit + } - $div->add($node->getLink()); - $div->add($this->getNodeIcons($node, $path)); + $summary->addHtml( + new Icon('caret-down', ['class' => 'collapse-icon']), + new Icon('caret-right', ['class' => 'expand-icon']) + ); - $div->add(Html::tag('span', null, $node->getAlias())); + $summary->add($this->getNodeIcons($node, $path)); + + $summary->add(Html::tag('span', null, $node->getAlias())); if ($node instanceof BpNode) { - $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); + $summary->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); } if ($node instanceof BpNode && $node->hasInfoUrl()) { - $div->add($this->createInfoAction($node)); + $summary->add($this->createInfoAction($node)); } $differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName(); if (! $this->isLocked() && !$differentConfig) { - $div->add($this->getActionIcons($bp, $node)); + $summary->add($this->getActionIcons($bp, $node)); } elseif ($differentConfig) { - $div->add($this->actionIcon( + $summary->add($this->actionIcon( 'forward', $this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(), mt('businessprocess', 'Show this process as part of its original configuration') @@ -240,7 +256,6 @@ class TreeRenderer extends Renderer ]) ->getAbsoluteUrl() ]); - $li->add($ul); $path[] = $differentConfig ? $node->getIdentifier() : $node->getName(); foreach ($node->getChildren() as $name => $child) { @@ -251,6 +266,10 @@ class TreeRenderer extends Renderer } } + $details->addHtml($summary); + $details->addHtml($ul); + $li->addHtml($details); + return $li; } diff --git a/public/css/module.less b/public/css/module.less index 7576834..1c2f57e 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -118,7 +118,7 @@ ul.bp { } } &[data-sortable-disabled="true"] { - li.process > div { + li.process summary { cursor: pointer; } } @@ -143,15 +143,17 @@ ul.bp { // ghost style &.sortable > li.sortable-ghost { - position: relative; - overflow: hidden; - max-height: 30em; - background-color: @gray-lighter; - border: .2em dotted @gray-light; - border-left-width: 0; - border-right-width: 0; + > details { + position: relative; + overflow: hidden; + max-height: 30em; + background-color: @gray-lighter; + border: .2em dotted @gray-light; + border-left-width: 0; + border-right-width: 0; + } - &.process:after { + &.process > .details:after { // TODO: Only apply if content overflows? content: " "; position: absolute; @@ -164,12 +166,14 @@ ul.bp { } // header style - li.process > div { + li.process summary { padding: .291666667em 0; border-bottom: 1px solid @gray-light; + user-select: none; - > a.toggle { - min-width: 1.25em; // So that process icons align with their node's icons + > .icon:nth-child(1), + > .icon:nth-child(2) { + min-width: 1.3em; // So that process icons align with their node's icons color: @gray; } @@ -187,8 +191,23 @@ ul.bp { } } + li.process.sortable-ghost details:not([open]) > summary { + border-bottom: none; + } + + // TODO: Remove once support for Icinga Web 2.10.x is dropped + li.process details:not(.collapsible) { + &[open] > summary .expand-icon { + display: none; + } + + &:not([open]) > summary .collapse-icon { + display: none; + } + } + // subprocess style - li.process > ul { + li.process > details ul { padding-left: 2em; list-style-type: none; @@ -216,7 +235,7 @@ ul.bp { } // horizontal layout - li.process > div, + li.process summary, li:not(.process) { display: flex; align-items: center; @@ -241,42 +260,23 @@ ul.bp { } // collapse handling - li.process { - // toggle, default - > div > a.toggle > i:before { - -webkit-transition: -webkit-transform 0.3s; - -moz-transition: -moz-transform 0.3s; - -o-transition: -o-transform 0.3s; - transition: transform 0.3s; - } + li.process details:not([open]) { + margin-bottom: (@vertical-tree-item-gap * 2); - // toggle, collapsed - &.collapsed > div > a.toggle > i:before { - -moz-transform:rotate(-90deg); - -ms-transform:rotate(-90deg); - -o-transform:rotate(-90deg); - -webkit-transform:rotate(-90deg); - transform:rotate(-90deg); - } - - &.collapsed { - margin-bottom: (@vertical-tree-item-gap * 2); - - > ul.bp { - display: none; - } + > ul.bp { + display: none; } } // hover style - li.process:hover > div { + li.process:hover summary { background-color: @tr-active-color; } li:not(.process):hover { background-color: @tr-active-color; } - li.process > div > .state-ball, + li.process summary > .state-ball, li:not(.process) > .state-ball { border: .15em solid @body-bg-color; diff --git a/public/js/module.js b/public/js/module.js index ca9e238..8bc6223 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -25,8 +25,7 @@ this.module.on('focus', 'form input, form textarea, form select', this.formElementFocus); - this.module.on('click', 'li.process a.toggle', this.processToggleClick); - this.module.on('click', 'li.process > div', this.processHeaderClick); + this.module.on('click', 'li.process summary:not(.collapsible-control)', this.processHeaderClick); this.module.on('end', 'ul.sortable', this.rowDropped); this.module.on('click', 'div.tiles > div', this.tileClick); @@ -42,42 +41,41 @@ onRendered: function (event) { var $container = $(event.currentTarget); this.fixFullscreen($container); - this.restoreCollapsedBps($container); + this.restoreCollapsedBps(event.target); this.highlightFormErrors($container); this.hideInactiveFormDescriptions($container); this.fixTileLinksOnDashboard($container); }, - processToggleClick: function (event) { + // TODO: Remove once support for Icinga Web 2.10.x is dropped + processHeaderClick: function (event) { event.stopPropagation(); + event.preventDefault(); - var $li = $(event.currentTarget).closest('li.process'); - $li.toggleClass('collapsed'); + let details = event.currentTarget.parentNode; + details.open = ! details.open; - var $bpUl = $(event.currentTarget).closest('.content > ul.bp'); - if (! $bpUl.length || !$bpUl.data('isRootConfig')) { + let bpUl = event.currentTarget.closest('.content > ul.bp'); + if (! bpUl || ! ('isRootConfig' in bpUl.dataset)) { return; } - var bpName = $bpUl.attr('id'); + let bpName = bpUl.id; if (typeof this.idCache[bpName] === 'undefined') { this.idCache[bpName] = []; } - var index = this.idCache[bpName].indexOf($li.attr('id')); - if ($li.is('.collapsed')) { + let li = details.parentNode; + let index = this.idCache[bpName].indexOf(li.id); + if (! details.open) { if (index === -1) { - this.idCache[bpName].push($li.attr('id')); + this.idCache[bpName].push(li.id); } } else if (index !== -1) { this.idCache[bpName].splice(index, 1); } }, - processHeaderClick: function (event) { - this.processToggleClick(event); - }, - hideInactiveFormDescriptions: function($container) { $container.find('dd').not('.active').find('p.description').hide(); }, @@ -226,23 +224,23 @@ } }, - restoreCollapsedBps: function($container) { - var $bpUl = $container.find('.content > ul.bp'); - if (! $bpUl.length || !$bpUl.data('isRootConfig')) { + // TODO: Remove once support for Icinga Web 2.10.x is dropped + restoreCollapsedBps: function(container) { + let bpUl = container.querySelector('.content > ul.bp'); + if (! bpUl || ! ('isRootConfig' in bpUl.dataset)) { return; } - var bpName = $bpUl.attr('id'); + let bpName = bpUl.id; if (typeof this.idCache[bpName] === 'undefined') { return; } - var _this = this; - $bpUl.find('li.process') - .filter(function () { - return _this.idCache[bpName].indexOf(this.id) !== -1; - }) - .addClass('collapsed'); + bpUl.querySelectorAll('li.process').forEach(li => { + if (this.idCache[bpName].indexOf(li.id) !== -1) { + li.querySelector(':scope > details').open = false; + } + }); }, /** BEGIN Form handling, borrowed from Director **/