@gitlab/ui
Version:
GitLab UI Components
287 lines (273 loc) • 9.38 kB
JavaScript
import { GridStack } from 'gridstack';
import pickBy from 'lodash/pickBy';
import { breakpoints } from '../../../../utils/breakpoints';
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
const CURSOR_GRABBING_CLASS = '!gl-cursor-grabbing';
var script = {
name: 'GlGridLayout',
props: {
value: {
type: Object,
required: true
},
isStatic: {
type: Boolean,
required: false,
default: true
}
},
data() {
return {
grid: undefined,
gridPanels: []
};
},
computed: {
gridConfig() {
return this.value.panels.map(panel => {
const {
gridAttributes,
...otherProps
} = panel;
return {
...this.getPanelGridItemConfig(panel),
props: otherProps
};
});
}
},
watch: {
isStatic(value) {
var _this$grid;
(_this$grid = this.grid) === null || _this$grid === void 0 ? void 0 : _this$grid.setStatic(value);
},
gridConfig: {
handler(config) {
var _this$grid2;
(_this$grid2 = this.grid) === null || _this$grid2 === void 0 ? void 0 : _this$grid2.load(config);
},
deep: true
},
/**
* Data flow:
* 1. Initial: mounted → initGridStack() → grid.load(gridConfig) →
* grid.getGridItems() → initGridPanelSlots → gridPanels populated with DOM references
* 2. Updates: value.panels changes → two parallel paths:
* a. gridConfig changes → grid.load() updates grid layout (but not gridPanels)
* b. this watcher updates gridPanels with new panel properties
*/
'value.panels': {
handler(newPanels) {
if (this.gridPanels.length === 0) return;
// Only update panels that have changed to improve performance
newPanels.forEach(updatedPanel => {
const panel = this.gridPanels.find(p => p.id === updatedPanel.id);
if (panel) {
// Exclude `gridAttributes` from being included in the panel props as it's not a valid prop for the panel component
const panelPropsWithoutGridAttributes = pickBy(updatedPanel, (_, k) => k !== 'gridAttributes');
panel.props = {
...panelPropsWithoutGridAttributes
};
}
});
},
deep: true
}
},
mounted() {
this.initGridStack();
},
beforeDestroy() {
var _this$grid3;
const removeDom = Boolean(this.$el.parentElement);
(_this$grid3 = this.grid) === null || _this$grid3 === void 0 ? void 0 : _this$grid3.destroy(removeDom);
},
methods: {
// TODO: Refactor this to use render methods once Vue 3 migration is complete
// https://gitlab.com/gitlab-org/gitlab/-/issues/549095
async mountGridComponents(panels) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
scrollIntoView: false
};
// Ensure new panels are always rendered first
await this.$nextTick();
panels.forEach(panel => {
var _this$$refs$panelWrap;
const wrapper = (_this$$refs$panelWrap = this.$refs.panelWrappers) === null || _this$$refs$panelWrap === void 0 ? void 0 : _this$$refs$panelWrap.find(w => w.id === panel.id);
const widgetContentEl = panel.el.querySelector('.grid-stack-item-content');
if (wrapper && widgetContentEl) {
widgetContentEl.appendChild(wrapper);
}
});
if (options.scrollIntoView) {
const mostRecent = panels[panels.length - 1];
mostRecent.el.scrollIntoView({
behavior: 'smooth'
});
}
},
getGridItemForElement(el) {
return this.gridConfig.find(item => item.id === el.getAttribute('gs-id'));
},
initGridPanelSlots(gridElements) {
if (!gridElements) return;
this.gridPanels = gridElements.map(el => ({
...this.getGridItemForElement(el),
el
}));
this.mountGridComponents(this.gridPanels);
},
initGridStack() {
// See https://github.com/gridstack/gridstack.js/tree/master/doc#grid-options
this.grid = GridStack.init({
// Uniform gap between panels
margin: '8px',
// CSS Selector for finding the drag handle element
handle: '.grid-stack-item-handle',
/* Magic number 125px:
* After allowing for padding, and the panel title row, this leaves us with minimum 48px height for the cell content.
* This means text/content with our spacing scale can fit up to 49px without scrolling.
*/
cellHeight: '125px',
// Setting 1 in minRow prevents the grid collapsing when all panels are removed
minRow: 1,
// Define the number of columns for anything below a set width, defaults to fill the available space
columnOpts: {
breakpoints: [{
w: breakpoints.md,
c: 1
}]
},
alwaysShowResizeHandle: true,
animate: true,
float: true,
// Toggles user-customization of grid layout
staticGrid: this.isStatic
}, this.$refs.grid).load(this.gridConfig);
// Sync Vue components array with gridstack items
this.initGridPanelSlots(this.grid.getGridItems());
this.grid.on('dragstart', () => {
this.$el.classList.add(CURSOR_GRABBING_CLASS);
});
this.grid.on('dragstop', () => {
this.$el.classList.remove(CURSOR_GRABBING_CLASS);
});
this.grid.on('change', (_, items) => {
if (!items) return;
this.emitLayoutChanges(items);
});
this.grid.on('added', (_, items) => {
this.addGridPanels(items);
});
this.grid.on('removed', (_, items) => {
this.removeGridPanels(items);
});
},
getPanelGridItemConfig(_ref) {
let {
gridAttributes: {
xPos,
yPos,
width,
height,
minHeight,
minWidth,
maxHeight,
maxWidth
},
id
} = _ref;
const filterUndefinedValues = obj => pickBy(obj, value => value !== undefined);
// GridStack renders undefined layout values so we need to filter them out.
return filterUndefinedValues({
x: xPos,
y: yPos,
w: width,
h: height,
minH: minHeight,
minW: minWidth,
maxH: maxHeight,
maxW: maxWidth,
id
});
},
convertToGridAttributes(gridStackItem) {
return {
yPos: gridStackItem.y,
xPos: gridStackItem.x,
width: gridStackItem.w,
height: gridStackItem.h
};
},
removeGridPanels(items) {
items.forEach(item => {
const index = this.gridPanels.findIndex(c => c.id === item.id);
this.gridPanels.splice(index, 1);
// Finally, remove the gridstack element
item.el.remove();
});
},
addGridPanels(items) {
const newPanels = items.map(_ref2 => {
let {
grid,
...rest
} = _ref2;
return {
...rest
};
});
this.gridPanels.push(...newPanels);
this.mountGridComponents(newPanels, {
scrollIntoView: true
});
},
emitLayoutChanges(items) {
/**
* Uses JSON parse and stringify to remove object references.
* Lodash's `cloneDeep` retains circular references.
* See https://github.com/lodash/lodash/issues/4710#issuecomment-606892867 for details on cloneDeep circular references
* See https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm for the underlying mechanism used by Lodash
*/
const newValue = JSON.parse(JSON.stringify(this.value));
items.forEach(item => {
const panel = newValue.panels.find(p => p.id === item.id);
if (!panel) return;
panel.gridAttributes = {
...panel.gridAttributes,
...this.convertToGridAttributes(item)
};
});
this.$emit('input', newValue);
}
}
};
/* script */
const __vue_script__ = script;
/* template */
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{ref:"grid",staticClass:"grid-stack",attrs:{"data-testid":"gridstack-grid"}},_vm._l((_vm.gridPanels),function(panel){return _c('div',{key:panel.id,ref:"panelWrappers",refInFor:true,staticClass:"gl-h-full",class:{ 'gl-cursor-grab': !_vm.isStatic },attrs:{"id":panel.id,"data-testid":"gridstack-panel"}},[_vm._t("panel",null,null,{ panel: panel.props })],2)}),0)};
var __vue_staticRenderFns__ = [];
/* style */
const __vue_inject_styles__ = undefined;
/* scoped */
const __vue_scope_id__ = undefined;
/* module identifier */
const __vue_module_identifier__ = undefined;
/* functional template */
const __vue_is_functional_template__ = false;
/* style inject */
/* style inject SSR */
/* style inject shadow dom */
const __vue_component__ = /*#__PURE__*/__vue_normalize__(
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
__vue_inject_styles__,
__vue_script__,
__vue_scope_id__,
__vue_is_functional_template__,
__vue_module_identifier__,
false,
undefined,
undefined,
undefined
);
export { __vue_component__ as default };