UNPKG

@gitlab/ui

Version:
287 lines (273 loc) 9.38 kB
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 };