From f4298034b9a34197438d716096a6a980e46db02b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 17 Dec 2018 14:40:47 +0100 Subject: [PATCH 01/99] js: Add drag&drop functionality --- configuration.php | 3 + public/css/module.less | 9 + public/js/behavior/sortable.js | 41 + public/js/module.js | 27 + public/js/vendor/jquery.fn.sortable.js | 1704 ++++++++++++++++++++ public/js/vendor/jquery.fn.sortable.min.js | 3 + 6 files changed, 1787 insertions(+) create mode 100644 public/js/behavior/sortable.js create mode 100644 public/js/vendor/jquery.fn.sortable.js create mode 100644 public/js/vendor/jquery.fn.sortable.min.js diff --git a/configuration.php b/configuration.php index 63c8ed9..358287d 100644 --- a/configuration.php +++ b/configuration.php @@ -57,3 +57,6 @@ $this->provideRestriction( 'businessprocess/prefix', $this->translate('Restrict access to configurations with the given prefix') ); + +$this->provideJsFile('behavior/sortable.js'); +$this->provideJsFile('vendor/jquery.fn.sortable.js'); \ No newline at end of file diff --git a/public/css/module.less b/public/css/module.less index 5e18cbf..b0547d8 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -23,6 +23,10 @@ div.bp { margin-bottom: 4px; } +div.bp.sortable > .sortable-ghost { + opacity: 0.5; +} + .simulation div.bp { border-right: 1em solid @colorCriticalHandled; padding-right: 1em; @@ -458,6 +462,11 @@ td > a > .badges { clear: both; } +.tiles.sortable > .sortable-ghost { + opacity: 0.5; + border: .2em dashed black; +} + .tiles > div { color: white; width: 12em; diff --git a/public/js/behavior/sortable.js b/public/js/behavior/sortable.js new file mode 100644 index 0000000..f641ca0 --- /dev/null +++ b/public/js/behavior/sortable.js @@ -0,0 +1,41 @@ +/*! Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +(function(Icinga, $) { + + 'use strict'; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Sortable = function (icinga) { + Icinga.EventListener.call(this, icinga); + this.on('rendered', this.onRendered, this); + }; + + Sortable.prototype = new Icinga.EventListener(); + + Sortable.prototype.onRendered = function(e) { + $(e.target).find('.sortable').each(function() { + var $el = $(this); + var options = { + onMove: function (/**Event*/ event, /**Event*/ originalEvent) { + if (typeof this.options['filter'] !== 'undefined' && $(event.related).is(this.options['filter'])) { + // Assumes the filtered item is either at the very start or end of the list and prevents the + // user from dropping other items before (if at the very start) or after it. + return false; + } + } + }; + + $.each($el.data(), function (i, v) { + if (i.length > 8 && i.startsWith('sortable')) { + options[i.charAt(8).toLowerCase() + i.substr(9)] = v; + } + }); + + $(this).sortable(options); + }); + }; + + Icinga.Behaviors.Sortable = Sortable; + +})(Icinga, jQuery); diff --git a/public/js/module.js b/public/js/module.js index 7ae5a85..4240811 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -35,6 +35,7 @@ this.module.on('click', 'div.tiles > div', this.tileClick); this.module.on('click', '.dashboard-tile', this.dashboardTileClick); + this.module.on('end', 'div.tiles.sortable', this.tileDropped); this.module.icinga.logger.debug('BP module initialized'); }, @@ -89,6 +90,32 @@ $(event.currentTarget).find('> .bp-link > a').first().trigger('click'); }, + tileDropped: function(event) { + var evt = event.originalEvent; + if (evt.oldIndex !== evt.newIndex) { + var $target = $(evt.to); + var actionUrl = icinga.utils.addUrlParams($target.data('actionUrl'), { + action: 'move', + movenode: $(evt.item).data('nodeName') + }); + + if (! $target.is('.few') && $('.addnew', $target).length === 2) { + // This assumes we're not moving things between different lists + evt.oldIndex -= 1; + evt.newIndex -= 1; + } + + var data = { + csrfToken: $target.data('csrfToken'), + movenode: 'movenode', // That's the submit button.. + from: evt.oldIndex, + to: evt.newIndex + }; + + icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'post'); + } + }, + /** * Add 'hovered' class to hovered title elements * diff --git a/public/js/vendor/jquery.fn.sortable.js b/public/js/vendor/jquery.fn.sortable.js new file mode 100644 index 0000000..d2b308c --- /dev/null +++ b/public/js/vendor/jquery.fn.sortable.js @@ -0,0 +1,1704 @@ +/** + * jQuery plugin for Sortable + * @author RubaXa + * @license MIT + */ +(function (factory) { + "use strict"; + + if (typeof define === "function" && define.amd) { + define(["jquery"], factory); + } + else { + /* jshint sub:true */ + factory(jQuery); + } +})(function ($) { + "use strict"; + + + var dragEl, + parentEl, + ghostEl, + cloneEl, + rootEl, + nextEl, + lastDownEl, + + scrollEl, + scrollParentEl, + scrollCustomFn, + + lastEl, + lastCSS, + lastParentCSS, + + oldIndex, + newIndex, + + activeGroup, + putSortable, + + autoScrolls = [], + + pointerElemChangedInterval, + lastPointerElemX, + lastPointerElemY, + + tapEvt, + touchEvt, + + moved, + + forRepaintDummy, + + /** @const */ + R_SPACE = /\s+/g, + R_FLOAT = /left|right|inline/, + + expando = 'Sortable' + (new Date).getTime(), + + win = window, + document = win.document, + parseInt = win.parseInt, + setTimeout = win.setTimeout, + + $ = win.jQuery || win.Zepto, + Polymer = win.Polymer, + + captureMode = false, + passiveMode = false, + + supportDraggable = ('draggable' in document.createElement('div')), + supportCssPointerEvents = (function (el) { + // false when IE11 + if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) { + return false; + } + el = document.createElement('x'); + el.style.cssText = 'pointer-events:auto'; + return el.style.pointerEvents === 'auto'; + })(), + + _silent = false, + + abs = Math.abs, + min = Math.min, + + savedInputChecked = [], + touchDragOverListeners = [], + + alwaysFalse = function () { return false; }, + + _getParentAutoScrollElement = function(rootEl, includeSelf) { + // will skip to window in _autoScroll + if (!rootEl || !rootEl.getBoundingClientRect) return; + + var elem = rootEl; + var gotSelf = false; + do { + if ( + (elem.clientWidth < elem.scrollWidth) || + (elem.clientHeight < elem.scrollHeight) + ) { + if (!elem || !elem.getBoundingClientRect || elem === document.body) return; + + if (gotSelf || includeSelf) return elem; + gotSelf = true; + } + + } while (elem = elem.parentNode); + }, + + _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { + // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 + if (options.scroll) { + var _this = rootEl ? rootEl[expando] : window, + rect, + sens = options.scrollSensitivity, + speed = options.scrollSpeed, + + x = evt.clientX, + y = evt.clientY, + + winWidth = window.innerWidth, + winHeight = window.innerHeight, + + vx, + vy, + + scrollOffsetX, + scrollOffsetY + ; + + // Detect scrollEl + if (scrollParentEl !== rootEl) { + _clearAutoScrolls(); + + scrollEl = options.scroll; + scrollCustomFn = options.scrollFn; + + if (scrollEl === true) { + scrollEl = _getParentAutoScrollElement(rootEl, true); + scrollParentEl = scrollEl; + } + } + + + var layersOut = 0; + var currentParent = scrollEl; + do { + var el; + + if (currentParent) { + el = currentParent; + rect = currentParent.getBoundingClientRect(); + vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); + vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); + } + + + if (!(vx || vy)) { + vx = (winWidth - x <= sens) - (x <= sens); + vy = (winHeight - y <= sens) - (y <= sens); + + /* jshint expr:true */ + (vx || vy) && (el = win); + } + + if (!autoScrolls[layersOut]) { + for (var i = 0; i <= layersOut; i++) { + if (!autoScrolls[i]) { + autoScrolls[i] = {}; + } + } + } + + if (autoScrolls[layersOut].vx !== vx || autoScrolls[layersOut].vy !== vy || autoScrolls[layersOut].el !== el) { + autoScrolls[layersOut].el = el; + autoScrolls[layersOut].vx = vx; + autoScrolls[layersOut].vy = vy; + + clearInterval(autoScrolls[layersOut].pid); + + if (el) { + autoScrolls[layersOut].pid = setInterval((function () { + scrollOffsetY = autoScrolls[this.layersOut].vy ? autoScrolls[this.layersOut].vy * speed : 0; + scrollOffsetX = autoScrolls[this.layersOut].vx ? autoScrolls[this.layersOut].vx * speed : 0; + + if ('function' === typeof(scrollCustomFn)) { + if (scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt, touchEvt, autoScrolls[this.layersOut].el) !== 'continue') { + return; + } + } + + if (autoScrolls[this.layersOut].el === win) { + win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); + } else { + autoScrolls[this.layersOut].el.scrollTop += scrollOffsetY; + autoScrolls[this.layersOut].el.scrollLeft += scrollOffsetX; + } + }).bind({layersOut: layersOut}), 24); + } + } + layersOut++; + } while (options.bubbleScroll && (currentParent = _getParentAutoScrollElement(currentParent, false))); + } + }, 30), + + _clearAutoScrolls = function () { + autoScrolls.forEach(function(autoScroll) { + clearInterval(autoScroll.pid); + }); + autoScrolls = []; + }, + + _prepareGroup = function (options) { + function toFn(value, pull) { + return function(to, from, dragEl, evt) { + var ret; + + if (value == null || value === false) { + ret = false; + } else if (pull && value === 'clone') { + ret = value; + } else if (typeof value === 'function') { + ret = value(to, from, dragEl, evt); + } else { + var otherGroup = (pull ? to : from).options.group.name; + + ret = (value === true || + (typeof value === 'string' && value === otherGroup) || + (value.join && value.indexOf(otherGroup) > -1)); + } + + return ret || to.options.group.name === from.options.group.name; + } + } + + var group = {}; + var originalGroup = options.group; + + if (!originalGroup || typeof originalGroup != 'object') { + originalGroup = {name: originalGroup}; + } + + group.name = originalGroup.name; + group.checkPull = toFn(originalGroup.pull, true); + group.checkPut = toFn(originalGroup.put); + group.revertClone = originalGroup.revertClone; + + options.group = group; + } + ; + + // Detect support a passive mode + try { + window.addEventListener('test', null, Object.defineProperty({}, 'passive', { + get: function () { + // `false`, because everything starts to work incorrectly and instead of d'n'd, + // begins the page has scrolled. + passiveMode = false; + captureMode = { + capture: false, + passive: passiveMode + }; + } + })); + } catch (err) {} + + /** + * @class Sortable + * @param {HTMLElement} el + * @param {Object} [options] + */ + function Sortable(el, options) { + if (!(el && el.nodeType && el.nodeType === 1)) { + throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); + } + + this.el = el; // root element + this.options = options = _extend({}, options); + + + // Export instance + el[expando] = this; + + // Default options + var defaults = { + group: null, + sort: true, + disabled: false, + store: null, + handle: null, + scroll: true, + scrollSensitivity: 30, + scrollSpeed: 10, + bubbleScroll: true, + draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + ignore: 'a, img', + filter: null, + preventOnFilter: true, + animation: 0, + setData: function (dataTransfer, dragEl) { + dataTransfer.setData('Text', dragEl.textContent); + }, + dropBubble: false, + dragoverBubble: false, + dataIdAttr: 'data-id', + delay: 0, + touchStartThreshold: parseInt(window.devicePixelRatio, 10) || 1, + forceFallback: false, + fallbackClass: 'sortable-fallback', + fallbackOnBody: false, + fallbackTolerance: 0, + fallbackOffset: {x: 0, y: 0}, + supportPointer: Sortable.supportPointer !== false + }; + + + // Set default options + for (var name in defaults) { + !(name in options) && (options[name] = defaults[name]); + } + + _prepareGroup(options); + + // Bind all private methods + for (var fn in this) { + if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { + this[fn] = this[fn].bind(this); + } + } + + // Setup drag mode + this.nativeDraggable = options.forceFallback ? false : supportDraggable; + + // Bind events + _on(el, 'mousedown', this._onTapStart); + _on(el, 'touchstart', this._onTapStart); + options.supportPointer && _on(el, 'pointerdown', this._onTapStart); + + if (this.nativeDraggable) { + _on(el, 'dragover', this); + _on(el, 'dragenter', this); + } + + touchDragOverListeners.push(this._onDragOver); + + // Restore sorting + options.store && this.sort(options.store.get(this)); + } + + + Sortable.prototype = /** @lends Sortable.prototype */ { + constructor: Sortable, + + _onTapStart: function (/** Event|TouchEvent */evt) { + var _this = this, + el = this.el, + options = this.options, + preventOnFilter = options.preventOnFilter, + type = evt.type, + touch = evt.touches && evt.touches[0], + target = (touch || evt).target, + originalTarget = evt.target.shadowRoot && ((evt.path && evt.path[0]) || (evt.composedPath && evt.composedPath()[0])) || target, + filter = options.filter, + startIndex; + + _saveInputCheckedState(el); + + + // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. + if (dragEl) { + return; + } + + if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { + return; // only left button or enabled + } + + // cancel dnd if original target is content editable + if (originalTarget.isContentEditable) { + return; + } + + target = _closest(target, options.draggable, el); + + if (!target) { + return; + } + + if (lastDownEl === target) { + // Ignoring duplicate `down` + return; + } + + // Get the index of the dragged element within its parent + startIndex = _index(target, options.draggable); + + // Check filter + if (typeof filter === 'function') { + if (filter.call(this, evt, target, this)) { + _dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex); + preventOnFilter && evt.preventDefault(); + return; // cancel dnd + } + } + else if (filter) { + filter = filter.split(',').some(function (criteria) { + criteria = _closest(originalTarget, criteria.trim(), el); + + if (criteria) { + _dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex); + return true; + } + }); + + if (filter) { + preventOnFilter && evt.preventDefault(); + return; // cancel dnd + } + } + + if (options.handle && !_closest(originalTarget, options.handle, el)) { + return; + } + + // Prepare `dragstart` + this._prepareDragStart(evt, touch, target, startIndex); + }, + + + _handleAutoScroll: function(evt) { + if (!dragEl || !this.options.scroll || (this.options.supportPointer && evt.type == 'touchmove')) return; + var + x = (evt.touches ? evt.touches[0] : evt).clientX, + y = (evt.touches ? evt.touches[0] : evt).clientY, + + elem = document.elementFromPoint(x, y), + _this = this + ; + + + // touch does not have native autoscroll, even with DnD enabled + if (!_this.nativeDraggable || evt.touches || (evt.pointerType && evt.pointerType == 'touch')) { + + _autoScroll(evt.touches ? evt.touches[0] : evt, _this.options, elem); + + // Listener for pointer element change + var ogElemScroller = _getParentAutoScrollElement(elem, true); + if (!pointerElemChangedInterval || + x != lastPointerElemX || + y != lastPointerElemY) { + + pointerElemChangedInterval && clearInterval(pointerElemChangedInterval); + // Detect for pointer elem change, emulating native DnD behaviour + pointerElemChangedInterval = setInterval(function() { + if (!dragEl) return; + var newElem = _getParentAutoScrollElement(document.elementFromPoint(x, y), true); + if (newElem != ogElemScroller) { + ogElemScroller = newElem; + _clearAutoScrolls(); + _autoScroll(evt.touches ? evt.touches[0] : evt, _this.options, ogElemScroller); + } + }, 10); + lastPointerElemX = x; + lastPointerElemY = y; + } + + } else { + // if DnD is enabled, first autoscroll will already scroll, so get parent autoscroll of first autoscroll + if (!_this.options.bubbleScroll) return; + _autoScroll(evt, _this.options, _getParentAutoScrollElement(elem, false)); + } + }, + + _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) { + var _this = this, + el = _this.el, + options = _this.options, + ownerDocument = el.ownerDocument, + dragStartFn; + + if (target && !dragEl && (target.parentNode === el)) { + tapEvt = evt; + + rootEl = el; + dragEl = target; + parentEl = dragEl.parentNode; + nextEl = dragEl.nextSibling; + lastDownEl = target; + activeGroup = options.group; + oldIndex = startIndex; + + this._lastX = (touch || evt).clientX; + this._lastY = (touch || evt).clientY; + + dragEl.style['will-change'] = 'all'; + + dragStartFn = function () { + // Delayed drag has been triggered + // we can re-enable the events: touchmove/mousemove + _this._disableDelayedDrag(); + + // Make the element draggable + dragEl.draggable = _this.nativeDraggable; + + // Bind the events: dragstart/dragend + _this._triggerDragStart(evt, touch); + + // Drag start event + _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex); + + // Chosen item + _toggleClass(dragEl, options.chosenClass, true); + }; + + // Disable "draggable" + options.ignore.split(',').forEach(function (criteria) { + _find(dragEl, criteria.trim(), _disableDraggable); + }); + + _on(ownerDocument, 'mouseup', _this._onDrop); + _on(ownerDocument, 'touchend', _this._onDrop); + _on(ownerDocument, 'touchcancel', _this._onDrop); + _on(ownerDocument, 'selectstart', _this); + options.supportPointer && _on(ownerDocument, 'pointercancel', _this._onDrop); + + if (options.delay) { + // If the user moves the pointer or let go the click or touch + // before the delay has been reached: + // disable the delayed drag + _on(ownerDocument, 'mouseup', _this._disableDelayedDrag); + _on(ownerDocument, 'touchend', _this._disableDelayedDrag); + _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); + _on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler); + _on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler); + options.supportPointer && _on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler); + + _this._dragStartTimer = setTimeout(dragStartFn.bind(_this), options.delay); + } else { + dragStartFn(); + } + + + } + }, + + _delayedDragTouchMoveHandler: function (/** TouchEvent|PointerEvent **/e) { + var touch = e.touches ? e.touches[0] : e; + if (min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) + >= this.options.touchStartThreshold + ) { + this._disableDelayedDrag(); + } + }, + + _disableDelayedDrag: function () { + var ownerDocument = this.el.ownerDocument; + + clearTimeout(this._dragStartTimer); + _off(ownerDocument, 'mouseup', this._disableDelayedDrag); + _off(ownerDocument, 'touchend', this._disableDelayedDrag); + _off(ownerDocument, 'touchcancel', this._disableDelayedDrag); + _off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler); + _off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler); + _off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler); + }, + + _triggerDragStart: function (/** Event */evt, /** Touch */touch) { + touch = touch || (evt.pointerType == 'touch' ? evt : null); + + + if (touch) { + // Touch device support + tapEvt = { + target: dragEl, + clientX: touch.clientX, + clientY: touch.clientY + }; + + this._onDragStart(tapEvt, 'touch'); + } + else if (!this.nativeDraggable) { + this._onDragStart(tapEvt, true); + } + else { + _on(dragEl, 'dragend', this); + _on(rootEl, 'dragstart', this._onDragStart); + } + + try { + if (document.selection) { + // Timeout neccessary for IE9 + _nextTick(function () { + document.selection.empty(); + }); + } else { + window.getSelection().removeAllRanges(); + } + } catch (err) { + } + }, + + _dragStarted: function () { + if (rootEl && dragEl) { + _on(document, 'drag', this._handleAutoScroll); + var options = this.options; + + // Apply effect + _toggleClass(dragEl, options.ghostClass, true); + _toggleClass(dragEl, options.dragClass, false); + + Sortable.active = this; + + // Drag start event + _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex); + } else { + this._nulling(); + } + }, + + _emulateDragOver: function () { + if (touchEvt) { + if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) { + return; + } + + this._lastX = touchEvt.clientX; + this._lastY = touchEvt.clientY; + + if (!supportCssPointerEvents) { + _css(ghostEl, 'display', 'none'); + } + + var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + var parent = target; + + while (target && target.shadowRoot) { + target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + parent = target; + } + + if (parent) { + do { + if (parent[expando]) { + var i = touchDragOverListeners.length; + while (i--) { + touchDragOverListeners[i]({ + clientX: touchEvt.clientX, + clientY: touchEvt.clientY, + target: target, + rootEl: parent + }); + } + + if (!this.options.dragoverBubble) { + break; + } + } + + target = parent; // store last element + } + /* jshint boss:true */ + while (parent = parent.parentNode); + } + + if (!supportCssPointerEvents) { + _css(ghostEl, 'display', ''); + } + } + }, + + + _onTouchMove: function (/**TouchEvent*/evt) { + if (tapEvt) { + var options = this.options, + fallbackTolerance = options.fallbackTolerance, + fallbackOffset = options.fallbackOffset, + touch = evt.touches ? evt.touches[0] : evt, + dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, + dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, + translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; + + // only set the status to dragging, when we are actually dragging + if (!Sortable.active) { + if (fallbackTolerance && + min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance + ) { + return; + } + + this._dragStarted(); + } + + // as well as creating the ghost element on the document body + this._appendGhost(); + + moved = true; + touchEvt = touch; + + _css(ghostEl, 'webkitTransform', translate3d); + _css(ghostEl, 'mozTransform', translate3d); + _css(ghostEl, 'msTransform', translate3d); + _css(ghostEl, 'transform', translate3d); + + evt.preventDefault(); + } + }, + + _appendGhost: function () { + if (!ghostEl) { + var rect = dragEl.getBoundingClientRect(), + css = _css(dragEl), + options = this.options, + ghostRect; + + ghostEl = dragEl.cloneNode(true); + + _toggleClass(ghostEl, options.ghostClass, false); + _toggleClass(ghostEl, options.fallbackClass, true); + _toggleClass(ghostEl, options.dragClass, true); + + _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); + _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); + _css(ghostEl, 'width', rect.width); + _css(ghostEl, 'height', rect.height); + _css(ghostEl, 'opacity', '0.8'); + _css(ghostEl, 'position', 'fixed'); + _css(ghostEl, 'zIndex', '100000'); + _css(ghostEl, 'pointerEvents', 'none'); + + options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl); + + // Fixing dimensions. + ghostRect = ghostEl.getBoundingClientRect(); + _css(ghostEl, 'width', rect.width * 2 - ghostRect.width); + _css(ghostEl, 'height', rect.height * 2 - ghostRect.height); + } + }, + + _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { + var _this = this; + var dataTransfer = evt.dataTransfer; + var options = _this.options; + + _this._offUpEvents(); + + if (activeGroup.checkPull(_this, _this, dragEl, evt)) { + cloneEl = _clone(dragEl); + + cloneEl.draggable = false; + cloneEl.style['will-change'] = ''; + + this._hideClone(); + + _toggleClass(cloneEl, _this.options.chosenClass, false); + + // #1143: IFrame support workaround + _this._cloneId = _nextTick(function () { + rootEl.insertBefore(cloneEl, dragEl); + _dispatchEvent(_this, rootEl, 'clone', dragEl); + }); + } + + _toggleClass(dragEl, options.dragClass, true); + + if (useFallback) { + if (useFallback === 'touch') { + // Bind touch events + _on(document, 'touchmove', _this._onTouchMove); + // onTouchMove before handleAutoScroll in this case, because onTouchMove sets touchEvt + _on(document, 'touchmove', _this._handleAutoScroll); + _on(document, 'touchend', _this._onDrop); + _on(document, 'touchcancel', _this._onDrop); + + if (options.supportPointer) { + _on(document, 'pointermove', _this._handleAutoScroll); + _on(document, 'pointermove', _this._onTouchMove); + _on(document, 'pointerup', _this._onDrop); + } + } else { + // Old brwoser + _on(document, 'mousemove', _this._handleAutoScroll); + _on(document, 'mousemove', _this._onTouchMove); + _on(document, 'mouseup', _this._onDrop); + } + + _this._loopId = setInterval(_this._emulateDragOver, 50); + } + else { + if (dataTransfer) { + dataTransfer.effectAllowed = 'move'; + options.setData && options.setData.call(_this, dataTransfer, dragEl); + } + + _on(document, 'drop', _this); + + // #1143: Бывает элемент с IFrame внутри блокирует `drop`, + // поэтому если вызвался `mouseover`, значит надо отменять весь d'n'd. + // Breaking Chrome 62+ + // _on(document, 'mouseover', _this); + + _this._dragStartId = _nextTick(_this._dragStarted); + } + }, + + _onDragOver: function (/**Event*/evt) { + var el = this.el, + target, + dragRect, + targetRect, + revert, + options = this.options, + group = options.group, + activeSortable = Sortable.active, + isOwner = (activeGroup === group), + isMovingBetweenSortable = false, + canSort = options.sort; + + if (evt.preventDefault !== void 0) { + evt.preventDefault(); + !options.dragoverBubble && evt.stopPropagation(); + } + + if (dragEl.animated) { + return; + } + + moved = true; + + target = evt.target == el ? evt.target : _closest(evt.target, options.draggable, el); + + if (target === el) return; + + if (activeSortable && !options.disabled && + (isOwner + ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list + : ( + putSortable === this || + ( + (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && + group.checkPut(this, activeSortable, dragEl, evt) + ) + ) + ) && + (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback + ) { + if (_silent) { + return; + } + + dragRect = dragEl.getBoundingClientRect(); + + if (putSortable !== this) { + putSortable = this; + isMovingBetweenSortable = true; + } + + if (revert) { + this._hideClone(); + parentEl = rootEl; // actualization + + if (cloneEl || nextEl) { + rootEl.insertBefore(dragEl, cloneEl || nextEl); + } + else if (!canSort) { + rootEl.appendChild(dragEl); + } + + return; + } + + + if ((el.children.length === 0) || (el.children[0] === ghostEl) || + (el === evt.target) && (_ghostIsLast(el, evt)) + ) { + //assign target only if condition is true + if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) { + target = el.lastElementChild; + } + + if (target) { + if (target.animated) { + return; + } + + targetRect = target.getBoundingClientRect(); + } + + if (isOwner) { + activeSortable._hideClone(); + } else { + activeSortable._showClone(this); + } + + if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) { + if (!dragEl.contains(el)) { + el.appendChild(dragEl); + parentEl = el; // actualization + } + + this._animate(dragRect, dragEl); + target && this._animate(targetRect, target); + } + } + else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) { + if (lastEl !== target) { + lastEl = target; + lastCSS = _css(target); + lastParentCSS = _css(target.parentNode); + } + + targetRect = target.getBoundingClientRect(); + + var width = targetRect.right - targetRect.left, + height = targetRect.bottom - targetRect.top, + floating = R_FLOAT.test(lastCSS.cssFloat + lastCSS.display) + || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), + isWide = (target.offsetWidth > dragEl.offsetWidth), + isLong = (target.offsetHeight > dragEl.offsetHeight), + halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, + nextSibling = target.nextElementSibling, + after = false + ; + + if (floating) { + var elTop = dragEl.offsetTop, + tgTop = target.offsetTop; + + if (elTop === tgTop) { + after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide; + } + else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) { + after = (evt.clientY - targetRect.top) / height > 0.5; + } else { + after = tgTop > elTop; + } + } else if (!isMovingBetweenSortable) { + after = (nextSibling !== dragEl) && !isLong || halfway && isLong; + } + + var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); + + if (moveVector !== false) { + if (moveVector === 1 || moveVector === -1) { + after = (moveVector === 1); + } + + _silent = true; + setTimeout(_unsilent, 30); + + if (isOwner) { + activeSortable._hideClone(); + } else { + activeSortable._showClone(this); + } + + if (!dragEl.contains(el)) { + if (after && !nextSibling) { + el.appendChild(dragEl); + } else { + target.parentNode.insertBefore(dragEl, after ? nextSibling : target); + } + } + + parentEl = dragEl.parentNode; // actualization + + this._animate(dragRect, dragEl); + this._animate(targetRect, target); + } + } + } + }, + + _animate: function (prevRect, target) { + var ms = this.options.animation; + + if (ms) { + var currentRect = target.getBoundingClientRect(); + + if (prevRect.nodeType === 1) { + prevRect = prevRect.getBoundingClientRect(); + } + + _css(target, 'transition', 'none'); + _css(target, 'transform', 'translate3d(' + + (prevRect.left - currentRect.left) + 'px,' + + (prevRect.top - currentRect.top) + 'px,0)' + ); + + forRepaintDummy = target.offsetWidth; // repaint + + _css(target, 'transition', 'all ' + ms + 'ms'); + _css(target, 'transform', 'translate3d(0,0,0)'); + + clearTimeout(target.animated); + target.animated = setTimeout(function () { + _css(target, 'transition', ''); + _css(target, 'transform', ''); + target.animated = false; + }, ms); + } + }, + + _offUpEvents: function () { + var ownerDocument = this.el.ownerDocument; + + _off(document, 'touchmove', this._handleAutoScroll); + _off(document, 'pointermove', this._handleAutoScroll); + _off(document, 'mousemove', this._handleAutoScroll); + _off(document, 'touchmove', this._onTouchMove); + _off(document, 'pointermove', this._onTouchMove); + _off(ownerDocument, 'mouseup', this._onDrop); + _off(ownerDocument, 'touchend', this._onDrop); + _off(ownerDocument, 'pointerup', this._onDrop); + _off(ownerDocument, 'touchcancel', this._onDrop); + _off(ownerDocument, 'pointercancel', this._onDrop); + _off(ownerDocument, 'selectstart', this); + }, + + _onDrop: function (/**Event*/evt) { + var el = this.el, + options = this.options; + + clearInterval(this._loopId); + + clearInterval(pointerElemChangedInterval); + _clearAutoScrolls(); + _cancelThrottle(); + + clearTimeout(this._dragStartTimer); + + _cancelNextTick(this._cloneId); + _cancelNextTick(this._dragStartId); + + // Unbind events + _off(document, 'mouseover', this); + _off(document, 'mousemove', this._onTouchMove); + + + if (this.nativeDraggable) { + _off(document, 'drop', this); + _off(el, 'dragstart', this._onDragStart); + _off(document, 'drag', this._handleAutoScroll); + } + + this._offUpEvents(); + + if (evt) { + if (moved) { + evt.preventDefault(); + !options.dropBubble && evt.stopPropagation(); + } + + ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); + + if (rootEl === parentEl || this.lastPutMode !== 'clone') { + // Remove clone + cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); + } + + if (dragEl) { + if (this.nativeDraggable) { + _off(dragEl, 'dragend', this); + } + + _disableDraggable(dragEl); + dragEl.style['will-change'] = ''; + + // Remove class's + _toggleClass(dragEl, this.options.ghostClass, false); + _toggleClass(dragEl, this.options.chosenClass, false); + + // Drag stop event + _dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex, null, evt); + + if (rootEl !== parentEl) { + newIndex = _index(dragEl, options.draggable); + + if (newIndex >= 0) { + // Add event + _dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + + // Remove event + _dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + + // drag from one list and drop into another + _dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + } + } + else { + if (dragEl.nextSibling !== nextEl) { + // Get the index of the dragged element within its parent + newIndex = _index(dragEl, options.draggable); + + if (newIndex >= 0) { + // drag & drop within the same list + _dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + } + } + } + + if (Sortable.active) { + /* jshint eqnull:true */ + if (newIndex == null || newIndex === -1) { + newIndex = oldIndex; + } + + _dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + + // Save sorting + this.save(); + } + } + + } + + this._nulling(); + }, + + _nulling: function() { + rootEl = + dragEl = + parentEl = + ghostEl = + nextEl = + cloneEl = + lastDownEl = + + scrollEl = + scrollParentEl = + + tapEvt = + touchEvt = + + moved = + newIndex = + + lastEl = + lastCSS = + + putSortable = + activeGroup = + Sortable.active = null; + + savedInputChecked.forEach(function (el) { + el.checked = true; + }); + savedInputChecked.length = 0; + }, + + handleEvent: function (/**Event*/evt) { + switch (evt.type) { + case 'drop': + case 'dragend': + this._onDrop(evt); + break; + + case 'dragover': + case 'dragenter': + if (dragEl) { + this._onDragOver(evt); + _globalDragOver(evt); + } + break; + + case 'mouseover': + this._onDrop(evt); + break; + + case 'selectstart': + evt.preventDefault(); + break; + } + }, + + + /** + * Serializes the item into an array of string. + * @returns {String[]} + */ + toArray: function () { + var order = [], + el, + children = this.el.children, + i = 0, + n = children.length, + options = this.options; + + for (; i < n; i++) { + el = children[i]; + if (_closest(el, options.draggable, this.el)) { + order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); + } + } + + return order; + }, + + + /** + * Sorts the elements according to the array. + * @param {String[]} order order of the items + */ + sort: function (order) { + var items = {}, rootEl = this.el; + + this.toArray().forEach(function (id, i) { + var el = rootEl.children[i]; + + if (_closest(el, this.options.draggable, rootEl)) { + items[id] = el; + } + }, this); + + order.forEach(function (id) { + if (items[id]) { + rootEl.removeChild(items[id]); + rootEl.appendChild(items[id]); + } + }); + }, + + + /** + * Save the current sorting + */ + save: function () { + var store = this.options.store; + store && store.set(this); + }, + + + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * @param {HTMLElement} el + * @param {String} [selector] default: `options.draggable` + * @returns {HTMLElement|null} + */ + closest: function (el, selector) { + return _closest(el, selector || this.options.draggable, this.el); + }, + + + /** + * Set/get option + * @param {string} name + * @param {*} [value] + * @returns {*} + */ + option: function (name, value) { + var options = this.options; + + if (value === void 0) { + return options[name]; + } else { + options[name] = value; + + if (name === 'group') { + _prepareGroup(options); + } + } + }, + + + /** + * Destroy + */ + destroy: function () { + var el = this.el; + + el[expando] = null; + + _off(el, 'mousedown', this._onTapStart); + _off(el, 'touchstart', this._onTapStart); + _off(el, 'pointerdown', this._onTapStart); + + if (this.nativeDraggable) { + _off(el, 'dragover', this); + _off(el, 'dragenter', this); + } + + // Remove draggable attributes + Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { + el.removeAttribute('draggable'); + }); + + touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); + + this._onDrop(); + + this.el = el = null; + }, + + _hideClone: function() { + if (!cloneEl.cloneHidden) { + _css(cloneEl, 'display', 'none'); + cloneEl.cloneHidden = true; + } + }, + + _showClone: function(putSortable) { + if (putSortable.lastPutMode !== 'clone') return; + + if (cloneEl.cloneHidden) { + // show clone at dragEl or original position + rootEl.insertBefore(cloneEl, rootEl.contains(dragEl) && !this.options.group.revertClone ? dragEl : nextEl); + + if (this.options.group.revertClone) { + this._animate(dragEl, cloneEl); + } + _css(cloneEl, 'display', ''); + cloneEl.cloneHidden = false; + } + } + }; + + function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) { + if (el) { + ctx = ctx || document; + + do { + if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { + return el; + } + /* jshint boss:true */ + } while (el = _getParentOrHost(el)); + } + + return null; + } + + + function _getParentOrHost(el) { + return (el.host && el !== document && el.host.nodeType) + ? el.host + : el.parentNode; + } + + + function _globalDragOver(/**Event*/evt) { + if (evt.dataTransfer) { + evt.dataTransfer.dropEffect = 'move'; + } + evt.preventDefault(); + } + + + function _on(el, event, fn) { + el.addEventListener(event, fn, captureMode); + } + + + function _off(el, event, fn) { + el.removeEventListener(event, fn, captureMode); + } + + + function _toggleClass(el, name, state) { + if (el) { + if (el.classList) { + el.classList[state ? 'add' : 'remove'](name); + } + else { + var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); + el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); + } + } + } + + + function _css(el, prop, val) { + var style = el && el.style; + + if (style) { + if (val === void 0) { + if (document.defaultView && document.defaultView.getComputedStyle) { + val = document.defaultView.getComputedStyle(el, ''); + } + else if (el.currentStyle) { + val = el.currentStyle; + } + + return prop === void 0 ? val : val[prop]; + } + else { + if (!(prop in style)) { + prop = '-webkit-' + prop; + } + + style[prop] = val + (typeof val === 'string' ? '' : 'px'); + } + } + } + + + function _find(ctx, tagName, iterator) { + if (ctx) { + var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; + + if (iterator) { + for (; i < n; i++) { + iterator(list[i], i); + } + } + + return list; + } + + return []; + } + + + + function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex, originalEvt) { + sortable = (sortable || rootEl[expando]); + + var evt = document.createEvent('Event'), + options = sortable.options, + onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); + + evt.initEvent(name, true, true); + + evt.to = toEl || rootEl; + evt.from = fromEl || rootEl; + evt.item = targetEl || rootEl; + evt.clone = cloneEl; + + evt.oldIndex = startIndex; + evt.newIndex = newIndex; + + evt.originalEvent = originalEvt; + + rootEl.dispatchEvent(evt); + + if (options[onName]) { + options[onName].call(sortable, evt); + } + } + + + function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) { + var evt, + sortable = fromEl[expando], + onMoveFn = sortable.options.onMove, + retVal; + + evt = document.createEvent('Event'); + evt.initEvent('move', true, true); + + evt.to = toEl; + evt.from = fromEl; + evt.dragged = dragEl; + evt.draggedRect = dragRect; + evt.related = targetEl || toEl; + evt.relatedRect = targetRect || toEl.getBoundingClientRect(); + evt.willInsertAfter = willInsertAfter; + + evt.originalEvent = originalEvt; + + fromEl.dispatchEvent(evt); + + if (onMoveFn) { + retVal = onMoveFn.call(sortable, evt, originalEvt); + } + + return retVal; + } + + + function _disableDraggable(el) { + el.draggable = false; + } + + + function _unsilent() { + _silent = false; + } + + + /** @returns {HTMLElement|false} */ + function _ghostIsLast(el, evt) { + var lastEl = el.lastElementChild, + rect = lastEl.getBoundingClientRect(); + + // 5 — min delta + // abs — нельзя добавлять, а то глюки при наведении сверху + return (evt.clientY - (rect.top + rect.height) > 5) || + (evt.clientX - (rect.left + rect.width) > 5); + } + + + /** + * Generate id + * @param {HTMLElement} el + * @returns {String} + * @private + */ + function _generateId(el) { + var str = el.tagName + el.className + el.src + el.href + el.textContent, + i = str.length, + sum = 0; + + while (i--) { + sum += str.charCodeAt(i); + } + + return sum.toString(36); + } + + /** + * Returns the index of an element within its parent for a selected set of + * elements + * @param {HTMLElement} el + * @param {selector} selector + * @return {number} + */ + function _index(el, selector) { + var index = 0; + + if (!el || !el.parentNode) { + return -1; + } + + while (el && (el = el.previousElementSibling)) { + if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) { + index++; + } + } + + return index; + } + + function _matches(/**HTMLElement*/el, /**String*/selector) { + if (el) { + try { + if (el.matches) { + return el.matches(selector); + } else if (el.msMatchesSelector) { + return el.msMatchesSelector(selector); + } + } catch(_) { + return false; + } + } + + return false; + } + + var _throttleTimeout; + function _throttle(callback, ms) { + return function () { + if (!_throttleTimeout) { + var args = arguments, + _this = this + ; + + _throttleTimeout = setTimeout(function () { + if (args.length === 1) { + callback.call(_this, args[0]); + } else { + callback.apply(_this, args); + } + + _throttleTimeout = void 0; + }, ms); + } + }; + } + + function _cancelThrottle() { + clearTimeout(_throttleTimeout); + _throttleTimeout = void 0; + } + + function _extend(dst, src) { + if (dst && src) { + for (var key in src) { + if (src.hasOwnProperty(key)) { + dst[key] = src[key]; + } + } + } + + return dst; + } + + function _clone(el) { + if (Polymer && Polymer.dom) { + return Polymer.dom(el).cloneNode(true); + } + else if ($) { + return $(el).clone(true)[0]; + } + else { + return el.cloneNode(true); + } + } + + function _saveInputCheckedState(root) { + savedInputChecked.length = 0; + + var inputs = root.getElementsByTagName('input'); + var idx = inputs.length; + + while (idx--) { + var el = inputs[idx]; + el.checked && savedInputChecked.push(el); + } + } + + function _nextTick(fn) { + return setTimeout(fn, 0); + } + + function _cancelNextTick(id) { + return clearTimeout(id); + } + + // Fixed #973: + _on(document, 'touchmove', function (evt) { + if (Sortable.active) { + evt.preventDefault(); + } + }); + + // Export utils + Sortable.utils = { + on: _on, + off: _off, + css: _css, + find: _find, + is: function (el, selector) { + return !!_closest(el, selector, el); + }, + extend: _extend, + throttle: _throttle, + closest: _closest, + toggleClass: _toggleClass, + clone: _clone, + index: _index, + nextTick: _nextTick, + cancelNextTick: _cancelNextTick + }; + + + /** + * Create sortable instance + * @param {HTMLElement} el + * @param {Object} [options] + */ + Sortable.create = function (el, options) { + return new Sortable(el, options); + }; + + + + + + /** + * jQuery plugin for Sortable + * @param {Object|String} options + * @param {..*} [args] + * @returns {jQuery|*} + */ + $.fn.sortable = function (options) { + var retVal, + args = arguments; + + this.each(function () { + var $el = $(this), + sortable = $el.data('sortable'); + + if (!sortable && (options instanceof Object || !options)) { + sortable = new Sortable(this, options); + $el.data('sortable', sortable); + } + + if (sortable) { + if (options === 'widget') { + retVal = sortable; + } + else if (options === 'destroy') { + sortable.destroy(); + $el.removeData('sortable'); + } + else if (typeof sortable[options] === 'function') { + retVal = sortable[options].apply(sortable, [].slice.call(args, 1)); + } + else if (options in sortable.options) { + retVal = sortable.option.apply(sortable, args); + } + } + }); + + return (retVal === void 0) ? this : retVal; + }; +}); diff --git a/public/js/vendor/jquery.fn.sortable.min.js b/public/js/vendor/jquery.fn.sortable.min.js new file mode 100644 index 0000000..b0eeaa5 --- /dev/null +++ b/public/js/vendor/jquery.fn.sortable.min.js @@ -0,0 +1,3 @@ +/*! Sortable 1.7.0 - MIT | git://github.com/rubaxa/Sortable.git */ + +!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)}(function(r){"use strict";var C,E,x,O,P,N,d,b,y,D,k,A,B,c,o,M,Y,a,l,s,h,T,X,t,i,S=[],u=/\s+/g,I=/left|right|inline/,R="Sortable"+(new Date).getTime(),w=window,p=w.document,f=w.parseInt,H=w.setTimeout,e=(r=w.jQuery||w.Zepto,w.Polymer),g=!1,n=!1,v="draggable"in p.createElement("div"),m=!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)&&((t=p.createElement("x")).style.cssText="pointer-events:auto","auto"===t.style.pointerEvents),F=!1,L=Math.abs,_=Math.min,j=[],W=[],U=function(t,e){if(t&&t.getBoundingClientRect){var n=t,o=!1;do{if(n.clientWidth*",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,touchStartThreshold:f(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==G.supportPointer};for(var o in n)!(o in e)&&(e[o]=n[o]);for(var i in z(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&v,Z(t,"mousedown",this._onTapStart),Z(t,"touchstart",this._onTapStart),e.supportPointer&&Z(t,"pointerdown",this._onTapStart),this.nativeDraggable&&(Z(t,"dragover",this),Z(t,"dragenter",this)),W.push(this._onDragOver),e.store&&this.sort(e.store.get(this))}function Q(t,e,n){if(t){n=n||p;do{if(">*"===e&&t.parentNode===n||lt(t,e))return t}while(t=(o=t).host&&o!==p&&o.host.nodeType?o.host:o.parentNode)}var o;return null}function Z(t,e,n){t.addEventListener(e,n,g)}function J(t,e,n){t.removeEventListener(e,n,g)}function K(t,e,n){if(t)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(u," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(u," ")}}function $(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return p.defaultView&&p.defaultView.getComputedStyle?n=p.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function tt(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i*"!==e&&!lt(t,e)||n++;return n}function lt(t,e){if(t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e)}catch(t){return!1}return!1}function st(n,o){return function(){if(!i){var t=arguments,e=this;i=H(function(){1===t.length?n.call(e,t[0]):n.apply(e,t),i=void 0},o)}}}function ct(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function ht(t){return e&&e.dom?e.dom(t).cloneNode(!0):r?r(t).clone(!0)[0]:t.cloneNode(!0)}function dt(t){return H(t,0)}function ut(t){return clearTimeout(t)}G.prototype={constructor:G,_onTapStart:function(t){var e,n=this,o=this.el,i=this.options,r=i.preventOnFilter,a=t.type,l=t.touches&&t.touches[0],s=(l||t).target,c=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||s,h=i.filter;if(function(t){j.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&j.push(o)}}(o),!C&&!(/mousedown|pointerdown/.test(a)&&0!==t.button||i.disabled)&&!c.isContentEditable&&(s=Q(s,i.draggable,o))&&d!==s){if(e=at(s,i.draggable),"function"==typeof h){if(h.call(this,t,s,this))return et(n,c,"filter",s,o,o,e),void(r&&t.preventDefault())}else if(h&&(h=h.split(",").some(function(t){if(t=Q(c,t.trim(),o))return et(n,t,"filter",s,o,o,e),!0})))return void(r&&t.preventDefault());i.handle&&!Q(c,i.handle,o)||this._prepareDragStart(t,l,s,e)}},_handleAutoScroll:function(e){if(C&&this.options.scroll&&(!this.options.supportPointer||"touchmove"!=e.type)){var n=(e.touches?e.touches[0]:e).clientX,o=(e.touches?e.touches[0]:e).clientY,t=p.elementFromPoint(n,o),i=this;if(!i.nativeDraggable||e.touches||e.pointerType&&"touch"==e.pointerType){V(e.touches?e.touches[0]:e,i.options,t);var r=U(t,!0);a&&n==l&&o==s||(a&&clearInterval(a),a=setInterval(function(){if(C){var t=U(p.elementFromPoint(n,o),!0);t!=r&&(r=t,q(),V(e.touches?e.touches[0]:e,i.options,r))}},10),l=n,s=o)}else{if(!i.options.bubbleScroll)return;V(e,i.options,U(t,!1))}}},_prepareDragStart:function(t,e,n,o){var i,r=this,a=r.el,l=r.options,s=a.ownerDocument;n&&!C&&n.parentNode===a&&(h=t,P=a,E=(C=n).parentNode,N=C.nextSibling,d=n,M=l.group,c=o,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,C.style["will-change"]="all",i=function(){r._disableDelayedDrag(),C.draggable=r.nativeDraggable,r._triggerDragStart(t,e),et(r,P,"choose",C,P,P,c),K(C,l.chosenClass,!0)},l.ignore.split(",").forEach(function(t){tt(C,t.trim(),ot)}),Z(s,"mouseup",r._onDrop),Z(s,"touchend",r._onDrop),Z(s,"touchcancel",r._onDrop),Z(s,"selectstart",r),l.supportPointer&&Z(s,"pointercancel",r._onDrop),l.delay?(Z(s,"mouseup",r._disableDelayedDrag),Z(s,"touchend",r._disableDelayedDrag),Z(s,"touchcancel",r._disableDelayedDrag),Z(s,"mousemove",r._delayedDragTouchMoveHandler),Z(s,"touchmove",r._delayedDragTouchMoveHandler),l.supportPointer&&Z(s,"pointermove",r._delayedDragTouchMoveHandler),r._dragStartTimer=H(i.bind(r),l.delay)):i())},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;_(L(e.clientX-this._lastX),L(e.clientY-this._lastY))>=this.options.touchStartThreshold&&this._disableDelayedDrag()},_disableDelayedDrag:function(){var t=this.el.ownerDocument;clearTimeout(this._dragStartTimer),J(t,"mouseup",this._disableDelayedDrag),J(t,"touchend",this._disableDelayedDrag),J(t,"touchcancel",this._disableDelayedDrag),J(t,"mousemove",this._delayedDragTouchMoveHandler),J(t,"touchmove",this._delayedDragTouchMoveHandler),J(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){(e=e||("touch"==t.pointerType?t:null))?(h={target:C,clientX:e.clientX,clientY:e.clientY},this._onDragStart(h,"touch")):this.nativeDraggable?(Z(C,"dragend",this),Z(P,"dragstart",this._onDragStart)):this._onDragStart(h,!0);try{p.selection?dt(function(){p.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(){if(P&&C){Z(p,"drag",this._handleAutoScroll);var t=this.options;K(C,t.ghostClass,!0),K(C,t.dragClass,!1),et(G.active=this,P,"start",C,P,P,c)}else this._nulling()},_emulateDragOver:function(){if(T){if(this._lastX===T.clientX&&this._lastY===T.clientY)return;this._lastX=T.clientX,this._lastY=T.clientY,m||$(x,"display","none");for(var t=p.elementFromPoint(T.clientX,T.clientY),e=t;t&&t.shadowRoot;)e=t=t.shadowRoot.elementFromPoint(T.clientX,T.clientY);if(e)do{if(e[R]){for(var n=W.length;n--;)W[n]({clientX:T.clientX,clientY:T.clientY,target:t,rootEl:e});if(!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);m||$(x,"display","")}},_onTouchMove:function(t){if(h){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=i.clientX-h.clientX+o.x,a=i.clientY-h.clientY+o.y,l=t.touches?"translate3d("+r+"px,"+a+"px,0)":"translate("+r+"px,"+a+"px)";if(!G.active){if(n&&_(L(i.clientX-this._lastX),L(i.clientY-this._lastY))C.offsetWidth,_=e.offsetHeight>C.offsetHeight,b=.5<(v?(t.clientX-o.left)/f:(t.clientY-o.top)/g),y=e.nextElementSibling,D=!1;if(v){var T=C.offsetTop,S=e.offsetTop;D=T===S?e.previousElementSibling===C&&!m||b&&m:e.previousElementSibling===C||C.previousElementSibling===e?.5<(t.clientY-o.top)/g:T Date: Mon, 17 Dec 2018 14:43:40 +0100 Subject: [PATCH 02/99] TileRenderer: Allow to reorder tiles by using drag&drop --- library/Businessprocess/Renderer/TileRenderer.php | 10 ++++++++-- .../Businessprocess/Renderer/TileRenderer/NodeTile.php | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index 856fde6..b6843ac 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Businessprocess\Renderer; use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile; +use Icinga\Module\Businessprocess\Web\Form\CsrfToken; use ipl\Html\Html; class TileRenderer extends Renderer @@ -16,8 +17,13 @@ class TileRenderer extends Renderer $nodesDiv = Html::tag( 'div', [ - 'class' => ['tiles', $this->howMany()], - 'data-base-target' => '_next' + 'class' => ['sortable', 'tiles', $this->howMany()], + 'data-base-target' => '_next', + 'data-sortable-disabled' => $this->isLocked(), + 'data-sortable-data-id-attr' => 'id', + 'data-sortable-filter' => '.addnew', + 'data-csrf-token' => CsrfToken::generate(), + 'data-action-url' => $this->getUrl()->getAbsoluteUrl() ] ); diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 1f7fb8e..56eb8ee 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -73,6 +73,9 @@ class NodeTile extends BaseHtmlElement $attributes = $this->getAttributes(); $attributes->add('class', $renderer->getNodeClasses($node)); $attributes->add('id', 'bp-' . (string) $node); + if (! $renderer->isLocked()) { + $attributes->add('data-node-name', (string) $node); + } $this->addActions(); From c7f25ba0c4519ca878065efa9c50b3ae06c00a69 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 17 Dec 2018 14:45:01 +0100 Subject: [PATCH 03/99] MetaData: Introduce new header option `ManualOrder' --- library/Businessprocess/Metadata.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/Businessprocess/Metadata.php b/library/Businessprocess/Metadata.php index 85e4f83..b640fb8 100644 --- a/library/Businessprocess/Metadata.php +++ b/library/Businessprocess/Metadata.php @@ -22,6 +22,7 @@ class Metadata 'AddToMenu' => null, 'Backend' => null, 'Statetype' => null, + 'ManualOrder' => null, // 'SLAHosts' => null ); @@ -251,6 +252,11 @@ class Metadata return false; } + public function isManuallyOrdered() + { + return $this->get('ManualOrder') === 'yes'; + } + protected function splitCommaSeparated($string) { return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY); From 876a577e85ab6637d9e59e4f15262263cda5de20 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 17 Dec 2018 14:45:56 +0100 Subject: [PATCH 04/99] Don't sort processes in case manual sorting has been applied --- application/forms/AddNodeForm.php | 4 +++- application/forms/EditNodeForm.php | 4 +++- library/Businessprocess/BpConfig.php | 33 +++++++++++++++++++++++++--- library/Businessprocess/BpNode.php | 9 ++++++-- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index ed3199b..42778ca 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -424,7 +424,9 @@ class AddNodeForm extends QuickForm } } - natcasesort($list); + if (! $this->bp->getMetadata()->isManuallyOrdered()) { + natcasesort($list); + } return $list; } diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php index 87b804a..5267c12 100644 --- a/application/forms/EditNodeForm.php +++ b/application/forms/EditNodeForm.php @@ -362,7 +362,9 @@ class EditNodeForm extends QuickForm } } - natcasesort($list); + if (! $this->bp->getMetadata()->isManuallyOrdered()) { + natcasesort($list); + } return $list; } diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index ae3ac9c..03db018 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -388,14 +388,32 @@ class BpConfig */ public function getRootNodes() { - ksort($this->root_nodes, SORT_NATURAL | SORT_FLAG_CASE); + if ($this->getMetadata()->isManuallyOrdered()) { + uasort($this->root_nodes, function (BpNode $a, BpNode $b) { + $a = $a->getDisplay(); + $b = $b->getDisplay(); + return $a > $b ? 1 : ($a < $b ? -1 : 0); + }); + } else { + ksort($this->root_nodes, SORT_NATURAL | SORT_FLAG_CASE); + } + return $this->root_nodes; } public function listRootNodes() { $names = array_keys($this->root_nodes); - natcasesort($names); + if ($this->getMetadata()->isManuallyOrdered()) { + uasort($names, function ($a, $b) { + $a = $this->root_nodes[$a]->getDisplay(); + $b = $this->root_nodes[$b]->getDisplay(); + return $a > $b ? 1 : ($a < $b ? -1 : 0); + }); + } else { + natcasesort($names); + } + return $names; } @@ -685,7 +703,16 @@ class BpConfig $nodes[$name] = $name === $alias ? $name : sprintf('%s (%s)', $alias, $node); } - natcasesort($nodes); + if ($this->getMetadata()->isManuallyOrdered()) { + uasort($nodes, function ($a, $b) { + $a = $this->nodes[$a]->getDisplay(); + $b = $this->nodes[$b]->getDisplay(); + return $a > $b ? 1 : ($a < $b ? -1 : 0); + }); + } else { + natcasesort($nodes); + } + return $nodes; } diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index 9441619..1ce5f0b 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -426,7 +426,10 @@ class BpNode extends Node public function setChildNames($names) { - natcasesort($names); + if (! $this->bp->getMetadata()->isManuallyOrdered()) { + natcasesort($names); + } + $this->childNames = $names; $this->children = null; return $this; @@ -446,7 +449,9 @@ class BpNode extends Node { if ($this->children === null) { $this->children = array(); - natcasesort($this->childNames); + if (! $this->bp->getMetadata()->isManuallyOrdered()) { + natcasesort($this->childNames); + } foreach ($this->childNames as $name) { $this->children[$name] = $this->bp->getNode($name); $this->children[$name]->addParent($this); From abafbacf1a75c9db1dffe3574db10e8d1e05dadc Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 17 Dec 2018 14:46:45 +0100 Subject: [PATCH 05/99] CsrfToken: Fix exception when validating tokens --- library/Businessprocess/Web/Form/CsrfToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Businessprocess/Web/Form/CsrfToken.php b/library/Businessprocess/Web/Form/CsrfToken.php index ce2288f..9eb24ef 100644 --- a/library/Businessprocess/Web/Form/CsrfToken.php +++ b/library/Businessprocess/Web/Form/CsrfToken.php @@ -17,7 +17,7 @@ class CsrfToken return false; } - list($seed, $token) = explode('|', $elementValue); + list($seed, $token) = explode('|', $token); if (!is_numeric($seed)) { return false; From c609f0c6b3c455d81ce3684c3449f50121b702a1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 17 Dec 2018 14:48:03 +0100 Subject: [PATCH 06/99] ProcessChanges: Add new action to apply manual sorting --- .../NodeApplyManualOrderAction.php | 31 +++++++++++++++++++ .../Modification/ProcessChanges.php | 10 ++++++ 2 files changed, 41 insertions(+) create mode 100644 library/Businessprocess/Modification/NodeApplyManualOrderAction.php diff --git a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php new file mode 100644 index 0000000..b2251ce --- /dev/null +++ b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php @@ -0,0 +1,31 @@ +getMetadata()->get('ManualOrder') !== 'yes'; + } + + public function applyTo(BpConfig $config) + { + $i = 0; + foreach ($config->getRootNodes() as $name => $node) { + /** @var BpNode $node */ + if ($node->getDisplay() > 0) { + $node->setDisplay(++$i); + } + + if ($node->hasChildren()) { + $node->setChildNames($node->getChildNames()); + } + } + + $config->getMetadata()->set('ManualOrder', 'yes'); + } +} diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php index 115b1dd..8a9f618 100644 --- a/library/Businessprocess/Modification/ProcessChanges.php +++ b/library/Businessprocess/Modification/ProcessChanges.php @@ -120,6 +120,16 @@ class ProcessChanges return $this->push($action); } + /** + * Apply manual order on the entire bp configuration file + * + * @return $this + */ + public function applyManualOrder() + { + return $this->push(new NodeApplyManualOrderAction()); + } + /** * Add a new action to the stack * From 0707d1d7e79f8b304d944153cf63830ed523e673 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 17 Dec 2018 14:48:46 +0100 Subject: [PATCH 07/99] ProcessChanges: Add new action to move processes/nodes --- .../Modification/NodeMoveAction.php | 125 ++++++++++++++++++ .../Modification/ProcessChanges.php | 20 +++ 2 files changed, 145 insertions(+) create mode 100644 library/Businessprocess/Modification/NodeMoveAction.php diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php new file mode 100644 index 0000000..ebe5aea --- /dev/null +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -0,0 +1,125 @@ +parentName = $name; + } + + public function getParentName() + { + return $this->parentName; + } + + public function setFrom($from) + { + $this->from = (int) $from; + } + + public function getFrom() + { + return $this->from; + } + + public function setTo($to) + { + $this->to = (int) $to; + } + + public function getTo() + { + return $this->to; + } + + public function appliesTo(BpConfig $config) + { + if (! $config->getMetadata()->isManuallyOrdered()) { + return false; + } + + $name = $this->getNodeName(); + if ($this->parentName !== null) { + if (! $config->hasBpNode($this->parentName)) { + return false; + } + $parent = $config->getBpNode($this->parentName); + if (! $parent->hasChild($name)) { + return false; + } + + $nodes = $parent->getChildNames(); + if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) { + return false; + } + } else { + if (! $config->hasNode($name)) { + return false; + } + + if ($config->getBpNode($name)->getDisplay() !== $this->getFrom()) { + return false; + } + } + + return true; + } + + public function applyTo(BpConfig $config) + { + $name = $this->getNodeName(); + if ($this->parentName !== null) { + $nodes = $config->getBpNode($this->parentName)->getChildren(); + } else { + $nodes = $config->getRootNodes(); + } + + $node = $nodes[$name]; + $nodes = array_merge( + array_slice($nodes, 0, $this->from, true), + array_slice($nodes, $this->from + 1, null, true) + ); + $nodes = array_merge( + array_slice($nodes, 0, $this->to, true), + [$name => $node], + array_slice($nodes, $this->to, null, true) + ); + + if ($this->parentName !== null) { + $config->getBpNode($this->parentName)->setChildNames(array_keys($nodes)); + } else { + $i = 0; + foreach ($nodes as $name => $node) { + /** @var BpNode $node */ + if ($node->getDisplay() > 0) { + $i += 1; + if ($node->getDisplay() !== $i) { + $node->setDisplay($i); + } + } + } + } + } +} diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php index 8a9f618..8dc324a 100644 --- a/library/Businessprocess/Modification/ProcessChanges.php +++ b/library/Businessprocess/Modification/ProcessChanges.php @@ -120,6 +120,26 @@ class ProcessChanges return $this->push($action); } + /** + * Move the given node + * + * @param Node $node + * @param int $from + * @param int $to + * @param string $parentName + * + * @return $this + */ + public function moveNode(Node $node, $from, $to, $parentName = null) + { + $action = new NodeMoveAction($node); + $action->setParentName($parentName); + $action->setFrom($from); + $action->setTo($to); + + return $this->push($action); + } + /** * Apply manual order on the entire bp configuration file * From 08bfbc462b8a3fe5860d60075bc656e662097e17 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 17 Dec 2018 14:49:26 +0100 Subject: [PATCH 08/99] Save user defined order of processes/nodes --- application/controllers/ProcessController.php | 8 + application/forms/MoveNodeForm.php | 147 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 application/forms/MoveNodeForm.php diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 61633d1..6ba5168 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -250,6 +250,14 @@ class ProcessController extends Controller ->setNode($bp->getNode($this->params->get('simulationnode'))) ->setSimulation(Simulation::fromSession($this->session())) ->handleRequest(); + } elseif ($action === 'move') { + $form = $this->loadForm('MoveNode') + ->setProcess($bp) + ->setParentNode($node) + ->setSession($this->session()) + ->setNode($bp->getNode($this->params->get('movenode'))) + ->setSuccessUrl(Url::fromRequest()->without(['action', 'movenode'])) + ->handleRequest(); } if ($form) { diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php new file mode 100644 index 0000000..04f648e --- /dev/null +++ b/application/forms/MoveNodeForm.php @@ -0,0 +1,147 @@ +addPrefixPaths(array( + array( + 'prefix' => 'Icinga\\Web\\Form\\Element\\', + 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Element'), + 'type' => static::ELEMENT + ), + array( + 'prefix' => 'Icinga\\Web\\Form\\Decorator\\', + 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Decorator'), + 'type' => static::DECORATOR + ) + )); + } + + public function setup() + { + $this->addElement( + 'number', + 'from', + [ + 'required' => true, + 'min' => 0 + ] + ); + $this->addElement( + 'number', + 'to', + [ + 'required' => true, + 'min' => 0 + ] + ); + $this->addElement( + 'hidden', + 'csrfToken', + [ + 'required' => true + ] + ); + + $this->setSubmitLabel('movenode'); + } + + /** + * @param BpConfig $process + * @return $this + */ + public function setProcess(BpConfig $process) + { + $this->bp = $process; + return $this; + } + + /** + * @param Node $node + * @return $this + */ + public function setNode(Node $node) + { + $this->node = $node; + return $this; + } + + /** + * @param BpNode|null $node + * @return $this + */ + public function setParentNode(BpNode $node = null) + { + $this->parentNode = $node; + return $this; + } + + /** + * @param SessionNamespace $session + * @return $this + */ + public function setSession(SessionNamespace $session) + { + $this->session = $session; + return $this; + } + + public function onSuccess() + { + if (! CsrfToken::isValid($this->getValue('csrfToken'))) { + throw new HttpException(403, 'nope'); + } + + $changes = ProcessChanges::construct($this->bp, $this->session); + if (! $this->bp->getMetadata()->isManuallyOrdered()) { + $changes->applyManualOrder(); + } + + $changes->moveNode( + $this->node, + $this->getValue('from'), + $this->getValue('to'), + $this->parentNode !== null ? $this->parentNode->getName() : null + ); + + // Trigger session destruction to make sure it get's stored. + unset($changes); + + $this->setSuccessMessage($this->translate('Node order updated')); + parent::onSuccess(); + } + + public function hasBeenSent() + { + return true; // This form has no id + } +} From 67ba5205e22a66bdd457ff3698d26bc3ff2fad0e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 20 Dec 2018 10:08:22 +0100 Subject: [PATCH 09/99] js: Upgrade Sortable.js to latest version (1.8.0-rc1) --- configuration.php | 1 + public/js/vendor/Sortable.js | 1940 ++++++++++++++++++++ public/js/vendor/jquery.fn.sortable.js | 1712 +---------------- public/js/vendor/jquery.fn.sortable.min.js | 3 - 4 files changed, 1983 insertions(+), 1673 deletions(-) create mode 100644 public/js/vendor/Sortable.js delete mode 100644 public/js/vendor/jquery.fn.sortable.min.js diff --git a/configuration.php b/configuration.php index 358287d..f09ee04 100644 --- a/configuration.php +++ b/configuration.php @@ -58,5 +58,6 @@ $this->provideRestriction( $this->translate('Restrict access to configurations with the given prefix') ); +$this->provideJsFile('vendor/Sortable.js'); $this->provideJsFile('behavior/sortable.js'); $this->provideJsFile('vendor/jquery.fn.sortable.js'); \ No newline at end of file diff --git a/public/js/vendor/Sortable.js b/public/js/vendor/Sortable.js new file mode 100644 index 0000000..33e639a --- /dev/null +++ b/public/js/vendor/Sortable.js @@ -0,0 +1,1940 @@ +/**! + * Sortable + * @author RubaXa + * @author owenm + * @license MIT + */ + +(function sortableModule(factory) { + "use strict"; + + if (typeof define === "function" && define.amd) { + define(factory); + } + else if (typeof module != "undefined" && typeof module.exports != "undefined") { + module.exports = factory(); + } + else { + /* jshint sub:true */ + window["Sortable"] = factory(); + } +})(function sortableFactory() { + "use strict"; + + if (typeof window === "undefined" || !window.document) { + return function sortableError() { + throw new Error("Sortable.js requires a window with a document"); + }; + } + + var dragEl, + parentEl, + ghostEl, + cloneEl, + rootEl, + nextEl, + lastDownEl, + + scrollEl, + scrollParentEl, + scrollCustomFn, + + oldIndex, + newIndex, + + activeGroup, + putSortable, + + autoScrolls = [], + scrolling = false, + + pointerElemChangedInterval, + lastPointerElemX, + lastPointerElemY, + + tapEvt, + touchEvt, + + moved, + + lastTarget, + lastDirection, + pastFirstInvertThresh = false, + isCircumstantialInvert = false, + + forRepaintDummy, + realDragElRect, // dragEl rect after current animation + + /** @const */ + R_SPACE = /\s+/g, + + expando = 'Sortable' + (new Date).getTime(), + + win = window, + document = win.document, + parseInt = win.parseInt, + setTimeout = win.setTimeout, + + $ = win.jQuery || win.Zepto, + Polymer = win.Polymer, + + captureMode = { + capture: false, + passive: false + }, + + supportDraggable = ('draggable' in document.createElement('div')), + supportCssPointerEvents = (function (el) { + // false when IE11 + if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) { + return false; + } + el = document.createElement('x'); + el.style.cssText = 'pointer-events:auto'; + return el.style.pointerEvents === 'auto'; + })(), + + _silent = false, + _alignedSilent = false, + + abs = Math.abs, + min = Math.min, + + savedInputChecked = [], + touchDragOverListeners = [], + + alwaysFalse = function () { return false; }, + + _detectDirection = function(el, options) { + var elCSS = _css(el), + elWidth = parseInt(elCSS.width), + child1 = _getChild(el, 0, options), + child2 = _getChild(el, 1, options), + firstChildCSS = child1 && _css(child1), + secondChildCSS = child2 && _css(child2), + firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + child1.getBoundingClientRect().width, + secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + child2.getBoundingClientRect().width + ; + if (elCSS.display === 'flex') { + return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse' + ? 'vertical' : 'horizontal'; + } + return (child1 && + ( + firstChildCSS.display === 'block' || + firstChildCSS.display === 'grid' || + firstChildWidth >= elWidth && + elCSS.float === 'none' || + child2 && + elCSS.float === 'none' && + firstChildWidth + secondChildWidth > elWidth + ) ? + 'vertical' : 'horizontal' + ); + }, + + _isInRowColumn = function(x, y, el, axis, options) { + var targetRect = realDragElRect || dragEl.getBoundingClientRect(), + targetS1Opp = axis === 'vertical' ? targetRect.left : targetRect.top, + targetS2Opp = axis === 'vertical' ? targetRect.right : targetRect.bottom, + mouseOnOppAxis = axis === 'vertical' ? x : y + ; + + return targetS1Opp < mouseOnOppAxis && mouseOnOppAxis < targetS2Opp; + }, + + _getParentAutoScrollElement = function(el, includeSelf) { + // skip to window + if (!el || !el.getBoundingClientRect) return win; + + var elem = el; + var gotSelf = false; + do { + // we don't need to get elem css if it isn't even overflowing in the first place (performance) + if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) { + var elemCSS = _css(elem); + if ( + elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll') || + elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll') + ) { + if (!elem || !elem.getBoundingClientRect || elem === document.body) return win; + + if (gotSelf || includeSelf) return elem; + gotSelf = true; + } + } + /* jshint boss:true */ + } while (elem = elem.parentNode); + + return win; + }, + + _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl, /**Boolean*/isFallback) { + // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 + if (options.scroll) { + var _this = rootEl ? rootEl[expando] : window, + rect, + css, + sens = options.scrollSensitivity, + speed = options.scrollSpeed, + + x = evt.clientX, + y = evt.clientY, + + winWidth = window.innerWidth, + winHeight = window.innerHeight, + + vx, + vy, + + scrollThisInstance = false + ; + + // Detect scrollEl + if (scrollParentEl !== rootEl) { + _clearAutoScrolls(); + + scrollEl = options.scroll; + scrollCustomFn = options.scrollFn; + + if (scrollEl === true) { + scrollEl = _getParentAutoScrollElement(rootEl, true); + scrollParentEl = scrollEl; + } + } + + + var layersOut = 0; + var currentParent = scrollEl; + do { + var el; + + if (currentParent && currentParent !== win) { + el = currentParent; + css = _css(el); + rect = currentParent.getBoundingClientRect(); + vx = el.clientWidth < el.scrollWidth && (css.overflowX == 'auto' || css.overflowX == 'scroll') && + ((abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens)); + + vy = el.clientHeight < el.scrollHeight && (css.overflowY == 'auto' || css.overflowY == 'scroll') && + ((abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens)); + } else if (currentParent === win) { + el = win; + vx = (winWidth - x <= sens) - (x <= sens); + vy = (winHeight - y <= sens) - (y <= sens); + } + + if (!autoScrolls[layersOut]) { + for (var i = 0; i <= layersOut; i++) { + if (!autoScrolls[i]) { + autoScrolls[i] = {}; + } + } + } + + if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) { + autoScrolls[layersOut].el = el; + autoScrolls[layersOut].vx = vx; + autoScrolls[layersOut].vy = vy; + + clearInterval(autoScrolls[layersOut].pid); + + if (el && (vx != 0 || vy != 0)) { + scrollThisInstance = true; + /* jshint loopfunc:true */ + autoScrolls[layersOut].pid = setInterval((function () { + // emulate drag over during autoscroll (fallback), emulating native DnD behaviour + if (isFallback && this.layer === 0) { + Sortable.active._emulateDragOver(true); + } + var scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0; + var scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0; + + if ('function' === typeof(scrollCustomFn)) { + if (scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt, touchEvt, autoScrolls[this.layer].el) !== 'continue') { + return; + } + } + if (autoScrolls[this.layer].el === win) { + win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); + } else { + autoScrolls[this.layer].el.scrollTop += scrollOffsetY; + autoScrolls[this.layer].el.scrollLeft += scrollOffsetX; + } + }).bind({layer: layersOut}), 24); + } + } + layersOut++; + } while (options.bubbleScroll && currentParent !== win && (currentParent = _getParentAutoScrollElement(currentParent, false))); + scrolling = scrollThisInstance; // in case another function catches scrolling as false in between when it is not + } + }, 30), + + _clearAutoScrolls = function () { + autoScrolls.forEach(function(autoScroll) { + clearInterval(autoScroll.pid); + }); + autoScrolls = []; + }, + + _prepareGroup = function (options) { + function toFn(value, pull) { + return function(to, from, dragEl, evt) { + var ret; + + if (value == null && pull) { + ret = true; // default pull value: true (backwards compatibility) + } else if (value == null || value === false) { + ret = false; + } else if (pull && value === 'clone') { + ret = value; + } else if (typeof value === 'function') { + ret = value(to, from, dragEl, evt); + } else { + var otherGroup = (pull ? to : from).options.group.name; + + ret = (value === true || + (typeof value === 'string' && value === otherGroup) || + (value.join && value.indexOf(otherGroup) > -1)); + } + return ret || (to.options.group.name && from.options.group.name && to.options.group.name === from.options.group.name); + }; + } + + var group = {}; + var originalGroup = options.group; + + if (!originalGroup || typeof originalGroup != 'object') { + originalGroup = {name: originalGroup}; + } + + group.name = originalGroup.name; + group.checkPull = toFn(originalGroup.pull, true); + group.checkPut = toFn(originalGroup.put); + group.revertClone = originalGroup.revertClone; + + options.group = group; + }, + + _checkAlignment = function(evt) { + if (!dragEl) return; + dragEl.parentNode[expando] && dragEl.parentNode[expando]._computeIsAligned(evt); + } + ; + + + /** + * @class Sortable + * @param {HTMLElement} el + * @param {Object} [options] + */ + function Sortable(el, options) { + if (!(el && el.nodeType && el.nodeType === 1)) { + throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); + } + + this.el = el; // root element + this.options = options = _extend({}, options); + + + // Export instance + el[expando] = this; + + // Default options + var defaults = { + group: null, + sort: true, + disabled: false, + store: null, + handle: null, + scroll: true, + scrollSensitivity: 30, + scrollSpeed: 10, + bubbleScroll: true, + draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', + + swapThreshold: 1, // percentage; 0 <= x <= 1 + invertSwap: false, // invert always + invertedSwapThreshold: null, // will be set to same as swapThreshold if default + + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + ignore: 'a, img', + filter: null, + preventOnFilter: true, + animation: 0, + setData: function (dataTransfer, dragEl) { + dataTransfer.setData('Text', dragEl.textContent); + }, + dropBubble: false, + dragoverBubble: false, + dataIdAttr: 'data-id', + delay: 0, + touchStartThreshold: parseInt(window.devicePixelRatio, 10) || 1, + forceFallback: false, + fallbackClass: 'sortable-fallback', + fallbackOnBody: false, + fallbackTolerance: 0, + fallbackOffset: {x: 0, y: 0}, + supportPointer: Sortable.supportPointer !== false && ( + ('PointerEvent' in window) || + window.navigator && ('msPointerEnabled' in window.navigator) // microsoft + ) + }; + + + // Set default options + for (var name in defaults) { + !(name in options) && (options[name] = defaults[name]); + } + + if (!('direction' in options)) { + options.direction = function() { + return _detectDirection(el, options); + }; + } + + _prepareGroup(options); + + options.invertedSwapThreshold == null && (options.invertedSwapThreshold = options.swapThreshold); + // Bind all private methods + for (var fn in this) { + if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { + this[fn] = this[fn].bind(this); + } + } + + // Setup drag mode + this.nativeDraggable = options.forceFallback ? false : supportDraggable; + + // Bind events + _on(el, 'mousedown', this._onTapStart); + _on(el, 'touchstart', this._onTapStart); + options.supportPointer && _on(el, 'pointerdown', this._onTapStart); + + if (this.nativeDraggable) { + _on(el, 'dragover', this); + _on(el, 'dragenter', this); + } + + touchDragOverListeners.push(this._onDragOver); + + // Restore sorting + options.store && options.store.get && this.sort(options.store.get(this) || []); + } + + Sortable.prototype = /** @lends Sortable.prototype */ { + constructor: Sortable, + + // is mouse aligned with dragEl? + _isAligned: true, + + _computeIsAligned: function(evt, isDragEl) { + if (_alignedSilent) return; + if (!dragEl || dragEl.parentNode !== this.el) return; + if (isDragEl !== true && isDragEl !== false) { + isDragEl = !!_closest(evt.target, null, dragEl, true); + } + this._isAligned = !scrolling && (isDragEl || this._isAligned && _isInRowColumn(evt.clientX, evt.clientY, this.el, this._getDirection(evt, null), this.options)); + _alignedSilent = true; + setTimeout(function() { + _alignedSilent = false; + }, 30); + }, + + _getDirection: function(evt, target) { + return (typeof this.options.direction === 'function') ? this.options.direction.call(this, evt, target, dragEl) : this.options.direction; + }, + + _onTapStart: function (/** Event|TouchEvent */evt) { + var _this = this, + el = this.el, + options = this.options, + preventOnFilter = options.preventOnFilter, + type = evt.type, + touch = evt.touches && evt.touches[0], + target = (touch || evt).target, + originalTarget = evt.target.shadowRoot && ((evt.path && evt.path[0]) || (evt.composedPath && evt.composedPath()[0])) || target, + filter = options.filter, + startIndex; + + _saveInputCheckedState(el); + + + // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. + if (dragEl) { + return; + } + + if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { + return; // only left button or enabled + } + + // cancel dnd if original target is content editable + if (originalTarget.isContentEditable) { + return; + } + + target = _closest(target, options.draggable, el, true); + + if (!target) { + return; + } + + if (lastDownEl === target) { + // Ignoring duplicate `down` + return; + } + + // Get the index of the dragged element within its parent + startIndex = _index(target, options.draggable); + + // Check filter + if (typeof filter === 'function') { + if (filter.call(this, evt, target, this)) { + _dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex); + preventOnFilter && evt.cancelable && evt.preventDefault(); + return; // cancel dnd + } + } + else if (filter) { + filter = filter.split(',').some(function (criteria) { + criteria = _closest(originalTarget, criteria.trim(), el, false); + + if (criteria) { + _dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex); + return true; + } + }); + + if (filter) { + preventOnFilter && evt.cancelable && evt.preventDefault(); + return; // cancel dnd + } + } + + if (options.handle && !_closest(originalTarget, options.handle, el, false)) { + return; + } + + // Prepare `dragstart` + this._prepareDragStart(evt, touch, target, startIndex); + }, + + + _handleAutoScroll: function(evt, fallback) { + if (!dragEl || !this.options.scroll) return; + var x = evt.clientX, + y = evt.clientY, + + elem = document.elementFromPoint(x, y), + _this = this + ; + + // firefox does not have native autoscroll + if (fallback || (window.navigator && window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1)) { + _autoScroll(evt, _this.options, elem, true); + + // Listener for pointer element change + var ogElemScroller = _getParentAutoScrollElement(elem, true); + if ( + scrolling && + ( + !pointerElemChangedInterval || + x !== lastPointerElemX || + y !== lastPointerElemY + ) + ) { + + pointerElemChangedInterval && clearInterval(pointerElemChangedInterval); + // Detect for pointer elem change, emulating native DnD behaviour + pointerElemChangedInterval = setInterval(function() { + if (!dragEl) return; + // could also check if scroll direction on newElem changes due to parent autoscrolling + var newElem = _getParentAutoScrollElement(document.elementFromPoint(x, y), true); + if (newElem !== ogElemScroller) { + ogElemScroller = newElem; + _clearAutoScrolls(); + _autoScroll(evt, _this.options, ogElemScroller, true); + } + }, 10); + lastPointerElemX = x; + lastPointerElemY = y; + } + + } else { + // if DnD is enabled (not on firefox), first autoscroll will already scroll, so get parent autoscroll of first autoscroll + if (!_this.options.bubbleScroll || _getParentAutoScrollElement(elem, true) === window) { + _clearAutoScrolls(); + return; + } + _autoScroll(evt, _this.options, _getParentAutoScrollElement(elem, false)); + } + }, + + _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) { + var _this = this, + el = _this.el, + options = _this.options, + ownerDocument = el.ownerDocument, + dragStartFn; + + if (target && !dragEl && (target.parentNode === el)) { + tapEvt = evt; + + rootEl = el; + dragEl = target; + parentEl = dragEl.parentNode; + nextEl = dragEl.nextSibling; + lastDownEl = target; + activeGroup = options.group; + oldIndex = startIndex; + + this._lastX = (touch || evt).clientX; + this._lastY = (touch || evt).clientY; + + dragEl.style['will-change'] = 'all'; + + dragStartFn = function () { + // Delayed drag has been triggered + // we can re-enable the events: touchmove/mousemove + _this._disableDelayedDrag(); + + // Make the element draggable + dragEl.draggable = _this.nativeDraggable; + + // Bind the events: dragstart/dragend + _this._triggerDragStart(evt, touch); + + // Drag start event + _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex); + + // Chosen item + _toggleClass(dragEl, options.chosenClass, true); + }; + + // Disable "draggable" + options.ignore.split(',').forEach(function (criteria) { + _find(dragEl, criteria.trim(), _disableDraggable); + }); + + _on(ownerDocument, 'mouseup', _this._onDrop); + _on(ownerDocument, 'touchend', _this._onDrop); + _on(ownerDocument, 'touchcancel', _this._onDrop); + options.supportPointer && _on(ownerDocument, 'pointercancel', _this._onDrop); + + if (options.delay) { + // If the user moves the pointer or let go the click or touch + // before the delay has been reached: + // disable the delayed drag + _on(ownerDocument, 'mouseup', _this._disableDelayedDrag); + _on(ownerDocument, 'touchend', _this._disableDelayedDrag); + _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); + _on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler); + _on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler); + options.supportPointer && _on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler); + + _this._dragStartTimer = setTimeout(dragStartFn.bind(_this), options.delay); + } else { + dragStartFn(); + } + + + } + }, + + _delayedDragTouchMoveHandler: function (/** TouchEvent|PointerEvent **/e) { + var touch = e.touches ? e.touches[0] : e; + if (min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) + >= this.options.touchStartThreshold + ) { + this._disableDelayedDrag(); + } + }, + + _disableDelayedDrag: function () { + var ownerDocument = this.el.ownerDocument; + + clearTimeout(this._dragStartTimer); + _off(ownerDocument, 'mouseup', this._disableDelayedDrag); + _off(ownerDocument, 'touchend', this._disableDelayedDrag); + _off(ownerDocument, 'touchcancel', this._disableDelayedDrag); + _off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler); + _off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler); + _off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler); + }, + + _triggerDragStart: function (/** Event */evt, /** Touch */touch) { + touch = touch || (evt.pointerType == 'touch' ? evt : null); + + + if (touch) { + // Touch device support + tapEvt = { + target: dragEl, + clientX: touch.clientX, + clientY: touch.clientY + }; + + this._onDragStart(tapEvt, 'touch'); + } + else if (!this.nativeDraggable) { + this._onDragStart(tapEvt, true); + } + else { + _on(dragEl, 'dragend', this); + _on(rootEl, 'dragstart', this._onDragStart); + } + + try { + if (document.selection) { + // Timeout neccessary for IE9 + _nextTick(function () { + document.selection.empty(); + }); + } else { + window.getSelection().removeAllRanges(); + } + } catch (err) { + } + }, + + _dragStarted: function () { + if (rootEl && dragEl) { + if (this.nativeDraggable) { + _on(document, 'dragover', this._handleAutoScroll); + _on(document, 'dragover', _checkAlignment); + } + var options = this.options; + + // Apply effect + _toggleClass(dragEl, options.dragClass, false); + _toggleClass(dragEl, options.ghostClass, true); + + _css(dragEl, 'transform', ''); + + Sortable.active = this; + + this._isAligned = true; + + // Drag start event + _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex); + } else { + this._nulling(); + } + }, + + _emulateDragOver: function (bypassLastTouchCheck) { + if (touchEvt) { + if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY && !bypassLastTouchCheck) { + return; + } + + this._lastX = touchEvt.clientX; + this._lastY = touchEvt.clientY; + + if (!supportCssPointerEvents) { + _css(ghostEl, 'display', 'none'); + } + + var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + var parent = target; + var isDragEl = !!_closest(target, null, dragEl, true); + + while (target && target.shadowRoot) { + target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + parent = target; + } + + if (parent) { + do { + if (parent[expando]) { + var i = touchDragOverListeners.length; + while (i--) { + touchDragOverListeners[i]({ + clientX: touchEvt.clientX, + clientY: touchEvt.clientY, + target: target, + rootEl: parent + }); + } + + if (!this.options.dragoverBubble) { + break; + } + } + + target = parent; // store last element + } + /* jshint boss:true */ + while (parent = parent.parentNode); + } + dragEl.parentNode[expando]._computeIsAligned(touchEvt, isDragEl); + + if (!supportCssPointerEvents) { + _css(ghostEl, 'display', ''); + } + } + }, + + + _onTouchMove: function (/**TouchEvent*/evt) { + if (tapEvt) { + var options = this.options, + fallbackTolerance = options.fallbackTolerance, + fallbackOffset = options.fallbackOffset, + touch = evt.touches ? evt.touches[0] : evt, + dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, + dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, + translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; + + // prevent duplicate event firing + if (this.options.supportPointer && evt.type === 'touchmove') return; + + // only set the status to dragging, when we are actually dragging + if (!Sortable.active) { + if (fallbackTolerance && + min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance + ) { + return; + } + + this._dragStarted(); + } + + // as well as creating the ghost element on the document body + this._appendGhost(); + + this._handleAutoScroll(touch, true); + + + moved = true; + touchEvt = touch; + + _css(ghostEl, 'webkitTransform', translate3d); + _css(ghostEl, 'mozTransform', translate3d); + _css(ghostEl, 'msTransform', translate3d); + _css(ghostEl, 'transform', translate3d); + + evt.cancelable && evt.preventDefault(); + } + }, + + _appendGhost: function () { + if (!ghostEl) { + var rect = dragEl.getBoundingClientRect(), + css = _css(dragEl), + options = this.options; + + ghostEl = dragEl.cloneNode(true); + + _toggleClass(ghostEl, options.ghostClass, false); + _toggleClass(ghostEl, options.fallbackClass, true); + _toggleClass(ghostEl, options.dragClass, true); + + _css(ghostEl, 'box-sizing', 'border-box'); + _css(ghostEl, 'margin', 0); + _css(ghostEl, 'top', rect.top); + _css(ghostEl, 'left', rect.left); + _css(ghostEl, 'width', rect.width); + _css(ghostEl, 'height', rect.height); + _css(ghostEl, 'opacity', '0.8'); + _css(ghostEl, 'position', 'fixed'); + _css(ghostEl, 'zIndex', '100000'); + _css(ghostEl, 'pointerEvents', 'none'); + + options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl); + } + }, + + _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { + var _this = this; + var dataTransfer = evt.dataTransfer; + var options = _this.options; + + _this._offUpEvents(); + if (activeGroup.checkPull(_this, _this, dragEl, evt)) { + cloneEl = _clone(dragEl); + + cloneEl.draggable = false; + cloneEl.style['will-change'] = ''; + + this._hideClone(); + + _toggleClass(cloneEl, _this.options.chosenClass, false); + + // #1143: IFrame support workaround + _this._cloneId = _nextTick(function () { + rootEl.insertBefore(cloneEl, dragEl); + _dispatchEvent(_this, rootEl, 'clone', dragEl); + }); + } + + _toggleClass(dragEl, options.dragClass, true); + + if (useFallback) { + if (useFallback === 'touch') { + // Fixed #973: + _on(document, 'touchmove', _preventScroll); + + // Bind touch events + _on(document, 'touchmove', _this._onTouchMove); + _on(document, 'touchend', _this._onDrop); + _on(document, 'touchcancel', _this._onDrop); + + if (options.supportPointer) { + _on(document, 'pointermove', _this._onTouchMove); + _on(document, 'pointerup', _this._onDrop); + } + } else { + // Old brwoser + _on(document, 'mousemove', _this._onTouchMove); + _on(document, 'mouseup', _this._onDrop); + } + + _this._loopId = setInterval(_this._emulateDragOver, 50); + _toggleClass(dragEl, options.dragClass, false); + } + else { + if (dataTransfer) { + dataTransfer.effectAllowed = 'move'; + options.setData && options.setData.call(_this, dataTransfer, dragEl); + } + + _on(document, 'drop', _this); + + // #1276 fix: + _css(dragEl, 'transform', 'translateZ(0)'); + + _this._dragStartId = _nextTick(_this._dragStarted); + } + _on(document, 'selectstart', _this); + }, + + _onDragOver: function (/**Event*/evt) { + var el = this.el, + target, + dragRect, + targetRect, + revert, + options = this.options, + group = options.group, + activeSortable = Sortable.active, + isOwner = (activeGroup === group), + isMovingBetweenSortable = false, + canSort = options.sort + ; + + + if (evt.rootEl !== void 0 && evt.rootEl !== this.el) return; // touch fallback + + // no bubbling and not fallback + if (!options.dragoverBubble && !evt.rootEl) { + this._handleAutoScroll(evt); + dragEl.parentNode[expando]._computeIsAligned(evt); + } + + if (evt.preventDefault !== void 0) { + evt.cancelable && evt.preventDefault(); + !options.dragoverBubble && evt.stopPropagation(); + } + + + moved = true; + + target = _closest(evt.target, options.draggable, el, true); + + + if (dragEl.animated && target === dragEl || target.animated || _silent) { + return; + } + + + if (target !== lastTarget) { + isCircumstantialInvert = false; + pastFirstInvertThresh = false; + lastTarget = null; + } + + + if (activeSortable && !options.disabled && + (isOwner + ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list + : ( + putSortable === this || + ( + (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && + group.checkPut(this, activeSortable, dragEl, evt) + ) + ) + ) + ) { + var direction; + var axis = this._getDirection(evt, target); + + + dragRect = dragEl.getBoundingClientRect(); + + if (putSortable !== this && this !== Sortable.active) { + putSortable = this; + isMovingBetweenSortable = true; + } else if (this === Sortable.active) { + isMovingBetweenSortable = false; + putSortable = null; + } + + if (revert) { + this._hideClone(); + parentEl = rootEl; // actualization + + if (cloneEl || nextEl) { + rootEl.insertBefore(dragEl, cloneEl || nextEl); + } + else if (!canSort) { + rootEl.appendChild(dragEl); + } + + return; + } + + if ((el.children.length === 0) || (el.children[0] === ghostEl) || + (el === evt.target) && _ghostIsLast(evt, axis, el) + ) { + //assign target only if condition is true + if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) { + target = _lastChild(el); + } + + if (target) { + if (target.animated) { + return; + } + + targetRect = target.getBoundingClientRect(); + } + + if (isOwner) { + activeSortable._hideClone(); + } else { + activeSortable._showClone(this); + } + + if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { + if (!dragEl.contains(el)) { + el.appendChild(dragEl); + parentEl = el; // actualization + this._isAligned = true; // must be for _ghostIsLast to pass + realDragElRect = null; + } + + this._animate(dragRect, dragEl); + target && this._animate(targetRect, target); + } + } + else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0) && target !== el) { + + isCircumstantialInvert = isCircumstantialInvert || options.invertSwap || dragEl.parentNode !== el || !this._isAligned; + direction = _getSwapDirection(evt, target, axis, + options.swapThreshold, options.invertedSwapThreshold, + isCircumstantialInvert, + lastTarget === target); + if (direction === 0) return; + realDragElRect = null; + + + this._isAligned = true; + + if (!lastTarget || lastTarget !== target && (!target || !target.animated)) { + pastFirstInvertThresh = false; + lastTarget = target; + } + + + lastDirection = direction; + + targetRect = target.getBoundingClientRect(); + + var nextSibling = target.nextElementSibling, + after = false + ; + + after = direction === 1; + + var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); + + if (moveVector !== false) { + if (moveVector === 1 || moveVector === -1) { + after = (moveVector === 1); + } + + _silent = true; + setTimeout(_unsilent, 30); + + if (isOwner) { + activeSortable._hideClone(); + } else { + activeSortable._showClone(this); + } + + if (!dragEl.contains(el)) { + if (after && !nextSibling) { + el.appendChild(dragEl); + } else { + target.parentNode.insertBefore(dragEl, after ? nextSibling : target); + } + } + + parentEl = dragEl.parentNode; // actualization + + this._animate(dragRect, dragEl); + this._animate(targetRect, target); + } + } + } + }, + + _animate: function (prevRect, target) { + var ms = this.options.animation; + + if (ms) { + var currentRect = target.getBoundingClientRect(); + + if (target === dragEl) { + realDragElRect = currentRect; + } + + if (prevRect.nodeType === 1) { + prevRect = prevRect.getBoundingClientRect(); + } + + // Check if actually moving position + if ((prevRect.left + prevRect.width / 2) === (currentRect.left + currentRect.width / 2) + && (prevRect.top + prevRect.height / 2) === (currentRect.top + currentRect.height / 2) + ) return; + + _css(target, 'transition', 'none'); + _css(target, 'transform', 'translate3d(' + + (prevRect.left - currentRect.left) + 'px,' + + (prevRect.top - currentRect.top) + 'px,0)' + ); + + forRepaintDummy = target.offsetWidth; // repaint + + _css(target, 'transition', 'all ' + ms + 'ms'); + _css(target, 'transform', 'translate3d(0,0,0)'); + + clearTimeout(target.animated); + target.animated = setTimeout(function () { + _css(target, 'transition', ''); + _css(target, 'transform', ''); + target.animated = false; + }, ms); + } + }, + + _offUpEvents: function () { + var ownerDocument = this.el.ownerDocument; + + _off(document, 'touchmove', _preventScroll); + _off(document, 'touchmove', this._onTouchMove); + _off(document, 'pointermove', this._onTouchMove); + _off(ownerDocument, 'mouseup', this._onDrop); + _off(ownerDocument, 'touchend', this._onDrop); + _off(ownerDocument, 'pointerup', this._onDrop); + _off(ownerDocument, 'touchcancel', this._onDrop); + _off(ownerDocument, 'pointercancel', this._onDrop); + _off(document, 'selectstart', this); + }, + + _onDrop: function (/**Event*/evt) { + var el = this.el, + options = this.options; + scrolling = false; + isCircumstantialInvert = false; + pastFirstInvertThresh = false; + + clearInterval(this._loopId); + + clearInterval(pointerElemChangedInterval); + _clearAutoScrolls(); + _cancelThrottle(); + + clearTimeout(this._dragStartTimer); + + _cancelNextTick(this._cloneId); + _cancelNextTick(this._dragStartId); + + // Unbind events + _off(document, 'mousemove', this._onTouchMove); + + + if (this.nativeDraggable) { + _off(document, 'drop', this); + _off(el, 'dragstart', this._onDragStart); + _off(document, 'dragover', this._handleAutoScroll); + _off(document, 'dragover', _checkAlignment); + } + + this._offUpEvents(); + + if (evt) { + if (moved) { + evt.cancelable && evt.preventDefault(); + !options.dropBubble && evt.stopPropagation(); + } + + ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); + + if (rootEl === parentEl || (putSortable && putSortable.lastPutMode !== 'clone')) { + // Remove clone + cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); + } + + if (dragEl) { + if (this.nativeDraggable) { + _off(dragEl, 'dragend', this); + } + + _disableDraggable(dragEl); + dragEl.style['will-change'] = ''; + + // Remove class's + _toggleClass(dragEl, this.options.ghostClass, false); + _toggleClass(dragEl, this.options.chosenClass, false); + + // Drag stop event + _dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex, null, evt); + + if (rootEl !== parentEl) { + newIndex = _index(dragEl, options.draggable); + + if (newIndex >= 0) { + // Add event + _dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + + // Remove event + _dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + + // drag from one list and drop into another + _dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + } + + putSortable && putSortable.save(); + } + else { + if (dragEl.nextSibling !== nextEl) { + // Get the index of the dragged element within its parent + newIndex = _index(dragEl, options.draggable); + + if (newIndex >= 0) { + // drag & drop within the same list + _dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + } + } + } + + if (Sortable.active) { + /* jshint eqnull:true */ + if (newIndex == null || newIndex === -1) { + newIndex = oldIndex; + } + + _dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); + + // Save sorting + this.save(); + } + } + + } + this._nulling(); + }, + + _nulling: function() { + rootEl = + dragEl = + parentEl = + ghostEl = + nextEl = + cloneEl = + lastDownEl = + + scrollEl = + scrollParentEl = + autoScrolls.length = + + pointerElemChangedInterval = + lastPointerElemX = + lastPointerElemY = + + tapEvt = + touchEvt = + + moved = + newIndex = + oldIndex = + + lastTarget = + lastDirection = + + forRepaintDummy = + realDragElRect = + + putSortable = + activeGroup = + Sortable.active = null; + + savedInputChecked.forEach(function (el) { + el.checked = true; + }); + savedInputChecked.length = 0; + }, + + handleEvent: function (/**Event*/evt) { + switch (evt.type) { + case 'drop': + case 'dragend': + this._onDrop(evt); + break; + + case 'dragenter': + case 'dragover': + if (dragEl) { + this._onDragOver(evt); + _globalDragOver(evt); + } + break; + + case 'selectstart': + evt.preventDefault(); + break; + } + }, + + + /** + * Serializes the item into an array of string. + * @returns {String[]} + */ + toArray: function () { + var order = [], + el, + children = this.el.children, + i = 0, + n = children.length, + options = this.options; + + for (; i < n; i++) { + el = children[i]; + if (_closest(el, options.draggable, this.el, false)) { + order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); + } + } + + return order; + }, + + + /** + * Sorts the elements according to the array. + * @param {String[]} order order of the items + */ + sort: function (order) { + var items = {}, rootEl = this.el; + + this.toArray().forEach(function (id, i) { + var el = rootEl.children[i]; + + if (_closest(el, this.options.draggable, rootEl, false)) { + items[id] = el; + } + }, this); + + order.forEach(function (id) { + if (items[id]) { + rootEl.removeChild(items[id]); + rootEl.appendChild(items[id]); + } + }); + }, + + + /** + * Save the current sorting + */ + save: function () { + var store = this.options.store; + store && store.set && store.set(this); + }, + + + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * @param {HTMLElement} el + * @param {String} [selector] default: `options.draggable` + * @returns {HTMLElement|null} + */ + closest: function (el, selector) { + return _closest(el, selector || this.options.draggable, this.el, false); + }, + + + /** + * Set/get option + * @param {string} name + * @param {*} [value] + * @returns {*} + */ + option: function (name, value) { + var options = this.options; + + if (value === void 0) { + return options[name]; + } else { + options[name] = value; + + if (name === 'group') { + _prepareGroup(options); + } + } + }, + + + /** + * Destroy + */ + destroy: function () { + var el = this.el; + + el[expando] = null; + + _off(el, 'mousedown', this._onTapStart); + _off(el, 'touchstart', this._onTapStart); + _off(el, 'pointerdown', this._onTapStart); + + if (this.nativeDraggable) { + _off(el, 'dragover', this); + _off(el, 'dragenter', this); + } + // Remove draggable attributes + Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { + el.removeAttribute('draggable'); + }); + + touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); + + this._onDrop(); + + this.el = el = null; + }, + + _hideClone: function() { + if (!cloneEl.cloneHidden) { + _css(cloneEl, 'display', 'none'); + cloneEl.cloneHidden = true; + } + }, + + _showClone: function(putSortable) { + if (putSortable.lastPutMode !== 'clone') { + this._hideClone(); + return; + } + + if (cloneEl.cloneHidden) { + // show clone at dragEl or original position + if (rootEl.contains(dragEl) && !this.options.group.revertClone) { + rootEl.insertBefore(cloneEl, dragEl); + } else if (nextEl) { + rootEl.insertBefore(cloneEl, nextEl); + } else { + rootEl.appendChild(cloneEl); + } + + if (this.options.group.revertClone) { + this._animate(dragEl, cloneEl); + } + _css(cloneEl, 'display', ''); + cloneEl.cloneHidden = false; + } + } + }; + + function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx, includeCTX) { + if (el) { + ctx = ctx || document; + + do { + if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector) || (includeCTX && el === ctx)) { + return el; + } + + if (el === ctx) break; + /* jshint boss:true */ + } while (el = _getParentOrHost(el)); + } + + return null; + } + + + function _getParentOrHost(el) { + return (el.host && el !== document && el.host.nodeType) + ? el.host + : el.parentNode; + } + + + function _globalDragOver(/**Event*/evt) { + if (evt.dataTransfer) { + evt.dataTransfer.dropEffect = 'move'; + } + evt.cancelable && evt.preventDefault(); + } + + + function _on(el, event, fn) { + el.addEventListener(event, fn, captureMode); + } + + + function _off(el, event, fn) { + el.removeEventListener(event, fn, captureMode); + } + + + function _toggleClass(el, name, state) { + if (el && name) { + if (el.classList) { + el.classList[state ? 'add' : 'remove'](name); + } + else { + var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); + el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); + } + } + } + + + function _css(el, prop, val) { + var style = el && el.style; + + if (style) { + if (val === void 0) { + if (document.defaultView && document.defaultView.getComputedStyle) { + val = document.defaultView.getComputedStyle(el, ''); + } + else if (el.currentStyle) { + val = el.currentStyle; + } + + return prop === void 0 ? val : val[prop]; + } + else { + if (!(prop in style)) { + prop = '-webkit-' + prop; + } + + style[prop] = val + (typeof val === 'string' ? '' : 'px'); + } + } + } + + + function _find(ctx, tagName, iterator) { + if (ctx) { + var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; + + if (iterator) { + for (; i < n; i++) { + iterator(list[i], i); + } + } + + return list; + } + + return []; + } + + + + function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex, originalEvt) { + sortable = (sortable || rootEl[expando]); + + var evt, + options = sortable.options, + onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); + // Support for new CustomEvent feature + if (window.CustomEvent) { + evt = new CustomEvent(name, { + bubbles: true, + cancelable: true + }); + } else { + evt = document.createEvent('Event'); + evt.initEvent(name, true, true); + } + + evt.to = toEl || rootEl; + evt.from = fromEl || rootEl; + evt.item = targetEl || rootEl; + evt.clone = cloneEl; + + evt.oldIndex = startIndex; + evt.newIndex = newIndex; + + evt.originalEvent = originalEvt; + + rootEl.dispatchEvent(evt); + + if (options[onName]) { + options[onName].call(sortable, evt); + } + } + + + function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) { + var evt, + sortable = fromEl[expando], + onMoveFn = sortable.options.onMove, + retVal; + // Support for new CustomEvent feature + if (window.CustomEvent) { + evt = new CustomEvent('move', { + bubbles: true, + cancelable: true + }); + } else { + evt = document.createEvent('Event'); + evt.initEvent('move', true, true); + } + + evt.to = toEl; + evt.from = fromEl; + evt.dragged = dragEl; + evt.draggedRect = dragRect; + evt.related = targetEl || toEl; + evt.relatedRect = targetRect || toEl.getBoundingClientRect(); + evt.willInsertAfter = willInsertAfter; + + evt.originalEvent = originalEvt; + + fromEl.dispatchEvent(evt); + + if (onMoveFn) { + retVal = onMoveFn.call(sortable, evt, originalEvt); + } + + return retVal; + } + + function _disableDraggable(el) { + el.draggable = false; + } + + function _unsilent() { + _silent = false; + } + + function _getChild(el, childNum, options) { + var currentChild = 0, + i = 0, + children = el.children + ; + while (i < children.length) { + if ( + children[i].style.display !== 'none' && + children[i] !== ghostEl && + children[i] !== dragEl && + _closest(children[i], options.draggable, el, false) + ) { + if (currentChild === childNum) { + return children[i]; + } + currentChild++; + } + + i++; + } + return null; + } + + function _lastChild(el) { + var last = el.lastElementChild; + + if (last === ghostEl) { + last = el.children[el.childElementCount - 2]; + } + + return last || null; + } + + function _ghostIsLast(evt, axis, el) { + var elRect = _lastChild(el).getBoundingClientRect(), + mouseOnAxis = axis === 'vertical' ? evt.clientY : evt.clientX, + mouseOnOppAxis = axis === 'vertical' ? evt.clientX : evt.clientY, + targetS2 = axis === 'vertical' ? elRect.bottom : elRect.right, + targetS1Opp = axis === 'vertical' ? elRect.left : elRect.top, + targetS2Opp = axis === 'vertical' ? elRect.right : elRect.bottom + ; + return ( + mouseOnOppAxis > targetS1Opp && + mouseOnOppAxis < targetS2Opp && + mouseOnAxis > targetS2 + ); + } + + function _getSwapDirection(evt, target, axis, swapThreshold, invertedSwapThreshold, invertSwap, inside) { + var targetRect = target.getBoundingClientRect(), + mouseOnAxis = axis === 'vertical' ? evt.clientY : evt.clientX, + targetLength = axis === 'vertical' ? targetRect.height : targetRect.width, + targetS1 = axis === 'vertical' ? targetRect.top : targetRect.left, + targetS2 = axis === 'vertical' ? targetRect.bottom : targetRect.right, + dragRect = dragEl.getBoundingClientRect(), + dragLength = axis === 'vertical' ? dragRect.height : dragRect.width, + invert = false + ; + var dragStyle = _css(dragEl); + dragLength += parseInt(dragStyle.marginLeft) + parseInt(dragStyle.marginRight); + + if (!invertSwap) { + // Never invert or create dragEl shadow when width causes mouse to move past the end of regular swapThreshold + if (inside && dragLength < targetLength * swapThreshold) { // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2 + // check if past first invert threshold on side opposite of lastDirection + if (!pastFirstInvertThresh && + (lastDirection === 1 ? + ( + mouseOnAxis > targetS1 + targetLength * invertedSwapThreshold / 2 + ) : + ( + mouseOnAxis < targetS2 - targetLength * invertedSwapThreshold / 2 + ) + ) + ) + { + // past first invert threshold, do not restrict inverted threshold to dragEl shadow + pastFirstInvertThresh = true; + } + + if (!pastFirstInvertThresh) { + var dragS1 = axis === 'vertical' ? dragRect.top : dragRect.left, + dragS2 = axis === 'vertical' ? dragRect.bottom : dragRect.right + ; + // dragEl shadow + if ( + lastDirection === 1 ? + ( + mouseOnAxis < targetS1 + dragLength // over dragEl shadow + ) : + ( + mouseOnAxis > targetS2 - dragLength + ) + ) + { + return lastDirection * -1; + } + } else { + invert = true; + } + } else { + // Regular + if ( + mouseOnAxis > targetS1 + (targetLength * (1 - swapThreshold) / 2) && + mouseOnAxis < targetS2 - (targetLength * (1 - swapThreshold) / 2) + ) { + return ((mouseOnAxis > targetS1 + targetLength / 2) ? -1 : 1); + } + } + } + + invert = invert || invertSwap; + + if (invert) { + // Invert of regular + if ( + mouseOnAxis < targetS1 + (targetLength * invertedSwapThreshold / 2) || + mouseOnAxis > targetS2 - (targetLength * invertedSwapThreshold / 2) + ) + { + return ((mouseOnAxis > targetS1 + targetLength / 2) ? 1 : -1); + } + } + + return 0; + } + + + /** + * Generate id + * @param {HTMLElement} el + * @returns {String} + * @private + */ + function _generateId(el) { + var str = el.tagName + el.className + el.src + el.href + el.textContent, + i = str.length, + sum = 0; + + while (i--) { + sum += str.charCodeAt(i); + } + + return sum.toString(36); + } + + /** + * Returns the index of an element within its parent for a selected set of + * elements + * @param {HTMLElement} el + * @param {selector} selector + * @return {number} + */ + function _index(el, selector) { + var index = 0; + + if (!el || !el.parentNode) { + return -1; + } + + while (el && (el = el.previousElementSibling)) { + if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) { + index++; + } + } + + return index; + } + + function _matches(/**HTMLElement*/el, /**String*/selector) { + if (el) { + try { + if (el.matches) { + return el.matches(selector); + } else if (el.msMatchesSelector) { + return el.msMatchesSelector(selector); + } + } catch(_) { + return false; + } + } + + return false; + } + + var _throttleTimeout; + function _throttle(callback, ms) { + return function () { + if (!_throttleTimeout) { + var args = arguments, + _this = this + ; + + _throttleTimeout = setTimeout(function () { + if (args.length === 1) { + callback.call(_this, args[0]); + } else { + callback.apply(_this, args); + } + + _throttleTimeout = void 0; + }, ms); + } + }; + } + + function _cancelThrottle() { + clearTimeout(_throttleTimeout); + _throttleTimeout = void 0; + } + + function _extend(dst, src) { + if (dst && src) { + for (var key in src) { + if (src.hasOwnProperty(key)) { + dst[key] = src[key]; + } + } + } + + return dst; + } + + function _clone(el) { + if (Polymer && Polymer.dom) { + return Polymer.dom(el).cloneNode(true); + } + else if ($) { + return $(el).clone(true)[0]; + } + else { + return el.cloneNode(true); + } + } + + function _saveInputCheckedState(root) { + savedInputChecked.length = 0; + + var inputs = root.getElementsByTagName('input'); + var idx = inputs.length; + + while (idx--) { + var el = inputs[idx]; + el.checked && savedInputChecked.push(el); + } + } + + function _nextTick(fn) { + return setTimeout(fn, 0); + } + + function _cancelNextTick(id) { + return clearTimeout(id); + } + + function _preventScroll(evt) { + if (Sortable.active && evt.cancelable) { + evt.preventDefault(); + } + } + + + // Export utils + Sortable.utils = { + on: _on, + off: _off, + css: _css, + find: _find, + is: function (el, selector) { + return !!_closest(el, selector, el, false); + }, + extend: _extend, + throttle: _throttle, + closest: _closest, + toggleClass: _toggleClass, + clone: _clone, + index: _index, + nextTick: _nextTick, + cancelNextTick: _cancelNextTick, + detectDirection: _detectDirection, + getChild: _getChild + }; + + + /** + * Create sortable instance + * @param {HTMLElement} el + * @param {Object} [options] + */ + Sortable.create = function (el, options) { + return new Sortable(el, options); + }; + + + // Export + Sortable.version = '1.8.0-rc1'; + return Sortable; +}); \ No newline at end of file diff --git a/public/js/vendor/jquery.fn.sortable.js b/public/js/vendor/jquery.fn.sortable.js index d2b308c..cd5189a 100644 --- a/public/js/vendor/jquery.fn.sortable.js +++ b/public/js/vendor/jquery.fn.sortable.js @@ -1,1674 +1,51 @@ -/** - * jQuery plugin for Sortable - * @author RubaXa - * @license MIT - */ (function (factory) { "use strict"; - - if (typeof define === "function" && define.amd) { - define(["jquery"], factory); - } - else { - /* jshint sub:true */ - factory(jQuery); - } -})(function ($) { - "use strict"; - - - var dragEl, - parentEl, - ghostEl, - cloneEl, - rootEl, - nextEl, - lastDownEl, - - scrollEl, - scrollParentEl, - scrollCustomFn, - - lastEl, - lastCSS, - lastParentCSS, - - oldIndex, - newIndex, - - activeGroup, - putSortable, - - autoScrolls = [], - - pointerElemChangedInterval, - lastPointerElemX, - lastPointerElemY, - - tapEvt, - touchEvt, - - moved, - - forRepaintDummy, - - /** @const */ - R_SPACE = /\s+/g, - R_FLOAT = /left|right|inline/, - - expando = 'Sortable' + (new Date).getTime(), - - win = window, - document = win.document, - parseInt = win.parseInt, - setTimeout = win.setTimeout, - - $ = win.jQuery || win.Zepto, - Polymer = win.Polymer, - - captureMode = false, - passiveMode = false, - - supportDraggable = ('draggable' in document.createElement('div')), - supportCssPointerEvents = (function (el) { - // false when IE11 - if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) { - return false; - } - el = document.createElement('x'); - el.style.cssText = 'pointer-events:auto'; - return el.style.pointerEvents === 'auto'; - })(), - - _silent = false, - - abs = Math.abs, - min = Math.min, - - savedInputChecked = [], - touchDragOverListeners = [], - - alwaysFalse = function () { return false; }, - - _getParentAutoScrollElement = function(rootEl, includeSelf) { - // will skip to window in _autoScroll - if (!rootEl || !rootEl.getBoundingClientRect) return; - - var elem = rootEl; - var gotSelf = false; - do { - if ( - (elem.clientWidth < elem.scrollWidth) || - (elem.clientHeight < elem.scrollHeight) - ) { - if (!elem || !elem.getBoundingClientRect || elem === document.body) return; - - if (gotSelf || includeSelf) return elem; - gotSelf = true; - } - - } while (elem = elem.parentNode); - }, - - _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { - // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 - if (options.scroll) { - var _this = rootEl ? rootEl[expando] : window, - rect, - sens = options.scrollSensitivity, - speed = options.scrollSpeed, - - x = evt.clientX, - y = evt.clientY, - - winWidth = window.innerWidth, - winHeight = window.innerHeight, - - vx, - vy, - - scrollOffsetX, - scrollOffsetY - ; - - // Detect scrollEl - if (scrollParentEl !== rootEl) { - _clearAutoScrolls(); - - scrollEl = options.scroll; - scrollCustomFn = options.scrollFn; - - if (scrollEl === true) { - scrollEl = _getParentAutoScrollElement(rootEl, true); - scrollParentEl = scrollEl; - } - } - - - var layersOut = 0; - var currentParent = scrollEl; - do { - var el; - - if (currentParent) { - el = currentParent; - rect = currentParent.getBoundingClientRect(); - vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); - vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); - } - - - if (!(vx || vy)) { - vx = (winWidth - x <= sens) - (x <= sens); - vy = (winHeight - y <= sens) - (y <= sens); - - /* jshint expr:true */ - (vx || vy) && (el = win); - } - - if (!autoScrolls[layersOut]) { - for (var i = 0; i <= layersOut; i++) { - if (!autoScrolls[i]) { - autoScrolls[i] = {}; - } - } - } - - if (autoScrolls[layersOut].vx !== vx || autoScrolls[layersOut].vy !== vy || autoScrolls[layersOut].el !== el) { - autoScrolls[layersOut].el = el; - autoScrolls[layersOut].vx = vx; - autoScrolls[layersOut].vy = vy; - - clearInterval(autoScrolls[layersOut].pid); - - if (el) { - autoScrolls[layersOut].pid = setInterval((function () { - scrollOffsetY = autoScrolls[this.layersOut].vy ? autoScrolls[this.layersOut].vy * speed : 0; - scrollOffsetX = autoScrolls[this.layersOut].vx ? autoScrolls[this.layersOut].vx * speed : 0; - - if ('function' === typeof(scrollCustomFn)) { - if (scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt, touchEvt, autoScrolls[this.layersOut].el) !== 'continue') { - return; - } - } - - if (autoScrolls[this.layersOut].el === win) { - win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); - } else { - autoScrolls[this.layersOut].el.scrollTop += scrollOffsetY; - autoScrolls[this.layersOut].el.scrollLeft += scrollOffsetX; - } - }).bind({layersOut: layersOut}), 24); - } - } - layersOut++; - } while (options.bubbleScroll && (currentParent = _getParentAutoScrollElement(currentParent, false))); - } - }, 30), - - _clearAutoScrolls = function () { - autoScrolls.forEach(function(autoScroll) { - clearInterval(autoScroll.pid); - }); - autoScrolls = []; - }, - - _prepareGroup = function (options) { - function toFn(value, pull) { - return function(to, from, dragEl, evt) { - var ret; - - if (value == null || value === false) { - ret = false; - } else if (pull && value === 'clone') { - ret = value; - } else if (typeof value === 'function') { - ret = value(to, from, dragEl, evt); - } else { - var otherGroup = (pull ? to : from).options.group.name; - - ret = (value === true || - (typeof value === 'string' && value === otherGroup) || - (value.join && value.indexOf(otherGroup) > -1)); - } - - return ret || to.options.group.name === from.options.group.name; - } - } - - var group = {}; - var originalGroup = options.group; - - if (!originalGroup || typeof originalGroup != 'object') { - originalGroup = {name: originalGroup}; - } - - group.name = originalGroup.name; - group.checkPull = toFn(originalGroup.pull, true); - group.checkPut = toFn(originalGroup.put); - group.revertClone = originalGroup.revertClone; - - options.group = group; - } + var sortable, + jq, + _this = this ; - // Detect support a passive mode - try { - window.addEventListener('test', null, Object.defineProperty({}, 'passive', { - get: function () { - // `false`, because everything starts to work incorrectly and instead of d'n'd, - // begins the page has scrolled. - passiveMode = false; - captureMode = { - capture: false, - passive: passiveMode - }; - } - })); - } catch (err) {} - - /** - * @class Sortable - * @param {HTMLElement} el - * @param {Object} [options] - */ - function Sortable(el, options) { - if (!(el && el.nodeType && el.nodeType === 1)) { - throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); - } - - this.el = el; // root element - this.options = options = _extend({}, options); - - - // Export instance - el[expando] = this; - - // Default options - var defaults = { - group: null, - sort: true, - disabled: false, - store: null, - handle: null, - scroll: true, - scrollSensitivity: 30, - scrollSpeed: 10, - bubbleScroll: true, - draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', - ghostClass: 'sortable-ghost', - chosenClass: 'sortable-chosen', - dragClass: 'sortable-drag', - ignore: 'a, img', - filter: null, - preventOnFilter: true, - animation: 0, - setData: function (dataTransfer, dragEl) { - dataTransfer.setData('Text', dragEl.textContent); - }, - dropBubble: false, - dragoverBubble: false, - dataIdAttr: 'data-id', - delay: 0, - touchStartThreshold: parseInt(window.devicePixelRatio, 10) || 1, - forceFallback: false, - fallbackClass: 'sortable-fallback', - fallbackOnBody: false, - fallbackTolerance: 0, - fallbackOffset: {x: 0, y: 0}, - supportPointer: Sortable.supportPointer !== false - }; - - - // Set default options - for (var name in defaults) { - !(name in options) && (options[name] = defaults[name]); - } - - _prepareGroup(options); - - // Bind all private methods - for (var fn in this) { - if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { - this[fn] = this[fn].bind(this); - } - } - - // Setup drag mode - this.nativeDraggable = options.forceFallback ? false : supportDraggable; - - // Bind events - _on(el, 'mousedown', this._onTapStart); - _on(el, 'touchstart', this._onTapStart); - options.supportPointer && _on(el, 'pointerdown', this._onTapStart); - - if (this.nativeDraggable) { - _on(el, 'dragover', this); - _on(el, 'dragenter', this); - } - - touchDragOverListeners.push(this._onDragOver); - - // Restore sorting - options.store && this.sort(options.store.get(this)); - } - - - Sortable.prototype = /** @lends Sortable.prototype */ { - constructor: Sortable, - - _onTapStart: function (/** Event|TouchEvent */evt) { - var _this = this, - el = this.el, - options = this.options, - preventOnFilter = options.preventOnFilter, - type = evt.type, - touch = evt.touches && evt.touches[0], - target = (touch || evt).target, - originalTarget = evt.target.shadowRoot && ((evt.path && evt.path[0]) || (evt.composedPath && evt.composedPath()[0])) || target, - filter = options.filter, - startIndex; - - _saveInputCheckedState(el); - - - // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. - if (dragEl) { - return; - } - - if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { - return; // only left button or enabled - } - - // cancel dnd if original target is content editable - if (originalTarget.isContentEditable) { - return; - } - - target = _closest(target, options.draggable, el); - - if (!target) { - return; - } - - if (lastDownEl === target) { - // Ignoring duplicate `down` - return; - } - - // Get the index of the dragged element within its parent - startIndex = _index(target, options.draggable); - - // Check filter - if (typeof filter === 'function') { - if (filter.call(this, evt, target, this)) { - _dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex); - preventOnFilter && evt.preventDefault(); - return; // cancel dnd - } - } - else if (filter) { - filter = filter.split(',').some(function (criteria) { - criteria = _closest(originalTarget, criteria.trim(), el); - - if (criteria) { - _dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex); - return true; - } - }); - - if (filter) { - preventOnFilter && evt.preventDefault(); - return; // cancel dnd - } - } - - if (options.handle && !_closest(originalTarget, options.handle, el)) { - return; - } - - // Prepare `dragstart` - this._prepareDragStart(evt, touch, target, startIndex); - }, - - - _handleAutoScroll: function(evt) { - if (!dragEl || !this.options.scroll || (this.options.supportPointer && evt.type == 'touchmove')) return; - var - x = (evt.touches ? evt.touches[0] : evt).clientX, - y = (evt.touches ? evt.touches[0] : evt).clientY, - - elem = document.elementFromPoint(x, y), - _this = this - ; - - - // touch does not have native autoscroll, even with DnD enabled - if (!_this.nativeDraggable || evt.touches || (evt.pointerType && evt.pointerType == 'touch')) { - - _autoScroll(evt.touches ? evt.touches[0] : evt, _this.options, elem); - - // Listener for pointer element change - var ogElemScroller = _getParentAutoScrollElement(elem, true); - if (!pointerElemChangedInterval || - x != lastPointerElemX || - y != lastPointerElemY) { - - pointerElemChangedInterval && clearInterval(pointerElemChangedInterval); - // Detect for pointer elem change, emulating native DnD behaviour - pointerElemChangedInterval = setInterval(function() { - if (!dragEl) return; - var newElem = _getParentAutoScrollElement(document.elementFromPoint(x, y), true); - if (newElem != ogElemScroller) { - ogElemScroller = newElem; - _clearAutoScrolls(); - _autoScroll(evt.touches ? evt.touches[0] : evt, _this.options, ogElemScroller); - } - }, 10); - lastPointerElemX = x; - lastPointerElemY = y; - } - - } else { - // if DnD is enabled, first autoscroll will already scroll, so get parent autoscroll of first autoscroll - if (!_this.options.bubbleScroll) return; - _autoScroll(evt, _this.options, _getParentAutoScrollElement(elem, false)); - } - }, - - _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) { - var _this = this, - el = _this.el, - options = _this.options, - ownerDocument = el.ownerDocument, - dragStartFn; - - if (target && !dragEl && (target.parentNode === el)) { - tapEvt = evt; - - rootEl = el; - dragEl = target; - parentEl = dragEl.parentNode; - nextEl = dragEl.nextSibling; - lastDownEl = target; - activeGroup = options.group; - oldIndex = startIndex; - - this._lastX = (touch || evt).clientX; - this._lastY = (touch || evt).clientY; - - dragEl.style['will-change'] = 'all'; - - dragStartFn = function () { - // Delayed drag has been triggered - // we can re-enable the events: touchmove/mousemove - _this._disableDelayedDrag(); - - // Make the element draggable - dragEl.draggable = _this.nativeDraggable; - - // Bind the events: dragstart/dragend - _this._triggerDragStart(evt, touch); - - // Drag start event - _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex); - - // Chosen item - _toggleClass(dragEl, options.chosenClass, true); - }; - - // Disable "draggable" - options.ignore.split(',').forEach(function (criteria) { - _find(dragEl, criteria.trim(), _disableDraggable); - }); - - _on(ownerDocument, 'mouseup', _this._onDrop); - _on(ownerDocument, 'touchend', _this._onDrop); - _on(ownerDocument, 'touchcancel', _this._onDrop); - _on(ownerDocument, 'selectstart', _this); - options.supportPointer && _on(ownerDocument, 'pointercancel', _this._onDrop); - - if (options.delay) { - // If the user moves the pointer or let go the click or touch - // before the delay has been reached: - // disable the delayed drag - _on(ownerDocument, 'mouseup', _this._disableDelayedDrag); - _on(ownerDocument, 'touchend', _this._disableDelayedDrag); - _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); - _on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler); - _on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler); - options.supportPointer && _on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler); - - _this._dragStartTimer = setTimeout(dragStartFn.bind(_this), options.delay); - } else { - dragStartFn(); - } - - - } - }, - - _delayedDragTouchMoveHandler: function (/** TouchEvent|PointerEvent **/e) { - var touch = e.touches ? e.touches[0] : e; - if (min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) - >= this.options.touchStartThreshold - ) { - this._disableDelayedDrag(); - } - }, - - _disableDelayedDrag: function () { - var ownerDocument = this.el.ownerDocument; - - clearTimeout(this._dragStartTimer); - _off(ownerDocument, 'mouseup', this._disableDelayedDrag); - _off(ownerDocument, 'touchend', this._disableDelayedDrag); - _off(ownerDocument, 'touchcancel', this._disableDelayedDrag); - _off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler); - _off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler); - _off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler); - }, - - _triggerDragStart: function (/** Event */evt, /** Touch */touch) { - touch = touch || (evt.pointerType == 'touch' ? evt : null); - - - if (touch) { - // Touch device support - tapEvt = { - target: dragEl, - clientX: touch.clientX, - clientY: touch.clientY - }; - - this._onDragStart(tapEvt, 'touch'); - } - else if (!this.nativeDraggable) { - this._onDragStart(tapEvt, true); - } - else { - _on(dragEl, 'dragend', this); - _on(rootEl, 'dragstart', this._onDragStart); - } - - try { - if (document.selection) { - // Timeout neccessary for IE9 - _nextTick(function () { - document.selection.empty(); - }); - } else { - window.getSelection().removeAllRanges(); - } - } catch (err) { - } - }, - - _dragStarted: function () { - if (rootEl && dragEl) { - _on(document, 'drag', this._handleAutoScroll); - var options = this.options; - - // Apply effect - _toggleClass(dragEl, options.ghostClass, true); - _toggleClass(dragEl, options.dragClass, false); - - Sortable.active = this; - - // Drag start event - _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex); - } else { - this._nulling(); - } - }, - - _emulateDragOver: function () { - if (touchEvt) { - if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) { - return; - } - - this._lastX = touchEvt.clientX; - this._lastY = touchEvt.clientY; - - if (!supportCssPointerEvents) { - _css(ghostEl, 'display', 'none'); - } - - var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); - var parent = target; - - while (target && target.shadowRoot) { - target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); - parent = target; - } - - if (parent) { - do { - if (parent[expando]) { - var i = touchDragOverListeners.length; - while (i--) { - touchDragOverListeners[i]({ - clientX: touchEvt.clientX, - clientY: touchEvt.clientY, - target: target, - rootEl: parent - }); - } - - if (!this.options.dragoverBubble) { - break; - } - } - - target = parent; // store last element - } - /* jshint boss:true */ - while (parent = parent.parentNode); - } - - if (!supportCssPointerEvents) { - _css(ghostEl, 'display', ''); - } - } - }, - - - _onTouchMove: function (/**TouchEvent*/evt) { - if (tapEvt) { - var options = this.options, - fallbackTolerance = options.fallbackTolerance, - fallbackOffset = options.fallbackOffset, - touch = evt.touches ? evt.touches[0] : evt, - dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, - dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, - translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; - - // only set the status to dragging, when we are actually dragging - if (!Sortable.active) { - if (fallbackTolerance && - min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance - ) { - return; - } - - this._dragStarted(); - } - - // as well as creating the ghost element on the document body - this._appendGhost(); - - moved = true; - touchEvt = touch; - - _css(ghostEl, 'webkitTransform', translate3d); - _css(ghostEl, 'mozTransform', translate3d); - _css(ghostEl, 'msTransform', translate3d); - _css(ghostEl, 'transform', translate3d); - - evt.preventDefault(); - } - }, - - _appendGhost: function () { - if (!ghostEl) { - var rect = dragEl.getBoundingClientRect(), - css = _css(dragEl), - options = this.options, - ghostRect; - - ghostEl = dragEl.cloneNode(true); - - _toggleClass(ghostEl, options.ghostClass, false); - _toggleClass(ghostEl, options.fallbackClass, true); - _toggleClass(ghostEl, options.dragClass, true); - - _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); - _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); - _css(ghostEl, 'width', rect.width); - _css(ghostEl, 'height', rect.height); - _css(ghostEl, 'opacity', '0.8'); - _css(ghostEl, 'position', 'fixed'); - _css(ghostEl, 'zIndex', '100000'); - _css(ghostEl, 'pointerEvents', 'none'); - - options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl); - - // Fixing dimensions. - ghostRect = ghostEl.getBoundingClientRect(); - _css(ghostEl, 'width', rect.width * 2 - ghostRect.width); - _css(ghostEl, 'height', rect.height * 2 - ghostRect.height); - } - }, - - _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { - var _this = this; - var dataTransfer = evt.dataTransfer; - var options = _this.options; - - _this._offUpEvents(); - - if (activeGroup.checkPull(_this, _this, dragEl, evt)) { - cloneEl = _clone(dragEl); - - cloneEl.draggable = false; - cloneEl.style['will-change'] = ''; - - this._hideClone(); - - _toggleClass(cloneEl, _this.options.chosenClass, false); - - // #1143: IFrame support workaround - _this._cloneId = _nextTick(function () { - rootEl.insertBefore(cloneEl, dragEl); - _dispatchEvent(_this, rootEl, 'clone', dragEl); - }); - } - - _toggleClass(dragEl, options.dragClass, true); - - if (useFallback) { - if (useFallback === 'touch') { - // Bind touch events - _on(document, 'touchmove', _this._onTouchMove); - // onTouchMove before handleAutoScroll in this case, because onTouchMove sets touchEvt - _on(document, 'touchmove', _this._handleAutoScroll); - _on(document, 'touchend', _this._onDrop); - _on(document, 'touchcancel', _this._onDrop); - - if (options.supportPointer) { - _on(document, 'pointermove', _this._handleAutoScroll); - _on(document, 'pointermove', _this._onTouchMove); - _on(document, 'pointerup', _this._onDrop); - } - } else { - // Old brwoser - _on(document, 'mousemove', _this._handleAutoScroll); - _on(document, 'mousemove', _this._onTouchMove); - _on(document, 'mouseup', _this._onDrop); - } - - _this._loopId = setInterval(_this._emulateDragOver, 50); - } - else { - if (dataTransfer) { - dataTransfer.effectAllowed = 'move'; - options.setData && options.setData.call(_this, dataTransfer, dragEl); - } - - _on(document, 'drop', _this); - - // #1143: Бывает элемент с IFrame внутри блокирует `drop`, - // поэтому если вызвался `mouseover`, значит надо отменять весь d'n'd. - // Breaking Chrome 62+ - // _on(document, 'mouseover', _this); - - _this._dragStartId = _nextTick(_this._dragStarted); - } - }, - - _onDragOver: function (/**Event*/evt) { - var el = this.el, - target, - dragRect, - targetRect, - revert, - options = this.options, - group = options.group, - activeSortable = Sortable.active, - isOwner = (activeGroup === group), - isMovingBetweenSortable = false, - canSort = options.sort; - - if (evt.preventDefault !== void 0) { - evt.preventDefault(); - !options.dragoverBubble && evt.stopPropagation(); - } - - if (dragEl.animated) { - return; - } - - moved = true; - - target = evt.target == el ? evt.target : _closest(evt.target, options.draggable, el); - - if (target === el) return; - - if (activeSortable && !options.disabled && - (isOwner - ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list - : ( - putSortable === this || - ( - (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && - group.checkPut(this, activeSortable, dragEl, evt) - ) - ) - ) && - (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback - ) { - if (_silent) { - return; - } - - dragRect = dragEl.getBoundingClientRect(); - - if (putSortable !== this) { - putSortable = this; - isMovingBetweenSortable = true; - } - - if (revert) { - this._hideClone(); - parentEl = rootEl; // actualization - - if (cloneEl || nextEl) { - rootEl.insertBefore(dragEl, cloneEl || nextEl); - } - else if (!canSort) { - rootEl.appendChild(dragEl); - } - - return; - } - - - if ((el.children.length === 0) || (el.children[0] === ghostEl) || - (el === evt.target) && (_ghostIsLast(el, evt)) - ) { - //assign target only if condition is true - if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) { - target = el.lastElementChild; - } - - if (target) { - if (target.animated) { - return; - } - - targetRect = target.getBoundingClientRect(); - } - - if (isOwner) { - activeSortable._hideClone(); - } else { - activeSortable._showClone(this); - } - - if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) { - if (!dragEl.contains(el)) { - el.appendChild(dragEl); - parentEl = el; // actualization - } - - this._animate(dragRect, dragEl); - target && this._animate(targetRect, target); - } - } - else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) { - if (lastEl !== target) { - lastEl = target; - lastCSS = _css(target); - lastParentCSS = _css(target.parentNode); - } - - targetRect = target.getBoundingClientRect(); - - var width = targetRect.right - targetRect.left, - height = targetRect.bottom - targetRect.top, - floating = R_FLOAT.test(lastCSS.cssFloat + lastCSS.display) - || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), - isWide = (target.offsetWidth > dragEl.offsetWidth), - isLong = (target.offsetHeight > dragEl.offsetHeight), - halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, - nextSibling = target.nextElementSibling, - after = false - ; - - if (floating) { - var elTop = dragEl.offsetTop, - tgTop = target.offsetTop; - - if (elTop === tgTop) { - after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide; - } - else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) { - after = (evt.clientY - targetRect.top) / height > 0.5; - } else { - after = tgTop > elTop; - } - } else if (!isMovingBetweenSortable) { - after = (nextSibling !== dragEl) && !isLong || halfway && isLong; - } - - var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); - - if (moveVector !== false) { - if (moveVector === 1 || moveVector === -1) { - after = (moveVector === 1); - } - - _silent = true; - setTimeout(_unsilent, 30); - - if (isOwner) { - activeSortable._hideClone(); - } else { - activeSortable._showClone(this); - } - - if (!dragEl.contains(el)) { - if (after && !nextSibling) { - el.appendChild(dragEl); - } else { - target.parentNode.insertBefore(dragEl, after ? nextSibling : target); - } - } - - parentEl = dragEl.parentNode; // actualization - - this._animate(dragRect, dragEl); - this._animate(targetRect, target); - } - } - } - }, - - _animate: function (prevRect, target) { - var ms = this.options.animation; - - if (ms) { - var currentRect = target.getBoundingClientRect(); - - if (prevRect.nodeType === 1) { - prevRect = prevRect.getBoundingClientRect(); - } - - _css(target, 'transition', 'none'); - _css(target, 'transform', 'translate3d(' - + (prevRect.left - currentRect.left) + 'px,' - + (prevRect.top - currentRect.top) + 'px,0)' - ); - - forRepaintDummy = target.offsetWidth; // repaint - - _css(target, 'transition', 'all ' + ms + 'ms'); - _css(target, 'transform', 'translate3d(0,0,0)'); - - clearTimeout(target.animated); - target.animated = setTimeout(function () { - _css(target, 'transition', ''); - _css(target, 'transform', ''); - target.animated = false; - }, ms); - } - }, - - _offUpEvents: function () { - var ownerDocument = this.el.ownerDocument; - - _off(document, 'touchmove', this._handleAutoScroll); - _off(document, 'pointermove', this._handleAutoScroll); - _off(document, 'mousemove', this._handleAutoScroll); - _off(document, 'touchmove', this._onTouchMove); - _off(document, 'pointermove', this._onTouchMove); - _off(ownerDocument, 'mouseup', this._onDrop); - _off(ownerDocument, 'touchend', this._onDrop); - _off(ownerDocument, 'pointerup', this._onDrop); - _off(ownerDocument, 'touchcancel', this._onDrop); - _off(ownerDocument, 'pointercancel', this._onDrop); - _off(ownerDocument, 'selectstart', this); - }, - - _onDrop: function (/**Event*/evt) { - var el = this.el, - options = this.options; - - clearInterval(this._loopId); - - clearInterval(pointerElemChangedInterval); - _clearAutoScrolls(); - _cancelThrottle(); - - clearTimeout(this._dragStartTimer); - - _cancelNextTick(this._cloneId); - _cancelNextTick(this._dragStartId); - - // Unbind events - _off(document, 'mouseover', this); - _off(document, 'mousemove', this._onTouchMove); - - - if (this.nativeDraggable) { - _off(document, 'drop', this); - _off(el, 'dragstart', this._onDragStart); - _off(document, 'drag', this._handleAutoScroll); - } - - this._offUpEvents(); - - if (evt) { - if (moved) { - evt.preventDefault(); - !options.dropBubble && evt.stopPropagation(); - } - - ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); - - if (rootEl === parentEl || this.lastPutMode !== 'clone') { - // Remove clone - cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); - } - - if (dragEl) { - if (this.nativeDraggable) { - _off(dragEl, 'dragend', this); - } - - _disableDraggable(dragEl); - dragEl.style['will-change'] = ''; - - // Remove class's - _toggleClass(dragEl, this.options.ghostClass, false); - _toggleClass(dragEl, this.options.chosenClass, false); - - // Drag stop event - _dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex, null, evt); - - if (rootEl !== parentEl) { - newIndex = _index(dragEl, options.draggable); - - if (newIndex >= 0) { - // Add event - _dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); - - // Remove event - _dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); - - // drag from one list and drop into another - _dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); - _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); - } - } - else { - if (dragEl.nextSibling !== nextEl) { - // Get the index of the dragged element within its parent - newIndex = _index(dragEl, options.draggable); - - if (newIndex >= 0) { - // drag & drop within the same list - _dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); - _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); - } - } - } - - if (Sortable.active) { - /* jshint eqnull:true */ - if (newIndex == null || newIndex === -1) { - newIndex = oldIndex; - } - - _dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex, evt); - - // Save sorting - this.save(); - } - } - - } - - this._nulling(); - }, - - _nulling: function() { - rootEl = - dragEl = - parentEl = - ghostEl = - nextEl = - cloneEl = - lastDownEl = - - scrollEl = - scrollParentEl = - - tapEvt = - touchEvt = - - moved = - newIndex = - - lastEl = - lastCSS = - - putSortable = - activeGroup = - Sortable.active = null; - - savedInputChecked.forEach(function (el) { - el.checked = true; + if (typeof define === "function" && define.amd) { + try { + define(["sortablejs", "jquery"], function(Sortable, $) { + sortable = Sortable; + jq = $; + checkErrors(); + factory(Sortable, $); }); - savedInputChecked.length = 0; - }, - - handleEvent: function (/**Event*/evt) { - switch (evt.type) { - case 'drop': - case 'dragend': - this._onDrop(evt); - break; - - case 'dragover': - case 'dragenter': - if (dragEl) { - this._onDragOver(evt); - _globalDragOver(evt); - } - break; - - case 'mouseover': - this._onDrop(evt); - break; - - case 'selectstart': - evt.preventDefault(); - break; - } - }, - - - /** - * Serializes the item into an array of string. - * @returns {String[]} - */ - toArray: function () { - var order = [], - el, - children = this.el.children, - i = 0, - n = children.length, - options = this.options; - - for (; i < n; i++) { - el = children[i]; - if (_closest(el, options.draggable, this.el)) { - order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); - } - } - - return order; - }, - - - /** - * Sorts the elements according to the array. - * @param {String[]} order order of the items - */ - sort: function (order) { - var items = {}, rootEl = this.el; - - this.toArray().forEach(function (id, i) { - var el = rootEl.children[i]; - - if (_closest(el, this.options.draggable, rootEl)) { - items[id] = el; - } - }, this); - - order.forEach(function (id) { - if (items[id]) { - rootEl.removeChild(items[id]); - rootEl.appendChild(items[id]); - } - }); - }, - - - /** - * Save the current sorting - */ - save: function () { - var store = this.options.store; - store && store.set(this); - }, - - - /** - * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. - * @param {HTMLElement} el - * @param {String} [selector] default: `options.draggable` - * @returns {HTMLElement|null} - */ - closest: function (el, selector) { - return _closest(el, selector || this.options.draggable, this.el); - }, - - - /** - * Set/get option - * @param {string} name - * @param {*} [value] - * @returns {*} - */ - option: function (name, value) { - var options = this.options; - - if (value === void 0) { - return options[name]; - } else { - options[name] = value; - - if (name === 'group') { - _prepareGroup(options); - } - } - }, - - - /** - * Destroy - */ - destroy: function () { - var el = this.el; - - el[expando] = null; - - _off(el, 'mousedown', this._onTapStart); - _off(el, 'touchstart', this._onTapStart); - _off(el, 'pointerdown', this._onTapStart); - - if (this.nativeDraggable) { - _off(el, 'dragover', this); - _off(el, 'dragenter', this); - } - - // Remove draggable attributes - Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { - el.removeAttribute('draggable'); - }); - - touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); - - this._onDrop(); - - this.el = el = null; - }, - - _hideClone: function() { - if (!cloneEl.cloneHidden) { - _css(cloneEl, 'display', 'none'); - cloneEl.cloneHidden = true; - } - }, - - _showClone: function(putSortable) { - if (putSortable.lastPutMode !== 'clone') return; - - if (cloneEl.cloneHidden) { - // show clone at dragEl or original position - rootEl.insertBefore(cloneEl, rootEl.contains(dragEl) && !this.options.group.revertClone ? dragEl : nextEl); - - if (this.options.group.revertClone) { - this._animate(dragEl, cloneEl); - } - _css(cloneEl, 'display', ''); - cloneEl.cloneHidden = false; - } + } catch(err) { + checkErrors(); } - }; + return; + } else if (typeof exports === 'object') { + try { + sortable = require('sortablejs'); + jq = require('jquery'); + } catch(err) { } + } - function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) { - if (el) { - ctx = ctx || document; + if (typeof jQuery === 'function' || typeof $ === 'function') { + jq = jQuery || $; + } - do { - if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { - return el; - } - /* jshint boss:true */ - } while (el = _getParentOrHost(el)); + if (typeof Sortable !== 'undefined') { + sortable = Sortable; + } + + function checkErrors() { + if (!jq) { + throw new Error('jQuery is required for jquery-sortablejs'); } - return null; - } - - - function _getParentOrHost(el) { - return (el.host && el !== document && el.host.nodeType) - ? el.host - : el.parentNode; - } - - - function _globalDragOver(/**Event*/evt) { - if (evt.dataTransfer) { - evt.dataTransfer.dropEffect = 'move'; - } - evt.preventDefault(); - } - - - function _on(el, event, fn) { - el.addEventListener(event, fn, captureMode); - } - - - function _off(el, event, fn) { - el.removeEventListener(event, fn, captureMode); - } - - - function _toggleClass(el, name, state) { - if (el) { - if (el.classList) { - el.classList[state ? 'add' : 'remove'](name); - } - else { - var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); - el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); - } + if (!sortable) { + throw new Error('SortableJS is required for jquery-sortablejs (https://github.com/SortableJS/Sortable)'); } } + checkErrors(); + factory(sortable, jq); +})(function (Sortable, $) { + "use strict"; - - function _css(el, prop, val) { - var style = el && el.style; - - if (style) { - if (val === void 0) { - if (document.defaultView && document.defaultView.getComputedStyle) { - val = document.defaultView.getComputedStyle(el, ''); - } - else if (el.currentStyle) { - val = el.currentStyle; - } - - return prop === void 0 ? val : val[prop]; - } - else { - if (!(prop in style)) { - prop = '-webkit-' + prop; - } - - style[prop] = val + (typeof val === 'string' ? '' : 'px'); - } - } - } - - - function _find(ctx, tagName, iterator) { - if (ctx) { - var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; - - if (iterator) { - for (; i < n; i++) { - iterator(list[i], i); - } - } - - return list; - } - - return []; - } - - - - function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex, originalEvt) { - sortable = (sortable || rootEl[expando]); - - var evt = document.createEvent('Event'), - options = sortable.options, - onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); - - evt.initEvent(name, true, true); - - evt.to = toEl || rootEl; - evt.from = fromEl || rootEl; - evt.item = targetEl || rootEl; - evt.clone = cloneEl; - - evt.oldIndex = startIndex; - evt.newIndex = newIndex; - - evt.originalEvent = originalEvt; - - rootEl.dispatchEvent(evt); - - if (options[onName]) { - options[onName].call(sortable, evt); - } - } - - - function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) { - var evt, - sortable = fromEl[expando], - onMoveFn = sortable.options.onMove, - retVal; - - evt = document.createEvent('Event'); - evt.initEvent('move', true, true); - - evt.to = toEl; - evt.from = fromEl; - evt.dragged = dragEl; - evt.draggedRect = dragRect; - evt.related = targetEl || toEl; - evt.relatedRect = targetRect || toEl.getBoundingClientRect(); - evt.willInsertAfter = willInsertAfter; - - evt.originalEvent = originalEvt; - - fromEl.dispatchEvent(evt); - - if (onMoveFn) { - retVal = onMoveFn.call(sortable, evt, originalEvt); - } - - return retVal; - } - - - function _disableDraggable(el) { - el.draggable = false; - } - - - function _unsilent() { - _silent = false; - } - - - /** @returns {HTMLElement|false} */ - function _ghostIsLast(el, evt) { - var lastEl = el.lastElementChild, - rect = lastEl.getBoundingClientRect(); - - // 5 — min delta - // abs — нельзя добавлять, а то глюки при наведении сверху - return (evt.clientY - (rect.top + rect.height) > 5) || - (evt.clientX - (rect.left + rect.width) > 5); - } - - - /** - * Generate id - * @param {HTMLElement} el - * @returns {String} - * @private - */ - function _generateId(el) { - var str = el.tagName + el.className + el.src + el.href + el.textContent, - i = str.length, - sum = 0; - - while (i--) { - sum += str.charCodeAt(i); - } - - return sum.toString(36); - } - - /** - * Returns the index of an element within its parent for a selected set of - * elements - * @param {HTMLElement} el - * @param {selector} selector - * @return {number} - */ - function _index(el, selector) { - var index = 0; - - if (!el || !el.parentNode) { - return -1; - } - - while (el && (el = el.previousElementSibling)) { - if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) { - index++; - } - } - - return index; - } - - function _matches(/**HTMLElement*/el, /**String*/selector) { - if (el) { - try { - if (el.matches) { - return el.matches(selector); - } else if (el.msMatchesSelector) { - return el.msMatchesSelector(selector); - } - } catch(_) { - return false; - } - } - - return false; - } - - var _throttleTimeout; - function _throttle(callback, ms) { - return function () { - if (!_throttleTimeout) { - var args = arguments, - _this = this - ; - - _throttleTimeout = setTimeout(function () { - if (args.length === 1) { - callback.call(_this, args[0]); - } else { - callback.apply(_this, args); - } - - _throttleTimeout = void 0; - }, ms); - } - }; - } - - function _cancelThrottle() { - clearTimeout(_throttleTimeout); - _throttleTimeout = void 0; - } - - function _extend(dst, src) { - if (dst && src) { - for (var key in src) { - if (src.hasOwnProperty(key)) { - dst[key] = src[key]; - } - } - } - - return dst; - } - - function _clone(el) { - if (Polymer && Polymer.dom) { - return Polymer.dom(el).cloneNode(true); - } - else if ($) { - return $(el).clone(true)[0]; - } - else { - return el.cloneNode(true); - } - } - - function _saveInputCheckedState(root) { - savedInputChecked.length = 0; - - var inputs = root.getElementsByTagName('input'); - var idx = inputs.length; - - while (idx--) { - var el = inputs[idx]; - el.checked && savedInputChecked.push(el); - } - } - - function _nextTick(fn) { - return setTimeout(fn, 0); - } - - function _cancelNextTick(id) { - return clearTimeout(id); - } - - // Fixed #973: - _on(document, 'touchmove', function (evt) { - if (Sortable.active) { - evt.preventDefault(); - } - }); - - // Export utils - Sortable.utils = { - on: _on, - off: _off, - css: _css, - find: _find, - is: function (el, selector) { - return !!_closest(el, selector, el); - }, - extend: _extend, - throttle: _throttle, - closest: _closest, - toggleClass: _toggleClass, - clone: _clone, - index: _index, - nextTick: _nextTick, - cancelNextTick: _cancelNextTick - }; - - - /** - * Create sortable instance - * @param {HTMLElement} el - * @param {Object} [options] - */ - Sortable.create = function (el, options) { - return new Sortable(el, options); - }; - - - - - - /** - * jQuery plugin for Sortable - * @param {Object|String} options - * @param {..*} [args] - * @returns {jQuery|*} - */ $.fn.sortable = function (options) { var retVal, args = arguments; @@ -1680,20 +57,15 @@ if (!sortable && (options instanceof Object || !options)) { sortable = new Sortable(this, options); $el.data('sortable', sortable); - } - - if (sortable) { - if (options === 'widget') { - retVal = sortable; - } - else if (options === 'destroy') { + } else if (sortable) { + if (options === 'destroy') { sortable.destroy(); $el.removeData('sortable'); - } - else if (typeof sortable[options] === 'function') { + } else if (options === 'widget') { + retVal = sortable; + } else if (typeof sortable[options] === 'function') { retVal = sortable[options].apply(sortable, [].slice.call(args, 1)); - } - else if (options in sortable.options) { + } else if (options in sortable.options) { retVal = sortable.option.apply(sortable, args); } } @@ -1701,4 +73,4 @@ return (retVal === void 0) ? this : retVal; }; -}); +}); \ No newline at end of file diff --git a/public/js/vendor/jquery.fn.sortable.min.js b/public/js/vendor/jquery.fn.sortable.min.js deleted file mode 100644 index b0eeaa5..0000000 --- a/public/js/vendor/jquery.fn.sortable.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! Sortable 1.7.0 - MIT | git://github.com/rubaxa/Sortable.git */ - -!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)}(function(r){"use strict";var C,E,x,O,P,N,d,b,y,D,k,A,B,c,o,M,Y,a,l,s,h,T,X,t,i,S=[],u=/\s+/g,I=/left|right|inline/,R="Sortable"+(new Date).getTime(),w=window,p=w.document,f=w.parseInt,H=w.setTimeout,e=(r=w.jQuery||w.Zepto,w.Polymer),g=!1,n=!1,v="draggable"in p.createElement("div"),m=!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)&&((t=p.createElement("x")).style.cssText="pointer-events:auto","auto"===t.style.pointerEvents),F=!1,L=Math.abs,_=Math.min,j=[],W=[],U=function(t,e){if(t&&t.getBoundingClientRect){var n=t,o=!1;do{if(n.clientWidth*",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,touchStartThreshold:f(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==G.supportPointer};for(var o in n)!(o in e)&&(e[o]=n[o]);for(var i in z(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&v,Z(t,"mousedown",this._onTapStart),Z(t,"touchstart",this._onTapStart),e.supportPointer&&Z(t,"pointerdown",this._onTapStart),this.nativeDraggable&&(Z(t,"dragover",this),Z(t,"dragenter",this)),W.push(this._onDragOver),e.store&&this.sort(e.store.get(this))}function Q(t,e,n){if(t){n=n||p;do{if(">*"===e&&t.parentNode===n||lt(t,e))return t}while(t=(o=t).host&&o!==p&&o.host.nodeType?o.host:o.parentNode)}var o;return null}function Z(t,e,n){t.addEventListener(e,n,g)}function J(t,e,n){t.removeEventListener(e,n,g)}function K(t,e,n){if(t)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(u," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(u," ")}}function $(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return p.defaultView&&p.defaultView.getComputedStyle?n=p.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function tt(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i*"!==e&&!lt(t,e)||n++;return n}function lt(t,e){if(t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e)}catch(t){return!1}return!1}function st(n,o){return function(){if(!i){var t=arguments,e=this;i=H(function(){1===t.length?n.call(e,t[0]):n.apply(e,t),i=void 0},o)}}}function ct(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function ht(t){return e&&e.dom?e.dom(t).cloneNode(!0):r?r(t).clone(!0)[0]:t.cloneNode(!0)}function dt(t){return H(t,0)}function ut(t){return clearTimeout(t)}G.prototype={constructor:G,_onTapStart:function(t){var e,n=this,o=this.el,i=this.options,r=i.preventOnFilter,a=t.type,l=t.touches&&t.touches[0],s=(l||t).target,c=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||s,h=i.filter;if(function(t){j.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&j.push(o)}}(o),!C&&!(/mousedown|pointerdown/.test(a)&&0!==t.button||i.disabled)&&!c.isContentEditable&&(s=Q(s,i.draggable,o))&&d!==s){if(e=at(s,i.draggable),"function"==typeof h){if(h.call(this,t,s,this))return et(n,c,"filter",s,o,o,e),void(r&&t.preventDefault())}else if(h&&(h=h.split(",").some(function(t){if(t=Q(c,t.trim(),o))return et(n,t,"filter",s,o,o,e),!0})))return void(r&&t.preventDefault());i.handle&&!Q(c,i.handle,o)||this._prepareDragStart(t,l,s,e)}},_handleAutoScroll:function(e){if(C&&this.options.scroll&&(!this.options.supportPointer||"touchmove"!=e.type)){var n=(e.touches?e.touches[0]:e).clientX,o=(e.touches?e.touches[0]:e).clientY,t=p.elementFromPoint(n,o),i=this;if(!i.nativeDraggable||e.touches||e.pointerType&&"touch"==e.pointerType){V(e.touches?e.touches[0]:e,i.options,t);var r=U(t,!0);a&&n==l&&o==s||(a&&clearInterval(a),a=setInterval(function(){if(C){var t=U(p.elementFromPoint(n,o),!0);t!=r&&(r=t,q(),V(e.touches?e.touches[0]:e,i.options,r))}},10),l=n,s=o)}else{if(!i.options.bubbleScroll)return;V(e,i.options,U(t,!1))}}},_prepareDragStart:function(t,e,n,o){var i,r=this,a=r.el,l=r.options,s=a.ownerDocument;n&&!C&&n.parentNode===a&&(h=t,P=a,E=(C=n).parentNode,N=C.nextSibling,d=n,M=l.group,c=o,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,C.style["will-change"]="all",i=function(){r._disableDelayedDrag(),C.draggable=r.nativeDraggable,r._triggerDragStart(t,e),et(r,P,"choose",C,P,P,c),K(C,l.chosenClass,!0)},l.ignore.split(",").forEach(function(t){tt(C,t.trim(),ot)}),Z(s,"mouseup",r._onDrop),Z(s,"touchend",r._onDrop),Z(s,"touchcancel",r._onDrop),Z(s,"selectstart",r),l.supportPointer&&Z(s,"pointercancel",r._onDrop),l.delay?(Z(s,"mouseup",r._disableDelayedDrag),Z(s,"touchend",r._disableDelayedDrag),Z(s,"touchcancel",r._disableDelayedDrag),Z(s,"mousemove",r._delayedDragTouchMoveHandler),Z(s,"touchmove",r._delayedDragTouchMoveHandler),l.supportPointer&&Z(s,"pointermove",r._delayedDragTouchMoveHandler),r._dragStartTimer=H(i.bind(r),l.delay)):i())},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;_(L(e.clientX-this._lastX),L(e.clientY-this._lastY))>=this.options.touchStartThreshold&&this._disableDelayedDrag()},_disableDelayedDrag:function(){var t=this.el.ownerDocument;clearTimeout(this._dragStartTimer),J(t,"mouseup",this._disableDelayedDrag),J(t,"touchend",this._disableDelayedDrag),J(t,"touchcancel",this._disableDelayedDrag),J(t,"mousemove",this._delayedDragTouchMoveHandler),J(t,"touchmove",this._delayedDragTouchMoveHandler),J(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){(e=e||("touch"==t.pointerType?t:null))?(h={target:C,clientX:e.clientX,clientY:e.clientY},this._onDragStart(h,"touch")):this.nativeDraggable?(Z(C,"dragend",this),Z(P,"dragstart",this._onDragStart)):this._onDragStart(h,!0);try{p.selection?dt(function(){p.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(){if(P&&C){Z(p,"drag",this._handleAutoScroll);var t=this.options;K(C,t.ghostClass,!0),K(C,t.dragClass,!1),et(G.active=this,P,"start",C,P,P,c)}else this._nulling()},_emulateDragOver:function(){if(T){if(this._lastX===T.clientX&&this._lastY===T.clientY)return;this._lastX=T.clientX,this._lastY=T.clientY,m||$(x,"display","none");for(var t=p.elementFromPoint(T.clientX,T.clientY),e=t;t&&t.shadowRoot;)e=t=t.shadowRoot.elementFromPoint(T.clientX,T.clientY);if(e)do{if(e[R]){for(var n=W.length;n--;)W[n]({clientX:T.clientX,clientY:T.clientY,target:t,rootEl:e});if(!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);m||$(x,"display","")}},_onTouchMove:function(t){if(h){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=i.clientX-h.clientX+o.x,a=i.clientY-h.clientY+o.y,l=t.touches?"translate3d("+r+"px,"+a+"px,0)":"translate("+r+"px,"+a+"px)";if(!G.active){if(n&&_(L(i.clientX-this._lastX),L(i.clientY-this._lastY))C.offsetWidth,_=e.offsetHeight>C.offsetHeight,b=.5<(v?(t.clientX-o.left)/f:(t.clientY-o.top)/g),y=e.nextElementSibling,D=!1;if(v){var T=C.offsetTop,S=e.offsetTop;D=T===S?e.previousElementSibling===C&&!m||b&&m:e.previousElementSibling===C||C.previousElementSibling===e?.5<(t.clientY-o.top)/g:T Date: Thu, 20 Dec 2018 10:08:58 +0100 Subject: [PATCH 10/99] TileRenderer: Explicitly set sortable direction to horizontal Since Sortable.js v1.8.0-rc1 the automatic detection seems unreliable. --- library/Businessprocess/Renderer/TileRenderer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index b6843ac..071c904 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -22,6 +22,7 @@ class TileRenderer extends Renderer 'data-sortable-disabled' => $this->isLocked(), 'data-sortable-data-id-attr' => 'id', 'data-sortable-filter' => '.addnew', + 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl()->getAbsoluteUrl() ] From c804177ca4e0851d510633b46cbce8b34b526f7d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 20 Dec 2018 10:56:05 +0100 Subject: [PATCH 11/99] Also save user defined order of processes nodes while in tree view --- application/controllers/ProcessController.php | 7 +++- .../Businessprocess/Renderer/TreeRenderer.php | 37 ++++++++++++++----- public/js/module.js | 21 +++++++++++ 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 6ba5168..73e0e8d 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -251,12 +251,17 @@ class ProcessController extends Controller ->setSimulation(Simulation::fromSession($this->session())) ->handleRequest(); } elseif ($action === 'move') { + $url = Url::fromRequest()->without(['action', 'movenode']); + if ($url->getParam('mode') === 'tree') { + $url = $url->without('node'); + } + $form = $this->loadForm('MoveNode') ->setProcess($bp) ->setParentNode($node) ->setSession($this->session()) ->setNode($bp->getNode($this->params->get('movenode'))) - ->setSuccessUrl(Url::fromRequest()->without(['action', 'movenode'])) + ->setSuccessUrl($url) ->handleRequest(); } diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 9fc27b8..86594fa 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Businessprocess\Renderer; use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\BpConfig; use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Web\Form\CsrfToken; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; @@ -19,8 +20,13 @@ class TreeRenderer extends Renderer $this->add(Html::tag( 'div', [ - 'id' => $bp->getHtmlId(), - 'class' => 'bp' + 'id' => $bp->getHtmlId(), + 'class' => ['bp', 'sortable'], + 'data-sortable-disabled' => $this->isLocked(), + 'data-sortable-data-id-attr' => 'id', + 'data-sortable-direction' => 'vertical', + 'data-csrf-token' => CsrfToken::generate(), + 'data-action-url' => $this->getUrl()->getAbsoluteUrl() ], $this->renderBp($bp) )); @@ -103,11 +109,9 @@ class TreeRenderer extends Renderer $table = Html::tag( 'table', [ - 'id' => $this->getId($node, $path), - 'class' => array( - 'bp', - $node->getObjectClassName() - ) + 'id' => $this->getId($node, $path), + 'class' => ['bp', $node->getObjectClassName()], + 'data-node-name' => $node->getName() ] ); $attributes = $table->getAttributes(); @@ -121,7 +125,17 @@ class TreeRenderer extends Renderer $attributes->add('class', 'node'); } - $tbody = Html::tag('tbody'); + $tbody = Html::tag('tbody', [ + 'class' => 'sortable', + 'data-sortable-disabled' => $this->isLocked(), + 'data-sortable-data-id-attr' => 'id', + 'data-sortable-draggable' => '.movable', + 'data-sortable-direction' => 'vertical', + 'data-csrf-token' => CsrfToken::generate(), + 'data-action-url' => $this->getUrl() + ->overwriteParams(['node' => (string) $node]) + ->getAbsoluteUrl() + ]); $table->add($tbody); $tr = Html::tag('tr'); $tbody->add($tr); @@ -161,10 +175,15 @@ class TreeRenderer extends Renderer $td->add($link); + $path[] = (string) $node; foreach ($node->getChildren() as $name => $child) { $tbody->add(Html::tag( 'tr', - null, + [ + 'class' => 'movable', + 'id' => $this->getId($child, $path), + 'data-node-name' => $name + ], Html::tag( 'td', null, diff --git a/public/js/module.js b/public/js/module.js index 4240811..f3856c9 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -36,6 +36,7 @@ this.module.on('click', 'div.tiles > div', this.tileClick); this.module.on('click', '.dashboard-tile', this.dashboardTileClick); this.module.on('end', 'div.tiles.sortable', this.tileDropped); + this.module.on('end', 'div.bp.sortable, table.bp tbody.sortable', this.rowDropped); this.module.icinga.logger.debug('BP module initialized'); }, @@ -116,6 +117,26 @@ } }, + rowDropped: function(event) { + var evt = event.originalEvent; + if (evt.oldIndex !== evt.newIndex) { + var $target = $(evt.to); + var actionUrl = icinga.utils.addUrlParams($target.data('actionUrl'), { + action: 'move', + movenode: $(evt.item).data('nodeName') + }); + + var data = { + csrfToken: $target.data('csrfToken'), + movenode: 'movenode', // That's the submit button.. + from: evt.oldIndex, + to: evt.newIndex + }; + + icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'post'); + } + }, + /** * Add 'hovered' class to hovered title elements * From eac7f3e76fe8effe313fda96c2fb05641a9f7047 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 21 Dec 2018 13:58:45 +0100 Subject: [PATCH 12/99] Enhance drag&drop in the tree view The style is not final, of course.. --- .../Businessprocess/Renderer/TreeRenderer.php | 114 +++++++++++------- public/css/module.less | 32 +++++ public/js/module.js | 7 +- 3 files changed, 111 insertions(+), 42 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 86594fa..0182dc0 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -21,9 +21,10 @@ class TreeRenderer extends Renderer 'div', [ 'id' => $bp->getHtmlId(), - 'class' => ['bp', 'sortable'], + 'class' => ['tree', 'sortable'], 'data-sortable-disabled' => $this->isLocked(), 'data-sortable-data-id-attr' => 'id', + 'data-sortable-filter' => '.placeholder', 'data-sortable-direction' => 'vertical', 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl()->getAbsoluteUrl() @@ -47,6 +48,7 @@ class TreeRenderer extends Renderer $nodes = $this->parent->getChildren(); } + $html[] = Html::tag('div', ['class' => 'placeholder']); foreach ($nodes as $name => $node) { $html[] = $this->renderNode($bp, $node); } @@ -107,10 +109,10 @@ class TreeRenderer extends Renderer public function renderNode(BpConfig $bp, Node $node, $path = array()) { $table = Html::tag( - 'table', + 'div', [ 'id' => $this->getId($node, $path), - 'class' => ['bp', $node->getObjectClassName()], + 'class' => ['bp', 'movable', $node->getObjectClassName()], 'data-node-name' => $node->getName() ] ); @@ -125,30 +127,16 @@ class TreeRenderer extends Renderer $attributes->add('class', 'node'); } - $tbody = Html::tag('tbody', [ - 'class' => 'sortable', - 'data-sortable-disabled' => $this->isLocked(), - 'data-sortable-data-id-attr' => 'id', - 'data-sortable-draggable' => '.movable', - 'data-sortable-direction' => 'vertical', - 'data-csrf-token' => CsrfToken::generate(), - 'data-action-url' => $this->getUrl() - ->overwriteParams(['node' => (string) $node]) - ->getAbsoluteUrl() - ]); - $table->add($tbody); - $tr = Html::tag('tr'); - $tbody->add($tr); - if ($node instanceof BpNode) { - $tr->add(Html::tag( - 'th', - ['rowspan' => $node->countChildren() + 1 + ($this->isLocked() ? 0 : 1)], + $table->add(Html::tag( + 'div', + null, Html::tag('span', ['class' => 'op'], $node->operatorHtml()) )); } - $td = Html::tag('td'); - $tr->add($td); + + $td = Html::tag('div'); + $table->add($td); if ($node instanceof BpNode && $node->hasInfoUrl()) { $td->add($this->createInfoAction($node)); @@ -175,38 +163,82 @@ class TreeRenderer extends Renderer $td->add($link); + $tbody = Html::tag('ul', [ + 'class' => 'sortable', + 'data-sortable-disabled' => $this->isLocked(), + 'data-sortable-data-id-attr' => 'id', + 'data-sortable-draggable' => '.movable', + 'data-sortable-direction' => 'vertical', + 'data-csrf-token' => CsrfToken::generate(), + 'data-action-url' => $this->getUrl() + ->overwriteParams(['node' => (string) $node]) + ->getAbsoluteUrl() + ]); + $table->add($tbody); + $path[] = (string) $node; foreach ($node->getChildren() as $name => $child) { - $tbody->add(Html::tag( - 'tr', - [ - 'class' => 'movable', - 'id' => $this->getId($child, $path), - 'data-node-name' => $name - ], - Html::tag( - 'td', - null, + if ($child->hasChildren()) { + $tbody->add(Html::tag( + 'li', + [ + 'class' => 'movable', + 'id' => $this->getId($child, $path), + 'data-node-name' => $name + ], $this->renderNode($bp, $child, $this->getCurrentPath()) - ) - )); + )); + } else { + $this->renderChild($bp, $tbody, $child, $path); + } } if (! $this->isLocked() && $node instanceof BpNode && $bp->getMetadata()->canModify()) { $tbody->add(Html::tag( - 'tr', + 'li', null, - Html::tag( - 'td', - null, - $this->renderAddNewNode($node) - ) + $this->renderAddNewNode($node) )); } return $table; } + protected function renderChild($bp, BaseHtmlElement $ul, $node, $path = null) + { + $li = Html::tag('li', [ + 'class' => 'movable', + 'id' => $this->getId($node, $path ?: []), + 'data-node-name' => (string) $node + ]); + $ul->add($li); + + if ($node instanceof BpNode && $node->hasInfoUrl()) { + $li->add($this->createInfoAction($node)); + } + + if (! $this->isLocked()) { + $li->add($this->getActionIcons($bp, $node)); + } + + $link = $node->getLink(); + $link->getAttributes()->set('data-base-target', '_next'); + $link->add($this->getNodeIcons($node)); + + if ($node->hasChildren()) { + $link->add($this->renderStateBadges($node->getStateSummary())); + } + + if ($time = $node->getLastStateChange()) { + $since = $this->timeSince($time)->prepend( + sprintf(' (%s ', $node->getStateName()) + )->add(')'); + $link->add($since); + } + + $li->add($link); + } + protected function getActionIcons(BpConfig $bp, Node $node) { if ($node instanceof BpNode) { diff --git a/public/css/module.less b/public/css/module.less index b0547d8..070cd63 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -33,6 +33,38 @@ div.bp.sortable > .sortable-ghost { background: white; } + + +/* New tree stuff START */ + +div.tree { + padding: 1em; + + div.placeholder { + // Helps to assist Sortable.js/the browser to properly place items at the top + height: 1em; + } + + > div.process, + ul.sortable li { + border: .2em solid @gray; + margin-bottom: .2em; + padding-left: .2em; + + &.sortable-ghost { + border: .2em dashed @gray-light; + } + } + + ul { + list-style-type: none; + } +} + +/* New tree stuff END */ + + + table.bp { /* Business process table styling starts here */ width: 100%; diff --git a/public/js/module.js b/public/js/module.js index f3856c9..7afc069 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -36,7 +36,7 @@ this.module.on('click', 'div.tiles > div', this.tileClick); this.module.on('click', '.dashboard-tile', this.dashboardTileClick); this.module.on('end', 'div.tiles.sortable', this.tileDropped); - this.module.on('end', 'div.bp.sortable, table.bp tbody.sortable', this.rowDropped); + this.module.on('end', 'div.tree.sortable, ul.sortable', this.rowDropped); this.module.icinga.logger.debug('BP module initialized'); }, @@ -126,6 +126,11 @@ movenode: $(evt.item).data('nodeName') }); + if ($('.placeholder', $target).length) { + evt.oldIndex -= 1; + evt.newIndex -= 1; + } + var data = { csrfToken: $target.data('csrfToken'), movenode: 'movenode', // That's the submit button.. From b46a633d6d10529a2fe8fa717587a2b4856df48f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 10 Jan 2019 15:44:50 +0100 Subject: [PATCH 13/99] ProcessController: Provide a new set of parameters when redirecting --- application/controllers/ProcessController.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 73e0e8d..3948a3d 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -251,9 +251,14 @@ class ProcessController extends Controller ->setSimulation(Simulation::fromSession($this->session())) ->handleRequest(); } elseif ($action === 'move') { - $url = Url::fromRequest()->without(['action', 'movenode']); - if ($url->getParam('mode') === 'tree') { - $url = $url->without('node'); + $url = Url::fromPath('businessprocess/process/show', ['config' => $bp->getName()]); + if ($this->url()->hasParam('unlocked')) { + $url->setParam('unlocked', $this->url()->getParam('unlocked')); + } + if ($this->url()->hasParam('mode')) { + $url->setParam('mode', $this->url()->getParam('mode')); + } elseif ($this->url()->hasParam('node')) { + $url->setParam('node', $this->url()->getParam('node')); } $form = $this->loadForm('MoveNode') From d2c858ddf8344434fb07d90ea9d4e3c5570a223e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 10 Jan 2019 15:47:02 +0100 Subject: [PATCH 14/99] js: Fix case of the desired method when submitting order changes --- public/js/module.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/module.js b/public/js/module.js index 7afc069..7e85b37 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -113,7 +113,7 @@ to: evt.newIndex }; - icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'post'); + icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'POST'); } }, @@ -138,7 +138,7 @@ to: evt.newIndex }; - icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'post'); + icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'POST'); } }, From 36dd4e0296d384adfbf64f2e92394992410d77bc Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 08:43:10 +0100 Subject: [PATCH 15/99] TreeRenderer: Use a more homogenous html structure i.e. no divs, just uls and lis --- .../Businessprocess/Renderer/TreeRenderer.php | 22 +++++-------------- public/css/module.less | 8 +++---- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 0182dc0..7c73683 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -18,7 +18,7 @@ class TreeRenderer extends Renderer { $bp = $this->config; $this->add(Html::tag( - 'div', + 'ul', [ 'id' => $bp->getHtmlId(), 'class' => ['tree', 'sortable'], @@ -48,7 +48,7 @@ class TreeRenderer extends Renderer $nodes = $this->parent->getChildren(); } - $html[] = Html::tag('div', ['class' => 'placeholder']); + $html[] = Html::tag('li', ['class' => 'placeholder']); foreach ($nodes as $name => $node) { $html[] = $this->renderNode($bp, $node); } @@ -109,7 +109,7 @@ class TreeRenderer extends Renderer public function renderNode(BpConfig $bp, Node $node, $path = array()) { $table = Html::tag( - 'div', + 'li', [ 'id' => $this->getId($node, $path), 'class' => ['bp', 'movable', $node->getObjectClassName()], @@ -128,11 +128,7 @@ class TreeRenderer extends Renderer } if ($node instanceof BpNode) { - $table->add(Html::tag( - 'div', - null, - Html::tag('span', ['class' => 'op'], $node->operatorHtml()) - )); + $table->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); } $td = Html::tag('div'); @@ -179,15 +175,7 @@ class TreeRenderer extends Renderer $path[] = (string) $node; foreach ($node->getChildren() as $name => $child) { if ($child->hasChildren()) { - $tbody->add(Html::tag( - 'li', - [ - 'class' => 'movable', - 'id' => $this->getId($child, $path), - 'data-node-name' => $name - ], - $this->renderNode($bp, $child, $this->getCurrentPath()) - )); + $tbody->add($this->renderNode($bp, $child, $this->getCurrentPath())); } else { $this->renderChild($bp, $tbody, $child, $path); } diff --git a/public/css/module.less b/public/css/module.less index 070cd63..86786cd 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -37,22 +37,22 @@ div.bp.sortable > .sortable-ghost { /* New tree stuff START */ -div.tree { +ul.tree { padding: 1em; - div.placeholder { + li.placeholder { // Helps to assist Sortable.js/the browser to properly place items at the top height: 1em; } - > div.process, + > li.process, ul.sortable li { - border: .2em solid @gray; margin-bottom: .2em; padding-left: .2em; &.sortable-ghost { border: .2em dashed @gray-light; + border-left-width: 0; } } From 48ba2a7bba511f589bcf9da678bc7e2c9a9580e1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 08:46:32 +0100 Subject: [PATCH 16/99] TreeRenderer: Render add button as action link in the top right --- .../Businessprocess/Renderer/TreeRenderer.php | 26 +++++-------------- public/css/module.less | 4 +++ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 7c73683..32dd4e8 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -181,14 +181,6 @@ class TreeRenderer extends Renderer } } - if (! $this->isLocked() && $node instanceof BpNode && $bp->getMetadata()->canModify()) { - $tbody->add(Html::tag( - 'li', - null, - $this->renderAddNewNode($node) - )); - } - return $table; } @@ -231,7 +223,7 @@ class TreeRenderer extends Renderer { if ($node instanceof BpNode) { if ($bp->getMetadata()->canModify()) { - return $this->createEditAction($bp, $node); + return [$this->createEditAction($bp, $node), $this->renderAddNewNode($node)]; } else { return ''; } @@ -290,16 +282,12 @@ class TreeRenderer extends Renderer protected function renderAddNewNode($parent) { - return Html::tag( - 'a', - [ - 'href' => $this->getUrl() - ->with('action', 'add') - ->with('node', $parent->getName()), - 'title' => mt('businessprocess', 'Add a new business process node'), - 'class' => 'addnew icon-plus' - ], - mt('businessprocess', 'Add') + return $this->actionIcon( + 'plus', + $this->getUrl() + ->with('action', 'add') + ->with('node', $parent->getName()), + mt('businessprocess', 'Add a new business process node') ); } } diff --git a/public/css/module.less b/public/css/module.less index 86786cd..0b4f5c0 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -54,6 +54,10 @@ ul.tree { border: .2em dashed @gray-light; border-left-width: 0; } + + a > div { + display: inline-block; + } } ul { From ea0a04065d38ee7cab48b24d69d7d513f1cf3b1e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 08:48:20 +0100 Subject: [PATCH 17/99] Add support to move nodes between processes --- application/forms/MoveNodeForm.php | 19 +++ .../Modification/NodeMoveAction.php | 114 ++++++++++++++---- .../Modification/ProcessChanges.php | 8 +- 3 files changed, 114 insertions(+), 27 deletions(-) diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php index 04f648e..f2146e4 100644 --- a/application/forms/MoveNodeForm.php +++ b/application/forms/MoveNodeForm.php @@ -48,6 +48,24 @@ class MoveNodeForm extends QuickForm public function setup() { + $this->addElement( + 'text', + 'parent', + [ + 'allowEmpty' => true, + 'filters' => ['Null'], + 'validators' => [ + ['Callback', true, [ + 'callback' => function($name) { + return empty($name) || $this->bp->hasBpNode($name); + }, + 'messages' => [ + 'callbackValue' => $this->translate('No process found with name %value%') + ] + ]] + ] + ] + ); $this->addElement( 'number', 'from', @@ -130,6 +148,7 @@ class MoveNodeForm extends QuickForm $this->node, $this->getValue('from'), $this->getValue('to'), + $this->getValue('parent'), $this->parentNode !== null ? $this->parentNode->getName() : null ); diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php index ebe5aea..f7428fc 100644 --- a/library/Businessprocess/Modification/NodeMoveAction.php +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -10,7 +10,12 @@ class NodeMoveAction extends NodeAction /** * @var string */ - protected $parentName; + protected $parent; + + /** + * @var string + */ + protected $newParent; /** * @var int @@ -22,16 +27,26 @@ class NodeMoveAction extends NodeAction */ protected $to; - protected $preserveProperties = ['parentName', 'from', 'to']; + protected $preserveProperties = ['parent', 'newParent', 'from', 'to']; - public function setParentName($name) + public function setParent($name) { - $this->parentName = $name; + $this->parent = $name; } - public function getParentName() + public function getParent() { - return $this->parentName; + return $this->parent; + } + + public function setNewParent($name) + { + $this->newParent = $name; + } + + public function getNewParent() + { + return $this->newParent; } public function setFrom($from) @@ -61,11 +76,11 @@ class NodeMoveAction extends NodeAction } $name = $this->getNodeName(); - if ($this->parentName !== null) { - if (! $config->hasBpNode($this->parentName)) { + if ($this->parent !== null) { + if (! $config->hasBpNode($this->parent)) { return false; } - $parent = $config->getBpNode($this->parentName); + $parent = $config->getBpNode($this->parent); if (! $parent->hasChild($name)) { return false; } @@ -84,14 +99,30 @@ class NodeMoveAction extends NodeAction } } + if ($this->parent !== $this->newParent) { + if ($this->newParent !== null) { + if (! $config->hasBpNode($this->newParent)) { + return false; + } + + $childrenCount = $config->getBpNode($this->newParent)->countChildren(); + } else { + $childrenCount = $config->countChildren(); + } + + if ($this->getTo() > 0 && $childrenCount < $this->getTo()) { + return false; + } + } + return true; } public function applyTo(BpConfig $config) { $name = $this->getNodeName(); - if ($this->parentName !== null) { - $nodes = $config->getBpNode($this->parentName)->getChildren(); + if ($this->parent !== null) { + $nodes = $config->getBpNode($this->parent)->getChildren(); } else { $nodes = $config->getRootNodes(); } @@ -101,22 +132,57 @@ class NodeMoveAction extends NodeAction array_slice($nodes, 0, $this->from, true), array_slice($nodes, $this->from + 1, null, true) ); - $nodes = array_merge( - array_slice($nodes, 0, $this->to, true), - [$name => $node], - array_slice($nodes, $this->to, null, true) - ); - - if ($this->parentName !== null) { - $config->getBpNode($this->parentName)->setChildNames(array_keys($nodes)); + if ($this->parent === $this->newParent) { + $nodes = array_merge( + array_slice($nodes, 0, $this->to, true), + [$name => $node], + array_slice($nodes, $this->to, null, true) + ); } else { + if ($this->newParent !== null) { + $newNodes = $config->getBpNode($this->newParent)->getChildren(); + } else { + $newNodes = $config->getRootNodes(); + } + + $newNodes = array_merge( + array_slice($newNodes, 0, $this->to, true), + [$name => $node], + array_slice($newNodes, $this->to, null, true) + ); + + if ($this->newParent !== null) { + $config->getBpNode($this->newParent)->setChildNames(array_keys($newNodes)); + } else { + $config->addRootNode($name); + + $i = 0; + foreach ($newNodes as $_ => $newNode) { + /** @var BpNode $newNode */ + if ($newNode->getDisplay() > 0) { + $i += 1; + if ($newNode->getDisplay() !== $i) { + $newNode->setDisplay($i); + } + } + } + } + } + + if ($this->parent !== null) { + $config->getBpNode($this->parent)->setChildNames(array_keys($nodes)); + } else { + if ($this->newParent !== null) { + $config->removeRootNode($name); + } + $i = 0; - foreach ($nodes as $name => $node) { - /** @var BpNode $node */ - if ($node->getDisplay() > 0) { + foreach ($nodes as $_ => $oldNode) { + /** @var BpNode $oldNode */ + if ($oldNode->getDisplay() > 0) { $i += 1; - if ($node->getDisplay() !== $i) { - $node->setDisplay($i); + if ($oldNode->getDisplay() !== $i) { + $oldNode->setDisplay($i); } } } diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php index 8dc324a..50a6226 100644 --- a/library/Businessprocess/Modification/ProcessChanges.php +++ b/library/Businessprocess/Modification/ProcessChanges.php @@ -126,14 +126,16 @@ class ProcessChanges * @param Node $node * @param int $from * @param int $to - * @param string $parentName + * @param string $newParent + * @param string $parent * * @return $this */ - public function moveNode(Node $node, $from, $to, $parentName = null) + public function moveNode(Node $node, $from, $to, $newParent, $parent = null) { $action = new NodeMoveAction($node); - $action->setParentName($parentName); + $action->setParent($parent); + $action->setNewParent($newParent); $action->setFrom($from); $action->setTo($to); From 877f86a7468aea929fc82f6a497e55c00098784d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 08:49:10 +0100 Subject: [PATCH 18/99] js: Allow to move nodes between processes --- .../Businessprocess/Renderer/TreeRenderer.php | 10 ++++- public/css/module.less | 4 ++ public/js/behavior/sortable.js | 5 +++ public/js/module.js | 43 +++++++++++++------ 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 32dd4e8..d96ba46 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -24,8 +24,12 @@ class TreeRenderer extends Renderer 'class' => ['tree', 'sortable'], 'data-sortable-disabled' => $this->isLocked(), 'data-sortable-data-id-attr' => 'id', - 'data-sortable-filter' => '.placeholder', 'data-sortable-direction' => 'vertical', + 'data-sortable-group' => json_encode([ + 'name' => 'root', + 'put' => 'function:rowPutAllowed' + ]), + 'data-sortable-invert-swap' => 'true', 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl()->getAbsoluteUrl() ], @@ -165,6 +169,10 @@ class TreeRenderer extends Renderer 'data-sortable-data-id-attr' => 'id', 'data-sortable-draggable' => '.movable', 'data-sortable-direction' => 'vertical', + 'data-sortable-group' => json_encode([ + 'name' => 'branch', + 'put' => 'function:rowPutAllowed' + ]), 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl() ->overwriteParams(['node' => (string) $node]) diff --git a/public/css/module.less b/public/css/module.less index 0b4f5c0..11c2421 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -45,6 +45,10 @@ ul.tree { height: 1em; } + ul.sortable { + min-height: 1em; // Required to be able to move items back to an otherwise empty list + } + > li.process, ul.sortable li { margin-bottom: .2em; diff --git a/public/js/behavior/sortable.js b/public/js/behavior/sortable.js index f641ca0..c02ce5d 100644 --- a/public/js/behavior/sortable.js +++ b/public/js/behavior/sortable.js @@ -32,6 +32,11 @@ } }); + if (typeof options.group !== 'undefined' && typeof options.group.put === 'string' && options.group.put.startsWith('function:')) { + var module = icinga.module($el.closest('.icinga-module').data('icingaModule')); + options.group.put = module.object[options.group.put.substr(9)]; + } + $(this).sortable(options); }); }; diff --git a/public/js/module.js b/public/js/module.js index 7e85b37..cca9518 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -118,27 +118,46 @@ }, rowDropped: function(event) { - var evt = event.originalEvent; - if (evt.oldIndex !== evt.newIndex) { - var $target = $(evt.to); - var actionUrl = icinga.utils.addUrlParams($target.data('actionUrl'), { - action: 'move', - movenode: $(evt.item).data('nodeName') - }); - - if ($('.placeholder', $target).length) { - evt.oldIndex -= 1; - evt.newIndex -= 1; - } + var evt = event.originalEvent, + $source = $(evt.from), + $target = $(evt.to); + if (evt.oldIndex !== evt.newIndex || !$target.is($source)) { var data = { csrfToken: $target.data('csrfToken'), movenode: 'movenode', // That's the submit button.. + parent: $target.parent('.process').data('nodeName') || '', from: evt.oldIndex, to: evt.newIndex }; + var actionUrl = icinga.utils.addUrlParams($source.data('actionUrl'), { + action: 'move', + movenode: $(evt.item).data('nodeName') + }); + icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'POST'); + event.stopPropagation(); + } + }, + + /** + * Called by Sortable.js while in Tree-View + * + * See group option on the sortable elements. + * + * @param to + * @param from + * @param item + * @param event + * @returns {*} + */ + rowPutAllowed: function(to, from, item, event) { + if (from.options.group.name === 'root') { + return true; + } + if (to.options.group.name === 'root') { + return $(item).is('.process'); } }, From eef8adb9be190bb79d436f8e16c787b20aa2d842 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 14:57:16 +0100 Subject: [PATCH 19/99] NodeAction: Allow method `appliesTo` to throw an exception Also implements it's usage in all available actions. --- .../Exception/ModificationError.php | 9 +++++ .../Modification/NodeAction.php | 25 ++++++++++++-- .../Modification/NodeAddChildrenAction.php | 6 ++-- .../Modification/NodeCreateAction.php | 12 ++++++- .../Modification/NodeModifyAction.php | 14 +++++--- .../Modification/NodeMoveAction.php | 33 ++++++++++++------- .../Modification/NodeRemoveAction.php | 13 ++++++-- 7 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 library/Businessprocess/Exception/ModificationError.php diff --git a/library/Businessprocess/Exception/ModificationError.php b/library/Businessprocess/Exception/ModificationError.php new file mode 100644 index 0000000..430d513 --- /dev/null +++ b/library/Businessprocess/Exception/ModificationError.php @@ -0,0 +1,9 @@ +getActionName() === $actionName; } + /** + * Throw a ModificationError + * + * @param string $msg + * @param mixed ... + * + * @throws ModificationError + */ + protected function error($msg) + { + $error = ModificationError::create(func_get_args()); + /** @var ModificationError $error */ + throw $error; + } + /** * Create an instance of a given actionName for a specific Node * diff --git a/library/Businessprocess/Modification/NodeAddChildrenAction.php b/library/Businessprocess/Modification/NodeAddChildrenAction.php index 65e8444..def37a1 100644 --- a/library/Businessprocess/Modification/NodeAddChildrenAction.php +++ b/library/Businessprocess/Modification/NodeAddChildrenAction.php @@ -17,11 +17,11 @@ class NodeAddChildrenAction extends NodeAction { $name = $this->getNodeName(); - if (! $config->hasNode($name)) { - return false; + if (! $config->hasBpNode($name)) { + $this->error('Process "%s" not found', $name); } - return $config->getNode($name) instanceof BpNode; + return true; } /** diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php index 69dc77c..e33c128 100644 --- a/library/Businessprocess/Modification/NodeCreateAction.php +++ b/library/Businessprocess/Modification/NodeCreateAction.php @@ -72,7 +72,17 @@ class NodeCreateAction extends NodeAction */ public function appliesTo(BpConfig $config) { - return ! $config->hasNode($this->getNodeName()); + $name = $this->getNodeName(); + if ($config->hasNode($name)) { + $this->error('A node with name "%s" already exists', $name); + } + + $parent = $this->getParentName(); + if ($parent !== null && !$config->hasBpNode($parent)) { + $this->error('Parent process "%s" missing', $parent); + } + + return true; } /** diff --git a/library/Businessprocess/Modification/NodeModifyAction.php b/library/Businessprocess/Modification/NodeModifyAction.php index 8cb240e..1b33094 100644 --- a/library/Businessprocess/Modification/NodeModifyAction.php +++ b/library/Businessprocess/Modification/NodeModifyAction.php @@ -47,15 +47,21 @@ class NodeModifyAction extends NodeAction $name = $this->getNodeName(); if (! $config->hasNode($name)) { - return false; + $this->error('Node "%s" not found', $name); } $node = $config->getNode($name); foreach ($this->properties as $key => $val) { - $func = 'get' . ucfirst($key); - if ($this->formerProperties[$key] !== $node->$func()) { - return false; + $currentVal = $node->{'get' . ucfirst($key)}(); + if ($this->formerProperties[$key] !== $currentVal) { + $this->error( + 'Property %s of node "%s" changed its value from "%s" to "%s"', + $key, + $name, + $this->formerProperties[$key], + $currentVal + ); } } diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php index f7428fc..c25bafb 100644 --- a/library/Businessprocess/Modification/NodeMoveAction.php +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -72,46 +72,55 @@ class NodeMoveAction extends NodeAction public function appliesTo(BpConfig $config) { if (! $config->getMetadata()->isManuallyOrdered()) { - return false; + $this->error('Process configuration is not manually ordered yet'); } $name = $this->getNodeName(); if ($this->parent !== null) { if (! $config->hasBpNode($this->parent)) { - return false; + $this->error('Parent process "%s" missing', $this->parent); } $parent = $config->getBpNode($this->parent); if (! $parent->hasChild($name)) { - return false; + $this->error('Node "%s" not found in process "%s"', $name, $this->parent); } $nodes = $parent->getChildNames(); if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) { - return false; + $this->error('Node "%s" not found at position %d', $name, $this->from); } } else { if (! $config->hasNode($name)) { - return false; + $this->error('Toplevel process "%s" not found', $name); } - if ($config->getBpNode($name)->getDisplay() !== $this->getFrom()) { - return false; + if ($config->getBpNode($name)->getDisplay() !== $this->from) { + $this->error('Toplevel process "%s" not found at position %d', $name, $this->from); } } if ($this->parent !== $this->newParent) { if ($this->newParent !== null) { if (! $config->hasBpNode($this->newParent)) { - return false; + $this->error('New parent process "%s" missing', $this->newParent); } $childrenCount = $config->getBpNode($this->newParent)->countChildren(); + if ($this->to > 0 && $childrenCount < $this->to) { + $this->error( + 'New parent process "%s" has not enough children. Target position %d out of range', + $this->newParent, + $this->to + ); + } } else { $childrenCount = $config->countChildren(); - } - - if ($this->getTo() > 0 && $childrenCount < $this->getTo()) { - return false; + if ($this->to > 0 && $childrenCount < $this->to) { + $this->error( + 'Process configuration has not enough toplevel processes. Target position %d out of range', + $this->to + ); + } } } diff --git a/library/Businessprocess/Modification/NodeRemoveAction.php b/library/Businessprocess/Modification/NodeRemoveAction.php index 09a20d2..fd8aa2b 100644 --- a/library/Businessprocess/Modification/NodeRemoveAction.php +++ b/library/Businessprocess/Modification/NodeRemoveAction.php @@ -40,12 +40,21 @@ class NodeRemoveAction extends NodeAction */ public function appliesTo(BpConfig $config) { + $name = $this->getNodeName(); $parent = $this->getParentName(); if ($parent === null) { - return $config->hasNode($this->getNodeName()); + if (!$config->hasNode($name)) { + $this->error('Toplevel process "%s" not found', $name); + } } else { - return $config->hasNode($this->getNodeName()) && $config->hasNode($this->getParentName()); + if (! $config->hasNode($parent)) { + $this->error('Parent process "%s" missing', $parent); + } elseif (! $config->getBpNode($parent)->hasChild($name)) { + $this->error('Node "%s" not found in process "%s"', $name, $parent); + } } + + return true; } /** From eb043b32c83593b944d835eb8c3d73cfc95191c4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 14:58:39 +0100 Subject: [PATCH 20/99] ProcessChanges: Verify that actions can be applied before pushing --- .../Modification/ProcessChanges.php | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php index 50a6226..0ed574c 100644 --- a/library/Businessprocess/Modification/ProcessChanges.php +++ b/library/Businessprocess/Modification/ProcessChanges.php @@ -14,6 +14,9 @@ class ProcessChanges /** @var Session */ protected $session; + /** @var BpConfig */ + protected $config; + /** @var bool */ protected $hasBeenModified = false; @@ -47,6 +50,7 @@ class ProcessChanges } } $changes->session = $session; + $changes->config = $bp; return $changes; } @@ -61,7 +65,7 @@ class ProcessChanges { $action = new NodeModifyAction($node); $action->setNodeProperties($node, $properties); - return $this->push($action); + return $this->push($action, true); } /** @@ -74,7 +78,7 @@ class ProcessChanges { $action = new NodeAddChildrenAction($node); $action->setChildren($children); - return $this->push($action); + return $this->push($action, true); } /** @@ -91,7 +95,7 @@ class ProcessChanges if ($parent !== null) { $action->setParent($parent); } - return $this->push($action); + return $this->push($action, true); } /** @@ -117,7 +121,7 @@ class ProcessChanges $action->setParentName($parentName); } - return $this->push($action); + return $this->push($action, true); } /** @@ -139,7 +143,7 @@ class ProcessChanges $action->setFrom($from); $action->setTo($to); - return $this->push($action); + return $this->push($action, true); } /** @@ -149,18 +153,23 @@ class ProcessChanges */ public function applyManualOrder() { - return $this->push(new NodeApplyManualOrderAction()); + return $this->push(new NodeApplyManualOrderAction(), true); } /** * Add a new action to the stack * - * @param NodeAction $change + * @param NodeAction $change + * @param bool $apply * * @return $this */ - public function push(NodeAction $change) + public function push(NodeAction $change, $apply = false) { + if ($apply && $change->appliesTo($this->config)) { + $change->applyTo($this->config); + } + $this->changes[] = $change; $this->hasBeenModified = true; return $this; From 3146bfda6a7f51cc75ce8352c0dd4cff4673217b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 14:59:56 +0100 Subject: [PATCH 21/99] MoveNodeForm: Show ModificationErrors as notifications --- application/forms/MoveNodeForm.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php index f2146e4..06ada01 100644 --- a/application/forms/MoveNodeForm.php +++ b/application/forms/MoveNodeForm.php @@ -6,6 +6,7 @@ use Icinga\Application\Icinga; use Icinga\Exception\Http\HttpException; use Icinga\Module\Businessprocess\BpConfig; use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Exception\ModificationError; use Icinga\Module\Businessprocess\Modification\ProcessChanges; use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\Web\Form\CsrfToken; @@ -144,13 +145,18 @@ class MoveNodeForm extends QuickForm $changes->applyManualOrder(); } - $changes->moveNode( - $this->node, - $this->getValue('from'), - $this->getValue('to'), - $this->getValue('parent'), - $this->parentNode !== null ? $this->parentNode->getName() : null - ); + try { + $changes->moveNode( + $this->node, + $this->getValue('from'), + $this->getValue('to'), + $this->getValue('parent'), + $this->parentNode !== null ? $this->parentNode->getName() : null + ); + } catch (ModificationError $e) { + $this->notifyError($e->getMessage()) + ->redirectAndExit($this->getSuccessUrl()); + } // Trigger session destruction to make sure it get's stored. unset($changes); From da4bf7ffc687137d27cf4ee4ac2477e1673b0174 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 15:00:39 +0100 Subject: [PATCH 22/99] NodeMoveAction: Don't allow to accidentally override the placement of nodes --- .../Businessprocess/Modification/NodeMoveAction.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php index c25bafb..1da93ae 100644 --- a/library/Businessprocess/Modification/NodeMoveAction.php +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -103,6 +103,12 @@ class NodeMoveAction extends NodeAction if ($this->newParent !== null) { if (! $config->hasBpNode($this->newParent)) { $this->error('New parent process "%s" missing', $this->newParent); + } elseif ($config->getBpNode($this->newParent)->hasChild($name)) { + $this->error( + 'New parent process "%s" already has a node with the name "%s"', + $this->newParent, + $name + ); } $childrenCount = $config->getBpNode($this->newParent)->countChildren(); @@ -114,6 +120,10 @@ class NodeMoveAction extends NodeAction ); } } else { + if ($config->hasRootNode($name)) { + $this->error('Process "%s" is already a toplevel process', $name); + } + $childrenCount = $config->countChildren(); if ($this->to > 0 && $childrenCount < $this->to) { $this->error( From 72f3af9fc9ed6cb5be278959e66d23ba462eb0be Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 15:01:43 +0100 Subject: [PATCH 23/99] BpNode: Reset keys when automatically sorting nodes --- library/Businessprocess/BpNode.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index 1ce5f0b..dc0c3fa 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -428,6 +428,7 @@ class BpNode extends Node { if (! $this->bp->getMetadata()->isManuallyOrdered()) { natcasesort($names); + $names = array_values($names); } $this->childNames = $names; @@ -451,6 +452,7 @@ class BpNode extends Node $this->children = array(); if (! $this->bp->getMetadata()->isManuallyOrdered()) { natcasesort($this->childNames); + $this->childNames = array_values($this->childNames); } foreach ($this->childNames as $name) { $this->children[$name] = $this->bp->getNode($name); From 0d0ac5381eb19965d8757585a2c38fe21d1a3aeb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 15:02:24 +0100 Subject: [PATCH 24/99] NodeApplyManualOrderAction: Also include non-toplevel processes --- .../Modification/NodeApplyManualOrderAction.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php index b2251ce..9be77e9 100644 --- a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php +++ b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php @@ -3,7 +3,6 @@ namespace Icinga\Module\Businessprocess\Modification; use Icinga\Module\Businessprocess\BpConfig; -use Icinga\Module\Businessprocess\BpNode; class NodeApplyManualOrderAction extends NodeAction { @@ -15,8 +14,7 @@ class NodeApplyManualOrderAction extends NodeAction public function applyTo(BpConfig $config) { $i = 0; - foreach ($config->getRootNodes() as $name => $node) { - /** @var BpNode $node */ + foreach ($config->getBpNodes() as $name => $node) { if ($node->getDisplay() > 0) { $node->setDisplay(++$i); } From ec14a7dbe165713bf566fb8ac2bc7c19a6387e73 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 15:03:10 +0100 Subject: [PATCH 25/99] NodeMoveAction: Properly check `display` values --- library/Businessprocess/Modification/NodeMoveAction.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php index 1da93ae..b2d8638 100644 --- a/library/Businessprocess/Modification/NodeMoveAction.php +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -94,8 +94,8 @@ class NodeMoveAction extends NodeAction $this->error('Toplevel process "%s" not found', $name); } - if ($config->getBpNode($name)->getDisplay() !== $this->from) { - $this->error('Toplevel process "%s" not found at position %d', $name, $this->from); + if ($config->getBpNode($name)->getDisplay() !== $this->from + 1) { + $this->error('Toplevel process "%s" not found at position %d', $name, $this->from + 1); } } From 98f2ed8f48be151b9c86f3bb6012d6e14db98146 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 15:03:37 +0100 Subject: [PATCH 26/99] TreeRenderer: Remove obsolete placeholder element --- library/Businessprocess/Renderer/TreeRenderer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index d96ba46..e2cbb8a 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -52,7 +52,6 @@ class TreeRenderer extends Renderer $nodes = $this->parent->getChildren(); } - $html[] = Html::tag('li', ['class' => 'placeholder']); foreach ($nodes as $name => $node) { $html[] = $this->renderNode($bp, $node); } From f022c0f101664a7e1e20de5bc3d5ae3d1c895edb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 14 Jan 2019 15:54:05 +0100 Subject: [PATCH 27/99] NodeMoveAction: Properly handle unbound nodes --- library/Businessprocess/Modification/NodeMoveAction.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php index b2d8638..4b1735d 100644 --- a/library/Businessprocess/Modification/NodeMoveAction.php +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -176,9 +176,9 @@ class NodeMoveAction extends NodeAction $config->addRootNode($name); $i = 0; - foreach ($newNodes as $_ => $newNode) { + foreach ($newNodes as $newName => $newNode) { /** @var BpNode $newNode */ - if ($newNode->getDisplay() > 0) { + if ($newNode->getDisplay() > 0 || $newName === $name) { $i += 1; if ($newNode->getDisplay() !== $i) { $newNode->setDisplay($i); @@ -193,6 +193,7 @@ class NodeMoveAction extends NodeAction } else { if ($this->newParent !== null) { $config->removeRootNode($name); + $node->setDisplay(0); } $i = 0; From bc03569765cf8ca68c21be30e952ca356b7b6e75 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 15 Jan 2019 09:35:57 +0100 Subject: [PATCH 28/99] Properly handle display values when performing other actions than moving nodes --- application/forms/AddNodeForm.php | 9 +++++++-- application/forms/EditNodeForm.php | 5 +++-- application/forms/ProcessForm.php | 9 +++++++-- .../Businessprocess/Modification/NodeCreateAction.php | 9 +++++++++ .../Businessprocess/Modification/NodeRemoveAction.php | 11 +++++++++++ 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index 42778ca..2efa1b0 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -122,15 +122,20 @@ class AddNodeForm extends QuickForm ) )); + $display = 1; + if ($this->bp->getMetadata()->isManuallyOrdered() && !$this->bp->isEmpty()) { + $rootNodes = $this->bp->getRootNodes(); + $display = end($rootNodes)->getDisplay() + 1; + } $this->addElement('select', 'display', array( 'label' => $this->translate('Visualization'), 'required' => true, 'description' => $this->translate( 'Where to show this process' ), - 'value' => $this->hasParentNode() ? '0' : '1', + 'value' => $this->hasParentNode() ? '0' : "$display", 'multiOptions' => array( - '1' => $this->translate('Toplevel Process'), + "$display" => $this->translate('Toplevel Process'), '0' => $this->translate('Subprocess only'), ) )); diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php index 5267c12..f1fd29c 100644 --- a/application/forms/EditNodeForm.php +++ b/application/forms/EditNodeForm.php @@ -126,15 +126,16 @@ class EditNodeForm extends QuickForm ) )); + $display = $this->getNode()->getDisplay() ?: 1; $this->addElement('select', 'display', array( 'label' => $this->translate('Visualization'), 'required' => true, 'description' => $this->translate( 'Where to show this process' ), - 'value' => $this->hasParentNode() ? '0' : '1', + 'value' => $display, 'multiOptions' => array( - '1' => $this->translate('Toplevel Process'), + "$display" => $this->translate('Toplevel Process'), '0' => $this->translate('Subprocess only'), ) )); diff --git a/application/forms/ProcessForm.php b/application/forms/ProcessForm.php index 9ee390d..bf233e4 100644 --- a/application/forms/ProcessForm.php +++ b/application/forms/ProcessForm.php @@ -73,14 +73,20 @@ class ProcessForm extends QuickForm ) )); + if ($this->node !== null) { + $display = $this->node->getDisplay() ?: 1; + } else { + $display = 1; + } $this->addElement('select', 'display', array( 'label' => $this->translate('Visualization'), 'required' => true, 'description' => $this->translate( 'Where to show this process' ), + 'value' => $display, 'multiOptions' => array( - '1' => $this->translate('Toplevel Process'), + "$display" => $this->translate('Toplevel Process'), '0' => $this->translate('Subprocess only'), ) )); @@ -97,7 +103,6 @@ class ProcessForm extends QuickForm $this->getElement('alias')->setValue($node->getAlias()); } $this->getElement('operator')->setValue($node->getOperator()); - $this->getElement('display')->setValue($node->getDisplay()); if ($node->hasInfoUrl()) { $this->getElement('url')->setValue($node->getInfoUrl()); } diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php index e33c128..2e0fc6b 100644 --- a/library/Businessprocess/Modification/NodeCreateAction.php +++ b/library/Businessprocess/Modification/NodeCreateAction.php @@ -112,6 +112,15 @@ class NodeCreateAction extends NodeAction $node->$func($val); } + if ($node->getDisplay() > 1) { + $i = $node->getDisplay(); + foreach ($config->getRootNodes() as $_ => $rootNode) { + if ($rootNode->getDisplay() >= $node->getDisplay()) { + $rootNode->setDisplay(++$i); + } + } + } + $config->addNode($name, $node); return $node; diff --git a/library/Businessprocess/Modification/NodeRemoveAction.php b/library/Businessprocess/Modification/NodeRemoveAction.php index fd8aa2b..64d8901 100644 --- a/library/Businessprocess/Modification/NodeRemoveAction.php +++ b/library/Businessprocess/Modification/NodeRemoveAction.php @@ -66,7 +66,18 @@ class NodeRemoveAction extends NodeAction $name = $this->getNodeName(); $parentName = $this->getParentName(); if ($parentName === null) { + $oldDisplay = $config->getBpNode($name)->getDisplay(); $config->removeNode($name); + if ($config->getMetadata()->isManuallyOrdered()) { + foreach ($config->getRootNodes() as $_ => $node) { + $nodeDisplay = $node->getDisplay(); + if ($nodeDisplay > $oldDisplay) { + $node->setDisplay($node->getDisplay() - 1); + } elseif ($nodeDisplay === $oldDisplay) { + break; // Stop immediately to not make things worse ;) + } + } + } } else { $node = $config->getNode($name); $parent = $config->getBpNode($parentName); From 93a2589909ebba43510620e7aae9934ed6a1f9ca Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 15 Jan 2019 09:50:14 +0100 Subject: [PATCH 29/99] NodeMoveAction: Interpret `$from` as index and not as display value --- library/Businessprocess/Modification/NodeMoveAction.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php index 4b1735d..5754717 100644 --- a/library/Businessprocess/Modification/NodeMoveAction.php +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -90,12 +90,13 @@ class NodeMoveAction extends NodeAction $this->error('Node "%s" not found at position %d', $name, $this->from); } } else { - if (! $config->hasNode($name)) { + if (! $config->hasRootNode($name)) { $this->error('Toplevel process "%s" not found', $name); } - if ($config->getBpNode($name)->getDisplay() !== $this->from + 1) { - $this->error('Toplevel process "%s" not found at position %d', $name, $this->from + 1); + $nodes = $config->listRootNodes(); + if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) { + $this->error('Toplevel process "%s" not found at position %d', $name, $this->from); } } From b795d278ee256b7480a3a483f076c3fa2dbb0f84 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 15 Jan 2019 10:35:02 +0100 Subject: [PATCH 30/99] TreeRenderer: Properly render empty processes --- library/Businessprocess/Renderer/TreeRenderer.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index e2cbb8a..7981bd5 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -181,7 +181,7 @@ class TreeRenderer extends Renderer $path[] = (string) $node; foreach ($node->getChildren() as $name => $child) { - if ($child->hasChildren()) { + if ($child instanceof BpNode) { $tbody->add($this->renderNode($bp, $child, $this->getCurrentPath())); } else { $this->renderChild($bp, $tbody, $child, $path); @@ -191,7 +191,7 @@ class TreeRenderer extends Renderer return $table; } - protected function renderChild($bp, BaseHtmlElement $ul, $node, $path = null) + protected function renderChild($bp, BaseHtmlElement $ul, Node $node, $path = null) { $li = Html::tag('li', [ 'class' => 'movable', @@ -200,10 +200,6 @@ class TreeRenderer extends Renderer ]); $ul->add($li); - if ($node instanceof BpNode && $node->hasInfoUrl()) { - $li->add($this->createInfoAction($node)); - } - if (! $this->isLocked()) { $li->add($this->getActionIcons($bp, $node)); } @@ -212,10 +208,6 @@ class TreeRenderer extends Renderer $link->getAttributes()->set('data-base-target', '_next'); $link->add($this->getNodeIcons($node)); - if ($node->hasChildren()) { - $link->add($this->renderStateBadges($node->getStateSummary())); - } - if ($time = $node->getLastStateChange()) { $since = $this->timeSince($time)->prepend( sprintf(' (%s ', $node->getStateName()) From 493328ff20833248de9da3e3bc47279a0010490e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 15 Jan 2019 12:41:02 +0100 Subject: [PATCH 31/99] Make moving non-root tiles working again --- library/Businessprocess/Renderer/TileRenderer.php | 3 +++ public/js/module.js | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index 071c904..098318d 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -27,6 +27,9 @@ class TileRenderer extends Renderer 'data-action-url' => $this->getUrl()->getAbsoluteUrl() ] ); + if (! $this->wantsRootNodes()) { + $nodesDiv->attributes()->add('data-node-name', $this->parent->getName()); + } $nodes = $this->getChildNodes(); diff --git a/public/js/module.js b/public/js/module.js index cca9518..88a6ec4 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -94,26 +94,27 @@ tileDropped: function(event) { var evt = event.originalEvent; if (evt.oldIndex !== evt.newIndex) { - var $target = $(evt.to); - var actionUrl = icinga.utils.addUrlParams($target.data('actionUrl'), { + var $source = $(evt.from); + var actionUrl = icinga.utils.addUrlParams($source.data('actionUrl'), { action: 'move', movenode: $(evt.item).data('nodeName') }); - if (! $target.is('.few') && $('.addnew', $target).length === 2) { + if (! $source.is('.few') && $('.addnew', $source).length === 2) { // This assumes we're not moving things between different lists evt.oldIndex -= 1; evt.newIndex -= 1; } var data = { - csrfToken: $target.data('csrfToken'), + csrfToken: $source.data('csrfToken'), movenode: 'movenode', // That's the submit button.. + parent: $(evt.to).data('nodeName') || '', from: evt.oldIndex, to: evt.newIndex }; - icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'POST'); + icinga.loader.loadUrl(actionUrl, $source.closest('.container'), data, 'POST'); } }, From fe8f4d8c15386343be164551481a0fba6ab6d7bb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 16 Jan 2019 13:38:30 +0100 Subject: [PATCH 32/99] js: Don't permit to create loops --- .../Businessprocess/Renderer/TreeRenderer.php | 5 +++-- public/js/module.js | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 7981bd5..f27cd10 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -111,10 +111,11 @@ class TreeRenderer extends Renderer */ public function renderNode(BpConfig $bp, Node $node, $path = array()) { + $htmlId = $this->getId($node, $path); $table = Html::tag( 'li', [ - 'id' => $this->getId($node, $path), + 'id' => $htmlId, 'class' => ['bp', 'movable', $node->getObjectClassName()], 'data-node-name' => $node->getName() ] @@ -169,7 +170,7 @@ class TreeRenderer extends Renderer 'data-sortable-draggable' => '.movable', 'data-sortable-direction' => 'vertical', 'data-sortable-group' => json_encode([ - 'name' => 'branch', + 'name' => $htmlId, // Unique, so that the function below is the only deciding factor 'put' => 'function:rowPutAllowed' ]), 'data-csrf-token' => CsrfToken::generate(), diff --git a/public/js/module.js b/public/js/module.js index 88a6ec4..6010fb4 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -151,15 +151,24 @@ * @param from * @param item * @param event - * @returns {*} + * @returns boolean */ rowPutAllowed: function(to, from, item, event) { - if (from.options.group.name === 'root') { - return true; - } if (to.options.group.name === 'root') { return $(item).is('.process'); } + + // Otherwise we're facing a nesting error next + var $item = $(item), + childrenNames = $item.find('.process').map(function () { + return $(this).data('nodeName'); + }).get(); + childrenNames.push($item.data('nodeName')); + var loopDetected = $(to.el).parents('.process').toArray().some(function (parent) { + return childrenNames.indexOf($(parent).data('nodeName')) !== -1; + }); + + return !loopDetected; }, /** From 3ba48429338913fe4fb410c96334d7b1b0f595d3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 11:19:24 +0100 Subject: [PATCH 33/99] BpNode: Use all uppercase for operator names --- library/Businessprocess/BpNode.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index dc0c3fa..89586fa 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -506,13 +506,13 @@ class BpNode extends Node { switch ($this->operator) { case self::OP_AND: - return 'and'; + return 'AND'; break; case self::OP_OR: - return 'or'; + return 'OR'; break; case self::OP_NOT: - return 'not'; + return 'NOT'; break; default: // MIN From 505471c51926755f472a691de0789ae652042b3a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 11:21:40 +0100 Subject: [PATCH 34/99] Node: Introduce `$icon` property --- library/Businessprocess/BpNode.php | 6 ++++++ library/Businessprocess/HostNode.php | 2 ++ library/Businessprocess/ImportedNode.php | 2 ++ library/Businessprocess/Node.php | 12 ++++++++++++ library/Businessprocess/ServiceNode.php | 2 ++ 5 files changed, 24 insertions(+) diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index 89586fa..f244343 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -520,4 +520,10 @@ class BpNode extends Node return 'min:' . $this->operator; } } + + public function getIcon() + { + $this->icon = $this->hasParents() ? 'cubes' : 'sitemap'; + return parent::getIcon(); + } } diff --git a/library/Businessprocess/HostNode.php b/library/Businessprocess/HostNode.php index 130cebe..cea73c2 100644 --- a/library/Businessprocess/HostNode.php +++ b/library/Businessprocess/HostNode.php @@ -32,6 +32,8 @@ class HostNode extends MonitoredNode protected $className = 'host'; + protected $icon = 'host'; + public function __construct(BpConfig $bp, $object) { $this->name = $object->hostname . ';Hoststatus'; diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php index 00c65f7..dcb9944 100644 --- a/library/Businessprocess/ImportedNode.php +++ b/library/Businessprocess/ImportedNode.php @@ -22,6 +22,8 @@ class ImportedNode extends Node protected $className = 'subtree'; + protected $icon = 'download'; + /** @var BpConfig */ private $config; diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 0247382..6784965 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -83,6 +83,13 @@ abstract class Node // obsolete protected $duration; + /** + * This node's icon + * + * @var string + */ + protected $icon; + /** * Last state change, unix timestamp * @@ -382,6 +389,11 @@ abstract class Node return Html::tag('a', ['href' => '#'], $this->getAlias()); } + public function getIcon() + { + return Html::tag('i', ['class' => 'icon icon-' . ($this->icon ?: 'attention-circled')]); + } + public function operatorHtml() { return ' '; diff --git a/library/Businessprocess/ServiceNode.php b/library/Businessprocess/ServiceNode.php index 517b059..153b010 100644 --- a/library/Businessprocess/ServiceNode.php +++ b/library/Businessprocess/ServiceNode.php @@ -12,6 +12,8 @@ class ServiceNode extends MonitoredNode protected $className = 'service'; + protected $icon = 'service'; + public function __construct(BpConfig $bp, $object) { $this->name = $object->hostname . ';' . $object->service; From 35fe4cb943b8e25957daf6f2777926cb3e1a0714 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 11:32:14 +0100 Subject: [PATCH 35/99] Tree-/TileRenderer: Use a literal bool for `data-sortable-disabled` --- library/Businessprocess/Renderer/TileRenderer.php | 2 +- library/Businessprocess/Renderer/TreeRenderer.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index 098318d..d9e979e 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -19,7 +19,7 @@ class TileRenderer extends Renderer [ 'class' => ['sortable', 'tiles', $this->howMany()], 'data-base-target' => '_next', - 'data-sortable-disabled' => $this->isLocked(), + 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', 'data-sortable-data-id-attr' => 'id', 'data-sortable-filter' => '.addnew', 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index f27cd10..fdc0705 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -22,7 +22,7 @@ class TreeRenderer extends Renderer [ 'id' => $bp->getHtmlId(), 'class' => ['tree', 'sortable'], - 'data-sortable-disabled' => $this->isLocked(), + 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', 'data-sortable-data-id-attr' => 'id', 'data-sortable-direction' => 'vertical', 'data-sortable-group' => json_encode([ @@ -165,7 +165,7 @@ class TreeRenderer extends Renderer $tbody = Html::tag('ul', [ 'class' => 'sortable', - 'data-sortable-disabled' => $this->isLocked(), + 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', 'data-sortable-data-id-attr' => 'id', 'data-sortable-draggable' => '.movable', 'data-sortable-direction' => 'vertical', From bb62fe704859f66d55994c6865f3f5a7b2453f80 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 11:40:33 +0100 Subject: [PATCH 36/99] TreeRenderer: Properly render sub-processes if viewed individually --- library/Businessprocess/Renderer/TreeRenderer.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index fdc0705..c71d1be 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -53,7 +53,11 @@ class TreeRenderer extends Renderer } foreach ($nodes as $name => $node) { - $html[] = $this->renderNode($bp, $node); + if ($node instanceof BpNode) { + $html[] = $this->renderNode($bp, $node); + } else { + $html[] = $this->renderChild($bp, $node); + } } return $html; @@ -183,23 +187,22 @@ class TreeRenderer extends Renderer $path[] = (string) $node; foreach ($node->getChildren() as $name => $child) { if ($child instanceof BpNode) { - $tbody->add($this->renderNode($bp, $child, $this->getCurrentPath())); + $tbody->add($this->renderNode($bp, $child, $path)); } else { - $this->renderChild($bp, $tbody, $child, $path); + $tbody->add($this->renderChild($bp, $child, $path)); } } return $table; } - protected function renderChild($bp, BaseHtmlElement $ul, Node $node, $path = null) + protected function renderChild($bp, Node $node, $path = null) { $li = Html::tag('li', [ 'class' => 'movable', 'id' => $this->getId($node, $path ?: []), 'data-node-name' => (string) $node ]); - $ul->add($li); if (! $this->isLocked()) { $li->add($this->getActionIcons($bp, $node)); @@ -216,7 +219,7 @@ class TreeRenderer extends Renderer $link->add($since); } - $li->add($link); + return $li; } protected function getActionIcons(BpConfig $bp, Node $node) From d880291c0220c02ed8bced3e1804447bd8ee2fb8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 11:44:05 +0100 Subject: [PATCH 37/99] TreeRenderer: Restructure node layout --- library/Businessprocess/Node.php | 4 +- library/Businessprocess/Renderer/Renderer.php | 16 --- .../Businessprocess/Renderer/TreeRenderer.php | 97 ++++++++++--------- 3 files changed, 52 insertions(+), 65 deletions(-) diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 6784965..05d1a2d 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -386,7 +386,9 @@ abstract class Node public function getLink() { - return Html::tag('a', ['href' => '#'], $this->getAlias()); + return Html::tag('a', ['href' => '#', 'class' => 'toggle'], Html::tag('i', [ + 'class' => 'icon icon-down-dir' + ])); } public function getIcon() diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index e076438..08898e4 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -298,22 +298,6 @@ abstract class Renderer extends HtmlDocument return $this->isBreadcrumb; } - public function timeSince($time, $timeOnly = false) - { - if (! $time) { - return HtmlString::create(''); - } - - return Html::tag( - 'span', - [ - 'class' => ['relative-time', 'time-since'], - 'title' => DateFormatter::formatDateTime($time) - ], - DateFormatter::timeSince($time, $timeOnly) - ); - } - protected function createUnboundParent(BpConfig $bp) { return $bp->getNode('__unbound__'); diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index c71d1be..0280f5c 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -2,12 +2,14 @@ namespace Icinga\Module\Businessprocess\Renderer; +use Icinga\Date\DateFormatter; use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\BpConfig; use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\Web\Form\CsrfToken; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; +use ipl\Web\Widget\StateBall; class TreeRenderer extends Renderer { @@ -17,19 +19,21 @@ class TreeRenderer extends Renderer public function render() { $bp = $this->config; + $htmlId = $bp->getHtmlId(); $this->add(Html::tag( 'ul', [ - 'id' => $bp->getHtmlId(), - 'class' => ['tree', 'sortable'], + 'id' => $htmlId, + 'class' => ['bp', 'sortable'], 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', 'data-sortable-data-id-attr' => 'id', 'data-sortable-direction' => 'vertical', 'data-sortable-group' => json_encode([ - 'name' => 'root', + 'name' => $this->wantsRootNodes() ? 'root' : $htmlId, 'put' => 'function:rowPutAllowed' ]), 'data-sortable-invert-swap' => 'true', + 'data-is-root-config' => $this->wantsRootNodes() ? 'true' : 'false', 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl()->getAbsoluteUrl() ], @@ -92,11 +96,24 @@ class TreeRenderer extends Renderer /** * @param Node $node + * @param array $path * @return BaseHtmlElement[] */ - public function getNodeIcons(Node $node) + public function getNodeIcons(Node $node, array $path = null) { - $icons = array(); + $icons = []; + if (empty($path)) { + $icons[] = Html::tag('i', ['class' => 'icon icon-sitemap']); + } else { + $icons[] = $node->getIcon(); + } + $icons[] = (new StateBall(strtolower($node->getStateName())))->addAttributes([ + 'title' => sprintf( + '%s %s', + $node->getStateName(), + DateFormatter::timeSince($node->getLastStateChange()) + ) + ]); if ($node->isInDowntime()) { $icons[] = Html::tag('i', ['class' => 'icon icon-moon']); } @@ -116,7 +133,7 @@ class TreeRenderer extends Renderer public function renderNode(BpConfig $bp, Node $node, $path = array()) { $htmlId = $this->getId($node, $path); - $table = Html::tag( + $li = Html::tag( 'li', [ 'id' => $htmlId, @@ -124,7 +141,7 @@ class TreeRenderer extends Renderer 'data-node-name' => $node->getName() ] ); - $attributes = $table->getAttributes(); + $attributes = $li->getAttributes(); $attributes->add('class', $this->getStateClassNames($node)); if ($node->isHandled()) { $attributes->add('class', 'handled'); @@ -135,41 +152,30 @@ class TreeRenderer extends Renderer $attributes->add('class', 'node'); } - if ($node instanceof BpNode) { - $table->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); - } + $div = Html::tag('div'); + $li->add($div); - $td = Html::tag('div'); - $table->add($td); + $div->add($node->getLink()); + $div->add($this->getNodeIcons($node, $path)); if ($node instanceof BpNode && $node->hasInfoUrl()) { - $td->add($this->createInfoAction($node)); + $div->add($this->createInfoAction($node)); + } + + $div->add(Html::tag('span', null, $node->getAlias())); + + if ($node instanceof BpNode) { + $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); } if (! $this->isLocked()) { - $td->add($this->getActionIcons($bp, $node)); + $div->add($this->getActionIcons($bp, $node)); } - $link = $node->getLink(); - $link->getAttributes()->set('data-base-target', '_next'); - $link->add($this->getNodeIcons($node)); - - if ($node->hasChildren()) { - $link->add($this->renderStateBadges($node->getStateSummary())); - } - - if ($time = $node->getLastStateChange()) { - $since = $this->timeSince($time)->prepend( - sprintf(' (%s ', $node->getStateName()) - )->add(')'); - $link->add($since); - } - - $td->add($link); - - $tbody = Html::tag('ul', [ - 'class' => 'sortable', + $ul = Html::tag('ul', [ + 'class' => ['bp', 'sortable'], 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', + 'data-sortable-invert-swap' => 'true', 'data-sortable-data-id-attr' => 'id', 'data-sortable-draggable' => '.movable', 'data-sortable-direction' => 'vertical', @@ -182,18 +188,18 @@ class TreeRenderer extends Renderer ->overwriteParams(['node' => (string) $node]) ->getAbsoluteUrl() ]); - $table->add($tbody); + $li->add($ul); $path[] = (string) $node; foreach ($node->getChildren() as $name => $child) { if ($child instanceof BpNode) { - $tbody->add($this->renderNode($bp, $child, $path)); + $ul->add($this->renderNode($bp, $child, $path)); } else { - $tbody->add($this->renderChild($bp, $child, $path)); + $ul->add($this->renderChild($bp, $child, $path)); } } - return $table; + return $li; } protected function renderChild($bp, Node $node, $path = null) @@ -204,19 +210,14 @@ class TreeRenderer extends Renderer 'data-node-name' => (string) $node ]); - if (! $this->isLocked()) { - $li->add($this->getActionIcons($bp, $node)); - } + $li->add($this->getNodeIcons($node, $path)); $link = $node->getLink(); $link->getAttributes()->set('data-base-target', '_next'); - $link->add($this->getNodeIcons($node)); + $li->add($link); - if ($time = $node->getLastStateChange()) { - $since = $this->timeSince($time)->prepend( - sprintf(' (%s ', $node->getStateName()) - )->add(')'); - $link->add($since); + if (! $this->isLocked()) { + $li->add($this->getActionIcons($bp, $node)); } return $li; @@ -238,7 +239,7 @@ class TreeRenderer extends Renderer protected function createEditAction(BpConfig $bp, BpNode $node) { return $this->actionIcon( - 'wrench', + 'edit', $this->getUrl()->with(array( 'action' => 'edit', 'editnode' => $node->getName() @@ -277,7 +278,7 @@ class TreeRenderer extends Renderer [ 'href' => $url, 'title' => $title, - 'style' => 'float: right' + 'class' => 'action-link' ], Html::tag('i', ['class' => 'icon icon-' . $icon]) ); From a3ce6e1543c1a043b8abf2304895a81e605ec971 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 11:45:35 +0100 Subject: [PATCH 38/99] tree: Apply final design --- configuration.php | 4 +- public/css/module.less | 143 +++++++++++++++++++++++++++++++------ public/css/state-ball.less | 58 +++++++++++++++ 3 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 public/css/state-ball.less diff --git a/configuration.php b/configuration.php index f09ee04..724c23c 100644 --- a/configuration.php +++ b/configuration.php @@ -60,4 +60,6 @@ $this->provideRestriction( $this->provideJsFile('vendor/Sortable.js'); $this->provideJsFile('behavior/sortable.js'); -$this->provideJsFile('vendor/jquery.fn.sortable.js'); \ No newline at end of file +$this->provideJsFile('vendor/jquery.fn.sortable.js'); + +$this->provideCssFile('state-ball.less'); \ No newline at end of file diff --git a/public/css/module.less b/public/css/module.less index 11c2421..c39163b 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -37,35 +37,136 @@ div.bp.sortable > .sortable-ghost { /* New tree stuff START */ -ul.tree { - padding: 1em; +@vertical-tree-item-gap: .5em; - li.placeholder { - // Helps to assist Sortable.js/the browser to properly place items at the top - height: 1em; +ul.bp { + margin: 0; + padding: 0; + list-style-type: none; + + .action-link { + font-size: 1.3em; + line-height: 1; } - ul.sortable { - min-height: 1em; // Required to be able to move items back to an otherwise empty list - } + // 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; + mix-blend-mode: hard-light; - > li.process, - ul.sortable li { - margin-bottom: .2em; - padding-left: .2em; - - &.sortable-ghost { - border: .2em dashed @gray-light; - border-left-width: 0; - } - - a > div { - display: inline-block; + &.process:after { + // TODO: Only apply if content overflows? + content: " "; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50%; + background: linear-gradient(transparent, white); } } - ul { + // header style + li.process > div { + padding-bottom: .25em; + border-bottom: 1px solid @gray-light; + + > a.toggle { + color: @gray; + } + + > span { + font-size: 1.25em; + + &.op { + padding: .1em .5em; + border-radius: .5em; + background-color: @gray-light; + font-weight: bold; + font-size: 1em; + color: white; + } + } + } + + // subprocess style + li.process > ul { + padding-left: 2em; list-style-type: none; + + &.sortable { + min-height: 1em; // Required to be able to move items back to an otherwise empty list + } + } + + // vertical layout + > li { + padding: @vertical-tree-item-gap 0; + + &.process { + padding-bottom: 0; + + > div { + margin-bottom: @vertical-tree-item-gap; + } + } + } + + // horizontal layout + li.process > div, + li:not(.process) { + display: flex; + align-items: center; + + > * { + margin-right: .5em; + } + + > a.action-link { + margin-left: auto; // Let the first action link move everything to the right + + & + a.action-link { + margin-left: 0; // But really only the first one + } + } + } + + // collapse handling + li.process { + > div { + cursor: pointer; // So that users know they can interact + } + + // 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; + } + + // 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; + } + } } } diff --git a/public/css/state-ball.less b/public/css/state-ball.less new file mode 100644 index 0000000..54a8e7a --- /dev/null +++ b/public/css/state-ball.less @@ -0,0 +1,58 @@ +.state-ball { + border-radius: 50%; + display: inline-block; + text-align: center; + vertical-align: middle; + + &.state-critical, + &.state-down { + background-color: @color-critical; + } + + &.state-unknown { + background-color: @color-unknown; + } + + &.state-warning { + background-color: @color-warning; + } + + &.state-ok, + &.state-up { + background-color: @color-ok; + } + + &.state-pending { + background-color: @color-pending; + } + + &.size-xs { + line-height: 0.75em; + height: 0.75em; + width: 0.75em; + } + + &.size-s { + line-height: 1em; + height: 1em; + width: 1em; + } + + &.size-m { + line-height: 2em; + height: 2em; + width: 2em; + } + + &.size-l { + line-height: 2.5em; + height: 2.5em; + width: 2.5em; + } + + > i { + color: white; + font-style: normal; + text-transform: uppercase; + } +} From d42548216b86939223e5ac7cd4c9c3aaea5a15ec Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 11:46:25 +0100 Subject: [PATCH 39/99] js: Cleanup and make tree collapse handling work with the new layout --- public/js/module.js | 180 ++++++++++---------------------------------- 1 file changed, 41 insertions(+), 139 deletions(-) diff --git a/public/js/module.js b/public/js/module.js index 6010fb4..45de53f 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -21,22 +21,17 @@ /** * Tell Icinga about our event handlers */ - this.module.on('beforerender', this.rememberOpenedBps); - this.module.on('rendered', this.onRendered); + this.module.on('rendered', this.onRendered); - this.module.on('click', 'table.bp.process > tbody > tr:first-child > td > a:last-child', this.processTitleClick); - this.module.on('click', 'table.bp > tbody > tr:first-child > th', this.processOperatorClick); this.module.on('focus', 'form input, form textarea, form select', this.formElementFocus); - this.module.on('mouseenter', 'table.bp > tbody > tr > td > a', this.procMouseOver); - this.module.on('mouseenter', 'table.bp > tbody > tr > th', this.procMouseOver); - this.module.on('mouseenter', 'table.node.missing > tbody > tr > td > span', this.procMouseOver); - this.module.on('mouseleave', 'div.bp', this.procMouseOut); + this.module.on('click', 'li.process a.toggle', this.processToggleClick); + this.module.on('click', 'li.process > div', this.processHeaderClick); + this.module.on('end', 'div.tree.sortable, ul.sortable', this.rowDropped); this.module.on('click', 'div.tiles > div', this.tileClick); this.module.on('click', '.dashboard-tile', this.dashboardTileClick); this.module.on('end', 'div.tiles.sortable', this.tileDropped); - this.module.on('end', 'div.tree.sortable, ul.sortable', this.rowDropped); this.module.icinga.logger.debug('BP module initialized'); }, @@ -44,39 +39,40 @@ onRendered: function (event) { var $container = $(event.currentTarget); this.fixFullscreen($container); - this.fixOpenedBps($container); + this.restoreCollapsedBps($container); this.highlightFormErrors($container); this.hideInactiveFormDescriptions($container); this.fixTileLinksOnDashboard($container); }, - processTitleClick: function (event) { + processToggleClick: function (event) { event.stopPropagation(); - var $el = $(event.currentTarget).closest('table.bp'); - $el.toggleClass('collapsed'); - }, - processOperatorClick: function (event) { - event.stopPropagation(); - var $el = $(event.currentTarget).closest('table.bp'); + var $li = $(event.currentTarget).closest('li.process'); + $li.toggleClass('collapsed'); - // Click on arrow - $el.removeClass('collapsed'); - - var children = $el.find('> tbody > tr > td > table.bp.process'); - if (children.length === 0) { - $el.toggleClass('collapsed'); + var $bpUl = $(event.currentTarget).closest('.content > ul.bp'); + if (! $bpUl.length || !$bpUl.data('isRootConfig')) { return; } - if (children.filter('.collapsed').length) { - children.removeClass('collapsed'); - } else { - children.each(function(idx, el) { - var $el = $(el); - $el.addClass('collapsed'); - $el.find('table.bp.process').addClass('collapsed'); - }); + + var bpName = $bpUl.attr('id'); + if (typeof this.idCache[bpName] === 'undefined') { + this.idCache[bpName] = []; } + + var index = this.idCache[bpName].indexOf($li.attr('id')); + if ($li.is('.collapsed')) { + if (index === -1) { + this.idCache[bpName].push($li.attr('id')); + } + } else if (index !== -1) { + this.idCache[bpName].splice(index, 1); + } + }, + + processHeaderClick: function (event) { + this.processToggleClick(event); }, hideInactiveFormDescriptions: function($container) { @@ -171,75 +167,6 @@ return !loopDetected; }, - /** - * Add 'hovered' class to hovered title elements - * - * TODO: Skip on tablets - */ - procMouseOver: function (event) { - event.stopPropagation(); - var $hovered = $(event.currentTarget); - var $el = $hovered.closest('table.bp'); - - if ($el.is('.operator')) { - if (!$hovered.closest('tr').is('tr:first-child')) { - // Skip hovered space between cols - return; - } - } else { - // return; - } - - $('table.bp.hovered').not($el.parents('table.bp')).removeClass('hovered'); // not self & parents - $el.addClass('hovered'); - $el.parents('table.bp').addClass('hovered'); - }, - - /** - * Remove 'hovered' class from hovered title elements - * - * TODO: Skip on tablets - */ - procMouseOut: function (event) { - $('table.bp.hovered').removeClass('hovered'); - }, - - /** - * Handle clicks on operator or title element - * - * Title shows subelement, operator unfolds all subelements - */ - titleClicked: function (event) { - var self = this; - event.stopPropagation(); - event.preventDefault(); - var $el = $(event.currentTarget), - affected = [] - $container = $el.closest('.container'); - if ($el.hasClass('operator')) { - $affected = $el.closest('table').children('tbody') - .children('tr.children').children('td').children('table'); - - // Only if there are child BPs - if ($affected.find('th.operator').length < 1) { - $affected = $el.closest('table'); - } - } else { - $affected = $el.closest('table'); - } - $affected.each(function (key, el) { - var $bptable = $(el).closest('table'); - $bptable.toggleClass('collapsed'); - if ($bptable.hasClass('collapsed')) { - $bptable.find('table').addClass('collapsed'); - } - }); - - /*$container.data('refreshParams', { - opened: self.listOpenedBps($container) - });*/ - }, - fixTileLinksOnDashboard: function($container) { if ($container.closest('div.dashboard').length) { $container.find('div.tiles').data('baseTarget', '_next'); @@ -267,48 +194,23 @@ } }, - fixOpenedBps: function($container) { - var $bpDiv = $container.find('div.bp'); - var bpName = $bpDiv.attr('id'); - - if (typeof this.idCache[bpName] === 'undefined') { - return; - } - var $procs = $bpDiv.find('table.process'); - - $.each(this.idCache[bpName], function(idx, id) { - var $el = $('#' + id); - $procs = $procs.not($el); - - $el.parents('table.process').each(function (idx, el) { - $procs = $procs.not($(el)); - }); - }); - - $procs.addClass('collapsed'); - }, - - /** - * Get a list of all currently opened BPs. - * - * Only get the deepest nodes to keep requests as small as possible - */ - rememberOpenedBps: function (event) { - var ids = []; - var $bpDiv = $(event.currentTarget).find('div.bp'); - var $bpName = $bpDiv.attr('id'); - - $bpDiv.find('table.process') - .not('table.process.collapsed') - .not('table.process.collapsed table.process') - .each(function (key, el) { - ids.push($(el).attr('id')); - }); - if (ids.length === 0) { + restoreCollapsedBps: function($container) { + var $bpUl = $container.find('.content > ul.bp'); + if (! $bpUl.length || !$bpUl.data('isRootConfig')) { return; } - this.idCache[$bpName] = ids; + var bpName = $bpUl.attr('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'); }, /** BEGIN Form handling, borrowed from Director **/ From 7fcf5df31a0b2dacb8897757f2267f55f3f80ebf Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 12:01:49 +0100 Subject: [PATCH 40/99] module.less: Remove obsolete TreeView rules --- public/css/module.less | 317 +---------------------------------------- 1 file changed, 1 insertion(+), 316 deletions(-) diff --git a/public/css/module.less b/public/css/module.less index c39163b..5fe5f5d 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -34,8 +34,7 @@ div.bp.sortable > .sortable-ghost { } - -/* New tree stuff START */ +/* TreeView */ @vertical-tree-item-gap: .5em; @@ -170,320 +169,6 @@ ul.bp { } } -/* New tree stuff END */ - - - -table.bp { - /* Business process table styling starts here */ - width: 100%; - margin: 0; - padding: 0; - color: @text-color; - border-collapse: collapse; - border-spacing: 0; - box-sizing: border-box; - font-size: 1em; - font-weight: normal; - table-layout: fixed; - - /* Reset all paddings and margins, just to be on the safe side */ - th, td { - padding: 0; - margin: 0; - } - - /* Left outer margin on nested BPs */ - table.bp { - - width: 99.6%; - margin-left: .4%; - margin-top: 4px; - - } - - .time-since { - display: none; - } -} - -table.bp th { - font-weight: bold; -} - -/* END of font settings */ - -/* No focus outline on our links, look ugly */ -table.bp a:focus { - outline: none; -} - -/* No link underlining */ -table.bp a, table.bp a:hover { - text-decoration: none; -} - -/* White font for all hovered objects */ -table.bp.hovered { - color: white; - - > tbody > tr > td > a > .time-since { - display: inline; - } -} - -table.bp.handled.hovered { - color: #0a0a0a; -} - -table.bp a { - color: inherit; -} - -/* Show a pointer when hovering th, highlighting is JS-triggered */ -table.bp tr th { - cursor: pointer; -} - -/* Expand / collapse styling */ -table.bp.process { - - position: relative; - - > tbody > tr:first-child > td:before { - content: '\e81d'; - font-family: ifont; - position: absolute; - font-size: 1.5em; - margin-left: -0.8em; - -webkit-transition: -webkit-transform 0.3s; - -moz-transition: -moz-transform 0.3s; - -o-transition: -o-transform 0.3s; - transition: transform 0.3s; - } - - &.collapsed { - - > tbody > tr:first-child > td:before { - -moz-transform:rotate(-90deg); - -ms-transform:rotate(-90deg); - -o-transform:rotate(-90deg); - -webkit-transform:rotate(-90deg); - transform:rotate(-90deg); - } - - table.bp, th span { - display: none; - } - } -} - -table.bp th > a, table.bp td > a, table.bp td > span { - display: block; - text-decoration: none; -} - -table.bp span.op { - width: 1.5em; - min-height: 1.5em; - margin-top: 1em; - display: block; - line-height: 2em; - -moz-transform: rotate(-90deg); - -ms-transform: rotate(-90deg); - -o-transform: rotate(-90deg); - -webkit-transform: rotate(-90deg); - transform: rotate(-90deg); -} - -table.bp .icon { - float: left; - margin-right: 0.4em; -} - -table.bp.node { - td:before { - font-family: ifont; - z-index: 1; - font-size: 1.25em; - position: absolute; - margin-left: 1.25em; - margin-top: 0.25em; - } -} - -table.bp.node.subtree td:before { - content: '\e80e'; -} - -table.bp.node.service td:before { - content: '\e840'; -} - -table.bp.node.host td:before { - content: '\e866'; -} - -/* Border defaults */ -table.bp { - border-width: 0; - border-style: solid; - border-color: transparent; -} - -table.bp tr, table.bp tbody, table.bp th, table.bp td, table.bp.node td > a, table.node.missing td > span { - border-width: 0; - border-style: inherit; - border-color: inherit; -} - -table.bp td > a, table.node.missing td > span { - height: 2.5em; - line-height: 2.5em; - padding-left: 0.5em; - display: block; -} - -table.bp.node td > a:last-child, table.node.missing td > span { - padding-left: 2.5em; - background-repeat: no-repeat; - background-position: 0.5em 0.5em; - border-left-width: 0.8em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -table.bp.node.handled td > a:last-child, table.bp.node.ok td > a:last-child, - table.node.missing td > span, table.bp.node.up td > a:last-child -{ - border-left-width: 0.3em; - background-position: 1em 0.5em; - padding-left: 3em; -} - -table.bp th { - border-left-width: 0.8em; - width: 1.8em; -} - -table.process.missing th span { - display: none; -} - -table.bp.handled > tbody > tr > th, table.bp.ok > tbody > tr > th { - border-left-width: 0.3em; - width: 2em; -} - -/* Operator: upper line */ -table.bp.operator > tbody > tr:first-child > * { - border-top-width: 1px; - border-top-style: solid; -} - -table.bp.operator.hovered > tbody > tr:first-child > * { - border-top-style: solid; -} - -/* Set colors based on element state */ -table.bp { - &.ok { border-color: @colorOk; } - &.up { border-color: @colorOk; } - &.warning { border-color: @colorWarning; } - &.warning.handled { border-color: @colorWarningHandled; } - &.critical { border-color: @colorCritical; } - &.critical.handled { border-color: @colorCriticalHandled; } - &.down { border-color: @colorCritical; } - &.down.handled { border-color: @colorCriticalHandled; } - &.unknown { border-color: @colorUnknown; } - &.unknown.handled { border-color: @colorUnknownHandled; } - &.unreachable { border-color: @colorUnknown; } - &.unreachable.handled { border-color: @colorUnknownHandled; } - &.pending { border-color: @colorPending; } - &.missing { border-color: #ccc; } - &.hovered { - &.ok > tbody > tr > { - th, td > a { background-color: @colorOk; } - } - &.up > tbody > tr > { - th, td > a { background-color: @colorOk; } - } - &.warning > tbody > tr > { - th, td > a { background-color: @colorWarning; } - } - &.warning.handled > tbody > tr { - > th, > td > a { background-color: @colorWarningHandled; } - } - &.critical > tbody > tr > { - th, td > a { background-color: @colorCritical; } - } - &.critical.handled > tbody > tr > { - th, td > a { background-color: @colorCriticalHandled; } - } - &.down > tbody > tr > { - th, td > a { background-color: @colorCritical; } - } - &.down.handled > tbody > tr > { - th, td > a { background-color: @colorCriticalHandled; } - } - &.unknown > tbody > tr > { - th, td > a { background-color: @colorUnknown; } - } - &.unknown.handled > tbody > tr > { - th, td > a { background-color: @colorUnknownHandled; } - } - &.unreachable > tbody > tr > { - th, td > a { background-color: @colorUnknown; } - } - &.unreachable.handled > tbody > tr > { - th, td > a { background-color: @colorUnreachableHandled; } - } - &.pending > tbody > tr > { - th, td > a { background-color: @colorPending; } - } - &.missing > tbody > tr > { - th, td > a, td > span { background-color: #ccc; } - } - } -} - -/* Reduce font size after the 3rd level... */ -table.bp table.bp table.bp table.bp { - font-size: 0.95em; -} - -/* ...and keep it constant afterwards */ -table.bp table.bp table.bp table.bp table.bp { - font-size: 1em; -} -/* Transitions */ -div.knightrider table.bp { - - // That's ugly, I know - .transition(@val1, @val2) { - transition: @val1, @val2; - -moz-transition: @val1, @val2; - -o-transition: @val1, @val2; - -webkit-transition: @val1, @val2; - } - > tbody > tr > td > a:last-child, > tbody > tr > td > span, > tbody > tr > th { - // Fade out - .transition(color 0.5s 0.1s step-start, - background-color 0.5s 0.1s ease-out - ); - } - - &.hovered > tbody > tr { - > td > a:last-child, > td > span, > th { - // Fade in - .transition(color 0.0s 0.0s step-start, - background-color 0.0s 0.0s ease-out - ); - } - } -} - /** BEGIN Dashboard **/ .overview-dashboard { .header { From 494027710b1ac00f8bc5611c154b5c4691175c8d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 12:22:11 +0100 Subject: [PATCH 41/99] ActionBar: Restyle and move link to add new processes --- .../Component/RenderedProcessActionBar.php | 4 +-- public/css/module.less | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php index 996b1f4..abbe410 100644 --- a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -91,9 +91,9 @@ class RenderedProcessActionBar extends ActionBar [ 'href' => $url->with('action', 'add'), 'title' => mt('businessprocess', 'Add a new business process node'), - 'class' => 'icon-plus' + 'class' => 'icon-plus button-link' ], - mt('businessprocess', 'Add') + mt('businessprocess', 'Add Process') )); } } diff --git a/public/css/module.less b/public/css/module.less index 5fe5f5d..9014059 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -6,13 +6,35 @@ a:focus { } } -.action-bar a { - font-size: 1.3em; - color: @icinga-blue; - &:hover::before { - text-decoration: none; +.action-bar { + display: flex; + align-items: center; + + a { + font-size: 1.3em; + color: @icinga-blue; + + &:hover::before { + text-decoration: none; + } + + &:not(:last-child) { + margin-right: 1em; + } + + &.button-link { + color: white; + background: @icinga-blue; + + &:active, &:focus { + text-decoration: none; + } + + &:last-child { + margin-left: auto; + } + } } - margin-right: 1em; } form a { From f58e1844a1fa144090f6ea996a5512f56495bac4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 12:57:29 +0100 Subject: [PATCH 42/99] ActionBar: Remove h1 and adjust link labels --- application/controllers/ProcessController.php | 4 +--- .../Web/Component/RenderedProcessActionBar.php | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 3948a3d..4594756 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -121,6 +121,7 @@ class ProcessController extends Controller protected function prepareControls($bp, $renderer) { $controls = $this->controls(); + $controls->getAttributes()->add('class', 'separated'); if ($this->showFullscreen) { $controls->getAttributes()->add('class', 'want-fullscreen'); @@ -138,9 +139,6 @@ class ProcessController extends Controller if (! ($this->showFullscreen || $this->view->compact)) { $controls->add($this->getProcessTabs($bp, $renderer)); } - if (! $this->view->compact) { - $controls->add(Html::tag('h1')->setContent($this->view->title)); - } $controls->add(Breadcrumb::create($renderer)); if (! $this->showFullscreen && ! $this->view->compact) { $controls->add( diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php index abbe410..ae07807 100644 --- a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -58,7 +58,7 @@ class RenderedProcessActionBar extends ActionBar 'title' => mt('businessprocess', 'Click to unlock editing for this process'), 'class' => 'icon-lock' ], - mt('businessprocess', 'Editing locked') + mt('businessprocess', 'Unlock Editing') )); } elseif (! $hasChanges) { $this->add(Html::tag( @@ -68,7 +68,7 @@ class RenderedProcessActionBar extends ActionBar 'title' => mt('businessprocess', 'Click to lock editing for this process'), 'class' => 'icon-lock-open' ], - mt('businessprocess', 'Editing unlocked') + mt('businessprocess', 'Lock Editing') )); } From 5fe4983c3cb85fb2eadac1bfc427f3fbaf204b32 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 13:23:30 +0100 Subject: [PATCH 43/99] ActionBar: Add toggle to switch between tile- and tree-view --- .../Component/RenderedProcessActionBar.php | 42 +++++++++---------- public/css/module.less | 41 ++++++++++++++++-- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php index ae07807..939ce98 100644 --- a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -15,27 +15,27 @@ class RenderedProcessActionBar extends ActionBar { $meta = $config->getMetadata(); - if ($renderer instanceof TreeRenderer) { - $this->add(Html::tag( - 'a', - [ - 'href' => $url->with('mode', 'tile'), - 'title' => mt('businessprocess', 'Switch to Tile view'), - 'class' => 'icon-dashboard' - ], - mt('businessprocess', 'Tiles') - )); - } else { - $this->add(Html::tag( - 'a', - [ - 'href' => $url->with('mode', 'tree'), - 'title' => mt('businessprocess', 'Switch to Tree view'), - 'class' => 'icon-sitemap' - ], - mt('businessprocess', 'Tree') - )); - } + $toggle = Html::tag('div', ['class' => 'view-toggle']); + $toggle->add(Html::tag('span', null, mt('businessprocess', 'View'))); + $toggle->add(Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tile'), + 'title' => mt('businessprocess', 'Switch to Tile view'), + 'class' => $renderer instanceof TreeRenderer ? '' : 'active' + ], + Html::tag('i', ['class' => 'icon icon-dashboard']) + )); + $toggle->add(Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tree'), + 'title' => mt('businessprocess', 'Switch to Tree view'), + 'class' => $renderer instanceof TreeRenderer ? 'active' : '' + ], + Html::tag('i', ['class' => 'icon icon-sitemap']) + )); + $this->add($toggle); $this->add(Html::tag( 'a', diff --git a/public/css/module.less b/public/css/module.less index 9014059..41b97c6 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -9,11 +9,10 @@ a:focus { .action-bar { display: flex; align-items: center; + font-size: 1.3em; + color: @icinga-blue; - a { - font-size: 1.3em; - color: @icinga-blue; - + > a { &:hover::before { text-decoration: none; } @@ -35,6 +34,40 @@ a:focus { } } } + + > div.view-toggle { + margin-right: 1em; + + span { + color: @gray; + margin-right: .5em; + } + + a { + padding: .25em .5em; + border: 1px solid @icinga-blue; + + i::before { + margin-right: 0; + } + + &.active { + color: white; + background-color: @icinga-blue; + } + + &:first-of-type { + border-right-width: 0; + border-top-left-radius: .25em; + border-bottom-left-radius: .25em; + } + &:last-of-type { + border-left-width: 0; + border-top-right-radius: .25em; + border-bottom-right-radius: .25em; + } + } + } } form a { From fba4235a6a9826e2e7390a87cbcdaf95fd37f458 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 13:36:17 +0100 Subject: [PATCH 44/99] TileRenderer: Fix invalid call to `attributes()` --- library/Businessprocess/Renderer/TileRenderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index d9e979e..db66cb3 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -28,7 +28,7 @@ class TileRenderer extends Renderer ] ); if (! $this->wantsRootNodes()) { - $nodesDiv->attributes()->add('data-node-name', $this->parent->getName()); + $nodesDiv->getAttributes()->add('data-node-name', $this->parent->getName()); } $nodes = $this->getChildNodes(); From fb8c3c5a332690ef6a0a7913956a4717b255a65d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 22 Jan 2019 14:09:50 +0100 Subject: [PATCH 45/99] Redesign breadcrumbs --- .../Businessprocess/Renderer/Breadcrumb.php | 5 +- .../Renderer/TileRenderer/NodeTile.php | 46 ++++++++--------- public/css/module.less | 51 ++++++++++--------- 3 files changed, 50 insertions(+), 52 deletions(-) diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php index 42c197d..8b1df41 100644 --- a/library/Businessprocess/Renderer/Breadcrumb.php +++ b/library/Businessprocess/Renderer/Breadcrumb.php @@ -37,11 +37,11 @@ class Breadcrumb extends BaseHtmlElement 'href' => Url::fromPath('businessprocess'), 'title' => mt('businessprocess', 'Show Overview') ], - Html::tag('i', ['class' => 'icon icon-dashboard']) + Html::tag('i', ['class' => 'icon icon-home']) ) )); $breadcrumb->add(Html::tag('li')->add( - Html::tag('a', ['href' => $bpUrl], $bp->getTitle()) + Html::tag('a', ['href' => $bpUrl], mt('businessprocess', 'Root')) )); $path = $renderer->getCurrentPath(); @@ -70,7 +70,6 @@ class Breadcrumb extends BaseHtmlElement $renderer = clone($renderer); $renderer->lock()->setIsBreadcrumb(); $p = new NodeTile($renderer, (string) $node, $node, $path); - $p->getAttributes()->add('class', $renderer->getNodeClasses($node)); $p->setTag('li'); return $p; } diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 56eb8ee..c2e30ac 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -77,36 +77,30 @@ class NodeTile extends BaseHtmlElement $attributes->add('data-node-name', (string) $node); } - $this->addActions(); + if (! $renderer->isBreadcrumb()) { + $this->addDetailsActions(); + + if (! $renderer->isLocked()) { + $this->addActionLinks(); + } + } $link = $this->getMainNodeLink(); $this->add($link); - if ($node instanceof BpNode) { - if ($renderer->isBreadcrumb()) { - $link->add($renderer->renderStateBadges($node->getStateSummary())); - } else { - $this->add(Html::tag( - 'p', - ['class' => 'children-count'], - $node->hasChildren() - ? Html::tag( - 'span', - null, - sprintf('%u %s', $node->countChildren(), mt('businessprocess', 'Children')) - ) - : null - )); - $this->add($renderer->renderStateBadges($node->getStateSummary())); - } - } - - if (! $renderer->isBreadcrumb()) { - $this->addDetailsActions(); - } - - if (! $renderer->isLocked()) { - $this->addActionLinks(); + if ($node instanceof BpNode && !$renderer->isBreadcrumb()) { + $this->add(Html::tag( + 'p', + ['class' => 'children-count'], + $node->hasChildren() + ? Html::tag( + 'span', + null, + sprintf('%u %s', $node->countChildren(), mt('businessprocess', 'Children')) + ) + : null + )); + $this->add($renderer->renderStateBadges($node->getStateSummary())); } return parent::render(); diff --git a/public/css/module.less b/public/css/module.less index 41b97c6..94c5623 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -517,21 +517,9 @@ td > a > .badges { list-style: none; overflow: hidden; padding: 0; - - .badges { - background-color: transparent; - border-radius: 0; - display: inline-block; - padding: 0 0 0 0.5em; - .badge { - line-height: 1.25em; - font-size: 0.8em; - border: 1px solid white; - } - } } -.breadcrumb { +/*.breadcrumb { > .critical a { background: @colorCritical; } > .critical.handled a { background: @colorCriticalHandled; } > .unknown a { background: @colorUnknown; } @@ -549,7 +537,7 @@ td > a > .badges { > .warning a:after { border-left-color: @colorWarning; } > .warning.handled a:after { border-left-color: @colorWarningHandled; } > .ok a:after { border-left-color: @colorOk; } -} +}*/ .breadcrumb:after { content:''; @@ -566,13 +554,12 @@ td > a > .badges { } .breadcrumb li a { - color: white; + color: @icinga-blue; margin: 0; font-size: 1.2em; text-decoration: none; padding-left: 2em; line-height: 2.5em; - background: @icinga-blue; position: relative; display: block; float: left; @@ -580,8 +567,26 @@ td > a > .badges { outline: none; } } +.breadcrumb li { + border: 1px solid @gray-lighter; -.breadcrumb li a:before, .breadcrumb li a:after { + &:first-of-type { + border-radius: .25em; + } + + &:last-of-type { + border-radius: .25em; + border: 1px solid @icinga-blue; + background: @icinga-blue; + padding-right: 1.2em; + + a { + color: white; + } + } +} + +.breadcrumb li:not(:last-of-type) a:before, .breadcrumb li:not(:last-of-type) a:after { content: " "; display: block; width: 0; @@ -594,14 +599,14 @@ td > a > .badges { left: 100%; } -.breadcrumb li a:before { - border-left: 1.2em solid white; +.breadcrumb li:not(:last-of-type) a:before { + border-left: 1.2em solid @gray-lighter; margin-left: 1px; z-index: 1; } -.breadcrumb li a:after { - border-left: 1.2em solid @icinga-blue; +.breadcrumb li:not(:last-of-type) a:after { + border-left: 1.2em solid white; z-index: 2; } @@ -616,8 +621,8 @@ td > a > .badges { } -.breadcrumb li:not(:last-child) a:hover { background: @text-color; color: white; } -.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @text-color; } +.breadcrumb li:not(:last-child) a:hover { background: @icinga-blue; color: white; } +.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @icinga-blue; } .breadcrumb li a:focus { text-decoration: underline; From c3a2d72a3b7600cf8a8540f3fb8eda9d4e875605 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 23 Jan 2019 09:16:47 +0100 Subject: [PATCH 46/99] js: Remove obsolete delegation target for `rowDropped` --- public/js/module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/module.js b/public/js/module.js index 45de53f..4a52173 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -27,7 +27,7 @@ this.module.on('click', 'li.process a.toggle', this.processToggleClick); this.module.on('click', 'li.process > div', this.processHeaderClick); - this.module.on('end', 'div.tree.sortable, ul.sortable', this.rowDropped); + this.module.on('end', 'ul.sortable', this.rowDropped); this.module.on('click', 'div.tiles > div', this.tileClick); this.module.on('click', '.dashboard-tile', this.dashboardTileClick); From 14998958a307234e38732834ed64715f45d694e2 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 23 Jan 2019 12:23:14 +0100 Subject: [PATCH 47/99] sortable.js upgrade --- public/js/vendor/Sortable.js | 993 ++++++++++++++++++++++++----------- 1 file changed, 695 insertions(+), 298 deletions(-) diff --git a/public/js/vendor/Sortable.js b/public/js/vendor/Sortable.js index 33e639a..56bcb85 100644 --- a/public/js/vendor/Sortable.js +++ b/public/js/vendor/Sortable.js @@ -48,6 +48,10 @@ autoScrolls = [], scrolling = false, + awaitingDragStarted = false, + ignoreNextClick = false, + sortables = [], + pointerElemChangedInterval, lastPointerElemX, lastPointerElemY, @@ -57,10 +61,15 @@ moved, + lastTarget, lastDirection, pastFirstInvertThresh = false, isCircumstantialInvert = false, + lastMode, // 'swap' or 'insert' + + targetMoveDistance, + forRepaintDummy, realDragElRect, // dragEl rect after current animation @@ -83,13 +92,21 @@ passive: false }, + IE11OrLess = !!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie|iemobile)/i), + Edge = !!navigator.userAgent.match(/Edge/i), + // FireFox = !!navigator.userAgent.match(/firefox/i), + + CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float', + + // This will not pass for IE9, because IE9 DnD only works on anchors supportDraggable = ('draggable' in document.createElement('div')), - supportCssPointerEvents = (function (el) { - // false when IE11 - if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) { + + supportCssPointerEvents = (function() { + // false when <= IE11 + if (IE11OrLess) { return false; } - el = document.createElement('x'); + var el = document.createElement('x'); el.style.cssText = 'pointer-events:auto'; return el.style.pointerEvents === 'auto'; })(), @@ -101,9 +118,6 @@ min = Math.min, savedInputChecked = [], - touchDragOverListeners = [], - - alwaysFalse = function () { return false; }, _detectDirection = function(el, options) { var elCSS = _css(el), @@ -112,9 +126,8 @@ child2 = _getChild(el, 1, options), firstChildCSS = child1 && _css(child1), secondChildCSS = child2 && _css(child2), - firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + child1.getBoundingClientRect().width, - secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + child2.getBoundingClientRect().width - ; + firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + _getRect(child1).width, + secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + _getRect(child2).width; if (elCSS.display === 'flex') { return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse' ? 'vertical' : 'horizontal'; @@ -122,27 +135,66 @@ return (child1 && ( firstChildCSS.display === 'block' || + firstChildCSS.display === 'flex' || + firstChildCSS.display === 'table' || firstChildCSS.display === 'grid' || firstChildWidth >= elWidth && - elCSS.float === 'none' || + elCSS[CSSFloatProperty] === 'none' || child2 && - elCSS.float === 'none' && + elCSS[CSSFloatProperty] === 'none' && firstChildWidth + secondChildWidth > elWidth ) ? 'vertical' : 'horizontal' ); }, - _isInRowColumn = function(x, y, el, axis, options) { - var targetRect = realDragElRect || dragEl.getBoundingClientRect(), + /** + * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold. + * @param {Number} x X position + * @param {Number} y Y position + * @return {HTMLElement} Element of the first found nearest Sortable + */ + _detectNearestEmptySortable = function(x, y) { + for (var i = 0; i < sortables.length; i++) { + if (sortables[i].children.length) continue; + + var rect = _getRect(sortables[i]), + threshold = sortables[i][expando].options.emptyInsertThreshold, + insideHorizontally = x >= (rect.left - threshold) && x <= (rect.right + threshold), + insideVertically = y >= (rect.top - threshold) && y <= (rect.bottom + threshold); + + if (insideHorizontally && insideVertically) { + return sortables[i]; + } + } + }, + + _isClientInRowColumn = function(x, y, el, axis, options) { + var targetRect = _getRect(el), targetS1Opp = axis === 'vertical' ? targetRect.left : targetRect.top, targetS2Opp = axis === 'vertical' ? targetRect.right : targetRect.bottom, - mouseOnOppAxis = axis === 'vertical' ? x : y - ; + mouseOnOppAxis = axis === 'vertical' ? x : y; return targetS1Opp < mouseOnOppAxis && mouseOnOppAxis < targetS2Opp; }, + _isElInRowColumn = function(el1, el2, axis) { + var el1Rect = el1 === dragEl && realDragElRect || _getRect(el1), + el2Rect = el2 === dragEl && realDragElRect || _getRect(el2), + el1S1Opp = axis === 'vertical' ? el1Rect.left : el1Rect.top, + el1S2Opp = axis === 'vertical' ? el1Rect.right : el1Rect.bottom, + el1OppLength = axis === 'vertical' ? el1Rect.width : el1Rect.height, + el2S1Opp = axis === 'vertical' ? el2Rect.left : el2Rect.top, + el2S2Opp = axis === 'vertical' ? el2Rect.right : el2Rect.bottom, + el2OppLength = axis === 'vertical' ? el2Rect.width : el2Rect.height; + + return ( + el1S1Opp === el2S1Opp || + el1S2Opp === el2S2Opp || + (el1S1Opp + el1OppLength / 2) === (el2S1Opp + el2OppLength / 2) + ); + }, + _getParentAutoScrollElement = function(el, includeSelf) { // skip to window if (!el || !el.getBoundingClientRect) return win; @@ -173,8 +225,6 @@ // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 if (options.scroll) { var _this = rootEl ? rootEl[expando] : window, - rect, - css, sens = options.scrollSensitivity, speed = options.scrollSpeed, @@ -184,11 +234,7 @@ winWidth = window.innerWidth, winHeight = window.innerHeight, - vx, - vy, - - scrollThisInstance = false - ; + scrollThisInstance = false; // Detect scrollEl if (scrollParentEl !== rootEl) { @@ -207,23 +253,61 @@ var layersOut = 0; var currentParent = scrollEl; do { - var el; + var el = currentParent, + rect = _getRect(el), + + top = rect.top, + bottom = rect.bottom, + left = rect.left, + right = rect.right, + + width = rect.width, + height = rect.height, + + scrollWidth, + scrollHeight, + + css, + + vx, + vy, + + canScrollX, + canScrollY, + + scrollPosX, + scrollPosY; + + + if (el !== win) { + scrollWidth = el.scrollWidth; + scrollHeight = el.scrollHeight; - if (currentParent && currentParent !== win) { - el = currentParent; css = _css(el); - rect = currentParent.getBoundingClientRect(); - vx = el.clientWidth < el.scrollWidth && (css.overflowX == 'auto' || css.overflowX == 'scroll') && - ((abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens)); - vy = el.clientHeight < el.scrollHeight && (css.overflowY == 'auto' || css.overflowY == 'scroll') && - ((abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens)); - } else if (currentParent === win) { - el = win; - vx = (winWidth - x <= sens) - (x <= sens); - vy = (winHeight - y <= sens) - (y <= sens); + canScrollX = width < scrollWidth && (css.overflowX === 'auto' || css.overflowX === 'scroll'); + canScrollY = height < scrollHeight && (css.overflowY === 'auto' || css.overflowY === 'scroll'); + + scrollPosX = el.scrollLeft; + scrollPosY = el.scrollTop; + } else { + scrollWidth = document.documentElement.scrollWidth; + scrollHeight = document.documentElement.scrollHeight; + + css = _css(document.documentElement); + + canScrollX = width < scrollWidth && (css.overflowX === 'auto' || css.overflowX === 'scroll' || css.overflowX === 'visible'); + canScrollY = height < scrollHeight && (css.overflowY === 'auto' || css.overflowY === 'scroll' || css.overflowY === 'visible'); + + scrollPosX = document.documentElement.scrollLeft; + scrollPosY = document.documentElement.scrollTop; } + vx = canScrollX && (abs(right - x) <= sens && (scrollPosX + width) < scrollWidth) - (abs(left - x) <= sens && !!scrollPosX); + + vy = canScrollY && (abs(bottom - y) <= sens && (scrollPosY + height) < scrollHeight) - (abs(top - y) <= sens && !!scrollPosY); + + if (!autoScrolls[layersOut]) { for (var i = 0; i <= layersOut; i++) { if (!autoScrolls[i]) { @@ -280,24 +364,27 @@ _prepareGroup = function (options) { function toFn(value, pull) { return function(to, from, dragEl, evt) { - var ret; + var sameGroup = to.options.group.name && + from.options.group.name && + to.options.group.name === from.options.group.name; - if (value == null && pull) { - ret = true; // default pull value: true (backwards compatibility) + if (value == null && (pull || sameGroup)) { + // Default pull value + // Default pull and put value if same group + return true; } else if (value == null || value === false) { - ret = false; + return false; } else if (pull && value === 'clone') { - ret = value; + return value; } else if (typeof value === 'function') { - ret = value(to, from, dragEl, evt); + return toFn(value(to, from, dragEl, evt), pull)(to, from, dragEl, evt); } else { var otherGroup = (pull ? to : from).options.group.name; - ret = (value === true || + return (value === true || (typeof value === 'string' && value === otherGroup) || (value.join && value.indexOf(otherGroup) > -1)); } - return ret || (to.options.group.name && from.options.group.name && to.options.group.name === from.options.group.name); }; } @@ -317,11 +404,74 @@ }, _checkAlignment = function(evt) { - if (!dragEl) return; + if (!dragEl || !dragEl.parentNode) return; dragEl.parentNode[expando] && dragEl.parentNode[expando]._computeIsAligned(evt); - } - ; + }, + _isTrueParentSortable = function(el, target) { + var trueParent = target; + while (!trueParent[expando]) { + trueParent = trueParent.parentNode; + } + + return el === trueParent; + }, + + _artificalBubble = function(sortable, originalEvt, method) { + // Artificial IE bubbling + var nextParent = sortable.parentNode; + while (nextParent && !nextParent[expando]) { + nextParent = nextParent.parentNode; + } + + if (nextParent) { + nextParent[expando][method](_extend(originalEvt, { + artificialBubble: true + })); + } + }, + + _hideGhostForTarget = function() { + if (!supportCssPointerEvents && ghostEl) { + _css(ghostEl, 'display', 'none'); + } + }, + + _unhideGhostForTarget = function() { + if (!supportCssPointerEvents && ghostEl) { + _css(ghostEl, 'display', ''); + } + }; + + + // #1184 fix - Prevent click event on fallback if dragged but item not changed position + document.addEventListener('click', function(evt) { + if (ignoreNextClick) { + evt.preventDefault(); + evt.stopPropagation && evt.stopPropagation(); + evt.stopImmediatePropagation && evt.stopImmediatePropagation(); + ignoreNextClick = false; + return false; + } + }, true); + + var nearestEmptyInsertDetectEvent = function(evt) { + if (dragEl) { + var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY); + + if (nearest) { + nearest[expando]._onDragOver({ + clientX: evt.clientX, + clientY: evt.clientY, + target: nearest, + rootEl: nearest + }); + } + } + }; + // We do not want this to be triggered if completed (bubbling canceled), so only define it here + document.addEventListener('dragover', nearestEmptyInsertDetectEvent); + document.addEventListener('mousemove', nearestEmptyInsertDetectEvent); /** * @class Sortable @@ -330,7 +480,7 @@ */ function Sortable(el, options) { if (!(el && el.nodeType && el.nodeType === 1)) { - throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); + throw 'Sortable: `el` must be HTMLElement, not ' + {}.toString.call(el); } this.el = el; // root element @@ -352,11 +502,13 @@ scrollSpeed: 10, bubbleScroll: true, draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', - swapThreshold: 1, // percentage; 0 <= x <= 1 invertSwap: false, // invert always invertedSwapThreshold: null, // will be set to same as swapThreshold if default - + removeCloneOnHide: true, + direction: function() { + return _detectDirection(el, this.options); + }, ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', @@ -364,6 +516,7 @@ filter: null, preventOnFilter: true, animation: 0, + easing: null, setData: function (dataTransfer, dragEl) { dataTransfer.setData('Text', dragEl.textContent); }, @@ -380,7 +533,8 @@ supportPointer: Sortable.supportPointer !== false && ( ('PointerEvent' in window) || window.navigator && ('msPointerEnabled' in window.navigator) // microsoft - ) + ), + emptyInsertThreshold: 5 }; @@ -389,15 +543,8 @@ !(name in options) && (options[name] = defaults[name]); } - if (!('direction' in options)) { - options.direction = function() { - return _detectDirection(el, options); - }; - } - _prepareGroup(options); - options.invertedSwapThreshold == null && (options.invertedSwapThreshold = options.swapThreshold); // Bind all private methods for (var fn in this) { if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { @@ -409,16 +556,19 @@ this.nativeDraggable = options.forceFallback ? false : supportDraggable; // Bind events - _on(el, 'mousedown', this._onTapStart); - _on(el, 'touchstart', this._onTapStart); - options.supportPointer && _on(el, 'pointerdown', this._onTapStart); + if (options.supportPointer) { + _on(el, 'pointerdown', this._onTapStart); + } else { + _on(el, 'mousedown', this._onTapStart); + _on(el, 'touchstart', this._onTapStart); + } if (this.nativeDraggable) { _on(el, 'dragover', this); _on(el, 'dragenter', this); } - touchDragOverListeners.push(this._onDragOver); + sortables.push(this.el); // Restore sorting options.store && options.store.get && this.sort(options.store.get(this) || []); @@ -427,20 +577,38 @@ Sortable.prototype = /** @lends Sortable.prototype */ { constructor: Sortable, - // is mouse aligned with dragEl? - _isAligned: true, + _computeIsAligned: function(evt) { + var target; - _computeIsAligned: function(evt, isDragEl) { + if (ghostEl && !supportCssPointerEvents) { + _hideGhostForTarget(); + target = document.elementFromPoint(evt.clientX, evt.clientY); + _unhideGhostForTarget(); + } else { + target = evt.target; + } + + target = _closest(target, this.options.draggable, this.el, false); if (_alignedSilent) return; if (!dragEl || dragEl.parentNode !== this.el) return; - if (isDragEl !== true && isDragEl !== false) { - isDragEl = !!_closest(evt.target, null, dragEl, true); + + var children = this.el.children; + for (var i = 0; i < children.length; i++) { + // Don't change for target in case it is changed to aligned before onDragOver is fired + if (_closest(children[i], this.options.draggable, this.el, false) && children[i] !== target) { + children[i].sortableMouseAligned = _isClientInRowColumn(evt.clientX, evt.clientY, children[i], this._getDirection(evt, null), this.options); + } } - this._isAligned = !scrolling && (isDragEl || this._isAligned && _isInRowColumn(evt.clientX, evt.clientY, this.el, this._getDirection(evt, null), this.options)); + // Used for nulling last target when not in element, nothing to do with checking if aligned + if (!_closest(target, this.options.draggable, this.el, true)) { + lastTarget = null; + } + _alignedSilent = true; setTimeout(function() { _alignedSilent = false; }, 30); + }, _getDirection: function(evt, target) { @@ -448,6 +616,8 @@ }, _onTapStart: function (/** Event|TouchEvent */evt) { + if (!evt.cancelable) return; + var _this = this, el = this.el, options = this.options, @@ -462,13 +632,19 @@ _saveInputCheckedState(el); + // IE: Calls events in capture mode if event element is nested. This ensures only correct element's _onTapStart goes through. + // This process is also done in _onDragOver + if (IE11OrLess && !evt.artificialBubble && !_isTrueParentSortable(el, target)) { + return; + } + // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. if (dragEl) { return; } if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { - return; // only left button or enabled + return; // only left button and enabled } // cancel dnd if original target is content editable @@ -476,9 +652,12 @@ return; } - target = _closest(target, options.draggable, el, true); + target = _closest(target, options.draggable, el, false); if (!target) { + if (IE11OrLess) { + _artificalBubble(el, evt, '_onTapStart'); + } return; } @@ -529,12 +708,13 @@ y = evt.clientY, elem = document.elementFromPoint(x, y), - _this = this - ; + _this = this; - // firefox does not have native autoscroll - if (fallback || (window.navigator && window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1)) { - _autoScroll(evt, _this.options, elem, true); + // IE does not seem to have native autoscroll, + // Edge's autoscroll seems too conditional, + // Firefox and Chrome are good + if (fallback || Edge || IE11OrLess) { + _autoScroll(evt, _this.options, elem, fallback); // Listener for pointer element change var ogElemScroller = _getParentAutoScrollElement(elem, true); @@ -556,7 +736,7 @@ if (newElem !== ogElemScroller) { ogElemScroller = newElem; _clearAutoScrolls(); - _autoScroll(evt, _this.options, ogElemScroller, true); + _autoScroll(evt, _this.options, ogElemScroller, fallback); } }, 10); lastPointerElemX = x; @@ -564,12 +744,12 @@ } } else { - // if DnD is enabled (not on firefox), first autoscroll will already scroll, so get parent autoscroll of first autoscroll + // if DnD is enabled (and browser has good autoscrolling), first autoscroll will already scroll, so get parent autoscroll of first autoscroll if (!_this.options.bubbleScroll || _getParentAutoScrollElement(elem, true) === window) { _clearAutoScrolls(); return; } - _autoScroll(evt, _this.options, _getParentAutoScrollElement(elem, false)); + _autoScroll(evt, _this.options, _getParentAutoScrollElement(elem, false), false); } }, @@ -581,8 +761,6 @@ dragStartFn; if (target && !dragEl && (target.parentNode === el)) { - tapEvt = evt; - rootEl = el; dragEl = target; parentEl = dragEl.parentNode; @@ -591,10 +769,19 @@ activeGroup = options.group; oldIndex = startIndex; + tapEvt = { + target: dragEl, + clientX: (touch || evt).clientX, + clientY: (touch || evt).clientY + }; + this._lastX = (touch || evt).clientX; this._lastY = (touch || evt).clientY; dragEl.style['will-change'] = 'all'; + // undo animation if needed + dragEl.style.transition = ''; + dragEl.style.transform = ''; dragStartFn = function () { // Delayed drag has been triggered @@ -619,10 +806,14 @@ _find(dragEl, criteria.trim(), _disableDraggable); }); - _on(ownerDocument, 'mouseup', _this._onDrop); - _on(ownerDocument, 'touchend', _this._onDrop); - _on(ownerDocument, 'touchcancel', _this._onDrop); - options.supportPointer && _on(ownerDocument, 'pointercancel', _this._onDrop); + if (options.supportPointer) { + _on(ownerDocument, 'pointerup', _this._onDrop); + _on(ownerDocument, 'pointercancel', _this._onDrop); + } else { + _on(ownerDocument, 'mouseup', _this._onDrop); + _on(ownerDocument, 'touchend', _this._onDrop); + _on(ownerDocument, 'touchcancel', _this._onDrop); + } if (options.delay) { // If the user moves the pointer or let go the click or touch @@ -635,12 +826,10 @@ _on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler); options.supportPointer && _on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler); - _this._dragStartTimer = setTimeout(dragStartFn.bind(_this), options.delay); + _this._dragStartTimer = setTimeout(dragStartFn, options.delay); } else { dragStartFn(); } - - } }, @@ -668,21 +857,15 @@ _triggerDragStart: function (/** Event */evt, /** Touch */touch) { touch = touch || (evt.pointerType == 'touch' ? evt : null); - - if (touch) { - // Touch device support - tapEvt = { - target: dragEl, - clientX: touch.clientX, - clientY: touch.clientY - }; - - this._onDragStart(tapEvt, 'touch'); - } - else if (!this.nativeDraggable) { - this._onDragStart(tapEvt, true); - } - else { + if (!this.nativeDraggable || touch) { + if (this.options.supportPointer) { + _on(document, 'pointermove', this._onTouchMove); + } else if (touch) { + _on(document, 'touchmove', this._onTouchMove); + } else { + _on(document, 'mousemove', this._onTouchMove); + } + } else { _on(dragEl, 'dragend', this); _on(rootEl, 'dragstart', this._onDragStart); } @@ -700,7 +883,8 @@ } }, - _dragStarted: function () { + _dragStarted: function (fallback) { + awaitingDragStarted = false; if (rootEl && dragEl) { if (this.nativeDraggable) { _on(document, 'dragover', this._handleAutoScroll); @@ -709,14 +893,15 @@ var options = this.options; // Apply effect - _toggleClass(dragEl, options.dragClass, false); + !fallback && _toggleClass(dragEl, options.dragClass, false); _toggleClass(dragEl, options.ghostClass, true); + // In case dragging an animated element _css(dragEl, 'transform', ''); Sortable.active = this; - this._isAligned = true; + fallback && this._appendGhost(); // Drag start event _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex); @@ -730,17 +915,13 @@ if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY && !bypassLastTouchCheck) { return; } - this._lastX = touchEvt.clientX; this._lastY = touchEvt.clientY; - if (!supportCssPointerEvents) { - _css(ghostEl, 'display', 'none'); - } + _hideGhostForTarget(); var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); var parent = target; - var isDragEl = !!_closest(target, null, dragEl, true); while (target && target.shadowRoot) { target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); @@ -750,17 +931,16 @@ if (parent) { do { if (parent[expando]) { - var i = touchDragOverListeners.length; - while (i--) { - touchDragOverListeners[i]({ - clientX: touchEvt.clientX, - clientY: touchEvt.clientY, - target: target, - rootEl: parent - }); - } + var inserted; - if (!this.options.dragoverBubble) { + inserted = parent[expando]._onDragOver({ + clientX: touchEvt.clientX, + clientY: touchEvt.clientY, + target: target, + rootEl: parent + }); + + if (inserted && !this.options.dragoverBubble) { break; } } @@ -770,48 +950,45 @@ /* jshint boss:true */ while (parent = parent.parentNode); } - dragEl.parentNode[expando]._computeIsAligned(touchEvt, isDragEl); + dragEl.parentNode[expando]._computeIsAligned(touchEvt); - if (!supportCssPointerEvents) { - _css(ghostEl, 'display', ''); - } + _unhideGhostForTarget(); } }, _onTouchMove: function (/**TouchEvent*/evt) { if (tapEvt) { + if (!evt.cancelable) return; var options = this.options, fallbackTolerance = options.fallbackTolerance, fallbackOffset = options.fallbackOffset, touch = evt.touches ? evt.touches[0] : evt, - dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, - dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, + matrix = ghostEl && _matrix(ghostEl), + scaleX = ghostEl && matrix && matrix.a, + scaleY = ghostEl && matrix && matrix.d, + dx = ((touch.clientX - tapEvt.clientX) + fallbackOffset.x) / (scaleX ? scaleX : 1), + dy = ((touch.clientY - tapEvt.clientY) + fallbackOffset.y) / (scaleY ? scaleY : 1), translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; - // prevent duplicate event firing - if (this.options.supportPointer && evt.type === 'touchmove') return; // only set the status to dragging, when we are actually dragging - if (!Sortable.active) { + if (!Sortable.active && !awaitingDragStarted) { if (fallbackTolerance && min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance ) { return; } - - this._dragStarted(); + this._onDragStart(evt, true); } - // as well as creating the ghost element on the document body - this._appendGhost(); - this._handleAutoScroll(touch, true); moved = true; touchEvt = touch; + _css(ghostEl, 'webkitTransform', translate3d); _css(ghostEl, 'mozTransform', translate3d); _css(ghostEl, 'msTransform', translate3d); @@ -823,7 +1000,7 @@ _appendGhost: function () { if (!ghostEl) { - var rect = dragEl.getBoundingClientRect(), + var rect = _getRect(dragEl, this.options.fallbackOnBody ? document.body : rootEl, true), css = _css(dragEl), options = this.options; @@ -848,55 +1025,44 @@ } }, - _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { + _onDragStart: function (/**Event*/evt, /**boolean*/fallback) { var _this = this; var dataTransfer = evt.dataTransfer; var options = _this.options; - _this._offUpEvents(); - if (activeGroup.checkPull(_this, _this, dragEl, evt)) { - cloneEl = _clone(dragEl); + // Setup clone + cloneEl = _clone(dragEl); - cloneEl.draggable = false; - cloneEl.style['will-change'] = ''; + cloneEl.draggable = false; + cloneEl.style['will-change'] = ''; - this._hideClone(); + this._hideClone(); - _toggleClass(cloneEl, _this.options.chosenClass, false); + _toggleClass(cloneEl, _this.options.chosenClass, false); - // #1143: IFrame support workaround - _this._cloneId = _nextTick(function () { + + // #1143: IFrame support workaround + _this._cloneId = _nextTick(function () { + if (!_this.options.removeCloneOnHide) { rootEl.insertBefore(cloneEl, dragEl); - _dispatchEvent(_this, rootEl, 'clone', dragEl); - }); - } - - _toggleClass(dragEl, options.dragClass, true); - - if (useFallback) { - if (useFallback === 'touch') { - // Fixed #973: - _on(document, 'touchmove', _preventScroll); - - // Bind touch events - _on(document, 'touchmove', _this._onTouchMove); - _on(document, 'touchend', _this._onDrop); - _on(document, 'touchcancel', _this._onDrop); - - if (options.supportPointer) { - _on(document, 'pointermove', _this._onTouchMove); - _on(document, 'pointerup', _this._onDrop); - } - } else { - // Old brwoser - _on(document, 'mousemove', _this._onTouchMove); - _on(document, 'mouseup', _this._onDrop); } + _dispatchEvent(_this, rootEl, 'clone', dragEl); + }); + + !fallback && _toggleClass(dragEl, options.dragClass, true); + + // Set proper drop events + if (fallback) { + ignoreNextClick = true; _this._loopId = setInterval(_this._emulateDragOver, 50); - _toggleClass(dragEl, options.dragClass, false); - } - else { + } else { + // Undo what was set in _prepareDragStart before drag started + _off(document, 'mouseup', _this._onDrop); + _off(document, 'touchend', _this._onDrop); + _off(document, 'touchcancel', _this._onDrop); + _off(document, 'pointercancel', _this._onDrop); + if (dataTransfer) { dataTransfer.effectAllowed = 'move'; options.setData && options.setData.call(_this, dataTransfer, dragEl); @@ -906,15 +1072,18 @@ // #1276 fix: _css(dragEl, 'transform', 'translateZ(0)'); - - _this._dragStartId = _nextTick(_this._dragStarted); } + + awaitingDragStarted = true; + + _this._dragStartId = _nextTick(_this._dragStarted.bind(_this, fallback)); _on(document, 'selectstart', _this); }, + // Returns true - if no further action is needed (either inserted or another condition) _onDragOver: function (/**Event*/evt) { var el = this.el, - target, + target = evt.target, dragRect, targetRect, revert, @@ -922,42 +1091,70 @@ group = options.group, activeSortable = Sortable.active, isOwner = (activeGroup === group), - isMovingBetweenSortable = false, - canSort = options.sort - ; + canSort = options.sort, + _this = this; + if (_silent) return; - if (evt.rootEl !== void 0 && evt.rootEl !== this.el) return; // touch fallback - - // no bubbling and not fallback - if (!options.dragoverBubble && !evt.rootEl) { - this._handleAutoScroll(evt); - dragEl.parentNode[expando]._computeIsAligned(evt); + // IE event order fix + if (IE11OrLess && !evt.rootEl && !evt.artificialBubble && !_isTrueParentSortable(el, target)) { + return; } + // Return invocation when no further action is needed in another sortable + function completed() { + if (activeSortable) { + // Set ghost class to new sortable's ghost class + _toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : activeSortable.options.ghostClass, false); + _toggleClass(dragEl, options.ghostClass, true); + } + + if (putSortable !== _this && _this !== Sortable.active) { + putSortable = _this; + } else if (_this === Sortable.active) { + putSortable = null; + } + + + // Null lastTarget if it is not inside a previously swapped element + if ((target === dragEl && !dragEl.animated) || (target === el && !target.animated)) { + lastTarget = null; + } + // no bubbling and not fallback + if (!options.dragoverBubble && !evt.rootEl && target !== document) { + _this._handleAutoScroll(evt); + dragEl.parentNode[expando]._computeIsAligned(evt); + } + + !options.dragoverBubble && evt.stopPropagation && evt.stopPropagation(); + + return true; + } + + // Call when dragEl has been inserted + function changed() { + _dispatchEvent(_this, rootEl, 'change', target, el, rootEl, oldIndex, _index(dragEl, options.draggable), evt); + } + + if (evt.preventDefault !== void 0) { evt.cancelable && evt.preventDefault(); - !options.dragoverBubble && evt.stopPropagation(); } moved = true; - target = _closest(evt.target, options.draggable, el, true); + target = _closest(target, options.draggable, el, true); - - if (dragEl.animated && target === dragEl || target.animated || _silent) { - return; + // target is dragEl or target is animated + if (!!_closest(evt.target, null, dragEl, true) || target.animated) { + return completed(); } - - if (target !== lastTarget) { - isCircumstantialInvert = false; - pastFirstInvertThresh = false; - lastTarget = null; + if (target !== dragEl) { + ignoreNextClick = false; } - if (activeSortable && !options.disabled && (isOwner ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list @@ -970,36 +1167,25 @@ ) ) ) { - var direction; var axis = this._getDirection(evt, target); - - dragRect = dragEl.getBoundingClientRect(); - - if (putSortable !== this && this !== Sortable.active) { - putSortable = this; - isMovingBetweenSortable = true; - } else if (this === Sortable.active) { - isMovingBetweenSortable = false; - putSortable = null; - } + dragRect = _getRect(dragEl); if (revert) { this._hideClone(); parentEl = rootEl; // actualization - if (cloneEl || nextEl) { - rootEl.insertBefore(dragEl, cloneEl || nextEl); - } - else if (!canSort) { + if (nextEl) { + rootEl.insertBefore(dragEl, nextEl); + } else { rootEl.appendChild(dragEl); } - return; + return completed(); } if ((el.children.length === 0) || (el.children[0] === ghostEl) || - (el === evt.target) && _ghostIsLast(evt, axis, el) + _ghostIsLast(evt, axis, el) && !dragEl.animated ) { //assign target only if condition is true if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) { @@ -1007,11 +1193,7 @@ } if (target) { - if (target.animated) { - return; - } - - targetRect = target.getBoundingClientRect(); + targetRect = _getRect(target); } if (isOwner) { @@ -1021,43 +1203,66 @@ } if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { - if (!dragEl.contains(el)) { - el.appendChild(dragEl); - parentEl = el; // actualization - this._isAligned = true; // must be for _ghostIsLast to pass - realDragElRect = null; - } + el.appendChild(dragEl); + parentEl = el; // actualization + realDragElRect = null; + changed(); this._animate(dragRect, dragEl); target && this._animate(targetRect, target); + return completed(); } } - else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0) && target !== el) { + else if (target && target !== dragEl && (target.parentNode[expando] !== void 0) && target !== el) { + var direction = 0, + targetBeforeFirstSwap, + aligned = target.sortableMouseAligned, + differentLevel = dragEl.parentNode !== el, + scrolledPastTop = _isScrolledPast(target, axis === 'vertical' ? 'top' : 'left'); - isCircumstantialInvert = isCircumstantialInvert || options.invertSwap || dragEl.parentNode !== el || !this._isAligned; - direction = _getSwapDirection(evt, target, axis, - options.swapThreshold, options.invertedSwapThreshold, - isCircumstantialInvert, - lastTarget === target); - if (direction === 0) return; - realDragElRect = null; - - - this._isAligned = true; - - if (!lastTarget || lastTarget !== target && (!target || !target.animated)) { + if (lastTarget !== target) { + lastMode = null; + targetBeforeFirstSwap = _getRect(target)[axis === 'vertical' ? 'top' : 'left']; pastFirstInvertThresh = false; - lastTarget = target; } + // Reference: https://www.lucidchart.com/documents/view/10fa0e93-e362-4126-aca2-b709ee56bd8b/0 + if ( + _isElInRowColumn(dragEl, target, axis) && aligned || + differentLevel || + scrolledPastTop || + options.invertSwap || + lastMode === 'insert' || + // Needed, in the case that we are inside target and inserted because not aligned... aligned will stay false while inside + // and lastMode will change to 'insert', but we must swap + lastMode === 'swap' + ) { + // New target that we will be inside + if (lastMode !== 'swap') { + isCircumstantialInvert = options.invertSwap || differentLevel || scrolling || scrolledPastTop; + } + + direction = _getSwapDirection(evt, target, axis, + options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, + isCircumstantialInvert, + lastTarget === target); + lastMode = 'swap'; + } else { + // Insert at position + direction = _getInsertDirection(target, options); + lastMode = 'insert'; + } + if (direction === 0) return completed(); + + realDragElRect = null; + lastTarget = target; lastDirection = direction; - targetRect = target.getBoundingClientRect(); + targetRect = _getRect(target); var nextSibling = target.nextElementSibling, - after = false - ; + after = false; after = direction === 1; @@ -1077,54 +1282,72 @@ activeSortable._showClone(this); } - if (!dragEl.contains(el)) { - if (after && !nextSibling) { - el.appendChild(dragEl); - } else { - target.parentNode.insertBefore(dragEl, after ? nextSibling : target); - } + if (after && !nextSibling) { + el.appendChild(dragEl); + } else { + target.parentNode.insertBefore(dragEl, after ? nextSibling : target); } parentEl = dragEl.parentNode; // actualization + // must be done before animation + if (targetBeforeFirstSwap !== undefined && !isCircumstantialInvert) { + targetMoveDistance = abs(targetBeforeFirstSwap - _getRect(target)[axis === 'vertical' ? 'top' : 'left']); + } + changed(); + !differentLevel && this._animate(targetRect, target); this._animate(dragRect, dragEl); - this._animate(targetRect, target); + + return completed(); } } + + if (el.contains(dragEl)) { + return completed(); + } } + + if (IE11OrLess && !evt.rootEl) { + _artificalBubble(el, evt, '_onDragOver'); + } + + return false; }, _animate: function (prevRect, target) { var ms = this.options.animation; if (ms) { - var currentRect = target.getBoundingClientRect(); + var currentRect = _getRect(target); if (target === dragEl) { realDragElRect = currentRect; } if (prevRect.nodeType === 1) { - prevRect = prevRect.getBoundingClientRect(); + prevRect = _getRect(prevRect); } // Check if actually moving position - if ((prevRect.left + prevRect.width / 2) === (currentRect.left + currentRect.width / 2) - && (prevRect.top + prevRect.height / 2) === (currentRect.top + currentRect.height / 2) - ) return; + if ((prevRect.left + prevRect.width / 2) !== (currentRect.left + currentRect.width / 2) + || (prevRect.top + prevRect.height / 2) !== (currentRect.top + currentRect.height / 2) + ) { + var matrix = _matrix(this.el), + scaleX = matrix && matrix.a, + scaleY = matrix && matrix.d; - _css(target, 'transition', 'none'); - _css(target, 'transform', 'translate3d(' - + (prevRect.left - currentRect.left) + 'px,' - + (prevRect.top - currentRect.top) + 'px,0)' - ); + _css(target, 'transition', 'none'); + _css(target, 'transform', 'translate3d(' + + (prevRect.left - currentRect.left) / (scaleX ? scaleX : 1) + 'px,' + + (prevRect.top - currentRect.top) / (scaleY ? scaleY : 1) + 'px,0)' + ); - forRepaintDummy = target.offsetWidth; // repaint + forRepaintDummy = target.offsetWidth; // repaint + _css(target, 'transition', 'transform ' + ms + 'ms' + (this.options.easing ? ' ' + this.options.easing : '')); + _css(target, 'transform', 'translate3d(0,0,0)'); + } - _css(target, 'transition', 'all ' + ms + 'ms'); - _css(target, 'transform', 'translate3d(0,0,0)'); - - clearTimeout(target.animated); + (typeof target.animated === 'number') && clearTimeout(target.animated); target.animated = setTimeout(function () { _css(target, 'transition', ''); _css(target, 'transform', ''); @@ -1136,7 +1359,6 @@ _offUpEvents: function () { var ownerDocument = this.el.ownerDocument; - _off(document, 'touchmove', _preventScroll); _off(document, 'touchmove', this._onTouchMove); _off(document, 'pointermove', this._onTouchMove); _off(ownerDocument, 'mouseup', this._onDrop); @@ -1150,6 +1372,8 @@ _onDrop: function (/**Event*/evt) { var el = this.el, options = this.options; + + awaitingDragStarted = false; scrolling = false; isCircumstantialInvert = false; pastFirstInvertThresh = false; @@ -1200,7 +1424,7 @@ dragEl.style['will-change'] = ''; // Remove class's - _toggleClass(dragEl, this.options.ghostClass, false); + _toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : this.options.ghostClass, false); _toggleClass(dragEl, this.options.chosenClass, false); // Drag stop event @@ -1290,6 +1514,7 @@ savedInputChecked.forEach(function (el) { el.checked = true; }); + savedInputChecked.length = 0; }, @@ -1424,10 +1649,10 @@ el.removeAttribute('draggable'); }); - touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); - this._onDrop(); + sortables.splice(sortables.indexOf(this.el), 1); + this.el = el = null; }, @@ -1435,6 +1660,9 @@ if (!cloneEl.cloneHidden) { _css(cloneEl, 'display', 'none'); cloneEl.cloneHidden = true; + if (cloneEl.parentNode && this.options.removeCloneOnHide) { + cloneEl.parentNode.removeChild(cloneEl); + } } }, @@ -1534,7 +1762,7 @@ return prop === void 0 ? val : val[prop]; } else { - if (!(prop in style)) { + if (!(prop in style) && prop.indexOf('webkit') === -1) { prop = '-webkit-' + prop; } @@ -1543,6 +1771,26 @@ } } + function _matrix(el) { + var appliedTransforms = ''; + do { + var transform = _css(el, 'transform'); + + if (transform && transform !== 'none') { + appliedTransforms = transform + ' ' + appliedTransforms; + } + /* jshint boss:true */ + } while (el = el.parentNode); + + if (window.DOMMatrix) { + return new DOMMatrix(appliedTransforms); + } else if (window.WebKitCSSMatrix) { + return new WebKitCSSMatrix(appliedTransforms); + } else if (window.CSSMatrix) { + return new CSSMatrix(appliedTransforms); + } + } + function _find(ctx, tagName, iterator) { if (ctx) { @@ -1564,12 +1812,11 @@ function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex, originalEvt) { sortable = (sortable || rootEl[expando]); - var evt, options = sortable.options, onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); // Support for new CustomEvent feature - if (window.CustomEvent) { + if (window.CustomEvent && !IE11OrLess && !Edge) { evt = new CustomEvent(name, { bubbles: true, cancelable: true @@ -1589,7 +1836,9 @@ evt.originalEvent = originalEvt; - rootEl.dispatchEvent(evt); + if (rootEl) { + rootEl.dispatchEvent(evt); + } if (options[onName]) { options[onName].call(sortable, evt); @@ -1603,7 +1852,7 @@ onMoveFn = sortable.options.onMove, retVal; // Support for new CustomEvent feature - if (window.CustomEvent) { + if (window.CustomEvent && !IE11OrLess && !Edge) { evt = new CustomEvent('move', { bubbles: true, cancelable: true @@ -1618,7 +1867,7 @@ evt.dragged = dragEl; evt.draggedRect = dragRect; evt.related = targetEl || toEl; - evt.relatedRect = targetRect || toEl.getBoundingClientRect(); + evt.relatedRect = targetRect || _getRect(toEl); evt.willInsertAfter = willInsertAfter; evt.originalEvent = originalEvt; @@ -1640,11 +1889,19 @@ _silent = false; } + /** + * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible) + * and non-draggable elements + * @param {HTMLElement} el The parent element + * @param {Number} childNum The index of the child + * @param {Object} options Parent Sortable's options + * @return {HTMLElement} The child at index childNum, or null if not found + */ function _getChild(el, childNum, options) { var currentChild = 0, i = 0, - children = el.children - ; + children = el.children; + while (i < children.length) { if ( children[i].style.display !== 'none' && @@ -1663,6 +1920,11 @@ return null; } + /** + * Gets the last child in the el, ignoring ghostEl + * @param {HTMLElement} el Parent element + * @return {HTMLElement} The last child, ignoring ghostEl + */ function _lastChild(el) { var last = el.lastElementChild; @@ -1674,13 +1936,13 @@ } function _ghostIsLast(evt, axis, el) { - var elRect = _lastChild(el).getBoundingClientRect(), + var elRect = _getRect(_lastChild(el)), mouseOnAxis = axis === 'vertical' ? evt.clientY : evt.clientX, mouseOnOppAxis = axis === 'vertical' ? evt.clientX : evt.clientY, targetS2 = axis === 'vertical' ? elRect.bottom : elRect.right, targetS1Opp = axis === 'vertical' ? elRect.left : elRect.top, - targetS2Opp = axis === 'vertical' ? elRect.right : elRect.bottom - ; + targetS2Opp = axis === 'vertical' ? elRect.right : elRect.bottom; + return ( mouseOnOppAxis > targetS1Opp && mouseOnOppAxis < targetS2Opp && @@ -1688,22 +1950,19 @@ ); } - function _getSwapDirection(evt, target, axis, swapThreshold, invertedSwapThreshold, invertSwap, inside) { - var targetRect = target.getBoundingClientRect(), + function _getSwapDirection(evt, target, axis, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) { + var targetRect = _getRect(target), mouseOnAxis = axis === 'vertical' ? evt.clientY : evt.clientX, targetLength = axis === 'vertical' ? targetRect.height : targetRect.width, targetS1 = axis === 'vertical' ? targetRect.top : targetRect.left, targetS2 = axis === 'vertical' ? targetRect.bottom : targetRect.right, - dragRect = dragEl.getBoundingClientRect(), - dragLength = axis === 'vertical' ? dragRect.height : dragRect.width, - invert = false - ; - var dragStyle = _css(dragEl); - dragLength += parseInt(dragStyle.marginLeft) + parseInt(dragStyle.marginRight); + dragRect = _getRect(dragEl), + invert = false; + if (!invertSwap) { - // Never invert or create dragEl shadow when width causes mouse to move past the end of regular swapThreshold - if (inside && dragLength < targetLength * swapThreshold) { // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2 + // Never invert or create dragEl shadow when target movemenet causes mouse to move past the end of regular swapThreshold + if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) { // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2 // check if past first invert threshold on side opposite of lastDirection if (!pastFirstInvertThresh && (lastDirection === 1 ? @@ -1722,16 +1981,15 @@ if (!pastFirstInvertThresh) { var dragS1 = axis === 'vertical' ? dragRect.top : dragRect.left, - dragS2 = axis === 'vertical' ? dragRect.bottom : dragRect.right - ; - // dragEl shadow + dragS2 = axis === 'vertical' ? dragRect.bottom : dragRect.right; + // dragEl shadow (target move distance shadow) if ( lastDirection === 1 ? ( - mouseOnAxis < targetS1 + dragLength // over dragEl shadow + mouseOnAxis < targetS1 + targetMoveDistance // over dragEl shadow ) : ( - mouseOnAxis > targetS2 - dragLength + mouseOnAxis > targetS2 - targetMoveDistance ) ) { @@ -1767,6 +2025,24 @@ return 0; } + /** + * Gets the direction dragEl must be swapped relative to target in order to make it + * seem that dragEl has been "inserted" into that element's position + * @param {HTMLElement} target The target whose position dragEl is being inserted at + * @param {Object} options options of the parent sortable + * @return {Number} Direction dragEl must be swapped + */ + function _getInsertDirection(target, options) { + var dragElIndex = _index(dragEl, options.draggable), + targetIndex = _index(target, options.draggable); + + if (dragElIndex < targetIndex) { + return 1; + } else { + return -1; + } + } + /** * Generate id @@ -1801,7 +2077,7 @@ } while (el && (el = el.previousElementSibling)) { - if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) { + if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && el !== cloneEl) { index++; } } @@ -1816,6 +2092,8 @@ return el.matches(selector); } else if (el.msMatchesSelector) { return el.msMatchesSelector(selector); + } else if (el.webkitMatchesSelector) { + return el.webkitMatchesSelector(selector); } } catch(_) { return false; @@ -1830,8 +2108,7 @@ return function () { if (!_throttleTimeout) { var args = arguments, - _this = this - ; + _this = this; _throttleTimeout = setTimeout(function () { if (args.length === 1) { @@ -1895,11 +2172,131 @@ return clearTimeout(id); } - function _preventScroll(evt) { - if (Sortable.active && evt.cancelable) { + + /** + * Returns the "bounding client rect" of given element + * @param {HTMLElement} el The element whose boundingClientRect is wanted + * @param {[HTMLElement]} container the parent the element will be placed in + * @param {[Boolean]} adjustForTransform Whether the rect should compensate for parent's transform + * (used for fixed positioning on el) + * @return {Object} The boundingClientRect of el + */ + function _getRect(el, container, adjustForTransform) { + if (!el.getBoundingClientRect && el !== win) return; + + var elRect, + top, + left, + bottom, + right, + height, + width; + + if (el !== win) { + elRect = el.getBoundingClientRect(); + top = elRect.top; + left = elRect.left; + bottom = elRect.bottom; + right = elRect.right; + height = elRect.height; + width = elRect.width; + } else { + top = 0; + left = 0; + bottom = window.innerHeight; + right = window.innerWidth; + height = window.innerHeight; + width = window.innerWidth; + } + + if (adjustForTransform && el !== win) { + // Adjust for translate() + container = container || el.parentNode; + + // solves #1123 (see: https://stackoverflow.com/a/37953806/6088312) + // Not needed on <= IE11 + if (!IE11OrLess) { + do { + if (container && container.getBoundingClientRect && _css(container, 'transform') !== 'none') { + var containerRect = container.getBoundingClientRect(); + + // Set relative to edges of padding box of container + top -= containerRect.top + parseInt(_css(container, 'border-top-width')); + left -= containerRect.left + parseInt(_css(container, 'border-left-width')); + bottom = top + elRect.height; + right = left + elRect.width; + + break; + } + /* jshint boss:true */ + } while (container = container.parentNode); + } + + // Adjust for scale() + var matrix = _matrix(el), + scaleX = matrix && matrix.a, + scaleY = matrix && matrix.d; + + if (matrix) { + top /= scaleY; + left /= scaleX; + + width /= scaleX; + height /= scaleY; + + bottom = top + height; + right = left + width; + } + } + + return { + top: top, + left: left, + bottom: bottom, + right: right, + width: width, + height: height + }; + } + + + /** + * Checks if a side of an element is scrolled past a side of it's parents + * @param {HTMLElement} el The element who's side being scrolled out of view is in question + * @param {String} side Side of the element in question ('top', 'left', 'right', 'bottom') + * @return {Boolean} Whether the element is overflowing the viewport on the given side of it's parent + */ + function _isScrolledPast(el, side) { + var parent = _getParentAutoScrollElement(parent, true), + elSide = _getRect(el)[side]; + + /* jshint boss:true */ + while (parent) { + var parentSide = _getRect(parent)[side], + visible; + + if (side === 'top' || side === 'left') { + visible = elSide >= parentSide; + } else { + visible = elSide <= parentSide; + } + + if (!visible) return true; + + if (parent === win) break; + + parent = _getParentAutoScrollElement(parent, false); + } + + return false; + } + + // Fixed #973: + _on(document, 'touchmove', function(evt) { + if ((Sortable.active || awaitingDragStarted) && evt.cancelable) { evt.preventDefault(); } - } + }); // Export utils @@ -1935,6 +2332,6 @@ // Export - Sortable.version = '1.8.0-rc1'; + Sortable.version = '1.8.1'; return Sortable; }); \ No newline at end of file From c63e235489e15cffab7b338f44ea19d3c7368331 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 23 Jan 2019 12:51:28 +0100 Subject: [PATCH 48/99] js: Make auto-scroll to the top working --- public/js/behavior/sortable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/js/behavior/sortable.js b/public/js/behavior/sortable.js index c02ce5d..3ca5d1c 100644 --- a/public/js/behavior/sortable.js +++ b/public/js/behavior/sortable.js @@ -17,6 +17,7 @@ $(e.target).find('.sortable').each(function() { var $el = $(this); var options = { + scroll: $el.closest('.container')[0], onMove: function (/**Event*/ event, /**Event*/ originalEvent) { if (typeof this.options['filter'] !== 'undefined' && $(event.related).is(this.options['filter'])) { // Assumes the filtered item is either at the very start or end of the list and prevents the From 987f576d7615f2409d1de2604c76fccc38fcef33 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 25 Jan 2019 07:30:56 +0100 Subject: [PATCH 49/99] Upgrade Sortable.js Now on d373ea05e22da265b5f6fc8f218251b78f00ba71 --- public/js/vendor/Sortable.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/public/js/vendor/Sortable.js b/public/js/vendor/Sortable.js index 56bcb85..f1d85de 100644 --- a/public/js/vendor/Sortable.js +++ b/public/js/vendor/Sortable.js @@ -501,7 +501,7 @@ scrollSensitivity: 30, scrollSpeed: 10, bubbleScroll: true, - draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', + draggable: /[uo]l/i.test(el.nodeName) ? '>li' : '>*', swapThreshold: 1, // percentage; 0 <= x <= 1 invertSwap: false, // invert always invertedSwapThreshold: null, // will be set to same as swapThreshold if default @@ -617,7 +617,6 @@ _onTapStart: function (/** Event|TouchEvent */evt) { if (!evt.cancelable) return; - var _this = this, el = this.el, options = this.options, @@ -808,7 +807,6 @@ if (options.supportPointer) { _on(ownerDocument, 'pointerup', _this._onDrop); - _on(ownerDocument, 'pointercancel', _this._onDrop); } else { _on(ownerDocument, 'mouseup', _this._onDrop); _on(ownerDocument, 'touchend', _this._onDrop); @@ -1061,7 +1059,6 @@ _off(document, 'mouseup', _this._onDrop); _off(document, 'touchend', _this._onDrop); _off(document, 'touchcancel', _this._onDrop); - _off(document, 'pointercancel', _this._onDrop); if (dataTransfer) { dataTransfer.effectAllowed = 'move'; @@ -1213,7 +1210,7 @@ return completed(); } } - else if (target && target !== dragEl && (target.parentNode[expando] !== void 0) && target !== el) { + else if (target && target !== dragEl && target.parentNode === el) { var direction = 0, targetBeforeFirstSwap, aligned = target.sortableMouseAligned, @@ -1365,14 +1362,12 @@ _off(ownerDocument, 'touchend', this._onDrop); _off(ownerDocument, 'pointerup', this._onDrop); _off(ownerDocument, 'touchcancel', this._onDrop); - _off(ownerDocument, 'pointercancel', this._onDrop); _off(document, 'selectstart', this); }, _onDrop: function (/**Event*/evt) { var el = this.el, options = this.options; - awaitingDragStarted = false; scrolling = false; isCircumstantialInvert = false; @@ -1696,7 +1691,14 @@ ctx = ctx || document; do { - if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector) || (includeCTX && el === ctx)) { + if ( + selector != null && + ( + selector[0] === '>' && el.parentNode === ctx && _matches(el, selector.substring(1)) || + _matches(el, selector) || + (includeCTX && el === ctx) + ) + ) { return el; } @@ -1718,7 +1720,7 @@ function _globalDragOver(/**Event*/evt) { if (evt.dataTransfer) { - evt.dataTransfer.dropEffect = 'move'; + evt.dataTransfer.dropEffect = 'none'; } evt.cancelable && evt.preventDefault(); } From ccdadf68f27bec47f1cc0598c105215c7a560e8d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 25 Jan 2019 07:44:35 +0100 Subject: [PATCH 50/99] MoveNodeForm: Respond with a status of 400 in case of a ModificationError --- application/forms/MoveNodeForm.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php index 06ada01..d261eac 100644 --- a/application/forms/MoveNodeForm.php +++ b/application/forms/MoveNodeForm.php @@ -154,7 +154,9 @@ class MoveNodeForm extends QuickForm $this->parentNode !== null ? $this->parentNode->getName() : null ); } catch (ModificationError $e) { - $this->notifyError($e->getMessage()) + $this->notifyError($e->getMessage()); + Icinga::app()->getResponse() + ->setHttpResponseCode(400) ->redirectAndExit($this->getSuccessUrl()); } From b3a5c34c4516edbdf842cbbaee2303201cc42144 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 25 Jan 2019 10:43:50 +0100 Subject: [PATCH 51/99] ui: Set appropriate cursors when the user drags and drops stuff --- public/css/module.less | 24 ++++++++++++++++++++---- public/js/module.js | 3 +++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/public/css/module.less b/public/css/module.less index 94c5623..0a4ffaa 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -103,6 +103,26 @@ ul.bp { line-height: 1; } + // cursors!!!1 + &:not([data-sortable-disabled="true"]) { + .movable { + cursor: grab; + + &.sortable-chosen { + cursor: grabbing; + } + } + + &.progress .movable { + cursor: wait; + } + } + &[data-sortable-disabled="true"] { + li.process > div { + cursor: pointer; + } + } + // ghost style &.sortable > li.sortable-ghost { position: relative; @@ -193,10 +213,6 @@ ul.bp { // collapse handling li.process { - > div { - cursor: pointer; // So that users know they can interact - } - // toggle, default > div > a.toggle > i:before { -webkit-transition: -webkit-transform 0.3s; diff --git a/public/js/module.js b/public/js/module.js index 4a52173..8485e48 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -120,6 +120,9 @@ $target = $(evt.to); if (evt.oldIndex !== evt.newIndex || !$target.is($source)) { + var $root = $target.closest('.content > ul.bp'); + $root.addClass('progress'); + var data = { csrfToken: $target.data('csrfToken'), movenode: 'movenode', // That's the submit button.. From e4d802b7095e1fc7bd77daa0bd60d921d4904744 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 25 Jan 2019 10:44:46 +0100 Subject: [PATCH 52/99] js: Prevent the user from issuing further changes while a request is running Icinga Web 2 has the peculiarity to abort previous requests which are directed to the same container. In this case, this may lead to inconsistencies with the ui and the backend. --- public/js/module.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/js/module.js b/public/js/module.js index 8485e48..e7c6e1c 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -121,7 +121,12 @@ if (evt.oldIndex !== evt.newIndex || !$target.is($source)) { var $root = $target.closest('.content > ul.bp'); - $root.addClass('progress'); + $root.addClass('progress') + .find('ul.bp') + .add($root) + .each(function() { + $(this).data('sortable').option('disabled', true); + }); var data = { csrfToken: $target.data('csrfToken'), From 2123b41f83c9f890b0d700e20376902b21fac27f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 28 Jan 2019 08:50:42 +0100 Subject: [PATCH 53/99] Properly integrate imported nodes * Fixed navigation flow * Full tree rendering --- library/Businessprocess/BpConfig.php | 70 +++++- library/Businessprocess/BpNode.php | 26 ++- library/Businessprocess/ImportedNode.php | 210 ++++-------------- library/Businessprocess/Node.php | 8 + .../Renderer/TileRenderer/NodeTile.php | 21 +- .../Businessprocess/Renderer/TreeRenderer.php | 8 +- 6 files changed, 149 insertions(+), 194 deletions(-) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 03db018..04fd331 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -2,12 +2,15 @@ namespace Icinga\Module\Businessprocess; +use Exception; +use LogicException; +use Icinga\Application\Config; use Icinga\Exception\IcingaException; use Icinga\Exception\NotFoundError; use Icinga\Module\Businessprocess\Exception\NestingError; use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; use Icinga\Module\Monitoring\Backend\MonitoringBackend; -use Exception; class BpConfig { @@ -81,6 +84,13 @@ class BpConfig */ protected $root_nodes = array(); + /** + * Imported configs + * + * @var array + */ + protected $importedConfigs = []; + /** * All host names { 'hostA' => true, ... } * @@ -424,7 +434,17 @@ class BpConfig public function hasNode($name) { - return array_key_exists($name, $this->nodes); + if (array_key_exists($name, $this->nodes)) { + return true; + } elseif (! empty($this->importedConfigs)) { + foreach ($this->importedConfigs as $config) { + if ($config->hasNode($name)) { + return true; + } + } + } + + return false; } public function hasRootNode($name) @@ -474,7 +494,14 @@ class BpConfig public function listInvolvedHostNames() { - return array_keys($this->hosts); + $hosts = $this->hosts; + if (! empty($this->importedConfigs)) { + foreach ($this->importedConfigs as $config) { + $hosts += array_flip($config->listInvolvedHostNames()); + } + } + + return array_keys($hosts); } /** @@ -514,6 +541,18 @@ class BpConfig public function createImportedNode($config, $name = null) { + if (! isset($this->importedConfigs[$config])) { + $import = $this->storage()->loadProcess($config); + + if ($this->usesSoftStates()) { + $import->useSoftStates(); + } else { + $import->useHardStates(); + } + + $this->importedConfigs[$config] = $import; + } + $params = (object) array('configName' => $config); if ($name !== null) { $params->node = $name; @@ -524,6 +563,25 @@ class BpConfig return $node; } + public function getImportedConfig($name) + { + if (! isset($this->importedConfigs[$name])) { + throw new LogicException("Config $name not imported yet"); + } + + return $this->importedConfigs[$name]; + } + + /** + * @return LegacyStorage + */ + protected function storage() + { + return new LegacyStorage( + Config::module('businessprocess')->getSection('global') + ); + } + /** * @param $name * @return Node @@ -537,6 +595,12 @@ class BpConfig if (array_key_exists($name, $this->nodes)) { return $this->nodes[$name]; + } elseif (! empty($this->importedConfigs)) { + foreach ($this->importedConfigs as $config) { + if ($config->hasNode($name)) { + return $config->getNode($name); + } + } } // Fallback: if it is a service, create an empty one: diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index f244343..f514198 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -52,8 +52,8 @@ class BpNode extends Node { $this->bp = $bp; $this->name = $object->name; - $this->setOperator($object->operator); - $this->setChildNames($object->child_names); + $this->operator = $object->operator; + $this->childNames = $object->child_names; } public function getStateSummary() @@ -140,12 +140,12 @@ class BpNode extends Node public function hasChild($name) { - return in_array($name, $this->childNames); + return in_array($name, $this->getChildNames()); } public function removeChild($name) { - if (($key = array_search($name, $this->childNames)) !== false) { + if (($key = array_search($name, $this->getChildNames())) !== false) { unset($this->childNames[$key]); if (! empty($this->children)) { @@ -276,7 +276,7 @@ class BpNode extends Node public function hasAlias() { - return $this->alias !== null; + return $this->getAlias() !== null; } public function getAlias() @@ -357,7 +357,7 @@ class BpNode extends Node $this->setLastStateChange($lastStateChange); - switch ($this->operator) { + switch ($this->getOperator()) { case self::OP_AND: $sort_state = max($sort_states); break; @@ -438,7 +438,8 @@ class BpNode extends Node public function hasChildren($filter = null) { - return !empty($this->childNames); + $childNames = $this->getChildNames(); + return !empty($childNames); } public function getChildNames() @@ -451,10 +452,11 @@ class BpNode extends Node if ($this->children === null) { $this->children = array(); if (! $this->bp->getMetadata()->isManuallyOrdered()) { - natcasesort($this->childNames); - $this->childNames = array_values($this->childNames); + $childNames = $this->getChildNames(); + natcasesort($childNames); + $this->childNames = array_values($childNames); } - foreach ($this->childNames as $name) { + foreach ($this->getChildNames() as $name) { $this->children[$name] = $this->bp->getNode($name); $this->children[$name]->addParent($this); } @@ -497,14 +499,14 @@ class BpNode extends Node protected function assertNumericOperator() { - if (! is_numeric($this->operator)) { + if (! is_numeric($this->getOperator())) { throw new ConfigurationError('Got invalid operator: %s', $this->operator); } } public function operatorHtml() { - switch ($this->operator) { + switch ($this->getOperator()) { case self::OP_AND: return 'AND'; break; diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php index dcb9944..02d0ff2 100644 --- a/library/Businessprocess/ImportedNode.php +++ b/library/Businessprocess/ImportedNode.php @@ -2,15 +2,13 @@ namespace Icinga\Module\Businessprocess; -use Icinga\Application\Config; -use Icinga\Module\Businessprocess\State\MonitoringState; -use Icinga\Module\Businessprocess\Storage\LegacyStorage; use Exception; -use Icinga\Web\Url; -use ipl\Html\Html; -class ImportedNode extends Node +class ImportedNode extends BpNode { + /** @var BpConfig */ + protected $parentBp; + /** @var string */ protected $configName; @@ -18,38 +16,27 @@ class ImportedNode extends Node protected $nodeName; /** @var BpNode */ - private $node; + protected $importedNode; - protected $className = 'subtree'; + /** @var string */ + protected $className = 'process subtree'; + /** @var string */ protected $icon = 'download'; - /** @var BpConfig */ - private $config; - - /** - * @inheritdoc - */ public function __construct(BpConfig $bp, $object) { - $this->bp = $bp; + $this->parentBp = $bp; $this->configName = $object->configName; - $this->name = '@' . $object->configName; - if (property_exists($object, 'node')) { - $this->nodeName = $object->node; - $this->name .= ':' . $object->node; - } else { - $this->useAllRootNodes(); - } + $this->nodeName = $object->node; - if (isset($object->state)) { - $this->setState($object->state); - } - } - - public function hasNode() - { - return $this->nodeName !== null; + $importedConfig = $bp->getImportedConfig($this->configName); + parent::__construct($importedConfig, (object) [ + 'name' => '@' . $this->configName . ':' . $this->nodeName, + 'operator' => null, + 'child_names' => null + ] + ); } /** @@ -61,75 +48,38 @@ class ImportedNode extends Node } /** - * @inheritdoc + * @return string */ - public function getState() + public function getNodeName() { - if ($this->state === null) { - try { - MonitoringState::apply($this->importedConfig()); - } catch (Exception $e) { - } - - $this->state = $this->importedNode()->getState(); - $this->setMissing(false); - } - - return $this->state; + return $this->nodeName; } - /** - * @inheritdoc - */ public function getAlias() { - return $this->importedNode()->getAlias(); - } - - public function getUrl() - { - $params = array( - 'config' => $this->getConfigName(), - 'node' => $this->importedNode()->getName() - ); - - return Url::fromPath('businessprocess/process/show', $params); - } - - /** - * @inheritdoc - */ - public function isMissing() - { - $this->getState(); - // Probably doesn't work, as we create a fake node with worse state - return $this->missing; - } - - /** - * @inheritdoc - */ - public function isInDowntime() - { - if ($this->downtime === null) { - $this->getState(); - $this->downtime = $this->importedNode()->isInDowntime(); + if ($this->alias === null) { + $this->alias = $this->importedNode()->getAlias(); } - return $this->downtime; + return $this->alias; } - /** - * @inheritdoc - */ - public function isAcknowledged() + public function getOperator() { - if ($this->ack === null) { - $this->getState(); - $this->downtime = $this->importedNode()->isAcknowledged(); + if ($this->operator === null) { + $this->operator = $this->importedNode()->getOperator(); } - return $this->ack; + return $this->operator; + } + + public function getChildNames() + { + if ($this->childNames === null) { + $this->childNames = $this->importedNode()->getChildNames(); + } + + return $this->childNames; } /** @@ -137,68 +87,15 @@ class ImportedNode extends Node */ protected function importedNode() { - if ($this->node === null) { - $this->node = $this->loadImportedNode(); - } - - return $this->node; - } - - /** - * @return BpNode - */ - protected function loadImportedNode() - { - try { - $import = $this->importedConfig(); - - return $import->getNode($this->nodeName); - } catch (Exception $e) { - return $this->createFailedNode($e); - } - } - - protected function useAllRootNodes() - { - try { - $bp = $this->importedConfig(); - $this->node = new BpNode($bp, (object) array( - 'name' => $this->getName(), - 'operator' => '&', - 'child_names' => $bp->listRootNodes(), - )); - } catch (Exception $e) { - $this->createFailedNode($e); - } - } - - /** - * @return BpConfig - */ - protected function importedConfig() - { - if ($this->config === null) { - $import = $this->storage()->loadProcess($this->configName); - if ($this->bp->usesSoftStates()) { - $import->useSoftStates(); - } else { - $import->useHardStates(); + if ($this->importedNode === null) { + try { + $this->importedNode = $this->bp->getBpNode($this->nodeName); + } catch (Exception $e) { + return $this->createFailedNode($e); } - - $this->config = $import; } - return $this->config; - } - - /** - * @return LegacyStorage - */ - protected function storage() - { - return new LegacyStorage( - Config::module('businessprocess')->getSection('global') - ); + return $this->importedNode; } /** @@ -208,11 +105,11 @@ class ImportedNode extends Node */ protected function createFailedNode(Exception $e) { - $this->bp->addError($e->getMessage()); - $node = new BpNode($this->importedConfig(), (object) array( + $this->parentBp->addError($e->getMessage()); + $node = new BpNode($this->bp, (object) array( 'name' => $this->getName(), 'operator' => '&', - 'child_names' => array() + 'child_names' => [] )); $node->setState(2); $node->setMissing(false) @@ -222,21 +119,4 @@ class ImportedNode extends Node return $node; } - - /** - * @inheritdoc - */ - public function getLink() - { - return Html::tag( - 'a', - [ - 'href' => Url::fromPath('businessprocess/process/show', [ - 'config' => $this->configName, - 'node' => $this->nodeName - ]) - ], - $this->getAlias() - ); - } } diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 05d1a2d..5e2d58b 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -356,6 +356,14 @@ abstract class Node return $paths; } + /** + * @return BpConfig + */ + public function getBusinessProcess() + { + return $this->bp; + } + protected function stateToSortState($state) { if (array_key_exists($state, $this->stateToSortStateMap)) { diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index c2e30ac..63e4cce 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -119,24 +119,19 @@ class NodeTile extends BaseHtmlElement protected function buildBaseNodeUrl(Node $node) { $path = $this->path; - $name = $this->name; // TODO: ?? $renderer = $this->renderer; - $bp = $renderer->getBusinessProcess(); - $params = array( - 'config' => $node instanceof ImportedNode ? - $node->getConfigName() : - $bp->getName() - ); - - if ($name !== null) { - $params['node'] = $name; - } + $params = [ + 'config' => $node->getBusinessProcess()->getName(), + 'node' => $node instanceof ImportedNode + ? $node->getNodeName() + : $this->name + ]; $url = $renderer->getBaseUrl(); $p = $url->getParams(); $p->mergeValues($params); - if (! empty($path)) { + if (! empty($path) && !$node instanceof ImportedNode) { $p->addValues('path', $path); } @@ -186,7 +181,7 @@ class NodeTile extends BaseHtmlElement $link = Html::tag('a', ['href' => $url, 'data-base-target' => '_next'], $node->getHostname()); } else { $link = Html::tag('a', ['href' => $url], $node->getAlias()); - if ($node instanceof ImportedNode) { + if ($node->getBusinessProcess()->getName() !== $this->renderer->getBusinessProcess()->getName()) { $link->getAttributes()->add('data-base-target', '_next'); } else { $link->getAttributes()->add('data-base-target', '_self'); diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 0280f5c..d2a89f6 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Businessprocess\Renderer; use Icinga\Date\DateFormatter; use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\ImportedNode; use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\Web\Form\CsrfToken; use ipl\Html\BaseHtmlElement; @@ -185,7 +186,12 @@ class TreeRenderer extends Renderer ]), 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl() - ->overwriteParams(['node' => (string) $node]) + ->overwriteParams([ + 'config' => $node->getBusinessProcess()->getName(), + 'node' => $node instanceof ImportedNode + ? $node->getNodeName() + : (string) $node + ]) ->getAbsoluteUrl() ]); $li->add($ul); From 0194f9afe9bbf88f01f79d70079f57aab0b4cb43 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 28 Jan 2019 09:01:12 +0100 Subject: [PATCH 54/99] tree: Don't allow to change imported nodes The remaining ui and navigation flow doesn't incorporate differing bp configurations --- library/Businessprocess/Renderer/TreeRenderer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index d2a89f6..04e0ed9 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -169,13 +169,13 @@ class TreeRenderer extends Renderer $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); } - if (! $this->isLocked()) { + if (! $this->isLocked() && $node->getBusinessProcess()->getName() === $this->getBusinessProcess()->getName()) { $div->add($this->getActionIcons($bp, $node)); } $ul = Html::tag('ul', [ 'class' => ['bp', 'sortable'], - 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', + 'data-sortable-disabled' => $this->isLocked() || $node->getBusinessProcess()->getName() !== $this->getBusinessProcess()->getName() ? 'true' : 'false', 'data-sortable-invert-swap' => 'true', 'data-sortable-data-id-attr' => 'id', 'data-sortable-draggable' => '.movable', @@ -222,7 +222,7 @@ class TreeRenderer extends Renderer $link->getAttributes()->set('data-base-target', '_next'); $li->add($link); - if (! $this->isLocked()) { + if (! $this->isLocked() && $node->getBusinessProcess()->getName() === $this->getBusinessProcess()->getName()) { $li->add($this->getActionIcons($bp, $node)); } From 00b88055f7d9da7648db60f78cb9746283a76233 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 Feb 2019 08:37:36 +0100 Subject: [PATCH 55/99] BpConfig: Delay processing of imported configurations Solves the issue that two processes cannot import each other --- library/Businessprocess/BpConfig.php | 48 +++++++++++-------- library/Businessprocess/BpNode.php | 19 ++++---- library/Businessprocess/HostNode.php | 7 ++- library/Businessprocess/ImportedNode.php | 21 +++++--- .../Modification/NodeCreateAction.php | 3 +- library/Businessprocess/Node.php | 23 +++++---- .../Renderer/TileRenderer/NodeTile.php | 4 +- .../Businessprocess/Renderer/TreeRenderer.php | 8 ++-- library/Businessprocess/ServiceNode.php | 7 ++- .../Storage/LegacyConfigParser.php | 3 +- .../library/Businessprocess/HostNodeTest.php | 4 +- .../Businessprocess/ServiceNodeTest.php | 4 +- 12 files changed, 85 insertions(+), 66 deletions(-) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 04fd331..6fdc733 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -32,6 +32,11 @@ class BpConfig */ protected $backend; + /** + * @var LegacyStorage + */ + protected $storage; + /** @var Metadata */ protected $metadata; @@ -455,12 +460,12 @@ class BpConfig public function createService($host, $service) { $node = new ServiceNode( - $this, (object) array( 'hostname' => $host, 'service' => $service ) ); + $node->setBpConfig($this); $this->nodes[$host . ';' . $service] = $node; $this->hosts[$host] = true; return $node; @@ -468,7 +473,8 @@ class BpConfig public function createHost($host) { - $node = new HostNode($this, (object) array('hostname' => $host)); + $node = new HostNode((object) array('hostname' => $host)); + $node->setBpConfig($this); $this->nodes[$host . ';Hoststatus'] = $node; $this->hosts[$host] = true; return $node; @@ -514,11 +520,12 @@ class BpConfig */ public function createBp($name, $operator = '&') { - $node = new BpNode($this, (object) array( + $node = new BpNode((object) array( 'name' => $name, 'operator' => $operator, 'child_names' => array(), )); + $node->setBpConfig($this); $this->addNode($name, $node); return $node; @@ -541,18 +548,6 @@ class BpConfig public function createImportedNode($config, $name = null) { - if (! isset($this->importedConfigs[$config])) { - $import = $this->storage()->loadProcess($config); - - if ($this->usesSoftStates()) { - $import->useSoftStates(); - } else { - $import->useHardStates(); - } - - $this->importedConfigs[$config] = $import; - } - $params = (object) array('configName' => $config); if ($name !== null) { $params->node = $name; @@ -566,7 +561,15 @@ class BpConfig public function getImportedConfig($name) { if (! isset($this->importedConfigs[$name])) { - throw new LogicException("Config $name not imported yet"); + $import = $this->storage()->loadProcess($name); + + if ($this->usesSoftStates()) { + $import->useSoftStates(); + } else { + $import->useHardStates(); + } + + $this->importedConfigs[$name] = $import; } return $this->importedConfigs[$name]; @@ -577,9 +580,13 @@ class BpConfig */ protected function storage() { - return new LegacyStorage( - Config::module('businessprocess')->getSection('global') - ); + if ($this->storage === null) { + $this->storage = new LegacyStorage( + Config::module('businessprocess')->getSection('global') + ); + } + + return $this->storage; } /** @@ -632,11 +639,12 @@ class BpConfig $this->calculateAllStates(); $names = array_keys($this->getUnboundNodes()); - $bp = new BpNode($this, (object) array( + $bp = new BpNode((object) array( 'name' => '__unbound__', 'operator' => '&', 'child_names' => $names )); + $bp->setBpConfig($this); $bp->setAlias($this->translate('Unbound nodes')); return $bp; } diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index f514198..c2a8b4a 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -48,9 +48,8 @@ class BpNode extends Node protected $className = 'process'; - public function __construct(BpConfig $bp, $object) + public function __construct($object) { - $this->bp = $bp; $this->name = $object->name; $this->operator = $object->operator; $this->childNames = $object->child_names; @@ -178,7 +177,7 @@ class BpNode extends Node { if ($this->missing === null) { $exists = false; - $bp = $this->bp; + $bp = $this->getBpConfig(); $bp->beginLoopDetection($this->name); foreach ($this->getChildren() as $child) { if (! $child->isMissing()) { @@ -299,8 +298,8 @@ class BpNode extends Node try { $this->reCalculateState(); } catch (NestingError $e) { - $this->bp->addError( - $this->bp->translate('Nesting error detected: %s'), + $this->getBpConfig()->addError( + $this->getBpConfig()->translate('Nesting error detected: %s'), $e->getMessage() ); @@ -327,7 +326,7 @@ class BpNode extends Node */ public function reCalculateState() { - $bp = $this->bp; + $bp = $this->getBpConfig(); $sort_states = array(); $lastStateChange = 0; @@ -401,7 +400,7 @@ class BpNode extends Node public function checkForLoops() { - $bp = $this->bp; + $bp = $this->getBpConfig(); foreach ($this->getChildren() as $child) { $bp->beginLoopDetection($this->name); if ($child instanceof BpNode) { @@ -426,7 +425,7 @@ class BpNode extends Node public function setChildNames($names) { - if (! $this->bp->getMetadata()->isManuallyOrdered()) { + if (! $this->getBpConfig()->getMetadata()->isManuallyOrdered()) { natcasesort($names); $names = array_values($names); } @@ -451,13 +450,13 @@ class BpNode extends Node { if ($this->children === null) { $this->children = array(); - if (! $this->bp->getMetadata()->isManuallyOrdered()) { + if (! $this->getBpConfig()->getMetadata()->isManuallyOrdered()) { $childNames = $this->getChildNames(); natcasesort($childNames); $this->childNames = array_values($childNames); } foreach ($this->getChildNames() as $name) { - $this->children[$name] = $this->bp->getNode($name); + $this->children[$name] = $this->getBpConfig()->getNode($name); $this->children[$name]->addParent($this); } } diff --git a/library/Businessprocess/HostNode.php b/library/Businessprocess/HostNode.php index cea73c2..b02d3ec 100644 --- a/library/Businessprocess/HostNode.php +++ b/library/Businessprocess/HostNode.php @@ -34,11 +34,10 @@ class HostNode extends MonitoredNode protected $icon = 'host'; - public function __construct(BpConfig $bp, $object) + public function __construct($object) { $this->name = $object->hostname . ';Hoststatus'; $this->hostname = $object->hostname; - $this->bp = $bp; if (isset($object->state)) { $this->setState($object->state); } else { @@ -62,8 +61,8 @@ class HostNode extends MonitoredNode 'host' => $this->getHostname(), ); - if ($this->bp->hasBackendName()) { - $params['backend'] = $this->bp->getBackendName(); + if ($this->getBpConfig()->hasBackendName()) { + $params['backend'] = $this->getBpConfig()->getBackendName(); } return Url::fromPath('businessprocess/host/show', $params); diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php index 02d0ff2..22c244f 100644 --- a/library/Businessprocess/ImportedNode.php +++ b/library/Businessprocess/ImportedNode.php @@ -24,14 +24,13 @@ class ImportedNode extends BpNode /** @var string */ protected $icon = 'download'; - public function __construct(BpConfig $bp, $object) + public function __construct(BpConfig $parentBp, $object) { - $this->parentBp = $bp; + $this->parentBp = $parentBp; $this->configName = $object->configName; $this->nodeName = $object->node; - $importedConfig = $bp->getImportedConfig($this->configName); - parent::__construct($importedConfig, (object) [ + parent::__construct((object) [ 'name' => '@' . $this->configName . ':' . $this->nodeName, 'operator' => null, 'child_names' => null @@ -55,6 +54,15 @@ class ImportedNode extends BpNode return $this->nodeName; } + public function getBpConfig() + { + if ($this->bp === null) { + $this->bp = $this->parentBp->getImportedConfig($this->configName); + } + + return $this->bp; + } + public function getAlias() { if ($this->alias === null) { @@ -89,7 +97,7 @@ class ImportedNode extends BpNode { if ($this->importedNode === null) { try { - $this->importedNode = $this->bp->getBpNode($this->nodeName); + $this->importedNode = $this->getBpConfig()->getBpNode($this->nodeName); } catch (Exception $e) { return $this->createFailedNode($e); } @@ -106,11 +114,12 @@ class ImportedNode extends BpNode protected function createFailedNode(Exception $e) { $this->parentBp->addError($e->getMessage()); - $node = new BpNode($this->bp, (object) array( + $node = new BpNode((object) array( 'name' => $this->getName(), 'operator' => '&', 'child_names' => [] )); + $node->setBpConfig($this->getBpConfig()); $node->setState(2); $node->setMissing(false) ->setDowntime(false) diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php index 2e0fc6b..aa9162e 100644 --- a/library/Businessprocess/Modification/NodeCreateAction.php +++ b/library/Businessprocess/Modification/NodeCreateAction.php @@ -101,7 +101,8 @@ class NodeCreateAction extends NodeAction } else { $properties['child_names'] = array(); } - $node = new BpNode($config, (object) $properties); + $node = new BpNode((object) $properties); + $node->setBpConfig($config); foreach ($this->getProperties() as $key => $val) { if ($key === 'parentName') { diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 5e2d58b..662aca6 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -109,7 +109,18 @@ abstract class Node 99 => 'PENDING' ); - abstract public function __construct(BpConfig $bp, $object); + abstract public function __construct($object); + + public function setBpConfig(BpConfig $bp) + { + $this->bp = $bp; + return $this; + } + + public function getBpConfig() + { + return $this->bp; + } public function setMissing($missing = true) { @@ -341,7 +352,7 @@ abstract class Node */ public function getPaths() { - if ($this->bp->hasRootNode($this->getName())) { + if ($this->getBpConfig()->hasRootNode($this->getName())) { return array(array($this->getName())); } @@ -356,14 +367,6 @@ abstract class Node return $paths; } - /** - * @return BpConfig - */ - public function getBusinessProcess() - { - return $this->bp; - } - protected function stateToSortState($state) { if (array_key_exists($state, $this->stateToSortStateMap)) { diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 63e4cce..91f9bc6 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -122,7 +122,7 @@ class NodeTile extends BaseHtmlElement $renderer = $this->renderer; $params = [ - 'config' => $node->getBusinessProcess()->getName(), + 'config' => $node->getBpConfig()->getName(), 'node' => $node instanceof ImportedNode ? $node->getNodeName() : $this->name @@ -181,7 +181,7 @@ class NodeTile extends BaseHtmlElement $link = Html::tag('a', ['href' => $url, 'data-base-target' => '_next'], $node->getHostname()); } else { $link = Html::tag('a', ['href' => $url], $node->getAlias()); - if ($node->getBusinessProcess()->getName() !== $this->renderer->getBusinessProcess()->getName()) { + if ($node->getBpConfig()->getName() !== $this->renderer->getBusinessProcess()->getName()) { $link->getAttributes()->add('data-base-target', '_next'); } else { $link->getAttributes()->add('data-base-target', '_self'); diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 04e0ed9..7284ae6 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -169,13 +169,13 @@ class TreeRenderer extends Renderer $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); } - if (! $this->isLocked() && $node->getBusinessProcess()->getName() === $this->getBusinessProcess()->getName()) { + if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) { $div->add($this->getActionIcons($bp, $node)); } $ul = Html::tag('ul', [ 'class' => ['bp', 'sortable'], - 'data-sortable-disabled' => $this->isLocked() || $node->getBusinessProcess()->getName() !== $this->getBusinessProcess()->getName() ? 'true' : 'false', + 'data-sortable-disabled' => $this->isLocked() || $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName() ? 'true' : 'false', 'data-sortable-invert-swap' => 'true', 'data-sortable-data-id-attr' => 'id', 'data-sortable-draggable' => '.movable', @@ -187,7 +187,7 @@ class TreeRenderer extends Renderer 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl() ->overwriteParams([ - 'config' => $node->getBusinessProcess()->getName(), + 'config' => $node->getBpConfig()->getName(), 'node' => $node instanceof ImportedNode ? $node->getNodeName() : (string) $node @@ -222,7 +222,7 @@ class TreeRenderer extends Renderer $link->getAttributes()->set('data-base-target', '_next'); $li->add($link); - if (! $this->isLocked() && $node->getBusinessProcess()->getName() === $this->getBusinessProcess()->getName()) { + if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) { $li->add($this->getActionIcons($bp, $node)); } diff --git a/library/Businessprocess/ServiceNode.php b/library/Businessprocess/ServiceNode.php index 153b010..b40ba02 100644 --- a/library/Businessprocess/ServiceNode.php +++ b/library/Businessprocess/ServiceNode.php @@ -14,12 +14,11 @@ class ServiceNode extends MonitoredNode protected $icon = 'service'; - public function __construct(BpConfig $bp, $object) + public function __construct($object) { $this->name = $object->hostname . ';' . $object->service; $this->hostname = $object->hostname; $this->service = $object->service; - $this->bp = $bp; if (isset($object->state)) { $this->setState($object->state); } else { @@ -49,8 +48,8 @@ class ServiceNode extends MonitoredNode 'service' => $this->getServiceDescription() ); - if ($this->bp->hasBackendName()) { - $params['backend'] = $this->bp->getBackendName(); + if ($this->getBpConfig()->hasBackendName()) { + $params['backend'] = $this->getBpConfig()->getBackendName(); } return Url::fromPath('businessprocess/service/show', $params); diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php index 427b3ef..ddcdb29 100644 --- a/library/Businessprocess/Storage/LegacyConfigParser.php +++ b/library/Businessprocess/Storage/LegacyConfigParser.php @@ -333,11 +333,12 @@ class LegacyConfigParser $childNames[] = $val; } - $node = new BpNode($bp, (object) array( + $node = new BpNode((object) array( 'name' => $name, 'operator' => $op_name, 'child_names' => $childNames )); + $node->setBpConfig($bp); $bp->addNode($name, $node); } diff --git a/test/php/library/Businessprocess/HostNodeTest.php b/test/php/library/Businessprocess/HostNodeTest.php index 9146bee..ff23476 100644 --- a/test/php/library/Businessprocess/HostNodeTest.php +++ b/test/php/library/Businessprocess/HostNodeTest.php @@ -57,9 +57,9 @@ class HostNodeTest extends BaseTestCase protected function localhost() { $bp = new BpConfig(); - return new HostNode($bp, (object) array( + return (new HostNode((object) array( 'hostname' => 'localhost', 'state' => 0, - )); + )))->setBpConfig($bp); } } diff --git a/test/php/library/Businessprocess/ServiceNodeTest.php b/test/php/library/Businessprocess/ServiceNodeTest.php index c5fd719..d4b3e5a 100644 --- a/test/php/library/Businessprocess/ServiceNodeTest.php +++ b/test/php/library/Businessprocess/ServiceNodeTest.php @@ -47,10 +47,10 @@ class ServiceNodeTest extends BaseTestCase protected function pingOnLocalhost() { $bp = new BpConfig(); - return new ServiceNode($bp, (object) array( + return (new ServiceNode((object) array( 'hostname' => 'localhost', 'service' => 'ping <> pong', 'state' => 0, - )); + )))->setBpConfig($bp); } } From 1a36e4d94aed822984ba37eb9a60e2763847974f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 Feb 2019 09:17:07 +0100 Subject: [PATCH 56/99] LegacyConfigRenderer: Don't render import sub-nodes --- library/Businessprocess/Storage/LegacyConfigRenderer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/Businessprocess/Storage/LegacyConfigRenderer.php b/library/Businessprocess/Storage/LegacyConfigRenderer.php index 57bb1fb..d90db95 100644 --- a/library/Businessprocess/Storage/LegacyConfigRenderer.php +++ b/library/Businessprocess/Storage/LegacyConfigRenderer.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Businessprocess\Storage; use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\ImportedNode; class LegacyConfigRenderer { @@ -110,6 +111,10 @@ class LegacyConfigRenderer $cfg = ''; foreach ($node->getChildBpNodes() as $name => $child) { + if ($child instanceof ImportedNode) { + continue; + } + $cfg .= $this->requireRenderedBpNode($child) . "\n"; } From aa8f070a007e21714d261f5184b3edd84dd9acf3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 Feb 2019 13:47:22 +0100 Subject: [PATCH 57/99] BpConfig: Don't die due to circular references --- library/Businessprocess/BpConfig.php | 36 ++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 6fdc733..3a41eba 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -437,13 +437,18 @@ class BpConfig return $this->nodes; } - public function hasNode($name) + public function hasNode($name, &$usedConfigs = null) { if (array_key_exists($name, $this->nodes)) { return true; } elseif (! empty($this->importedConfigs)) { + $usedConfigs[$this->getName()] = true; foreach ($this->importedConfigs as $config) { - if ($config->hasNode($name)) { + if (isset($usedConfigs[$config->getName()])) { + continue; + } + + if ($config->hasNode($name, $usedConfigs)) { return true; } } @@ -498,12 +503,17 @@ class BpConfig return $this; } - public function listInvolvedHostNames() + public function listInvolvedHostNames(&$usedConfigs = null) { $hosts = $this->hosts; if (! empty($this->importedConfigs)) { + $usedConfigs[$this->getName()] = true; foreach ($this->importedConfigs as $config) { - $hosts += array_flip($config->listInvolvedHostNames()); + if (isset($usedConfigs[$config->getName()])) { + continue; + } + + $hosts += array_flip($config->listInvolvedHostNames($usedConfigs)); } } @@ -590,11 +600,12 @@ class BpConfig } /** - * @param $name - * @return Node - * @throws Exception + * @param string $name + * @param array $usedConfigs + * @return Node + * @throws Exception */ - public function getNode($name) + public function getNode($name, &$usedConfigs = null) { if ($name === '__unbound__') { return $this->getUnboundBaseNode(); @@ -603,9 +614,14 @@ class BpConfig if (array_key_exists($name, $this->nodes)) { return $this->nodes[$name]; } elseif (! empty($this->importedConfigs)) { + $usedConfigs[$this->getName()] = true; foreach ($this->importedConfigs as $config) { - if ($config->hasNode($name)) { - return $config->getNode($name); + if (isset($usedConfigs[$config->getName()])) { + continue; + } + + if ($config->hasNode($name, $usedConfigs)) { + return $config->getNode($name, $usedConfigs); } } } From a681d7e393f3d5c93e16b931c0376782a2332137 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 7 Feb 2019 12:04:55 +0100 Subject: [PATCH 58/99] Fix that imported nodes have no state, once and for all --- library/Businessprocess/BpConfig.php | 61 ++++++++++--------- .../Businessprocess/State/MonitoringState.php | 14 +++-- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 3a41eba..7295508 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -89,10 +89,17 @@ class BpConfig */ protected $root_nodes = array(); + /** + * Imported nodes + * + * @var ImportedNode[] + */ + protected $importedNodes = []; + /** * Imported configs * - * @var array + * @var BpConfig[] */ protected $importedConfigs = []; @@ -437,21 +444,10 @@ class BpConfig return $this->nodes; } - public function hasNode($name, &$usedConfigs = null) + public function hasNode($name) { if (array_key_exists($name, $this->nodes)) { return true; - } elseif (! empty($this->importedConfigs)) { - $usedConfigs[$this->getName()] = true; - foreach ($this->importedConfigs as $config) { - if (isset($usedConfigs[$config->getName()])) { - continue; - } - - if ($config->hasNode($name, $usedConfigs)) { - return true; - } - } } return false; @@ -506,14 +502,14 @@ class BpConfig public function listInvolvedHostNames(&$usedConfigs = null) { $hosts = $this->hosts; - if (! empty($this->importedConfigs)) { + if (! empty($this->importedNodes)) { $usedConfigs[$this->getName()] = true; - foreach ($this->importedConfigs as $config) { - if (isset($usedConfigs[$config->getName()])) { + foreach ($this->importedNodes as $node) { + if (isset($usedConfigs[$node->getConfigName()])) { continue; } - $hosts += array_flip($config->listInvolvedHostNames($usedConfigs)); + $hosts += array_flip($node->getBpConfig()->listInvolvedHostNames($usedConfigs)); } } @@ -564,6 +560,7 @@ class BpConfig } $node = new ImportedNode($this, $params); + $this->importedNodes[$node->getName()] = $node; $this->nodes[$node->getName()] = $node; return $node; } @@ -585,6 +582,22 @@ class BpConfig return $this->importedConfigs[$name]; } + public function listInvolvedConfigs(&$usedConfigs = null) + { + $configs = []; + foreach ($this->importedNodes as $node) { + $config = $node->getBpConfig(); + $configs[] = $config; + + if (! isset($usedConfigs[$node->getConfigName()])) { + $usedConfigs[$config->getName()] = true; + $configs = array_merge($configs, $config->listInvolvedConfigs($usedConfigs)); + } + } + + return $configs; + } + /** * @return LegacyStorage */ @@ -601,11 +614,10 @@ class BpConfig /** * @param string $name - * @param array $usedConfigs * @return Node * @throws Exception */ - public function getNode($name, &$usedConfigs = null) + public function getNode($name) { if ($name === '__unbound__') { return $this->getUnboundBaseNode(); @@ -613,17 +625,6 @@ class BpConfig if (array_key_exists($name, $this->nodes)) { return $this->nodes[$name]; - } elseif (! empty($this->importedConfigs)) { - $usedConfigs[$this->getName()] = true; - foreach ($this->importedConfigs as $config) { - if (isset($usedConfigs[$config->getName()])) { - continue; - } - - if ($config->hasNode($name, $usedConfigs)) { - return $config->getNode($name, $usedConfigs); - } - } } // Fallback: if it is a service, create an empty one: diff --git a/library/Businessprocess/State/MonitoringState.php b/library/Businessprocess/State/MonitoringState.php index 3f25465..1e48f65 100644 --- a/library/Businessprocess/State/MonitoringState.php +++ b/library/Businessprocess/State/MonitoringState.php @@ -93,13 +93,17 @@ class MonitoringState Benchmark::measure('Retrieved states for ' . count($serviceStatus) . ' services in ' . $config->getName()); - foreach ($serviceStatus as $row) { - $this->handleDbRow($row, $config); + $configs = $config->listInvolvedConfigs(); + $configs[] = $config; + foreach ($configs as $cfg) { + foreach ($serviceStatus as $row) { + $this->handleDbRow($row, $cfg); + } + foreach ($hostStatus as $row) { + $this->handleDbRow($row, $cfg); + } } - foreach ($hostStatus as $row) { - $this->handleDbRow($row, $config); - } // TODO: Union, single query? Benchmark::measure('Got states for business process ' . $config->getName()); From 195b3bf7b755acd0e7e8efc1571fa52b8947b901 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 14 Feb 2019 13:03:45 +0100 Subject: [PATCH 59/99] ProcessController: Don't unlock the renderer if fullscreen is enabled --- application/controllers/ProcessController.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 4594756..17c5622 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -88,12 +88,14 @@ class ProcessController extends Controller $renderer = $this->prepareRenderer($bp, $node); - if ($this->params->get('unlocked')) { - $renderer->unlock(); - } + if (! $this->showFullscreen) { + if ($this->params->get('unlocked')) { + $renderer->unlock(); + } - if ($bp->isEmpty() && $renderer->isLocked()) { - $this->redirectNow($this->url()->with('unlocked', true)); + if ($bp->isEmpty() && $renderer->isLocked()) { + $this->redirectNow($this->url()->with('unlocked', true)); + } } $this->handleFormatRequest($bp, $node); From df7e72398dc0cb05b768c7b5d94d5344504bca10 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 14 Feb 2019 13:50:43 +0100 Subject: [PATCH 60/99] RenderedProcessActionBar: Make the mode toggle a single anchor It's a toggle, there's no need for two distinct urls. Even more important, there's no need for two different click areas. --- .../Component/RenderedProcessActionBar.php | 49 +++++++++++-------- public/css/module.less | 38 +++++++------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php index 939ce98..8de8ac0 100644 --- a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -15,27 +15,34 @@ class RenderedProcessActionBar extends ActionBar { $meta = $config->getMetadata(); - $toggle = Html::tag('div', ['class' => 'view-toggle']); - $toggle->add(Html::tag('span', null, mt('businessprocess', 'View'))); - $toggle->add(Html::tag( - 'a', - [ - 'href' => $url->with('mode', 'tile'), - 'title' => mt('businessprocess', 'Switch to Tile view'), - 'class' => $renderer instanceof TreeRenderer ? '' : 'active' - ], - Html::tag('i', ['class' => 'icon icon-dashboard']) - )); - $toggle->add(Html::tag( - 'a', - [ - 'href' => $url->with('mode', 'tree'), - 'title' => mt('businessprocess', 'Switch to Tree view'), - 'class' => $renderer instanceof TreeRenderer ? 'active' : '' - ], - Html::tag('i', ['class' => 'icon icon-sitemap']) - )); - $this->add($toggle); + if ($renderer instanceof TreeRenderer) { + $link = Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tile'), + 'title' => mt('businessprocess', 'Switch to Tile view') + ] + ); + } else { + $link = Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tree'), + 'title' => mt('businessprocess', 'Switch to Tree view') + ] + ); + } + + $link->add([ + Html::tag('i', ['class' => 'icon icon-dashboard' . ($renderer instanceof TreeRenderer ? '' : ' active')]), + Html::tag('i', ['class' => 'icon icon-sitemap' . ($renderer instanceof TreeRenderer ? ' active' : '')]) + ]); + + $this->add( + Html::tag('div', ['class' => 'view-toggle']) + ->add(Html::tag('span', null, mt('businessprocess', 'View'))) + ->add($link) + ); $this->add(Html::tag( 'a', diff --git a/public/css/module.less b/public/css/module.less index 0a4ffaa..ff2bf09 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -44,27 +44,29 @@ a:focus { } a { - padding: .25em .5em; - border: 1px solid @icinga-blue; + display: inline-block; - i::before { - margin-right: 0; - } + i { + padding: .25em .5em; + border: 1px solid @icinga-blue; - &.active { - color: white; - background-color: @icinga-blue; - } + &:before { + margin-right: 0; + } - &:first-of-type { - border-right-width: 0; - border-top-left-radius: .25em; - border-bottom-left-radius: .25em; - } - &:last-of-type { - border-left-width: 0; - border-top-right-radius: .25em; - border-bottom-right-radius: .25em; + &.active { + color: white; + background-color: @icinga-blue; + } + + &:first-of-type { + border-top-left-radius: .25em; + border-bottom-left-radius: .25em; + } + &:last-of-type { + border-top-right-radius: .25em; + border-bottom-right-radius: .25em; + } } } } From 7a8c48c7e844f60d866202075290910d9148fd8d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 14 Feb 2019 15:32:51 +0100 Subject: [PATCH 61/99] Fix a few style issues --- application/forms/AddNodeForm.php | 6 +++--- application/forms/MoveNodeForm.php | 2 +- configuration.php | 2 +- library/Businessprocess/ImportedNode.php | 9 ++++----- library/Businessprocess/Renderer/TreeRenderer.php | 4 +++- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index 2efa1b0..ad24cc0 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -189,7 +189,7 @@ class AddNodeForm extends QuickForm protected function selectHost() { - $this->addElement('multiselect','children', [ + $this->addElement('multiselect', 'children', [ 'label' => $this->translate('Hosts'), 'required' => true, 'size' => 8, @@ -225,7 +225,7 @@ class AddNodeForm extends QuickForm protected function addServicesElement($host) { - $this->addElement('multiselect','children', [ + $this->addElement('multiselect', 'children', [ 'label' => $this->translate('Services'), 'required' => true, 'size' => 8, @@ -260,7 +260,7 @@ class AddNodeForm extends QuickForm } if (($file = $this->getSentValue('file')) || !$this->hasParentNode()) { - $this->addElement('multiselect','children', [ + $this->addElement('multiselect', 'children', [ 'label' => $this->translate('Process nodes'), 'required' => true, 'size' => 8, diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php index d261eac..5ec2ed9 100644 --- a/application/forms/MoveNodeForm.php +++ b/application/forms/MoveNodeForm.php @@ -57,7 +57,7 @@ class MoveNodeForm extends QuickForm 'filters' => ['Null'], 'validators' => [ ['Callback', true, [ - 'callback' => function($name) { + 'callback' => function ($name) { return empty($name) || $this->bp->hasBpNode($name); }, 'messages' => [ diff --git a/configuration.php b/configuration.php index 724c23c..38ef46a 100644 --- a/configuration.php +++ b/configuration.php @@ -62,4 +62,4 @@ $this->provideJsFile('vendor/Sortable.js'); $this->provideJsFile('behavior/sortable.js'); $this->provideJsFile('vendor/jquery.fn.sortable.js'); -$this->provideCssFile('state-ball.less'); \ No newline at end of file +$this->provideCssFile('state-ball.less'); diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php index 22c244f..cf40346 100644 --- a/library/Businessprocess/ImportedNode.php +++ b/library/Businessprocess/ImportedNode.php @@ -31,11 +31,10 @@ class ImportedNode extends BpNode $this->nodeName = $object->node; parent::__construct((object) [ - 'name' => '@' . $this->configName . ':' . $this->nodeName, - 'operator' => null, - 'child_names' => null - ] - ); + 'name' => '@' . $this->configName . ':' . $this->nodeName, + 'operator' => null, + 'child_names' => null + ]); } /** diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 7284ae6..54e6345 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -175,7 +175,9 @@ class TreeRenderer extends Renderer $ul = Html::tag('ul', [ 'class' => ['bp', 'sortable'], - 'data-sortable-disabled' => $this->isLocked() || $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName() ? 'true' : 'false', + 'data-sortable-disabled' => ( + $this->isLocked() || $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName() + ) ? 'true' : 'false', 'data-sortable-invert-swap' => 'true', 'data-sortable-data-id-attr' => 'id', 'data-sortable-draggable' => '.movable', From a9ed7f66a4ccf81df86a9863d23da153362e1b02 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 19 Feb 2019 09:24:30 +0100 Subject: [PATCH 62/99] Breadcrumb: Show a configuration's name again, "Root" is too vague --- library/Businessprocess/Renderer/Breadcrumb.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php index 8b1df41..aeb81b1 100644 --- a/library/Businessprocess/Renderer/Breadcrumb.php +++ b/library/Businessprocess/Renderer/Breadcrumb.php @@ -41,7 +41,7 @@ class Breadcrumb extends BaseHtmlElement ) )); $breadcrumb->add(Html::tag('li')->add( - Html::tag('a', ['href' => $bpUrl], mt('businessprocess', 'Root')) + Html::tag('a', ['href' => $bpUrl], $bp->getTitle()) )); $path = $renderer->getCurrentPath(); From 2799eff860efbc8c49c12b34ee040d08f4b1184c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 07:29:32 +0100 Subject: [PATCH 63/99] lib: Temporarily integrate the StateBall from ipl\Web\Widget --- .../Businessprocess/Renderer/TreeRenderer.php | 4 +-- .../Web/Component/StateBall.php | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 library/Businessprocess/Web/Component/StateBall.php diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 54e6345..d543373 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -3,14 +3,14 @@ namespace Icinga\Module\Businessprocess\Renderer; use Icinga\Date\DateFormatter; -use Icinga\Module\Businessprocess\BpNode; 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\Component\StateBall; use Icinga\Module\Businessprocess\Web\Form\CsrfToken; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; -use ipl\Web\Widget\StateBall; class TreeRenderer extends Renderer { diff --git a/library/Businessprocess/Web/Component/StateBall.php b/library/Businessprocess/Web/Component/StateBall.php new file mode 100644 index 0000000..7090c67 --- /dev/null +++ b/library/Businessprocess/Web/Component/StateBall.php @@ -0,0 +1,32 @@ +defaultAttributes = ['class' => "state-ball state-$state size-$size"]; + } +} From e0529ee8449688d0d164a7702183dd406e774a09 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 08:09:58 +0100 Subject: [PATCH 64/99] Revert "Node: Aggregate parents if none are registered yet" This reverts commit d1f32c59f1c9bf041fa9092cce5a91dc816b9f1d. --- application/controllers/NodeController.php | 9 +++++++++ library/Businessprocess/Node.php | 17 ++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index d557050..1d8fe0a 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -25,6 +25,15 @@ class NodeController extends Controller foreach ($this->storage()->listProcessNames() as $configName) { $config = $this->storage()->loadProcess($configName); + // TODO: Fix issues with children, they do not exist unless resolved :-/ + // This is a workaround: + foreach ($config->getRootNodes() as $node) { + $node->getState(); + } + foreach ($config->getRootNodes() as $node) { + $node->clearState(); + } + if (! $config->hasNode($name)) { continue; } diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 662aca6..11cd69e 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -50,7 +50,7 @@ abstract class Node * * @var array */ - protected $parents; + protected $parents = array(); /** * Node identifier @@ -304,7 +304,7 @@ abstract class Node public function hasParents() { - return count($this->getParents()) > 0; + return count($this->parents) > 0; } public function hasParentName($name) @@ -321,7 +321,7 @@ abstract class Node public function removeParent($name) { $this->parents = array_filter( - $this->getParents(), + $this->parents, function (BpNode $parent) use ($name) { return $parent->getName() !== $name; } @@ -335,15 +335,6 @@ abstract class Node */ public function getParents() { - if ($this->parents === null) { - $this->parents = []; - foreach ($this->bp->getBpNodes() as $name => $node) { - if ($node->hasChild($this->getName())) { - $this->parents[] = $node; - } - } - } - return $this->parents; } @@ -357,7 +348,7 @@ abstract class Node } $paths = array(); - foreach ($this->getParents() as $parent) { + foreach ($this->parents as $parent) { foreach ($parent->getPaths() as $path) { $path[] = $this->getName(); $paths[] = $path; From 8465bc0bc3c9f828201538395e25f71d1faf13a3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 08:11:59 +0100 Subject: [PATCH 65/99] LegacyConfigParser: Establish parent-child relationships This replaces commit d1f32c5 as this is the more efficient and proper solution. refs #134 --- application/controllers/NodeController.php | 9 ---- .../Storage/LegacyConfigParser.php | 44 ++++++++----------- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index 1d8fe0a..d557050 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -25,15 +25,6 @@ class NodeController extends Controller foreach ($this->storage()->listProcessNames() as $configName) { $config = $this->storage()->loadProcess($configName); - // TODO: Fix issues with children, they do not exist unless resolved :-/ - // This is a workaround: - foreach ($config->getRootNodes() as $node) { - $node->getState(); - } - foreach ($config->getRootNodes() as $node) { - $node->clearState(); - } - if (! $config->hasNode($name)) { continue; } diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php index ddcdb29..ca7b170 100644 --- a/library/Businessprocess/Storage/LegacyConfigParser.php +++ b/library/Businessprocess/Storage/LegacyConfigParser.php @@ -298,48 +298,42 @@ class LegacyConfigParser // New feature: $minWarn = $m[2]; $value = $m[3]; } - $cmps = preg_split('~\s*\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY); - $childNames = array(); + $node = new BpNode((object) array( + 'name' => $name, + 'operator' => $op_name, + 'child_names' => [] + )); + $node->setBpConfig($bp); + + $cmps = preg_split('~\s*\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY); foreach ($cmps as $val) { if (strpos($val, ';') !== false) { if ($bp->hasNode($val)) { - $childNames[] = $val; - continue; - } - - list($host, $service) = preg_split('~;~', $val, 2); - if ($service === 'Hoststatus') { - $bp->createHost($host); + $node->addChild($bp->getNode($val)); } else { - $bp->createService($host, $service); + list($host, $service) = preg_split('~;~', $val, 2); + if ($service === 'Hoststatus') { + $node->addChild($bp->createHost($host)); + } else { + $node->addChild($bp->createService($host, $service)); + } } - } - if ($val[0] === '@') { + } elseif ($val[0] === '@') { if (strpos($val, ':') === false) { throw new ConfigurationError( "I'm unable to import full external configs, a node needs to be provided for '%s'", $val ); - // TODO: this might work: - // $node = $bp->createImportedNode(substr($val, 1)); } else { list($config, $nodeName) = preg_split('~:\s*~', substr($val, 1), 2); - $node = $bp->createImportedNode($config, $nodeName); + $node->addChild($bp->createImportedNode($config, $nodeName)); } - $val = $node->getName(); + } else { + $node->addChild($bp->getNode($val)); } - - $childNames[] = $val; } - $node = new BpNode((object) array( - 'name' => $name, - 'operator' => $op_name, - 'child_names' => $childNames - )); - $node->setBpConfig($bp); - $bp->addNode($name, $node); } From 431a5e00852a44c4a0ad4828ca12ea352a4bb0f3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 11:32:32 +0100 Subject: [PATCH 66/99] Streamline usage of a node's name --- application/forms/AddNodeForm.php | 2 +- application/forms/EditNodeForm.php | 2 +- library/Businessprocess/BpNode.php | 8 ++++---- .../Modification/NodeCreateAction.php | 2 +- library/Businessprocess/Renderer/Breadcrumb.php | 2 +- library/Businessprocess/Renderer/TileRenderer.php | 5 ++--- .../Renderer/TileRenderer/NodeTile.php | 15 +++++++-------- library/Businessprocess/Renderer/TreeRenderer.php | 6 +++--- test/php/library/Businessprocess/HostNodeTest.php | 2 +- 9 files changed, 21 insertions(+), 23 deletions(-) diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index ad24cc0..2c0c36c 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -425,7 +425,7 @@ class AddNodeForm extends QuickForm $name = '@' . $file . ':' . $name; } - $list[$name] = (string) $node; // display name? + $list[$name] = $node->getName(); // display name? } } diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php index f1fd29c..02f9337 100644 --- a/application/forms/EditNodeForm.php +++ b/application/forms/EditNodeForm.php @@ -359,7 +359,7 @@ class EditNodeForm extends QuickForm foreach ($this->bp->getNodes() as $node) { if ($node instanceof BpNode && ! isset($parents[$node->getName()])) { - $list[(string) $node] = (string) $node; // display name? + $list[$node->getName()] = $node->getName(); // display name? } } diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index c2a8b4a..643af1b 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -160,7 +160,7 @@ class BpNode extends Node $tree = array(); foreach ($this->getProblematicChildren() as $child) { - $name = (string) $child; + $name = $child->getName(); $tree[$name] = array( 'node' => $child, 'children' => array() @@ -197,11 +197,11 @@ class BpNode extends Node foreach ($this->getChildren() as $child) { if ($child->isMissing()) { - $missing[(string) $child] = $child; + $missing[$child->getName()] = $child; } foreach ($child->getMissingChildren() as $m) { - $missing[(string) $m] = $m; + $missing[$m->getName()] = $m; } } @@ -313,7 +313,7 @@ class BpNode extends Node public function getHtmlId() { - return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', (string) $this); + return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName()); } protected function invertSortingState($state) diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php index aa9162e..167d3bc 100644 --- a/library/Businessprocess/Modification/NodeCreateAction.php +++ b/library/Businessprocess/Modification/NodeCreateAction.php @@ -22,7 +22,7 @@ class NodeCreateAction extends NodeAction */ public function setParent(Node $name) { - $this->parentName = (string) $name; + $this->parentName = $name->getName(); } /** diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php index aeb81b1..d848992 100644 --- a/library/Businessprocess/Renderer/Breadcrumb.php +++ b/library/Businessprocess/Renderer/Breadcrumb.php @@ -69,7 +69,7 @@ class Breadcrumb extends BaseHtmlElement // TODO: something more generic than NodeTile? $renderer = clone($renderer); $renderer->lock()->setIsBreadcrumb(); - $p = new NodeTile($renderer, (string) $node, $node, $path); + $p = new NodeTile($renderer, $node, $path); $p->setTag('li'); return $p; } diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index db66cb3..c77aa9f 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -35,14 +35,13 @@ class TileRenderer extends Renderer $path = $this->getCurrentPath(); foreach ($nodes as $name => $node) { - $this->add(new NodeTile($this, $name, $node, $path)); + $this->add(new NodeTile($this, $node, $path)); } if ($this->wantsRootNodes()) { $unbound = $this->createUnboundParent($bp); if ($unbound->hasChildren()) { - $name = $unbound->getName(); - $this->add(new NodeTile($this, $name, $unbound)); + $this->add(new NodeTile($this, $unbound)); } } diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 91f9bc6..9535da2 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -36,10 +36,9 @@ class NodeTile extends BaseHtmlElement * @param Node $node * @param null $path */ - public function __construct(Renderer $renderer, $name, Node $node, $path = null) + public function __construct(Renderer $renderer, Node $node, $path = null) { $this->renderer = $renderer; - $this->name = $name; $this->node = $node; $this->path = $path; } @@ -72,9 +71,9 @@ class NodeTile extends BaseHtmlElement $attributes = $this->getAttributes(); $attributes->add('class', $renderer->getNodeClasses($node)); - $attributes->add('id', 'bp-' . (string) $node); + $attributes->add('id', 'bp-' . $node->getName()); if (! $renderer->isLocked()) { - $attributes->add('data-node-name', (string) $node); + $attributes->add('data-node-name', $node->getName()); } if (! $renderer->isBreadcrumb()) { @@ -260,7 +259,7 @@ class NodeTile extends BaseHtmlElement [ 'href' => $renderer->getUrl() ->with('action', 'simulation') - ->with('simulationnode', $this->name), + ->with('simulationnode', $this->node->getName()), 'title' => mt( 'businessprocess', 'Show the business impact of this node by simulating a specific state' @@ -274,7 +273,7 @@ class NodeTile extends BaseHtmlElement [ 'href' => $renderer->getUrl() ->with('action', 'editmonitored') - ->with('editmonitorednode', $node->getName()), + ->with('editmonitorednode', $this->node->getName()), 'title' => mt('businessprocess', 'Modify this monitored node') ], Html::tag('i', ['class' => 'icon icon-edit']) @@ -282,7 +281,7 @@ class NodeTile extends BaseHtmlElement } if (! $this->renderer->getBusinessProcess()->getMetadata()->canModify() - || $node->getName() === '__unbound__' + || $this->node->getName() === '__unbound__' ) { return; } @@ -293,7 +292,7 @@ class NodeTile extends BaseHtmlElement [ 'href' => $renderer->getUrl() ->with('action', 'edit') - ->with('editnode', $node->getName()), + ->with('editnode', $this->node->getName()), 'title' => mt('businessprocess', 'Modify this business process node') ], Html::tag('i', ['class' => 'icon icon-edit']) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index d543373..5dce546 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -75,7 +75,7 @@ class TreeRenderer extends Renderer */ protected function getId(Node $node, $path) { - return md5(implode(';', $path) . (string) $node); + return md5(implode(';', $path) . $node->getName()); } protected function getStateClassNames(Node $node) @@ -192,7 +192,7 @@ class TreeRenderer extends Renderer 'config' => $node->getBpConfig()->getName(), 'node' => $node instanceof ImportedNode ? $node->getNodeName() - : (string) $node + : $node->getName() ]) ->getAbsoluteUrl() ]); @@ -215,7 +215,7 @@ class TreeRenderer extends Renderer $li = Html::tag('li', [ 'class' => 'movable', 'id' => $this->getId($node, $path ?: []), - 'data-node-name' => (string) $node + 'data-node-name' => $node->getName() ]); $li->add($this->getNodeIcons($node, $path)); diff --git a/test/php/library/Businessprocess/HostNodeTest.php b/test/php/library/Businessprocess/HostNodeTest.php index ff23476..069f432 100644 --- a/test/php/library/Businessprocess/HostNodeTest.php +++ b/test/php/library/Businessprocess/HostNodeTest.php @@ -20,7 +20,7 @@ class HostNodeTest extends BaseTestCase { $this->assertEquals( 'localhost;Hoststatus', - (string) $this->localhost() + $this->localhost()->getName() ); } From 42642c88d64111928b3578b7c45c2a7751e51a9c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 11:35:16 +0100 Subject: [PATCH 67/99] BpConfig: Flag imported configurations --- library/Businessprocess/BpConfig.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 7295508..cfa5268 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -3,7 +3,6 @@ namespace Icinga\Module\Businessprocess; use Exception; -use LogicException; use Icinga\Application\Config; use Icinga\Exception\IcingaException; use Icinga\Exception\NotFoundError; @@ -89,6 +88,13 @@ class BpConfig */ protected $root_nodes = array(); + /** + * Whether this configuration has been imported + * + * @var bool + */ + protected $imported = false; + /** * Imported nodes * @@ -552,6 +558,17 @@ class BpConfig return $missing; } + public function setImported($state = true) + { + $this->imported = (bool) $state; + return $this; + } + + public function isImported() + { + return $this->imported; + } + public function createImportedNode($config, $name = null) { $params = (object) array('configName' => $config); @@ -569,6 +586,7 @@ class BpConfig { if (! isset($this->importedConfigs[$name])) { $import = $this->storage()->loadProcess($name); + $import->setImported(); if ($this->usesSoftStates()) { $import->useSoftStates(); From f0162278d6350c5efcd3e7ff6dad8191890a4f3e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 11:36:30 +0100 Subject: [PATCH 68/99] BpConfig: Process node imports in hasNode() and getNode() --- library/Businessprocess/BpConfig.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index cfa5268..aafabd7 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -454,6 +454,9 @@ class BpConfig { if (array_key_exists($name, $this->nodes)) { return true; + } elseif ($name[0] === '@') { + list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2); + return $this->getImportedConfig($configName)->hasNode($nodeName); } return false; @@ -645,6 +648,11 @@ class BpConfig return $this->nodes[$name]; } + if ($name[0] === '@') { + list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2); + return $this->getImportedConfig($configName)->getNode($nodeName); + } + // Fallback: if it is a service, create an empty one: $this->warn(sprintf('The node "%s" doesn\'t exist', $name)); $pos = strpos($name, ';'); From c19854d05cd921c689c8830e8d8a033d4bf7ece4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 11:45:45 +0100 Subject: [PATCH 69/99] TileRenderer: Make the navigation through imported nodes fluent --- library/Businessprocess/ImportedNode.php | 5 ++ library/Businessprocess/Node.php | 22 ++++++--- library/Businessprocess/Renderer/Renderer.php | 4 +- .../Renderer/TileRenderer/NodeTile.php | 48 ++----------------- .../Businessprocess/Renderer/TreeRenderer.php | 2 +- 5 files changed, 29 insertions(+), 52 deletions(-) diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php index cf40346..3f0b460 100644 --- a/library/Businessprocess/ImportedNode.php +++ b/library/Businessprocess/ImportedNode.php @@ -53,6 +53,11 @@ class ImportedNode extends BpNode return $this->nodeName; } + public function getIdentifier() + { + return $this->getName(); + } + public function getBpConfig() { if ($this->bp === null) { diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 11cd69e..f796d77 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -343,18 +343,18 @@ abstract class Node */ public function getPaths() { - if ($this->getBpConfig()->hasRootNode($this->getName())) { - return array(array($this->getName())); - } - - $paths = array(); + $paths = []; foreach ($this->parents as $parent) { foreach ($parent->getPaths() as $path) { - $path[] = $this->getName(); + $path[] = $this->getIdentifier(); $paths[] = $path; } } + if (! $this instanceof ImportedNode && $this->getBpConfig()->hasRootNode($this->getName())) { + $paths[] = [$this->getIdentifier()]; + } + return $paths; } @@ -408,6 +408,16 @@ abstract class Node return $this->name; } + public function getIdentifier() + { + $prefix = ''; + if ($this->getBpConfig()->isImported()) { + $prefix = '@' . $this->getBpConfig()->getName() . ':'; + } + + return $prefix . $this->getName(); + } + public function __toString() { return $this->getName(); diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index 08898e4..431272e 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -194,7 +194,7 @@ abstract class Renderer extends HtmlDocument } /** - * @return string|null + * @return array */ public function getPath() { @@ -205,7 +205,7 @@ abstract class Renderer extends HtmlDocument { $path = $this->getPath(); if ($this->rendersSubNode()) { - $path[] = (string) $this->parent; + $path[] = $this->parent->getIdentifier(); } return $path; } diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 9535da2..18dc7c4 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -117,21 +117,12 @@ class NodeTile extends BaseHtmlElement protected function buildBaseNodeUrl(Node $node) { - $path = $this->path; - $renderer = $this->renderer; + $url = $this->renderer->getBaseUrl(); - $params = [ - 'config' => $node->getBpConfig()->getName(), - 'node' => $node instanceof ImportedNode - ? $node->getNodeName() - : $this->name - ]; - - $url = $renderer->getBaseUrl(); $p = $url->getParams(); - $p->mergeValues($params); - if (! empty($path) && !$node instanceof ImportedNode) { - $p->addValues('path', $path); + $p->set('node', $node->getIdentifier()); + if (! empty($this->path)) { + $p->addValues('path', $this->path); } return $url; @@ -142,31 +133,6 @@ class NodeTile extends BaseHtmlElement return $this->buildBaseNodeUrl($node); } - protected function makeMonitoredNodeUrl(MonitoredNode $node) - { - $path = $this->path; - $name = $this->name; // TODO: ?? - $renderer = $this->renderer; - - $bp = $renderer->getBusinessProcess(); - $params = array( - 'config' => $bp->getName() - ); - - if ($name !== null) { - $params['node'] = $node->getName(); - } - - $url = $renderer->getBaseUrl(); - $p = $url->getParams(); - $p->mergeValues($params); - if (! empty($path)) { - $p->addValues('path', $path); - } - - return $url; - } - /** * @return BaseHtmlElement */ @@ -180,11 +146,7 @@ class NodeTile extends BaseHtmlElement $link = Html::tag('a', ['href' => $url, 'data-base-target' => '_next'], $node->getHostname()); } else { $link = Html::tag('a', ['href' => $url], $node->getAlias()); - if ($node->getBpConfig()->getName() !== $this->renderer->getBusinessProcess()->getName()) { - $link->getAttributes()->add('data-base-target', '_next'); - } else { - $link->getAttributes()->add('data-base-target', '_self'); - } + $link->getAttributes()->add('data-base-target', '_self'); } return $link; diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 5dce546..ca82b54 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -198,7 +198,7 @@ class TreeRenderer extends Renderer ]); $li->add($ul); - $path[] = (string) $node; + $path[] = $node->getIdentifier(); foreach ($node->getChildren() as $name => $child) { if ($child instanceof BpNode) { $ul->add($this->renderNode($bp, $child, $path)); From 6cdaa48f688a977b935f2379a3bb0194eebdc0f5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 12:29:35 +0100 Subject: [PATCH 70/99] TileRenderer: Use the correct action urls for imported nodes --- .../Businessprocess/Renderer/TileRenderer.php | 22 +++++++-- .../Renderer/TileRenderer/NodeTile.php | 47 ++++++++++++++----- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index c77aa9f..ad938f6 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Businessprocess\Renderer; +use Icinga\Module\Businessprocess\ImportedNode; use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile; use Icinga\Module\Businessprocess\Web\Form\CsrfToken; use ipl\Html\Html; @@ -23,12 +24,25 @@ class TileRenderer extends Renderer 'data-sortable-data-id-attr' => 'id', 'data-sortable-filter' => '.addnew', 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists - 'data-csrf-token' => CsrfToken::generate(), - 'data-action-url' => $this->getUrl()->getAbsoluteUrl() + 'data-csrf-token' => CsrfToken::generate() ] ); - if (! $this->wantsRootNodes()) { - $nodesDiv->getAttributes()->add('data-node-name', $this->parent->getName()); + + if ($this->wantsRootNodes()) { + $nodesDiv->getAttributes()->add('data-action-url', $this->getUrl()->getAbsoluteUrl()); + } else { + $nodeName = $this->parent instanceof ImportedNode + ? $this->parent->getNodeName() + : $this->parent->getName(); + $nodesDiv->getAttributes() + ->add('data-node-name', $nodeName) + ->add('data-action-url', $this->getUrl() + ->without('path') + ->overwriteParams([ + 'config' => $this->parent->getBpConfig()->getName(), + 'node' => $nodeName + ]) + ->getAbsoluteUrl()); } $nodes = $this->getChildNodes(); diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 18dc7c4..1fa0ca0 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -9,6 +9,7 @@ use Icinga\Module\Businessprocess\MonitoredNode; use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\Renderer\Renderer; use Icinga\Module\Businessprocess\ServiceNode; +use Icinga\Web\Url; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; @@ -213,13 +214,27 @@ class NodeTile extends BaseHtmlElement protected function addActionLinks() { - $node = $this->node; - $renderer = $this->renderer; - if ($node instanceof MonitoredNode) { + $parent = $this->renderer->getParentNode(); + if ($parent !== null) { + $baseUrl = Url::fromPath('businessprocess/process/show', [ + 'config' => $parent->getBpConfig()->getName(), + 'node' => $parent instanceof ImportedNode + ? $parent->getNodeName() + : $parent->getName(), + 'unlocked' => true + ]); + } else { + $baseUrl = Url::fromPath('businessprocess/process/show', [ + 'config' => $this->node->getBpConfig()->getName(), + 'unlocked' => true + ]); + } + + if ($this->node instanceof MonitoredNode) { $this->actions()->add(Html::tag( 'a', [ - 'href' => $renderer->getUrl() + 'href' => $baseUrl ->with('action', 'simulation') ->with('simulationnode', $this->node->getName()), 'title' => mt( @@ -233,7 +248,7 @@ class NodeTile extends BaseHtmlElement $this->actions()->add(Html::tag( 'a', [ - 'href' => $renderer->getUrl() + 'href' => $baseUrl ->with('action', 'editmonitored') ->with('editmonitorednode', $this->node->getName()), 'title' => mt('businessprocess', 'Modify this monitored node') @@ -248,11 +263,11 @@ class NodeTile extends BaseHtmlElement return; } - if ($node instanceof BpNode) { + if ($this->node instanceof BpNode) { $this->actions()->add(Html::tag( 'a', [ - 'href' => $renderer->getUrl() + 'href' => $baseUrl ->with('action', 'edit') ->with('editnode', $this->node->getName()), 'title' => mt('businessprocess', 'Modify this business process node') @@ -263,10 +278,16 @@ class NodeTile extends BaseHtmlElement $this->actions()->add(Html::tag( 'a', [ - 'href' => $renderer->getUrl()->with([ - 'action' => 'add', - 'node' => $node->getName() - ]), + 'href' => $this->node instanceof ImportedNode + ? $baseUrl->with([ + 'config' => $this->node->getConfigName(), + 'node' => $this->node->getNodeName(), + 'action' => 'add' + ]) + : $baseUrl->with([ + 'node' => $this->node->getName(), + 'action' => 'add' + ]), 'title' => mt('businessprocess', 'Add a new sub-node to this business process') ], Html::tag('i', ['class' => 'icon icon-plus']) @@ -275,13 +296,13 @@ class NodeTile extends BaseHtmlElement $params = array( 'action' => 'delete', - 'deletenode' => $node->getName(), + 'deletenode' => $this->node->getName(), ); $this->actions()->add(Html::tag( 'a', [ - 'href' => $renderer->getUrl()->with($params), + 'href' => $baseUrl->with($params), 'title' => mt('businessprocess', 'Delete this node') ], Html::tag('i', ['class' => 'icon icon-cancel']) From 718a4305b4dca97ac9eb53f8ce78a2e68959b937 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 12:34:07 +0100 Subject: [PATCH 71/99] NodeController: Show all processes affected by an impact refs #143 --- application/controllers/NodeController.php | 65 ++++++++++++++++++---- library/Businessprocess/BpConfig.php | 5 ++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index d557050..d04a80c 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -25,23 +25,66 @@ class NodeController extends Controller foreach ($this->storage()->listProcessNames() as $configName) { $config = $this->storage()->loadProcess($configName); - if (! $config->hasNode($name)) { + $parents = []; + if ($config->hasNode($name)) { + foreach ($config->getNode($name)->getPaths() as $path) { + array_pop($path); // Remove the monitored node + $immediateParentName = array_pop($path); // The directly affected process + $parents[] = [$config->getNode($immediateParentName), $path]; + } + } + + $askedConfigs = []; + foreach ($config->getImportedNodes() as $importedNode) { + $importedConfig = $importedNode->getBpConfig(); + + if (isset($askedConfigs[$importedConfig->getName()])) { + continue; + } else { + $askedConfigs[$importedConfig->getName()] = true; + } + + if ($importedConfig->hasNode($name)) { + $node = $importedConfig->getNode($name); + $nativePaths = $node->getPaths(); + + do { + $path = array_pop($nativePaths); + $importedNodePos = array_search($importedNode->getIdentifier(), $path, true); + if ($importedNodePos !== false) { + array_pop($path); // Remove the monitored node + $immediateParentName = array_pop($path); // The directly affected process + $importedPath = array_slice($path, $importedNodePos + 1); + foreach ($importedNode->getPaths() as $targetPath) { + if ($targetPath[count($targetPath) - 1] === $immediateParentName) { + array_pop($targetPath); + $parent = $importedNode; + } else { + $parent = $importedConfig->getNode($immediateParentName); + } + + $parents[] = [$parent, array_merge($targetPath, $importedPath)]; + } + + // We may get multiple native paths. Though, the right hand of the path is everywhere the + // same and the left hand not of any interest since that's where the import location is. + break; // So, once we've found a match, we're done here (Otherwise we'll get duplicates) + } + } while (! empty($nativePaths)); + } + } + + if (empty($parents)) { continue; } MonitoringState::apply($config); $config->applySimulation($simulation); - foreach ($config->getNode($name)->getPaths() as $path) { - array_pop($path); - $node = array_pop($path); - $renderer = new TileRenderer($config, $config->getNode($node)); - $renderer->setUrl( - Url::fromPath( - 'businessprocess/process/show', - array('config' => $configName) - ) - )->setPath($path); + foreach ($parents as $parentAndPath) { + $renderer = (new TileRenderer($config, array_shift($parentAndPath))) + ->setUrl(Url::fromPath('businessprocess/process/show', ['config' => $configName])) + ->setPath(array_shift($parentAndPath)); $bc = Breadcrumb::create($renderer); $bc->getAttributes()->set('data-base-target', '_next'); diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index aafabd7..cfe2700 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -585,6 +585,11 @@ class BpConfig return $node; } + public function getImportedNodes() + { + return $this->importedNodes; + } + public function getImportedConfig($name) { if (! isset($this->importedConfigs[$name])) { From ff2274c048827d2ffdf50750a8019b8929bcf892 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 Feb 2019 14:01:29 +0100 Subject: [PATCH 72/99] node/impact: Fix that a process' state is not displayed --- application/controllers/NodeController.php | 4 +- public/css/module.less | 58 ++++++++++++---------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index d04a80c..fa20e8d 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -87,7 +87,9 @@ class NodeController extends Controller ->setPath(array_shift($parentAndPath)); $bc = Breadcrumb::create($renderer); - $bc->getAttributes()->set('data-base-target', '_next'); + $bc->getAttributes() + ->set('data-base-target', '_next') + ->add('class', 'with-states'); $content->add($bc); } } diff --git a/public/css/module.less b/public/css/module.less index ff2bf09..f40dec2 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -537,26 +537,6 @@ td > a > .badges { padding: 0; } -/*.breadcrumb { - > .critical a { background: @colorCritical; } - > .critical.handled a { background: @colorCriticalHandled; } - > .unknown a { background: @colorUnknown; } - > .unknown.handled a { background: @colorUnknownHandled; } - > .warning a { background: @colorWarning; } - > .warning.handled a { background: @colorWarningHandled; } - > .ok a { background: @colorOk; } -} - -.breadcrumb { - > .critical a:after { border-left-color: @colorCritical; } - > .critical.handled a:after { border-left-color: @colorCriticalHandled; } - > .unknown a:after { border-left-color: @colorUnknown; } - > .unknown.handled a:after { border-left-color: @colorUnknownHandled; } - > .warning a:after { border-left-color: @colorWarning; } - > .warning.handled a:after { border-left-color: @colorWarningHandled; } - > .ok a:after { border-left-color: @colorOk; } -}*/ - .breadcrumb:after { content:''; display:block; @@ -603,6 +583,35 @@ td > a > .badges { } } } +.breadcrumb.with-states li { + &.critical { background: @colorCritical; } + &.critical.handled { background: @colorCriticalHandled; } + &.unknown { background: @colorUnknown; } + &.unknown.handled { background: @colorUnknownHandled; } + &.warning { background: @colorWarning; } + &.warning.handled { background: @colorWarningHandled; } + &.ok { background: @colorOk; } + + &.process-node a { color: white; } +} +.breadcrumb.with-states li:not(:last-of-type) { + &.critical a:after { border-left-color: @colorCritical; } + &.critical.handled a:after { border-left-color: @colorCriticalHandled; } + &.unknown a:after { border-left-color: @colorUnknown; } + &.unknown.handled a:after { border-left-color: @colorUnknownHandled; } + &.warning a:after { border-left-color: @colorWarning; } + &.warning.handled a:after { border-left-color: @colorWarningHandled; } + &.ok a:after { border-left-color: @colorOk; } +} +.breadcrumb.with-states li:last-of-type { + &.critical { border-color: @colorCritical; } + &.critical.handled { border-color: @colorCriticalHandled; } + &.unknown { border-color: @colorUnknown; } + &.unknown.handled { border-color: @colorUnknownHandled; } + &.warning { border-color: @colorWarning; } + &.warning.handled { border-color: @colorWarningHandled; } + &.ok { border-color: @colorOk; } +} .breadcrumb li:not(:last-of-type) a:before, .breadcrumb li:not(:last-of-type) a:after { content: " "; @@ -632,15 +641,10 @@ td > a > .badges { padding-left: 1em; padding-right: 0.5em; } -.breadcrumb li:last-child a { - cursor: default; -} -.breadcrumb li:last-child a:hover { - -} .breadcrumb li:not(:last-child) a:hover { background: @icinga-blue; color: white; } -.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @icinga-blue; } +.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @icinga-blue !important; } +.breadcrumb li:last-child:hover, .breadcrumb li:last-child a:hover { background: @icinga-blue; border-color: @icinga-blue !important; } .breadcrumb li a:focus { text-decoration: underline; From 673135c0f3e6b40c700d178803b5cb9e07efe7d1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 09:12:07 +0100 Subject: [PATCH 73/99] Don't redirect after a successful move action but refresh --- application/controllers/ProcessController.php | 11 -------- application/forms/MoveNodeForm.php | 19 ++++++++++--- .../Businessprocess/Renderer/TileRenderer.php | 8 ++++-- .../Businessprocess/Renderer/TreeRenderer.php | 28 +++++++++++++++---- public/js/module.js | 15 ++++++++-- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 17c5622..c3e7071 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -251,22 +251,11 @@ class ProcessController extends Controller ->setSimulation(Simulation::fromSession($this->session())) ->handleRequest(); } elseif ($action === 'move') { - $url = Url::fromPath('businessprocess/process/show', ['config' => $bp->getName()]); - if ($this->url()->hasParam('unlocked')) { - $url->setParam('unlocked', $this->url()->getParam('unlocked')); - } - if ($this->url()->hasParam('mode')) { - $url->setParam('mode', $this->url()->getParam('mode')); - } elseif ($this->url()->hasParam('node')) { - $url->setParam('node', $this->url()->getParam('node')); - } - $form = $this->loadForm('MoveNode') ->setProcess($bp) ->setParentNode($node) ->setSession($this->session()) ->setNode($bp->getNode($this->params->get('movenode'))) - ->setSuccessUrl($url) ->handleRequest(); } diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php index 5ec2ed9..8e77f87 100644 --- a/application/forms/MoveNodeForm.php +++ b/application/forms/MoveNodeForm.php @@ -11,6 +11,7 @@ use Icinga\Module\Businessprocess\Modification\ProcessChanges; use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\Web\Form\CsrfToken; use Icinga\Module\Businessprocess\Web\Form\QuickForm; +use Icinga\Web\Session; use Icinga\Web\Session\SessionNamespace; class MoveNodeForm extends QuickForm @@ -156,15 +157,25 @@ class MoveNodeForm extends QuickForm } catch (ModificationError $e) { $this->notifyError($e->getMessage()); Icinga::app()->getResponse() - ->setHttpResponseCode(400) - ->redirectAndExit($this->getSuccessUrl()); + // Web 2's JS forces a content update for non-200s. Our own JS + // can't prevent this, hence we're not making this a 400 :( + //->setHttpResponseCode(400) + ->setHeader('X-Icinga-Container', 'ignore') + ->sendResponse(); + exit; } // Trigger session destruction to make sure it get's stored. unset($changes); - $this->setSuccessMessage($this->translate('Node order updated')); - parent::onSuccess(); + $this->notifySuccess($this->getSuccessMessage($this->translate('Node order updated'))); + + $response = $this->getRequest()->getResponse() + ->setHeader('X-Icinga-Container', 'ignore'); + + Session::getSession()->write(); + $response->sendResponse(); + exit; } public function hasBeenSent() diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index ad938f6..58cd54a 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -29,7 +29,10 @@ class TileRenderer extends Renderer ); if ($this->wantsRootNodes()) { - $nodesDiv->getAttributes()->add('data-action-url', $this->getUrl()->getAbsoluteUrl()); + $nodesDiv->getAttributes()->add( + 'data-action-url', + $this->getUrl()->setParams(['config' => $bp->getName()])->getAbsoluteUrl() + ); } else { $nodeName = $this->parent instanceof ImportedNode ? $this->parent->getNodeName() @@ -37,8 +40,7 @@ class TileRenderer extends Renderer $nodesDiv->getAttributes() ->add('data-node-name', $nodeName) ->add('data-action-url', $this->getUrl() - ->without('path') - ->overwriteParams([ + ->setParams([ 'config' => $this->parent->getBpConfig()->getName(), 'node' => $nodeName ]) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index ca82b54..f432db8 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -21,7 +21,7 @@ class TreeRenderer extends Renderer { $bp = $this->config; $htmlId = $bp->getHtmlId(); - $this->add(Html::tag( + $tree = Html::tag( 'ul', [ 'id' => $htmlId, @@ -35,12 +35,30 @@ class TreeRenderer extends Renderer ]), 'data-sortable-invert-swap' => 'true', 'data-is-root-config' => $this->wantsRootNodes() ? 'true' : 'false', - 'data-csrf-token' => CsrfToken::generate(), - 'data-action-url' => $this->getUrl()->getAbsoluteUrl() + 'data-csrf-token' => CsrfToken::generate() ], $this->renderBp($bp) - )); + ); + if ($this->wantsRootNodes()) { + $tree->getAttributes()->add( + 'data-action-url', + $this->getUrl()->setParams(['config' => $bp->getName()])->getAbsoluteUrl() + ); + } else { + $nodeName = $this->parent instanceof ImportedNode + ? $this->parent->getNodeName() + : $this->parent->getName(); + $tree->getAttributes() + ->add('data-node-name', $nodeName) + ->add('data-action-url', $this->getUrl() + ->setParams([ + 'config' => $this->parent->getBpConfig()->getName(), + 'node' => $nodeName + ]) + ->getAbsoluteUrl()); + } + $this->add($tree); return parent::render(); } @@ -188,7 +206,7 @@ class TreeRenderer extends Renderer ]), 'data-csrf-token' => CsrfToken::generate(), 'data-action-url' => $this->getUrl() - ->overwriteParams([ + ->setParams([ 'config' => $node->getBpConfig()->getName(), 'node' => $node instanceof ImportedNode ? $node->getNodeName() diff --git a/public/js/module.js b/public/js/module.js index e7c6e1c..97a51f7 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -110,7 +110,12 @@ to: evt.newIndex }; - icinga.loader.loadUrl(actionUrl, $source.closest('.container'), data, 'POST'); + var $container = $source.closest('.container'); + var req = icinga.loader.loadUrl(actionUrl, $container, data, 'POST'); + req.complete(function (req, textStatus) { + icinga.loader.loadUrl( + $container.data('icingaUrl'), $container, undefined, undefined, undefined, true); + }); } }, @@ -139,9 +144,13 @@ var actionUrl = icinga.utils.addUrlParams($source.data('actionUrl'), { action: 'move', movenode: $(evt.item).data('nodeName') - }); - icinga.loader.loadUrl(actionUrl, $target.closest('.container'), data, 'POST'); + var $container = $target.closest('.container'); + var req = icinga.loader.loadUrl(actionUrl, $container, data, 'POST'); + req.complete(function (req, textStatus) { + icinga.loader.loadUrl( + $container.data('icingaUrl'), $container, undefined, undefined, undefined, true); + }); event.stopPropagation(); } }, From c73cd65f6fd90443fa804d547a9a3410faa000a5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 09:14:04 +0100 Subject: [PATCH 74/99] js: Correctly identify a node's parent in case of first level sub-trees --- library/Businessprocess/Renderer/TreeRenderer.php | 2 +- public/js/module.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index f432db8..66e65e4 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -25,7 +25,7 @@ class TreeRenderer extends Renderer 'ul', [ 'id' => $htmlId, - 'class' => ['bp', 'sortable'], + 'class' => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'], 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', 'data-sortable-data-id-attr' => 'id', 'data-sortable-direction' => 'vertical', diff --git a/public/js/module.js b/public/js/module.js index 97a51f7..ec14cd0 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -136,7 +136,7 @@ var data = { csrfToken: $target.data('csrfToken'), movenode: 'movenode', // That's the submit button.. - parent: $target.parent('.process').data('nodeName') || '', + parent: $target.closest('.process').data('nodeName') || '', from: evt.oldIndex, to: evt.newIndex }; From 728d4cb2ae2b0cdaaf4c587e77415aa55d48f9db Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 09:14:54 +0100 Subject: [PATCH 75/99] TreeRenderer: Show the correct icon for monitored nodes at the root level --- library/Businessprocess/Renderer/TreeRenderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 66e65e4..c890368 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -121,7 +121,7 @@ class TreeRenderer extends Renderer public function getNodeIcons(Node $node, array $path = null) { $icons = []; - if (empty($path)) { + if (empty($path) && $node instanceof BpNode) { $icons[] = Html::tag('i', ['class' => 'icon icon-sitemap']); } else { $icons[] = $node->getIcon(); From 4995dc39d01ecc55e016393b6818e28f369dbf32 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 09:16:30 +0100 Subject: [PATCH 76/99] js: Don't use icinga.utils.addUrlParams, that's broken and not necessary here broken? Try to keep sequenced parameters when adding different ones. Good luck. --- public/js/module.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/public/js/module.js b/public/js/module.js index ec14cd0..39de745 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -91,10 +91,11 @@ var evt = event.originalEvent; if (evt.oldIndex !== evt.newIndex) { var $source = $(evt.from); - var actionUrl = icinga.utils.addUrlParams($source.data('actionUrl'), { - action: 'move', - movenode: $(evt.item).data('nodeName') - }); + var actionUrl = [ + $source.data('actionUrl'), + 'action=move', + 'movenode=' + $(evt.item).data('nodeName') + ].join('&'); if (! $source.is('.few') && $('.addnew', $source).length === 2) { // This assumes we're not moving things between different lists @@ -141,9 +142,11 @@ to: evt.newIndex }; - var actionUrl = icinga.utils.addUrlParams($source.data('actionUrl'), { - action: 'move', - movenode: $(evt.item).data('nodeName') + var actionUrl = [ + $source.data('actionUrl'), + 'action=move', + 'movenode=' + $(evt.item).data('nodeName') + ].join('&'); var $container = $target.closest('.container'); var req = icinga.loader.loadUrl(actionUrl, $container, data, 'POST'); From a1bb91a99981b962b6d448df76f4c10aad0fed6f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 09:18:16 +0100 Subject: [PATCH 77/99] js: Remove redundant handling for non-node tiles --- library/Businessprocess/Renderer/TileRenderer.php | 1 - public/js/module.js | 6 ------ 2 files changed, 7 deletions(-) diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index 58cd54a..4b85732 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -22,7 +22,6 @@ class TileRenderer extends Renderer 'data-base-target' => '_next', 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', 'data-sortable-data-id-attr' => 'id', - 'data-sortable-filter' => '.addnew', 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists 'data-csrf-token' => CsrfToken::generate() ] diff --git a/public/js/module.js b/public/js/module.js index 39de745..c38db67 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -97,12 +97,6 @@ 'movenode=' + $(evt.item).data('nodeName') ].join('&'); - if (! $source.is('.few') && $('.addnew', $source).length === 2) { - // This assumes we're not moving things between different lists - evt.oldIndex -= 1; - evt.newIndex -= 1; - } - var data = { csrfToken: $source.data('csrfToken'), movenode: 'movenode', // That's the submit button.. From e36e918d324965fe34f0fc1e74ef970d3c28140c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 09:28:30 +0100 Subject: [PATCH 78/99] RenderedProcessActionBar: Use term "Node" instead of "Process" A process is also a node but a node not necessarily a process. --- .../Businessprocess/Web/Component/RenderedProcessActionBar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php index 8de8ac0..4d0c21d 100644 --- a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -100,7 +100,7 @@ class RenderedProcessActionBar extends ActionBar 'title' => mt('businessprocess', 'Add a new business process node'), 'class' => 'icon-plus button-link' ], - mt('businessprocess', 'Add Process') + mt('businessprocess', 'Add Node') )); } } From 1529ec46027993e4d8c61b2865dd143bfc664694 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 09:42:17 +0100 Subject: [PATCH 79/99] NodeTile: Hash ids the same way as in the tree view --- library/Businessprocess/Renderer/Renderer.php | 12 ++++++++++-- .../Renderer/TileRenderer/NodeTile.php | 2 +- library/Businessprocess/Renderer/TreeRenderer.php | 10 ---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index 431272e..6ebe481 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -2,7 +2,6 @@ namespace Icinga\Module\Businessprocess\Renderer; -use Icinga\Date\DateFormatter; use Icinga\Exception\ProgrammingError; use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\BpConfig; @@ -11,7 +10,6 @@ use Icinga\Module\Businessprocess\Web\Url; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlDocument; -use ipl\Html\HtmlString; abstract class Renderer extends HtmlDocument { @@ -187,6 +185,16 @@ abstract class Renderer extends HtmlDocument return $classes; } + /** + * @param Node $node + * @param $path + * @return string + */ + public function getId(Node $node, $path) + { + return md5(implode(';', $path) . $node->getName()); + } + public function setPath(array $path) { $this->path = $path; diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 1fa0ca0..e171c80 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -72,7 +72,7 @@ class NodeTile extends BaseHtmlElement $attributes = $this->getAttributes(); $attributes->add('class', $renderer->getNodeClasses($node)); - $attributes->add('id', 'bp-' . $node->getName()); + $attributes->add('id', $renderer->getId($node, $this->path)); if (! $renderer->isLocked()) { $attributes->add('data-node-name', $node->getName()); } diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index c890368..0919de6 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -86,16 +86,6 @@ class TreeRenderer extends Renderer return $html; } - /** - * @param Node $node - * @param $path - * @return string - */ - protected function getId(Node $node, $path) - { - return md5(implode(';', $path) . $node->getName()); - } - protected function getStateClassNames(Node $node) { $state = strtolower($node->getStateName()); From 40c406ac9e4714cb53da367208cc7c1ec4f03f2e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 10:17:54 +0100 Subject: [PATCH 80/99] TreeRenderer: Render the info_url action where all other action urls are --- library/Businessprocess/Renderer/TreeRenderer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 0919de6..80acc84 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -167,16 +167,16 @@ class TreeRenderer extends Renderer $div->add($node->getLink()); $div->add($this->getNodeIcons($node, $path)); - if ($node instanceof BpNode && $node->hasInfoUrl()) { - $div->add($this->createInfoAction($node)); - } - $div->add(Html::tag('span', null, $node->getAlias())); if ($node instanceof BpNode) { $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); } + if ($node instanceof BpNode && $node->hasInfoUrl()) { + $div->add($this->createInfoAction($node)); + } + if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) { $div->add($this->getActionIcons($bp, $node)); } From 75a3d1fd5e1287cc127f519cbdfeaff26b6315b3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 11:41:44 +0100 Subject: [PATCH 81/99] LegacyConfigParser: Restore ability to defer referenced process initialization --- .../Storage/LegacyConfigParser.php | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php index ca7b170..10cfe7b 100644 --- a/library/Businessprocess/Storage/LegacyConfigParser.php +++ b/library/Businessprocess/Storage/LegacyConfigParser.php @@ -22,6 +22,9 @@ class LegacyConfigParser /** @var BpConfig */ protected $config; + /** @var array */ + protected $missingNodes = []; + /** * LegacyConfigParser constructor * @@ -77,6 +80,8 @@ class LegacyConfigParser $parser->parseLine($line); } + $parser->resolveMissingNodes(); + Benchmark::measure('Business process ' . $name . ' loaded'); return $config; } @@ -99,11 +104,28 @@ class LegacyConfigParser $this->parseLine($line); } + $this->resolveMissingNodes(); + fclose($fh); unset($this->currentLineNumber); unset($this->currentFilename); } + /** + * Resolve previously missed business process nodes + * + * @throws ConfigurationError In case a referenced process does not exist + */ + protected function resolveMissingNodes() + { + foreach ($this->missingNodes as $name => $parents) { + foreach ($parents as $parent) { + /** @var BpNode $parent */ + $parent->addChild($this->config->getNode($name)); + } + } + } + public static function readMetadataFromFileHeader($name, $filename) { $metadata = new Metadata($name); @@ -329,8 +351,10 @@ class LegacyConfigParser list($config, $nodeName) = preg_split('~:\s*~', substr($val, 1), 2); $node->addChild($bp->createImportedNode($config, $nodeName)); } - } else { + } elseif ($bp->hasNode($val)) { $node->addChild($bp->getNode($val)); + } else { + $this->missingNodes[$val][] = $node; } } From af11c3e715ef8e9dfda70e5955d4cf24e3ef7316 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 11:43:02 +0100 Subject: [PATCH 82/99] tests: Fix that some test cases work with invalid configurations This has been no problem previously. But since the parser establishes parent-child relationships these are not parseable anymore. They'll now work with monitored nodes instead of processes. --- .../Operators/AndOperatorTest.php | 42 ++++++------- .../Operators/MinOperatorTest.php | 59 +++++++++---------- .../Operators/NotOperatorTest.php | 59 +++++++++---------- .../Operators/OrOperatorTest.php | 45 +++++++------- 4 files changed, 99 insertions(+), 106 deletions(-) diff --git a/test/php/library/Businessprocess/Operators/AndOperatorTest.php b/test/php/library/Businessprocess/Operators/AndOperatorTest.php index 93e8d80..9e87cf1 100644 --- a/test/php/library/Businessprocess/Operators/AndOperatorTest.php +++ b/test/php/library/Businessprocess/Operators/AndOperatorTest.php @@ -12,8 +12,8 @@ class AndOperatorTest extends BaseTestCase { $storage = new LegacyStorage($this->emptyConfigSection()); $expressions = array( - 'a = b', - 'a = b & c & d', + 'a = b;c', + 'a = b;c & c;d & d;e', ); foreach ($expressions as $expression) { @@ -27,9 +27,9 @@ class AndOperatorTest extends BaseTestCase public function testThreeTimesCriticalIsCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 2); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 2); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'CRITICAL', @@ -40,9 +40,9 @@ class AndOperatorTest extends BaseTestCase public function testTwoTimesCriticalAndOkIsCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 0); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 0); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'CRITICAL', @@ -53,9 +53,9 @@ class AndOperatorTest extends BaseTestCase public function testCriticalAndWarningAndOkIsCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 0); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 0); $this->assertEquals( 'CRITICAL', @@ -66,9 +66,9 @@ class AndOperatorTest extends BaseTestCase public function testUnknownAndWarningAndOkIsUnknown() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 3); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 3); $this->assertEquals( 'UNKNOWN', @@ -79,9 +79,9 @@ class AndOperatorTest extends BaseTestCase public function testTwoTimesWarningAndOkIsWarning() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'WARNING', @@ -92,9 +92,9 @@ class AndOperatorTest extends BaseTestCase public function testThreeTimesOkIsOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 0); - $bp->setNodeState('d', 0); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 0); + $bp->setNodeState('d;e', 0); $this->assertEquals( 'OK', @@ -203,7 +203,7 @@ class AndOperatorTest extends BaseTestCase protected function getBp() { $storage = new LegacyStorage($this->emptyConfigSection()); - $expression = 'a = b & c & d'; + $expression = 'a = b;c & c;d & d;e'; $bp = $storage->loadFromString('dummy', $expression); $bp->createBp('b'); $bp->createBp('c'); diff --git a/test/php/library/Businessprocess/Operators/MinOperatorTest.php b/test/php/library/Businessprocess/Operators/MinOperatorTest.php index 43fa0a1..986589a 100644 --- a/test/php/library/Businessprocess/Operators/MinOperatorTest.php +++ b/test/php/library/Businessprocess/Operators/MinOperatorTest.php @@ -12,8 +12,8 @@ class MinOperatorTest extends BaseTestCase { $storage = new LegacyStorage($this->emptyConfigSection()); $expressions = array( - 'a = 1 of: b', - 'a = 2 of: b + c + d', + 'a = 1 of: b;c', + 'a = 2 of: b;c + c;d + d;e', ); $this->getName(); foreach ($expressions as $expression) { @@ -26,9 +26,9 @@ class MinOperatorTest extends BaseTestCase public function testTwoOfThreeTimesCriticalAreAtLeastCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 2); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 2); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'CRITICAL', @@ -39,9 +39,9 @@ class MinOperatorTest extends BaseTestCase public function testTwoOfTwoTimesCriticalAndUnknownAreAtLeastCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 3); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 3); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'CRITICAL', @@ -52,9 +52,9 @@ class MinOperatorTest extends BaseTestCase public function testTwoOfCriticalAndWarningAndOkAreAtLeastCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 0); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 0); $this->assertEquals( 'CRITICAL', @@ -65,9 +65,9 @@ class MinOperatorTest extends BaseTestCase public function testTwoOfUnknownAndWarningAndCriticalAreAtLeastCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 3); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 3); $this->assertEquals( 'CRITICAL', @@ -78,9 +78,9 @@ class MinOperatorTest extends BaseTestCase public function testTwoOfTwoTimesWarningAndUnknownAreAtLeastUnknown() { $bp = $this->getBp(); - $bp->setNodeState('b', 3); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 3); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'UNKNOWN', @@ -91,9 +91,9 @@ class MinOperatorTest extends BaseTestCase public function testTwoOfThreeTimesOkAreAtLeastOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 0); - $bp->setNodeState('d', 0); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 0); + $bp->setNodeState('d;e', 0); $this->assertEquals( 'OK', @@ -114,8 +114,8 @@ class MinOperatorTest extends BaseTestCase public function testTenWithOnlyTwoCritical() { $bp = $this->getBp(10, 8, 0); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 2); $this->assertEquals( 'OK', @@ -126,9 +126,9 @@ class MinOperatorTest extends BaseTestCase public function testTenWithThreeCritical() { $bp = $this->getBp(10, 8, 0); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 2); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 2); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'CRITICAL', @@ -139,9 +139,9 @@ class MinOperatorTest extends BaseTestCase public function testTenWithThreeWarning() { $bp = $this->getBp(10, 8, 0); - $bp->setNodeState('b', 1); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 1); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'WARNING', @@ -157,14 +157,13 @@ class MinOperatorTest extends BaseTestCase $names = array(); $a = 97; for ($i = 1; $i <= $count; $i++) { - $names[] = chr($a + $i); + $names[] = chr($a + $i) . ';' . chr($a + $i + 1); } $storage = new LegacyStorage($this->emptyConfigSection()); $expression = sprintf('a = %d of: %s', $min, join(' + ', $names)); $bp = $storage->loadFromString('dummy', $expression); foreach ($names as $n) { - $bp->createBp($n); if ($defaultState !== null) { $bp->setNodeState($n, $defaultState); } diff --git a/test/php/library/Businessprocess/Operators/NotOperatorTest.php b/test/php/library/Businessprocess/Operators/NotOperatorTest.php index dad8042..fb62545 100644 --- a/test/php/library/Businessprocess/Operators/NotOperatorTest.php +++ b/test/php/library/Businessprocess/Operators/NotOperatorTest.php @@ -12,10 +12,10 @@ class NotOperatorTest extends BaseTestCase { $storage = new LegacyStorage($this->emptyConfigSection()); $expressions = array( - 'a = !b', - 'a = ! b', - 'a = b ! c ! d', - 'a = ! b ! c ! d !', + 'a = !b;c', + 'a = ! b;c', + 'a = b;c ! c;d ! d;e', + 'a = ! b;c ! c;d ! d;e !', ); foreach ($expressions as $expression) { @@ -29,10 +29,10 @@ class NotOperatorTest extends BaseTestCase public function testASimpleNegationGivesTheCorrectResult() { $storage = new LegacyStorage($this->emptyConfigSection()); - $expression = 'a = !b'; + $expression = 'a = !b;c'; $bp = $storage->loadFromString('dummy', $expression); $a = $bp->getNode('a'); - $b = $bp->createBp('b')->setState(3); + $b = $bp->getNode('b;c')->setState(3); $this->assertEquals( 'OK', $a->getStateName() @@ -49,9 +49,9 @@ class NotOperatorTest extends BaseTestCase public function testThreeTimesCriticalIsOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 2); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 2); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'OK', @@ -62,9 +62,9 @@ class NotOperatorTest extends BaseTestCase public function testThreeTimesUnknownIsOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 3); - $bp->setNodeState('c', 3); - $bp->setNodeState('d', 3); + $bp->setNodeState('b;c', 3); + $bp->setNodeState('c;d', 3); + $bp->setNodeState('d;e', 3); $this->assertEquals( 'OK', @@ -75,9 +75,9 @@ class NotOperatorTest extends BaseTestCase public function testThreeTimesWarningIsWarning() { $bp = $this->getBp(); - $bp->setNodeState('b', 1); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 1); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'WARNING', @@ -88,9 +88,9 @@ class NotOperatorTest extends BaseTestCase public function testThreeTimesOkIsCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 0); - $bp->setNodeState('d', 0); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 0); + $bp->setNodeState('d;e', 0); $this->assertEquals( 'CRITICAL', @@ -101,9 +101,9 @@ class NotOperatorTest extends BaseTestCase public function testNotOkAndWarningAndCriticalIsOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'OK', @@ -114,9 +114,9 @@ class NotOperatorTest extends BaseTestCase public function testNotWarningAndUnknownAndCriticalIsOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 3); - $bp->setNodeState('c', 2); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 3); + $bp->setNodeState('c;d', 2); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'OK', @@ -127,9 +127,9 @@ class NotOperatorTest extends BaseTestCase public function testNotTwoTimesWarningAndOkIsWarning() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'WARNING', @@ -143,11 +143,8 @@ class NotOperatorTest extends BaseTestCase protected function getBp() { $storage = new LegacyStorage($this->emptyConfigSection()); - $expression = 'a = ! b ! c ! d'; + $expression = 'a = ! b;c ! c;d ! d;e'; $bp = $storage->loadFromString('dummy', $expression); - $bp->createBp('b'); - $bp->createBp('c'); - $bp->createBp('d'); return $bp; } diff --git a/test/php/library/Businessprocess/Operators/OrOperatorTest.php b/test/php/library/Businessprocess/Operators/OrOperatorTest.php index a9f5b1a..02043d0 100644 --- a/test/php/library/Businessprocess/Operators/OrOperatorTest.php +++ b/test/php/library/Businessprocess/Operators/OrOperatorTest.php @@ -12,8 +12,8 @@ class OrOperatorTest extends BaseTestCase { $storage = new LegacyStorage($this->emptyConfigSection()); $expressions = array( - 'a = b', - 'a = b | c | d', + 'a = b;c', + 'a = b;c | c;d | d;e', ); foreach ($expressions as $expression) { @@ -27,9 +27,9 @@ class OrOperatorTest extends BaseTestCase public function testThreeTimesCriticalIsCritical() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 2); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 2); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'CRITICAL', @@ -40,9 +40,9 @@ class OrOperatorTest extends BaseTestCase public function testTwoTimesCriticalOrUnknownIsUnknown() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 3); - $bp->setNodeState('d', 2); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 3); + $bp->setNodeState('d;e', 2); $this->assertEquals( 'UNKNOWN', @@ -53,9 +53,9 @@ class OrOperatorTest extends BaseTestCase public function testCriticalOrWarningOrOkIsOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 0); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 0); $this->assertEquals( 'OK', @@ -66,9 +66,9 @@ class OrOperatorTest extends BaseTestCase public function testUnknownOrWarningOrCriticalIsWarning() { $bp = $this->getBp(); - $bp->setNodeState('b', 2); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 3); + $bp->setNodeState('b;c', 2); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 3); $this->assertEquals( 'WARNING', @@ -79,9 +79,9 @@ class OrOperatorTest extends BaseTestCase public function testTwoTimesWarningAndOkIsOk() { $bp = $this->getBp(); - $bp->setNodeState('b', 0); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 0); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'OK', @@ -92,9 +92,9 @@ class OrOperatorTest extends BaseTestCase public function testThreeTimesWarningIsWarning() { $bp = $this->getBp(); - $bp->setNodeState('b', 1); - $bp->setNodeState('c', 1); - $bp->setNodeState('d', 1); + $bp->setNodeState('b;c', 1); + $bp->setNodeState('c;d', 1); + $bp->setNodeState('d;e', 1); $this->assertEquals( 'WARNING', @@ -108,11 +108,8 @@ class OrOperatorTest extends BaseTestCase protected function getBp() { $storage = new LegacyStorage($this->emptyConfigSection()); - $expression = 'a = b | c | d'; + $expression = 'a = b;c | c;d | d;e'; $bp = $storage->loadFromString('dummy', $expression); - $bp->createBp('b'); - $bp->createBp('c'); - $bp->createBp('d'); return $bp; } From cfe87e440a1b6a9638cde1afb4794900fe68302e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 13:26:43 +0100 Subject: [PATCH 83/99] css: Increase z-index of Web 2's export dropdown Otherwise parts of the breadcrumbs show through. --- public/css/module.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/css/module.less b/public/css/module.less index f40dec2..f57ccd4 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -637,6 +637,10 @@ td > a > .badges { z-index: 2; } +.tabs > .dropdown-nav-item > ul { + z-index: 100; +} + .breadcrumb li:first-child a { padding-left: 1em; padding-right: 0.5em; From 0d262bf0d0da7d33410853d714aed191641074e3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 14:11:03 +0100 Subject: [PATCH 84/99] Renderer: Don't fail while generating ids for unbound nodes --- library/Businessprocess/Renderer/Renderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index 6ebe481..2c6d34c 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -192,7 +192,7 @@ abstract class Renderer extends HtmlDocument */ public function getId(Node $node, $path) { - return md5(implode(';', $path) . $node->getName()); + return md5((empty($path) ? '' : implode(';', $path)) . $node->getName()); } public function setPath(array $path) From cacc24f5017142a0ed6491be16ff99ab74f86fa4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 14:30:06 +0100 Subject: [PATCH 85/99] NodeAddChildrenAction: Really import nodes if instructed to do so --- library/Businessprocess/Modification/NodeAddChildrenAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Businessprocess/Modification/NodeAddChildrenAction.php b/library/Businessprocess/Modification/NodeAddChildrenAction.php index def37a1..5d5ab29 100644 --- a/library/Businessprocess/Modification/NodeAddChildrenAction.php +++ b/library/Businessprocess/Modification/NodeAddChildrenAction.php @@ -32,7 +32,7 @@ class NodeAddChildrenAction extends NodeAction $node = $config->getBpNode($this->getNodeName()); foreach ($this->children as $name) { - if (! $config->hasNode($name)) { + if (! $config->hasNode($name) || $config->getNode($name)->getBpConfig()->getName() !== $config->getName()) { if (strpos($name, ';') !== false) { list($host, $service) = preg_split('/;/', $name, 2); From 6ae4cf745cbc39a45801c04cd3247a13b88b50d9 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 Feb 2019 14:41:53 +0100 Subject: [PATCH 86/99] RenderedProcessActionBar: Don't allow to unlock imported nodes --- application/controllers/ProcessController.php | 2 +- .../Component/RenderedProcessActionBar.php | 31 +++++++++++++------ public/css/module.less | 4 +++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index c3e7071..92482c3 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -88,7 +88,7 @@ class ProcessController extends Controller $renderer = $this->prepareRenderer($bp, $node); - if (! $this->showFullscreen) { + if (! $this->showFullscreen && ($node === null || ! $node->getBpConfig()->isImported())) { if ($this->params->get('unlocked')) { $renderer->unlock(); } diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php index 4d0c21d..c6f126a 100644 --- a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -58,15 +58,28 @@ class RenderedProcessActionBar extends ActionBar $hasChanges = $config->hasSimulations() || $config->hasBeenChanged(); if ($renderer->isLocked()) { - $this->add(Html::tag( - 'a', - [ - 'href' => $url->with('unlocked', true), - 'title' => mt('businessprocess', 'Click to unlock editing for this process'), - 'class' => 'icon-lock' - ], - mt('businessprocess', 'Unlock Editing') - )); + if (! $renderer->wantsRootNodes() && $renderer->getParentNode()->getBpConfig()->isImported()) { + $span = Html::tag('span', [ + 'class' => 'disabled', + 'title' => mt( + 'businessprocess', + 'Imported processes can only be changed in their original configuration' + ) + ]); + $span->add(Html::tag('i', ['class' => 'icon icon-lock'])) + ->add(mt('businessprocess', 'Editing Locked')); + $this->add($span); + } else { + $this->add(Html::tag( + 'a', + [ + 'href' => $url->with('unlocked', true), + 'title' => mt('businessprocess', 'Click to unlock editing for this process'), + 'class' => 'icon-lock' + ], + mt('businessprocess', 'Unlock Editing') + )); + } } elseif (! $hasChanges) { $this->add(Html::tag( 'a', diff --git a/public/css/module.less b/public/css/module.less index f57ccd4..06f0f07 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -70,6 +70,10 @@ a:focus { } } } + + span.disabled { + color: @gray; + } } form a { From 6f3ffe48e2a31ba386d9e24f12d23b4b4bbc99c4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 25 Feb 2019 08:19:39 +0100 Subject: [PATCH 87/99] BpConfig: Only list each involved configuration once --- library/Businessprocess/BpConfig.php | 17 +++++++++-------- .../Businessprocess/State/MonitoringState.php | 1 - 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index cfe2700..861505a 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -608,16 +608,17 @@ class BpConfig return $this->importedConfigs[$name]; } - public function listInvolvedConfigs(&$usedConfigs = null) + public function listInvolvedConfigs(&$configs = null) { - $configs = []; - foreach ($this->importedNodes as $node) { - $config = $node->getBpConfig(); - $configs[] = $config; + if ($configs === null) { + $configs[$this->getName()] = $this; + } - if (! isset($usedConfigs[$node->getConfigName()])) { - $usedConfigs[$config->getName()] = true; - $configs = array_merge($configs, $config->listInvolvedConfigs($usedConfigs)); + foreach ($this->importedNodes as $node) { + if (! isset($configs[$node->getConfigName()])) { + $config = $node->getBpConfig(); + $configs[$node->getConfigName()] = $config; + $config->listInvolvedConfigs($configs); } } diff --git a/library/Businessprocess/State/MonitoringState.php b/library/Businessprocess/State/MonitoringState.php index 1e48f65..80d5f41 100644 --- a/library/Businessprocess/State/MonitoringState.php +++ b/library/Businessprocess/State/MonitoringState.php @@ -94,7 +94,6 @@ class MonitoringState Benchmark::measure('Retrieved states for ' . count($serviceStatus) . ' services in ' . $config->getName()); $configs = $config->listInvolvedConfigs(); - $configs[] = $config; foreach ($configs as $cfg) { foreach ($serviceStatus as $row) { $this->handleDbRow($row, $cfg); From 40538c51cecf90bc9a237cc3d56f1f2c42f73f91 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 25 Feb 2019 09:41:23 +0100 Subject: [PATCH 88/99] LegacyStorage: Add cache to avoid parsing configurations multiple times --- application/clicommands/ProcessCommand.php | 2 +- configuration.php | 4 +--- library/Businessprocess/BpConfig.php | 4 +--- .../Director/ShipConfigFiles.php | 5 +--- .../Businessprocess/Storage/LegacyStorage.php | 23 +++++++++++++++---- library/Businessprocess/Storage/Storage.php | 15 ++++++++++++ library/Businessprocess/Web/Controller.php | 4 +--- 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/application/clicommands/ProcessCommand.php b/application/clicommands/ProcessCommand.php index 5280e37..a11a202 100644 --- a/application/clicommands/ProcessCommand.php +++ b/application/clicommands/ProcessCommand.php @@ -34,7 +34,7 @@ class ProcessCommand extends Command public function init() { - $this->storage = new LegacyStorage($this->Config()->getSection('global')); + $this->storage = LegacyStorage::getInstance(); } /** diff --git a/configuration.php b/configuration.php index 38ef46a..cf5c86a 100644 --- a/configuration.php +++ b/configuration.php @@ -10,9 +10,7 @@ $section = $this->menuSection(N_('Business Processes'), array( )); try { - $storage = new LegacyStorage( - $this->getConfig()->getSection('global') - ); + $storage = LegacyStorage::getInstance(); $prio = 0; foreach ($storage->listProcessNames() as $name) { diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 861505a..93d76c3 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -631,9 +631,7 @@ class BpConfig protected function storage() { if ($this->storage === null) { - $this->storage = new LegacyStorage( - Config::module('businessprocess')->getSection('global') - ); + $this->storage = LegacyStorage::getInstance(); } return $this->storage; diff --git a/library/Businessprocess/Director/ShipConfigFiles.php b/library/Businessprocess/Director/ShipConfigFiles.php index 35019d9..17b9e1f 100644 --- a/library/Businessprocess/Director/ShipConfigFiles.php +++ b/library/Businessprocess/Director/ShipConfigFiles.php @@ -2,7 +2,6 @@ namespace Icinga\Module\Businessprocess\Director; -use Icinga\Application\Config; use Icinga\Module\Director\Hook\ShipConfigFilesHook; use Icinga\Module\Businessprocess\Storage\LegacyStorage; @@ -12,9 +11,7 @@ class ShipConfigFiles extends ShipConfigFilesHook { $files = array(); - $storage = new LegacyStorage( - Config::module('businessprocess')->getSection('global') - ); + $storage = LegacyStorage::getInstance(); foreach ($storage->listProcesses() as $name => $title) { $files['processes/' . $name . '.bp'] = $storage->getSource($name); diff --git a/library/Businessprocess/Storage/LegacyStorage.php b/library/Businessprocess/Storage/LegacyStorage.php index 8cbe89b..8d042b0 100644 --- a/library/Businessprocess/Storage/LegacyStorage.php +++ b/library/Businessprocess/Storage/LegacyStorage.php @@ -9,6 +9,13 @@ use Icinga\Exception\SystemPermissionException; class LegacyStorage extends Storage { + /** + * All parsed configurations + * + * @var BpConfig[] + */ + protected $configs = []; + /** @var string */ protected $configDir; @@ -116,10 +123,14 @@ class LegacyStorage extends Storage */ public function loadProcess($name) { - return LegacyConfigParser::parseFile( - $name, - $this->getFilename($name) - ); + if (! isset($this->configs[$name])) { + $this->configs[$name] = LegacyConfigParser::parseFile( + $name, + $this->getFilename($name) + ); + } + + return $this->configs[$name]; } /** @@ -146,6 +157,10 @@ class LegacyStorage extends Storage */ public function loadMetadata($name) { + if (isset($this->configs[$name])) { + return $this->configs[$name]->getMetadata(); + } + return LegacyConfigParser::readMetadataFromFileHeader( $name, $this->getFilename($name) diff --git a/library/Businessprocess/Storage/Storage.php b/library/Businessprocess/Storage/Storage.php index de3d939..c8a07ba 100644 --- a/library/Businessprocess/Storage/Storage.php +++ b/library/Businessprocess/Storage/Storage.php @@ -2,12 +2,18 @@ namespace Icinga\Module\Businessprocess\Storage; +use Icinga\Application\Config; use Icinga\Data\ConfigObject; use Icinga\Module\Businessprocess\BpConfig; use Icinga\Module\Businessprocess\Metadata; abstract class Storage { + /** + * @var static + */ + protected static $instance; + /** * @var ConfigObject */ @@ -27,6 +33,15 @@ abstract class Storage { } + public static function getInstance() + { + if (static::$instance === null) { + static::$instance = new static(Config::module('businessprocess')->getSection('global')); + } + + return static::$instance; + } + /** * All processes readable by the current user * diff --git a/library/Businessprocess/Web/Controller.php b/library/Businessprocess/Web/Controller.php index b69f20e..d1104d8 100644 --- a/library/Businessprocess/Web/Controller.php +++ b/library/Businessprocess/Web/Controller.php @@ -262,9 +262,7 @@ class Controller extends ModuleController protected function storage() { if ($this->storage === null) { - $this->storage = new LegacyStorage( - $this->Config()->getSection('global') - ); + $this->storage = LegacyStorage::getInstance(); } return $this->storage; From eaf5b85538341204d7c7f505ecddc6c404695578 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 25 Feb 2019 13:58:18 +0100 Subject: [PATCH 89/99] Change how imported nodes are recognized Since configurations are cached this cannot be done by using a flag. Instead the rendering and path aggregation are now able to distinguish nodes themselves. --- application/controllers/NodeController.php | 4 ++-- application/controllers/ProcessController.php | 4 ++-- library/Businessprocess/BpConfig.php | 19 --------------- library/Businessprocess/Node.php | 24 +++++++++++-------- .../Businessprocess/Renderer/Breadcrumb.php | 6 +++-- library/Businessprocess/Renderer/Renderer.php | 16 ++++++++++++- .../Renderer/TileRenderer/NodeTile.php | 7 +++++- .../Businessprocess/Renderer/TreeRenderer.php | 9 ++++--- .../Component/RenderedProcessActionBar.php | 2 +- 9 files changed, 48 insertions(+), 43 deletions(-) diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index fa20e8d..bbb82fb 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -46,7 +46,7 @@ class NodeController extends Controller if ($importedConfig->hasNode($name)) { $node = $importedConfig->getNode($name); - $nativePaths = $node->getPaths(); + $nativePaths = $node->getPaths($config); do { $path = array_pop($nativePaths); @@ -55,7 +55,7 @@ class NodeController extends Controller array_pop($path); // Remove the monitored node $immediateParentName = array_pop($path); // The directly affected process $importedPath = array_slice($path, $importedNodePos + 1); - foreach ($importedNode->getPaths() as $targetPath) { + foreach ($importedNode->getPaths($config) as $targetPath) { if ($targetPath[count($targetPath) - 1] === $immediateParentName) { array_pop($targetPath); $parent = $importedNode; diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 92482c3..9b91858 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -88,7 +88,7 @@ class ProcessController extends Controller $renderer = $this->prepareRenderer($bp, $node); - if (! $this->showFullscreen && ($node === null || ! $node->getBpConfig()->isImported())) { + if (! $this->showFullscreen && ($node === null || ! $renderer->rendersImportedNode())) { if ($this->params->get('unlocked')) { $renderer->unlock(); } @@ -141,7 +141,7 @@ class ProcessController extends Controller if (! ($this->showFullscreen || $this->view->compact)) { $controls->add($this->getProcessTabs($bp, $renderer)); } - $controls->add(Breadcrumb::create($renderer)); + $controls->add(Breadcrumb::create(clone $renderer)); if (! $this->showFullscreen && ! $this->view->compact) { $controls->add( new RenderedProcessActionBar($bp, $renderer, $this->Auth(), $this->url()) diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 93d76c3..2121b40 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -88,13 +88,6 @@ class BpConfig */ protected $root_nodes = array(); - /** - * Whether this configuration has been imported - * - * @var bool - */ - protected $imported = false; - /** * Imported nodes * @@ -561,17 +554,6 @@ class BpConfig return $missing; } - public function setImported($state = true) - { - $this->imported = (bool) $state; - return $this; - } - - public function isImported() - { - return $this->imported; - } - public function createImportedNode($config, $name = null) { $params = (object) array('configName' => $config); @@ -594,7 +576,6 @@ class BpConfig { if (! isset($this->importedConfigs[$name])) { $import = $this->storage()->loadProcess($name); - $import->setImported(); if ($this->usesSoftStates()) { $import->useSoftStates(); diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index f796d77..2c9e7a8 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -339,20 +339,29 @@ abstract class Node } /** + * @param BpConfig $rootConfig + * * @return array */ - public function getPaths() + public function getPaths($rootConfig = null) { + $differentConfig = false; + if ($rootConfig === null) { + $rootConfig = $this->getBpConfig(); + } else { + $differentConfig = $this->getBpConfig()->getName() !== $rootConfig->getName(); + } + $paths = []; foreach ($this->parents as $parent) { - foreach ($parent->getPaths() as $path) { - $path[] = $this->getIdentifier(); + foreach ($parent->getPaths($rootConfig) as $path) { + $path[] = $differentConfig ? $this->getIdentifier() : $this->getName(); $paths[] = $path; } } if (! $this instanceof ImportedNode && $this->getBpConfig()->hasRootNode($this->getName())) { - $paths[] = [$this->getIdentifier()]; + $paths[] = [$differentConfig ? $this->getIdentifier() : $this->getName()]; } return $paths; @@ -410,12 +419,7 @@ abstract class Node public function getIdentifier() { - $prefix = ''; - if ($this->getBpConfig()->isImported()) { - $prefix = '@' . $this->getBpConfig()->getName() . ':'; - } - - return $prefix . $this->getName(); + return '@' . $this->getBpConfig()->getName() . ':' . $this->getName(); } public function __toString() diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php index d848992..56c41aa 100644 --- a/library/Businessprocess/Renderer/Breadcrumb.php +++ b/library/Businessprocess/Renderer/Breadcrumb.php @@ -46,10 +46,12 @@ class Breadcrumb extends BaseHtmlElement $path = $renderer->getCurrentPath(); $parts = array(); - while ($node = array_pop($path)) { + while ($nodeName = array_pop($path)) { + $node = $bp->getNode($nodeName); + $renderer->setParentNode($node); array_unshift( $parts, - static::renderNode($bp->getNode($node), $path, $renderer) + static::renderNode($node, $path, $renderer) ); } $breadcrumb->add($parts); diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index 2c6d34c..ebbe05f 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -74,6 +74,17 @@ abstract class Renderer extends HtmlDocument return $this->parent !== null; } + public function rendersImportedNode() + { + return $this->parent !== null && $this->parent->getBpConfig()->getName() !== $this->config->getName(); + } + + public function setParentNode(BpNode $node) + { + $this->parent = $node; + return $this; + } + /** * @return BpNode */ @@ -213,8 +224,11 @@ abstract class Renderer extends HtmlDocument { $path = $this->getPath(); if ($this->rendersSubNode()) { - $path[] = $this->parent->getIdentifier(); + $path[] = $this->rendersImportedNode() + ? $this->parent->getIdentifier() + : $this->parent->getName(); } + return $path; } diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index e171c80..f25e5d8 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -121,7 +121,12 @@ class NodeTile extends BaseHtmlElement $url = $this->renderer->getBaseUrl(); $p = $url->getParams(); - $p->set('node', $node->getIdentifier()); + if ($this->renderer->rendersImportedNode()) { + $p->set('node', $node->getIdentifier()); + } else { + $p->set('node', $node->getName()); + } + if (! empty($this->path)) { $p->addValues('path', $this->path); } diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 80acc84..d0726fa 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -177,15 +177,14 @@ class TreeRenderer extends Renderer $div->add($this->createInfoAction($node)); } - if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) { + $differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName(); + if (! $this->isLocked() && !$differentConfig) { $div->add($this->getActionIcons($bp, $node)); } $ul = Html::tag('ul', [ 'class' => ['bp', 'sortable'], - 'data-sortable-disabled' => ( - $this->isLocked() || $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName() - ) ? 'true' : 'false', + 'data-sortable-disabled' => ($this->isLocked() || $differentConfig) ? 'true' : 'false', 'data-sortable-invert-swap' => 'true', 'data-sortable-data-id-attr' => 'id', 'data-sortable-draggable' => '.movable', @@ -206,7 +205,7 @@ class TreeRenderer extends Renderer ]); $li->add($ul); - $path[] = $node->getIdentifier(); + $path[] = $differentConfig ? $node->getIdentifier() : $node->getName(); foreach ($node->getChildren() as $name => $child) { if ($child instanceof BpNode) { $ul->add($this->renderNode($bp, $child, $path)); diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php index c6f126a..86dfbca 100644 --- a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -58,7 +58,7 @@ class RenderedProcessActionBar extends ActionBar $hasChanges = $config->hasSimulations() || $config->hasBeenChanged(); if ($renderer->isLocked()) { - if (! $renderer->wantsRootNodes() && $renderer->getParentNode()->getBpConfig()->isImported()) { + if (! $renderer->wantsRootNodes() && $renderer->rendersImportedNode()) { $span = Html::tag('span', [ 'class' => 'disabled', 'title' => mt( From 3431540700a46e2f1b3de8ee97db9048c83a7511 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 25 Feb 2019 14:23:41 +0100 Subject: [PATCH 90/99] NodeTile: Don't use a node's identifier if it's the same config anyway --- library/Businessprocess/Renderer/TileRenderer/NodeTile.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index f25e5d8..a55ef50 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -121,7 +121,11 @@ class NodeTile extends BaseHtmlElement $url = $this->renderer->getBaseUrl(); $p = $url->getParams(); - if ($this->renderer->rendersImportedNode()) { + if ($node instanceof ImportedNode + && $this->renderer->getBusinessProcess()->getName() === $node->getBpConfig()->getName() + ) { + $p->set('node', $node->getNodeName()); + } elseif ($this->renderer->rendersImportedNode()) { $p->set('node', $node->getIdentifier()); } else { $p->set('node', $node->getName()); From 21980a394fe9a1bf4199a9d9c8733bce301cce1f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 25 Feb 2019 15:09:34 +0100 Subject: [PATCH 91/99] NodeController: Also show impact on imported sub processes --- application/controllers/NodeController.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index bbb82fb..2a5af66 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -55,6 +55,14 @@ class NodeController extends Controller array_pop($path); // Remove the monitored node $immediateParentName = array_pop($path); // The directly affected process $importedPath = array_slice($path, $importedNodePos + 1); + + // We may get multiple native paths. Though, only the right hand of the path + // is what we're interested in. The left part is not what is getting imported. + $antiDuplicator = join('|', $importedPath) . '|' . $immediateParentName; + if (isset($parents[$antiDuplicator])) { + continue; + } + foreach ($importedNode->getPaths($config) as $targetPath) { if ($targetPath[count($targetPath) - 1] === $immediateParentName) { array_pop($targetPath); @@ -63,12 +71,8 @@ class NodeController extends Controller $parent = $importedConfig->getNode($immediateParentName); } - $parents[] = [$parent, array_merge($targetPath, $importedPath)]; + $parents[$antiDuplicator] = [$parent, array_merge($targetPath, $importedPath)]; } - - // We may get multiple native paths. Though, the right hand of the path is everywhere the - // same and the left hand not of any interest since that's where the import location is. - break; // So, once we've found a match, we're done here (Otherwise we'll get duplicates) } } while (! empty($nativePaths)); } From 0a5ada9bf1d7bc554822ba5ce85a0087f6aaaf40 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 25 Feb 2019 15:50:42 +0100 Subject: [PATCH 92/99] js: Suspend Web 2's autorefresh while dragging items If a refresh happens while dragging the drop wont trigger any events and the change is not applied. --- public/js/module.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/public/js/module.js b/public/js/module.js index c38db67..d028b24 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -33,6 +33,9 @@ this.module.on('click', '.dashboard-tile', this.dashboardTileClick); this.module.on('end', 'div.tiles.sortable', this.tileDropped); + this.module.on('choose', '.sortable', this.suspendAutoRefresh); + this.module.on('unchoose', '.sortable', this.resumeAutoRefresh); + this.module.icinga.logger.debug('BP module initialized'); }, @@ -87,6 +90,18 @@ $(event.currentTarget).find('> .bp-link > a').first().trigger('click'); }, + suspendAutoRefresh: function(event) { + // TODO: If there is a better approach some time, let me know + $(event.originalEvent.from).closest('.container').data('lastUpdate', (new Date()).getTime() + 3600 * 1000); + event.stopPropagation(); + }, + + resumeAutoRefresh: function(event) { + var $container = $(event.originalEvent.from).closest('.container'); + $container.data('lastUpdate', (new Date()).getTime() - ($container.data('icingaRefresh') || 10) * 1000); + event.stopPropagation(); + }, + tileDropped: function(event) { var evt = event.originalEvent; if (evt.oldIndex !== evt.newIndex) { From d690c07ed8067c951bca177be7b4b0b72dfeb81a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 Feb 2019 08:57:46 +0100 Subject: [PATCH 93/99] process/show: Don't show the control separator when in fullscreen or on the dashboard --- application/controllers/ProcessController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 9b91858..c1a3dc8 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -123,7 +123,6 @@ class ProcessController extends Controller protected function prepareControls($bp, $renderer) { $controls = $this->controls(); - $controls->getAttributes()->add('class', 'separated'); if ($this->showFullscreen) { $controls->getAttributes()->add('class', 'want-fullscreen'); @@ -140,7 +139,9 @@ class ProcessController extends Controller if (! ($this->showFullscreen || $this->view->compact)) { $controls->add($this->getProcessTabs($bp, $renderer)); + $controls->getAttributes()->add('class', 'separated'); } + $controls->add(Breadcrumb::create(clone $renderer)); if (! $this->showFullscreen && ! $this->view->compact) { $controls->add( From f93a4801073fe79a8452c8a59d83792be2b105f3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 Feb 2019 12:28:09 +0100 Subject: [PATCH 94/99] tree: Add hover effect --- public/css/module.less | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/public/css/module.less b/public/css/module.less index 06f0f07..af18a0f 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -154,10 +154,11 @@ ul.bp { // header style li.process > div { - padding-bottom: .25em; + padding: .291666667em 0; border-bottom: 1px solid @gray-light; > a.toggle { + min-width: 1.25em; // So that process icons align with their node's icons color: @gray; } @@ -189,11 +190,16 @@ ul.bp { > li { padding: @vertical-tree-item-gap 0; + &:first-child { + margin-top: @vertical-tree-item-gap; + } + &.process { padding-bottom: 0; - > div { - margin-bottom: @vertical-tree-item-gap; + &:first-child { + margin-top: 0; + padding-top: 0; } } } @@ -203,6 +209,7 @@ ul.bp { li:not(.process) { display: flex; align-items: center; + padding-left: .25em; > * { margin-right: .5em; @@ -244,6 +251,25 @@ ul.bp { } } } + + // hover style + li.process:hover > div { + background-color: #dae4e6; + } + li:not(.process):hover { + background-color: #dae4e6; + } + + li.process > div > .state-ball, + li:not(.process) > .state-ball { + border: .2em solid white; + + &.size-s { + width: 1.2em; + height: 1.2em; + line-height: 1.2em; + } + } } /** BEGIN Dashboard **/ From 670a29e5f78d87b3fbc57d2635ffd6242a96305c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 Feb 2019 13:15:14 +0100 Subject: [PATCH 95/99] Breadcrumb: Show a node's state --- .../Renderer/TileRenderer/NodeTile.php | 12 ++++++++++++ public/css/module.less | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index a55ef50..108f84b 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Businessprocess\Renderer\TileRenderer; +use Icinga\Date\DateFormatter; use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\HostNode; use Icinga\Module\Businessprocess\ImportedNode; @@ -9,6 +10,7 @@ use Icinga\Module\Businessprocess\MonitoredNode; use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\Renderer\Renderer; use Icinga\Module\Businessprocess\ServiceNode; +use Icinga\Module\Businessprocess\Web\Component\StateBall; use Icinga\Web\Url; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; @@ -86,6 +88,16 @@ class NodeTile extends BaseHtmlElement } $link = $this->getMainNodeLink(); + if ($renderer->isBreadcrumb()) { + $link->prepend((new StateBall(strtolower($node->getStateName())))->addAttributes([ + 'title' => sprintf( + '%s %s', + $node->getStateName(), + DateFormatter::timeSince($node->getLastStateChange()) + ) + ])); + } + $this->add($link); if ($node instanceof BpNode && !$renderer->isBreadcrumb()) { diff --git a/public/css/module.less b/public/css/module.less index af18a0f..d74ae6e 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -262,12 +262,12 @@ ul.bp { li.process > div > .state-ball, li:not(.process) > .state-ball { - border: .2em solid white; + border: .15em solid white; &.size-s { - width: 1.2em; - height: 1.2em; - line-height: 1.2em; + width: 1.15em; + height: 1.15em; + line-height: 1.15em; } } } @@ -594,6 +594,17 @@ td > a > .badges { &:focus { outline: none; } + + > .state-ball { + margin-right: .5em; + border: .15em solid white; + + &.size-s { + width: 1.15em; + height: 1.15em; + line-height: 1.15em; + } + } } .breadcrumb li { border: 1px solid @gray-lighter; From 0fbb8956e919c9df48b95d21555902ca1a0dbf27 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 Feb 2019 15:04:25 +0100 Subject: [PATCH 96/99] node/impact: Don't show colored background for breadcrumbs There are stateballs now. --- application/controllers/NodeController.php | 4 +-- public/css/module.less | 29 ---------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index 2a5af66..dc6c5fc 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -91,9 +91,7 @@ class NodeController extends Controller ->setPath(array_shift($parentAndPath)); $bc = Breadcrumb::create($renderer); - $bc->getAttributes() - ->set('data-base-target', '_next') - ->add('class', 'with-states'); + $bc->getAttributes()->set('data-base-target', '_next'); $content->add($bc); } } diff --git a/public/css/module.less b/public/css/module.less index d74ae6e..68e1a4d 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -624,35 +624,6 @@ td > a > .badges { } } } -.breadcrumb.with-states li { - &.critical { background: @colorCritical; } - &.critical.handled { background: @colorCriticalHandled; } - &.unknown { background: @colorUnknown; } - &.unknown.handled { background: @colorUnknownHandled; } - &.warning { background: @colorWarning; } - &.warning.handled { background: @colorWarningHandled; } - &.ok { background: @colorOk; } - - &.process-node a { color: white; } -} -.breadcrumb.with-states li:not(:last-of-type) { - &.critical a:after { border-left-color: @colorCritical; } - &.critical.handled a:after { border-left-color: @colorCriticalHandled; } - &.unknown a:after { border-left-color: @colorUnknown; } - &.unknown.handled a:after { border-left-color: @colorUnknownHandled; } - &.warning a:after { border-left-color: @colorWarning; } - &.warning.handled a:after { border-left-color: @colorWarningHandled; } - &.ok a:after { border-left-color: @colorOk; } -} -.breadcrumb.with-states li:last-of-type { - &.critical { border-color: @colorCritical; } - &.critical.handled { border-color: @colorCriticalHandled; } - &.unknown { border-color: @colorUnknown; } - &.unknown.handled { border-color: @colorUnknownHandled; } - &.warning { border-color: @colorWarning; } - &.warning.handled { border-color: @colorWarningHandled; } - &.ok { border-color: @colorOk; } -} .breadcrumb li:not(:last-of-type) a:before, .breadcrumb li:not(:last-of-type) a:after { content: " "; From 617d43daa4fc0ecf89dbe07c0296d6a145267efe Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 Feb 2019 10:22:50 +0100 Subject: [PATCH 97/99] css: Make it easier for browsers to render state-balls circular --- public/css/module.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/css/module.less b/public/css/module.less index 68e1a4d..0ba5dab 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -265,9 +265,9 @@ ul.bp { border: .15em solid white; &.size-s { - width: 1.15em; - height: 1.15em; - line-height: 1.15em; + width: 7em/6em; + height: 7em/6em; + line-height: 7em/6em; } } } From 6ca68ab46b90900497088044b9d697979e385b27 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 Feb 2019 13:23:54 +0100 Subject: [PATCH 98/99] css: Make state-balls in breadcrumbs also easier to render --- public/css/module.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/css/module.less b/public/css/module.less index 0ba5dab..56c4223 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -600,9 +600,9 @@ td > a > .badges { border: .15em solid white; &.size-s { - width: 1.15em; - height: 1.15em; - line-height: 1.15em; + width: 7em/6em; + height: 7em/6em; + line-height: 7em/6em; } } } From 77a63cb94e0cd8423070cf5a741777af405f2a5f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 Feb 2019 14:01:44 +0100 Subject: [PATCH 99/99] Upgrade Sortable.js Now on tag/1.8.3 --- public/js/vendor/Sortable.js | 40 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/public/js/vendor/Sortable.js b/public/js/vendor/Sortable.js index f1d85de..edb4e1c 100644 --- a/public/js/vendor/Sortable.js +++ b/public/js/vendor/Sortable.js @@ -132,6 +132,12 @@ return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse' ? 'vertical' : 'horizontal'; } + if (child1 && firstChildCSS.float !== 'none') { + var touchingSideChild2 = firstChildCSS.float === 'left' ? 'left' : 'right'; + + return child2 && (secondChildCSS.clear === 'both' || secondChildCSS.clear === touchingSideChild2) ? + 'vertical' : 'horizontal'; + } return (child1 && ( firstChildCSS.display === 'block' || @@ -456,6 +462,7 @@ }, true); var nearestEmptyInsertDetectEvent = function(evt) { + evt = evt.touches ? evt.touches[0] : evt; if (dragEl) { var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY); @@ -470,8 +477,9 @@ } }; // We do not want this to be triggered if completed (bubbling canceled), so only define it here - document.addEventListener('dragover', nearestEmptyInsertDetectEvent); - document.addEventListener('mousemove', nearestEmptyInsertDetectEvent); + _on(document, 'dragover', nearestEmptyInsertDetectEvent); + _on(document, 'mousemove', nearestEmptyInsertDetectEvent); + _on(document, 'touchmove', nearestEmptyInsertDetectEvent); /** * @class Sortable @@ -957,7 +965,6 @@ _onTouchMove: function (/**TouchEvent*/evt) { if (tapEvt) { - if (!evt.cancelable) return; var options = this.options, fallbackTolerance = options.fallbackTolerance, fallbackOffset = options.fallbackOffset, @@ -1695,9 +1702,9 @@ selector != null && ( selector[0] === '>' && el.parentNode === ctx && _matches(el, selector.substring(1)) || - _matches(el, selector) || - (includeCTX && el === ctx) - ) + _matches(el, selector) + ) || + includeCTX && el === ctx ) { return el; } @@ -1720,7 +1727,7 @@ function _globalDragOver(/**Event*/evt) { if (evt.dataTransfer) { - evt.dataTransfer.dropEffect = 'none'; + evt.dataTransfer.dropEffect = 'move'; } evt.cancelable && evt.preventDefault(); } @@ -1923,15 +1930,17 @@ } /** - * Gets the last child in the el, ignoring ghostEl + * Gets the last child in the el, ignoring ghostEl or invisible elements (clones) * @param {HTMLElement} el Parent element * @return {HTMLElement} The last child, ignoring ghostEl */ function _lastChild(el) { var last = el.lastElementChild; - if (last === ghostEl) { - last = el.children[el.childElementCount - 2]; + while (last === ghostEl || last.style.display === 'none') { + last = last.previousElementSibling; + + if (!last) break; } return last || null; @@ -1943,12 +1952,13 @@ mouseOnOppAxis = axis === 'vertical' ? evt.clientX : evt.clientY, targetS2 = axis === 'vertical' ? elRect.bottom : elRect.right, targetS1Opp = axis === 'vertical' ? elRect.left : elRect.top, - targetS2Opp = axis === 'vertical' ? elRect.right : elRect.bottom; + targetS2Opp = axis === 'vertical' ? elRect.right : elRect.bottom, + spacer = 10; return ( - mouseOnOppAxis > targetS1Opp && - mouseOnOppAxis < targetS2Opp && - mouseOnAxis > targetS2 + axis === 'vertical' ? + (mouseOnOppAxis > targetS2Opp + spacer || mouseOnOppAxis <= targetS2Opp && mouseOnAxis > targetS2 && mouseOnOppAxis >= targetS1Opp) : + (mouseOnAxis > targetS2 && mouseOnOppAxis > targetS1Opp || mouseOnAxis <= targetS2 && mouseOnOppAxis > targetS2Opp + spacer) ); } @@ -2334,6 +2344,6 @@ // Export - Sortable.version = '1.8.1'; + Sortable.version = '1.8.3'; return Sortable; }); \ No newline at end of file