From 606cf12e743c4b6102532e1d8aef70b391c8695b Mon Sep 17 00:00:00 2001 From: Ran Byron Date: Thu, 16 May 2019 06:43:46 -0600 Subject: [PATCH] Dashboard grid React migration #1 (#3722) * Dashboard grid React migration * Updated tests * Fixes comments * One col layout * Tests unskipped * Test fixes * Test fix * AutoHeight feature * Kebab-cased * Get rid of lazyInjector * Replace react-grid-layout with patched fork to fix performance issues * Fix issue with initial layout when page has a scrollbar * Decrease polling interval (500ms is too slow) * Rename file to match it's contents * Added some notes and very minor fixes * Fix Remove widget button (should be visible only in editing mode); fix widget actions menu * Fixed missing grid markings * Enhanced resize handle * Updated placeholder color * Render DashboardGrid only when dashboard is loaded --- .../dashboards/AutoHeightController.js | 110 +++++ .../components/dashboards/DashboardGrid.jsx | 213 ++++++++++ .../components/dashboards/dashboard-grid.less | 7 + .../dashboards/gridstack/gridstack.js | 87 ---- .../dashboards/gridstack/gridstack.less | 55 --- .../components/dashboards/gridstack/index.js | 400 ------------------ client/app/components/dashboards/widget.html | 11 +- client/app/components/dashboards/widget.js | 30 +- client/app/components/dashboards/widget.less | 30 ++ client/app/config/dashboard-grid-options.js | 8 +- client/app/config/index.js | 3 - client/app/pages/dashboards/dashboard.html | 25 +- client/app/pages/dashboards/dashboard.js | 108 +++-- client/app/pages/dashboards/dashboard.less | 61 +-- .../dashboards/public-dashboard-page.html | 18 +- .../pages/dashboards/public-dashboard-page.js | 5 +- client/app/services/dashboard.js | 3 +- client/app/services/widget.js | 23 +- .../integration/dashboard/dashboard_spec.js | 49 ++- package-lock.json | 39 +- package.json | 2 +- 21 files changed, 574 insertions(+), 713 deletions(-) create mode 100644 client/app/components/dashboards/AutoHeightController.js create mode 100644 client/app/components/dashboards/DashboardGrid.jsx create mode 100644 client/app/components/dashboards/dashboard-grid.less delete mode 100644 client/app/components/dashboards/gridstack/gridstack.js delete mode 100644 client/app/components/dashboards/gridstack/gridstack.less delete mode 100644 client/app/components/dashboards/gridstack/index.js diff --git a/client/app/components/dashboards/AutoHeightController.js b/client/app/components/dashboards/AutoHeightController.js new file mode 100644 index 0000000000..eddab575ab --- /dev/null +++ b/client/app/components/dashboards/AutoHeightController.js @@ -0,0 +1,110 @@ +import { includes, reduce, some } from 'lodash'; + +// TODO: Revisit this implementation when migrating widget component to React + +const WIDGET_SELECTOR = '[data-widgetid="{0}"]'; +const WIDGET_CONTENT_SELECTOR = [ + '.widget-header', // header + 'visualization-renderer', // visualization + '.scrollbox .alert', // error state + '.spinner-container', // loading state + '.tile__bottom-control', // footer +].join(','); +const INTERVAL = 200; + +export default class AutoHeightController { + widgets = {}; + + interval = null; + + onHeightChange = null; + + constructor(handler) { + this.onHeightChange = handler; + } + + update(widgets) { + const newWidgetIds = widgets + .filter(widget => widget.options.position.autoHeight) + .map(widget => widget.id.toString()); + + // added + newWidgetIds + .filter(id => !includes(Object.keys(this.widgets), id)) + .forEach(this.add); + + // removed + Object.keys(this.widgets) + .filter(id => !includes(newWidgetIds, id)) + .forEach(this.remove); + } + + add = (id) => { + if (this.isEmpty()) { + this.start(); + } + + const selector = WIDGET_SELECTOR.replace('{0}', id); + this.widgets[id] = [ + function getHeight() { + const widgetEl = document.querySelector(selector); + if (!widgetEl) { + return undefined; // safety + } + + // get all content elements + const els = widgetEl.querySelectorAll(WIDGET_CONTENT_SELECTOR); + + // calculate accumulated height + return reduce(els, (acc, el) => { + const height = el ? el.getBoundingClientRect().height : 0; + return acc + height; + }, 0); + }, + ]; + }; + + remove = (id) => { + // not actually deleting from this.widgets to prevent case of unwanted re-adding + this.widgets[id.toString()] = false; + + if (this.isEmpty()) { + this.stop(); + } + }; + + exists = id => !!this.widgets[id.toString()]; + + isEmpty = () => !some(this.widgets); + + checkHeightChanges = () => { + Object.keys(this.widgets).forEach((id) => { + const [getHeight, prevHeight] = this.widgets[id]; + const height = getHeight(); + if (height && height !== prevHeight) { + this.widgets[id][1] = height; // save + this.onHeightChange(id, height); // dispatch + } + }); + }; + + start = () => { + this.stop(); + this.interval = setInterval(this.checkHeightChanges, INTERVAL); + }; + + stop = () => { + clearInterval(this.interval); + }; + + resume = () => { + if (!this.isEmpty()) { + this.start(); + } + }; + + destroy = () => { + this.stop(); + this.widgets = null; + } +} diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx new file mode 100644 index 0000000000..d8a0fe3da2 --- /dev/null +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -0,0 +1,213 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { chain, cloneDeep, find } from 'lodash'; +import { react2angular } from 'react2angular'; +import cx from 'classnames'; +import { Responsive, WidthProvider } from 'react-grid-layout'; +import { DashboardWidget } from '@/components/dashboards/widget'; +import { FiltersType } from '@/components/Filters'; +import cfg from '@/config/dashboard-grid-options'; +import AutoHeightController from './AutoHeightController'; + +import 'react-grid-layout/css/styles.css'; +import './dashboard-grid.less'; + +const ResponsiveGridLayout = WidthProvider(Responsive); + +const WidgetType = PropTypes.shape({ + id: PropTypes.number.isRequired, + options: PropTypes.shape({ + position: PropTypes.shape({ + col: PropTypes.number.isRequired, + row: PropTypes.number.isRequired, + sizeY: PropTypes.number.isRequired, + minSizeY: PropTypes.number.isRequired, + maxSizeY: PropTypes.number.isRequired, + sizeX: PropTypes.number.isRequired, + minSizeX: PropTypes.number.isRequired, + maxSizeX: PropTypes.number.isRequired, + }).isRequired, + }).isRequired, +}); + +const SINGLE = 'single-column'; +const MULTI = 'multi-column'; + +class DashboardGrid extends React.Component { + static propTypes = { + isEditing: PropTypes.bool.isRequired, + dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + widgets: PropTypes.arrayOf(WidgetType).isRequired, + filters: FiltersType, + onBreakpointChange: PropTypes.func, + onRemoveWidget: PropTypes.func, + onLayoutChange: PropTypes.func, + }; + + static defaultProps = { + filters: [], + onRemoveWidget: () => {}, + onLayoutChange: () => {}, + onBreakpointChange: () => {}, + }; + + static normalizeFrom(widget) { + const { id, options: { position: pos } } = widget; + + return { + i: id.toString(), + x: pos.col, + y: pos.row, + w: pos.sizeX, + h: pos.sizeY, + minW: pos.minSizeX, + maxW: pos.maxSizeX, + minH: pos.minSizeY, + maxH: pos.maxSizeY, + }; + } + + mode = null; + + autoHeightCtrl = null; + + constructor(props) { + super(props); + + this.state = { + layouts: {}, + disableAnimations: true, + }; + + // init AutoHeightController + this.autoHeightCtrl = new AutoHeightController(this.onWidgetHeightUpdated); + this.autoHeightCtrl.update(this.props.widgets); + } + + componentDidMount() { + this.onBreakpointChange(document.body.offsetWidth <= cfg.mobileBreakPoint ? SINGLE : MULTI); + // Work-around to disable initial animation on widgets; `measureBeforeMount` doesn't work properly: + // it disables animation, but it cannot detect scrollbars. + setTimeout(() => { + this.setState({ disableAnimations: false }); + }, 50); + } + + componentDidUpdate() { + // update, in case widgets added or removed + this.autoHeightCtrl.update(this.props.widgets); + } + + componentWillUnmount() { + this.autoHeightCtrl.destroy(); + } + + onLayoutChange = (_, layouts) => { + // workaround for when dashboard starts at single mode and then multi is empty or carries single col data + // fixes test dashboard_spec['shows widgets with full width'] + // TODO: open react-grid-layout issue + if (layouts[MULTI]) { + this.setState({ layouts }); + } + + // workaround for https://github.com/STRML/react-grid-layout/issues/889 + // remove next line when fix lands + this.mode = document.body.offsetWidth <= cfg.mobileBreakPoint ? SINGLE : MULTI; + // end workaround + + // don't save single column mode layout + if (this.mode === SINGLE) { + return; + } + + const normalized = chain(layouts[MULTI]) + .keyBy('i') + .mapValues(this.normalizeTo) + .value(); + + this.props.onLayoutChange(normalized); + }; + + onBreakpointChange = (mode) => { + this.mode = mode; + this.props.onBreakpointChange(mode === SINGLE); + }; + + // height updated by auto-height + onWidgetHeightUpdated = (widgetId, newHeight) => { + this.setState(({ layouts }) => { + const layout = cloneDeep(layouts[MULTI]); // must clone to allow react-grid-layout to compare prev/next state + const item = find(layout, { i: widgetId.toString() }); + if (item) { + // update widget height + item.h = Math.ceil((newHeight + cfg.margins) / cfg.rowHeight); + } + + return { layouts: { [MULTI]: layout } }; + }); + }; + + // height updated by manual resize + onWidgetResize = (layout, oldItem, newItem) => { + if (oldItem.h !== newItem.h) { + this.autoHeightCtrl.remove(Number(newItem.i)); + } + + this.autoHeightCtrl.resume(); + }; + + normalizeTo = layout => ({ + col: layout.x, + row: layout.y, + sizeX: layout.w, + sizeY: layout.h, + autoHeight: this.autoHeightCtrl.exists(layout.i), + }); + + render() { + const className = cx('dashboard-wrapper', this.props.isEditing ? 'editing-mode' : 'preview-mode'); + const { onRemoveWidget, dashboard, widgets } = this.props; + + return ( +
+ + {widgets.map(widget => ( +
+ onRemoveWidget(widget.id)} + /> +
+ ))} +
+
+ ); + } +} + +export default function init(ngModule) { + ngModule.component('dashboardGrid', react2angular(DashboardGrid)); +} + +init.init = true; diff --git a/client/app/components/dashboards/dashboard-grid.less b/client/app/components/dashboards/dashboard-grid.less new file mode 100644 index 0000000000..83af1a7843 --- /dev/null +++ b/client/app/components/dashboards/dashboard-grid.less @@ -0,0 +1,7 @@ +.react-grid-layout { + &.disable-animations { + & > .react-grid-item { + transition: none !important; + } + } +} diff --git a/client/app/components/dashboards/gridstack/gridstack.js b/client/app/components/dashboards/gridstack/gridstack.js deleted file mode 100644 index 24e71b66e2..0000000000 --- a/client/app/components/dashboards/gridstack/gridstack.js +++ /dev/null @@ -1,87 +0,0 @@ -import $ from 'jquery'; -import _ from 'lodash'; -import 'jquery-ui/ui/widgets/draggable'; -import 'jquery-ui/ui/widgets/droppable'; -import 'jquery-ui/ui/widgets/resizable'; -import 'gridstack/dist/gridstack.css'; - -// eslint-disable-next-line import/first -import gridstack from 'gridstack'; - -function sequence(...fns) { - fns = _.filter(fns, _.isFunction); - if (fns.length > 0) { - return function sequenceWrapper(...args) { - for (let i = 0; i < fns.length; i += 1) { - fns[i].apply(this, args); - } - }; - } - return _.noop; -} - -// eslint-disable-next-line import/prefer-default-export -function JQueryUIGridStackDragDropPlugin(grid) { - gridstack.GridStackDragDropPlugin.call(this, grid); -} - -gridstack.GridStackDragDropPlugin.registerPlugin(JQueryUIGridStackDragDropPlugin); - -JQueryUIGridStackDragDropPlugin.prototype = Object.create(gridstack.GridStackDragDropPlugin.prototype); -JQueryUIGridStackDragDropPlugin.prototype.constructor = JQueryUIGridStackDragDropPlugin; - -JQueryUIGridStackDragDropPlugin.prototype.resizable = function resizable(el, opts, key, value) { - el = $(el); - if (opts === 'disable' || opts === 'enable') { - el.resizable(opts); - } else if (opts === 'option') { - el.resizable(opts, key, value); - } else { - el.resizable(_.extend({}, this.grid.opts.resizable, { - // run user-defined callback before internal one - start: sequence(this.grid.opts.resizable.start, opts.start), - // this and next - run user-defined callback after internal one - stop: sequence(opts.stop, this.grid.opts.resizable.stop), - resize: sequence(opts.resize, this.grid.opts.resizable.resize), - })); - } - return this; -}; - -JQueryUIGridStackDragDropPlugin.prototype.draggable = function draggable(el, opts) { - el = $(el); - if (opts === 'disable' || opts === 'enable') { - el.draggable(opts); - } else { - el.draggable(_.extend({}, this.grid.opts.draggable, { - containment: this.grid.opts.isNested ? this.grid.container.parent() : null, - // run user-defined callback before internal one - start: sequence(this.grid.opts.draggable.start, opts.start), - // this and next - run user-defined callback after internal one - stop: sequence(opts.stop, this.grid.opts.draggable.stop), - drag: sequence(opts.drag, this.grid.opts.draggable.drag), - })); - } - return this; -}; - -JQueryUIGridStackDragDropPlugin.prototype.droppable = function droppable(el, opts) { - el = $(el); - if (opts === 'disable' || opts === 'enable') { - el.droppable(opts); - } else { - el.droppable({ - accept: opts.accept, - }); - } - return this; -}; - -JQueryUIGridStackDragDropPlugin.prototype.isDroppable = function isDroppable(el) { - return Boolean($(el).data('droppable')); -}; - -JQueryUIGridStackDragDropPlugin.prototype.on = function on(el, eventName, callback) { - $(el).on(eventName, callback); - return this; -}; diff --git a/client/app/components/dashboards/gridstack/gridstack.less b/client/app/components/dashboards/gridstack/gridstack.less deleted file mode 100644 index 53fa032bb1..0000000000 --- a/client/app/components/dashboards/gridstack/gridstack.less +++ /dev/null @@ -1,55 +0,0 @@ -.grid-stack { - // Same options as in JS - @gridstack-margin: 15px; - @gridstack-width: 6; - - margin-right: -@gridstack-margin; - - .gridstack-columns(@column, @total) when (@column > 0) { - @value: 100% * (@column / @total); - > .grid-stack-item[data-gs-min-width="@{column}"] { min-width: @value } - > .grid-stack-item[data-gs-max-width="@{column}"] { max-width: @value } - > .grid-stack-item[data-gs-width="@{column}"] { width: @value } - > .grid-stack-item[data-gs-x="@{column}"] { left: @value } - - .gridstack-columns((@column - 1), @total); // next iteration - } - - .gridstack-columns(@gridstack-width, @gridstack-width); - - .grid-stack-item { - .grid-stack-item-content { - overflow: visible !important; - box-shadow: none !important; - opacity: 1 !important; - left: 0 !important; - right: @gridstack-margin !important; - } - - .ui-resizable-handle { - background: none !important; - - &.ui-resizable-w, - &.ui-resizable-sw { - left: 0 !important; - } - - &.ui-resizable-e, - &.ui-resizable-se { - right: @gridstack-margin !important; - } - } - - &.grid-stack-placeholder > .placeholder-content { - border: 0; - background: rgba(0, 0, 0, 0.05); - border-radius: 3px; - left: 0 !important; - right: @gridstack-margin !important; - } - } - - &.grid-stack-one-column-mode > .grid-stack-item { - margin-bottom: @gridstack-margin !important; - } -} diff --git a/client/app/components/dashboards/gridstack/index.js b/client/app/components/dashboards/gridstack/index.js deleted file mode 100644 index 7189540f0d..0000000000 --- a/client/app/components/dashboards/gridstack/index.js +++ /dev/null @@ -1,400 +0,0 @@ -import $ from 'jquery'; -import _ from 'lodash'; -import './gridstack'; -import './gridstack.less'; - -function toggleAutoHeightClass($element, isEnabled) { - const className = 'widget-auto-height-enabled'; - if (isEnabled) { - $element.addClass(className); - } else { - $element.removeClass(className); - } -} - -function computeAutoHeight($element, grid, node, minHeight, maxHeight) { - const wrapper = $element[0]; - const element = wrapper.querySelector('.scrollbox, .spinner-container'); - - let resultHeight = _.isObject(node) ? node.height : 1; - if (element) { - const childrenBounds = _.chain(element.children) - .map((child) => { - const bounds = child.getBoundingClientRect(); - const style = window.getComputedStyle(child); - return { - top: bounds.top - parseFloat(style.marginTop), - bottom: bounds.bottom + parseFloat(style.marginBottom), - }; - }) - .reduce((result, bounds) => ({ - top: Math.min(result.top, bounds.top), - bottom: Math.max(result.bottom, bounds.bottom), - })) - .value() || { top: 0, bottom: 0 }; - - // Height of controls outside visualization area - const bodyWrapper = wrapper.querySelector('.body-container'); - if (bodyWrapper) { - const elementStyle = window.getComputedStyle(element); - const controlsHeight = _.chain(bodyWrapper.children) - .filter(n => n !== element) - .reduce((result, n) => { - const b = n.getBoundingClientRect(); - return result + (b.bottom - b.top); - }, 0) - .value(); - - const additionalHeight = grid.opts.verticalMargin + - // include container paddings too - parseFloat(elementStyle.paddingTop) + parseFloat(elementStyle.paddingBottom) + - // add few pixels for scrollbar (if visible) - (element.scrollWidth > element.offsetWidth ? 16 : 0); - - const contentsHeight = childrenBounds.bottom - childrenBounds.top; - - const cellHeight = grid.cellHeight() + grid.opts.verticalMargin; - resultHeight = Math.ceil(Math.round(controlsHeight + contentsHeight + additionalHeight) / cellHeight); - } - } - - // minHeight <= resultHeight <= maxHeight - return Math.min(Math.max(minHeight, resultHeight), maxHeight); -} - -function gridstack($parse, dashboardGridOptions) { - return { - restrict: 'A', - replace: false, - scope: { - editing: '=', - batchUpdate: '=', // set by directive - for using in wrapper components - onLayoutChanged: '=', - isOneColumnMode: '=', - }, - controller() { - this.$el = null; - - this.resizingWidget = null; - this.draggingWidget = null; - - this.grid = () => (this.$el ? this.$el.data('gridstack') : null); - - this._updateStyles = () => { - const grid = this.grid(); - if (grid) { - // compute real grid height; `gridstack` sometimes uses only "dirty" - // items and computes wrong height - const gridHeight = _.chain(grid.grid.nodes) - .map(node => node.y + node.height) - .max() - .value(); - // `_updateStyles` is internal, but grid sometimes "forgets" - // to rebuild stylesheet, so we need to force it - if (_.isObject(grid._styles)) { - grid._styles._max = 0; // reset size cache - } - grid._updateStyles(gridHeight + 10); - } - }; - - this.addWidget = ($element, item, itemId) => { - const grid = this.grid(); - if (grid) { - grid.addWidget( - $element, - item.col, item.row, item.sizeX, item.sizeY, - false, // auto position - item.minSizeX, item.maxSizeX, item.minSizeY, item.maxSizeY, - itemId, - ); - this._updateStyles(); - } - }; - - this.updateWidget = ($element, item) => { - this.update((grid) => { - grid.update($element, item.col, item.row, item.sizeX, item.sizeY); - grid.minWidth($element, item.minSizeX); - grid.maxWidth($element, item.maxSizeX); - grid.minHeight($element, item.minSizeY); - grid.maxHeight($element, item.maxSizeY); - }); - }; - - this.removeWidget = ($element) => { - const grid = this.grid(); - if (grid) { - grid.removeWidget($element, false); - this._updateStyles(); - } - }; - - this.getNodeByElement = (element) => { - const grid = this.grid(); - if (grid && grid.grid) { - // This method seems to be internal - return grid.grid.getNodeDataByDOMEl($(element)); - } - }; - - this.setWidgetId = ($element, id) => { - // `gridstack` has no API method to change node id; but since it's not used - // by library, we can just update grid and DOM node - const node = this.getNodeByElement($element); - if (node) { - node.id = id; - $element.attr('data-gs-id', _.isUndefined(id) ? null : id); - } - }; - - this.setEditing = (value) => { - const grid = this.grid(); - if (grid) { - if (value) { - grid.enable(); - } else { - grid.disable(); - } - } - }; - - this.update = (callback) => { - const grid = this.grid(); - if (grid) { - grid.batchUpdate(); - try { - if (_.isFunction(callback)) { - callback(grid); - } - } finally { - grid.commit(); - this._updateStyles(); - } - } - }; - }, - link: ($scope, $element, $attr, controller) => { - const isOneColumnModeAssignable = _.isFunction($parse($attr.onLayoutChanged).assign); - let enablePolling = true; - - $element.addClass('grid-stack'); - $element.gridstack({ - auto: false, - verticalMargin: dashboardGridOptions.margins, - // real row height will be `cellHeight` + `verticalMargin` - cellHeight: dashboardGridOptions.rowHeight - dashboardGridOptions.margins, - width: dashboardGridOptions.columns, // columns - height: 0, // max rows (0 for unlimited) - animate: true, - float: false, - minWidth: dashboardGridOptions.mobileBreakPoint, - resizable: { - handles: 'e, se, s, sw, w', - start: (event, ui) => { - controller.resizingWidget = ui.element; - $(ui.element).trigger( - 'gridstack.resize-start', - controller.getNodeByElement(ui.element), - ); - }, - stop: (event, ui) => { - controller.resizingWidget = null; - $(ui.element).trigger( - 'gridstack.resize-end', - controller.getNodeByElement(ui.element), - ); - controller.update(); - }, - }, - draggable: { - start: (event, ui) => { - controller.draggingWidget = ui.helper; - $(ui.helper).trigger( - 'gridstack.drag-start', - controller.getNodeByElement(ui.helper), - ); - }, - stop: (event, ui) => { - controller.draggingWidget = null; - $(ui.helper).trigger( - 'gridstack.drag-end', - controller.getNodeByElement(ui.helper), - ); - controller.update(); - }, - }, - }); - controller.$el = $element; - - // `change` events sometimes fire too frequently (for example, - // on initial rendering when all widgets add themselves to grid, grid - // will fire `change` event will _all_ items available at that moment). - // Collect changed items, and then delegate event with some delay - let changedNodes = {}; - const triggerChange = _.debounce(() => { - _.each(changedNodes, (node) => { - if (node.el) { - $(node.el).trigger('gridstack.changed', node); - } - }); - if ($scope.onLayoutChanged) { - $scope.onLayoutChanged(); - } - changedNodes = {}; - }); - - $element.on('change', (event, nodes) => { - nodes = _.isArray(nodes) ? nodes : []; - _.each(nodes, (node) => { - changedNodes[node.id] = node; - }); - triggerChange(); - }); - - $scope.$watch('editing', (value) => { - controller.setEditing(!!value); - }); - - $scope.$on('$destroy', () => { - enablePolling = false; - controller.$el = null; - }); - - // `gridstack` does not provide API to detect when one-column mode changes. - // Just watch `$element` for specific class - function updateOneColumnMode() { - const grid = controller.grid(); - if (grid) { - const isOneColumnMode = $element.hasClass(grid.opts.oneColumnModeClass); - if ($scope.isOneColumnMode !== isOneColumnMode) { - $scope.isOneColumnMode = isOneColumnMode; - $scope.$applyAsync(); - } - } - - if (enablePolling) { - setTimeout(updateOneColumnMode, 150); - } - } - - // Start polling only if we can update scope binding; otherwise it - // will just waisting CPU time (example: public dashboards don't need it) - if (isOneColumnModeAssignable) { - updateOneColumnMode(); - } - }, - }; -} - -function gridstackItem($timeout) { - return { - restrict: 'A', - replace: false, - require: '^gridstack', - scope: { - gridstackItem: '=', - gridstackItemId: '@', - }, - link: ($scope, $element, $attr, controller) => { - let enablePolling = true; - let heightBeforeResize = null; - - controller.addWidget($element, $scope.gridstackItem, $scope.gridstackItemId); - - // these events are triggered only on user interaction - $element.on('gridstack.resize-start', () => { - const node = controller.getNodeByElement($element); - heightBeforeResize = _.isObject(node) ? node.height : null; - }); - $element.on('gridstack.resize-end', (event, node) => { - const item = $scope.gridstackItem; - if ( - _.isObject(node) && _.isObject(item) && - (node.height !== heightBeforeResize) && - (heightBeforeResize !== null) - ) { - item.autoHeight = false; - toggleAutoHeightClass($element, item.autoHeight); - $scope.$applyAsync(); - } - }); - - $element.on('gridstack.changed', (event, node) => { - const item = $scope.gridstackItem; - if (_.isObject(node) && _.isObject(item)) { - let dirty = false; - if (node.x !== item.col) { - item.col = node.x; - dirty = true; - } - if (node.y !== item.row) { - item.row = node.y; - dirty = true; - } - if (node.width !== item.sizeX) { - item.sizeX = node.width; - dirty = true; - } - if (node.height !== item.sizeY) { - item.sizeY = node.height; - dirty = true; - } - if (dirty) { - $scope.$applyAsync(); - } - } - }); - - $scope.$watch('gridstackItem.autoHeight', () => { - const item = $scope.gridstackItem; - if (_.isObject(item)) { - toggleAutoHeightClass($element, item.autoHeight); - } else { - toggleAutoHeightClass($element, false); - } - }); - - $scope.$watch('gridstackItemId', () => { - controller.setWidgetId($element, $scope.gridstackItemId); - }); - - $scope.$on('$destroy', () => { - enablePolling = false; - $timeout(() => { - controller.removeWidget($element); - }); - }); - - function update() { - if (!controller.resizingWidget && !controller.draggingWidget) { - const item = $scope.gridstackItem; - const grid = controller.grid(); - if (grid && _.isObject(item) && item.autoHeight) { - const sizeY = computeAutoHeight( - $element, grid, controller.getNodeByElement($element), - item.minSizeY, item.maxSizeY, - ); - if (sizeY !== item.sizeY) { - item.sizeY = sizeY; - controller.updateWidget($element, { sizeY }); - $scope.$applyAsync(); - } - } - } - if (enablePolling) { - setTimeout(update, 150); - } - } - - update(); - }, - }; -} - -export default function init(ngModule) { - ngModule.directive('gridstack', gridstack); - ngModule.directive('gridstackItem', gridstackItem); -} - -init.init = true; diff --git a/client/app/components/dashboards/widget.html b/client/app/components/dashboards/widget.html index b532ae0a4b..e1be1bf6d4 100644 --- a/client/app/components/dashboards/widget.html +++ b/client/app/components/dashboards/widget.html @@ -1,7 +1,7 @@
-
+
-