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 @@