gristlabs_grist-core/app/client/components/buildViewSectionDom.ts
Jarosław Sadziński 59cf654190 (core) Minimazing widgets
Summary:
A feature that allows minimizing widgets on the ViewLayout.
- Code in ViewLayout and Layout hasn't been changed. Only some methods or variables were made public, and some events are now triggered when a section is dragged.
- Widgets can be collapsed or expanded (added back to the main area)
- Collapsed widgets can be expanded and shown as a popup
- Collapsed widgets support drugging, reordering, and transferring between the main and collapsed areas.

Test Plan: New test

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3779
2023-03-02 11:22:49 +01:00

209 lines
7.0 KiB
TypeScript

import BaseView from 'app/client/components/BaseView';
import {GristDoc} from 'app/client/components/GristDoc';
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
import {filterBar} from 'app/client/ui/FilterBar';
import {cssIcon} from 'app/client/ui/RightPanelStyles';
import {makeCollapsedLayoutMenu} from 'app/client/ui/ViewLayoutMenu';
import {cssDotsIconWrapper, cssMenu, viewSectionMenu} from 'app/client/ui/ViewSectionMenu';
import {buildWidgetTitle} from 'app/client/ui/WidgetTitle';
import {getWidgetTypes} from 'app/client/ui/widgetTypes';
import {colors, isNarrowScreenObs, mediaSmall, testId, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {menu} from 'app/client/ui2018/menus';
import {Computed, dom, DomElementArg, Observable, styled} from 'grainjs';
import {defaultMenuOptions} from 'popweasel';
export function buildCollapsedSectionDom(options: {
gristDoc: GristDoc,
sectionRowId: number|string,
}, ...domArgs: DomElementArg[]) {
const {gristDoc, sectionRowId} = options;
if (typeof sectionRowId === 'string') {
return cssMiniSection(
dom('span.viewsection_title_font',
'Empty'
)
);
}
const vs: ViewSectionRec = gristDoc.docModel.viewSections.getRowModel(sectionRowId);
const typeComputed = Computed.create(null, use => getWidgetTypes(use(vs.parentKey) as any).icon);
return cssMiniSection(
testId(`minilayout-section-${sectionRowId}`),
testId(`minilayout-section`),
cssDragHandle(
dom.domComputed(typeComputed, (type) => icon(type)),
dom('div', {style: 'margin-right: 16px;'}),
dom.maybe((use) => use(use(vs.table).summarySourceTable), () => cssSigmaIcon('Pivot', testId('sigma'))),
dom('span.viewsection_title_font', testId('viewsection-title'),
dom.text(vs.titleDef),
),
),
cssMenu(
testId('section-menu-viewLayout'),
cssDotsIconWrapper(cssIcon('Dots')),
menu(_ctl => makeCollapsedLayoutMenu(vs, gristDoc), {
...defaultMenuOptions,
placement: 'bottom-end',
})
),
...domArgs
);
}
export function buildViewSectionDom(options: {
gristDoc: GristDoc,
sectionRowId: number,
isResizing?: Observable<boolean>
viewModel?: ViewRec,
// Should show drag anchor.
draggable?: boolean, /* defaults to true */
// Should show green bar on the left (but preserves active-section class).
focusable?: boolean, /* defaults to true */
tableNameHidden?: boolean,
widgetNameHidden?: boolean,
}) {
const isResizing = options.isResizing ?? Observable.create(null, false);
const {gristDoc, sectionRowId, viewModel, draggable = true, focusable = true} = options;
// Creating normal section dom
const vs: ViewSectionRec = gristDoc.docModel.viewSections.getRowModel(sectionRowId);
return dom('div.view_leaf.viewsection_content.flexvbox.flexauto',
testId(`viewlayout-section-${sectionRowId}`),
!options.isResizing ? dom.autoDispose(isResizing) : null,
cssViewLeaf.cls(''),
cssViewLeafInactive.cls('', (use) => !vs.isDisposed() && !use(vs.hasFocus)),
dom.cls('active_section', vs.hasFocus),
dom.cls('active_section--no-indicator', !focusable),
dom.maybe<BaseView|null>((use) => use(vs.viewInstance), (viewInstance) => dom('div.viewsection_title.flexhbox',
cssDragIcon('DragDrop',
dom.cls("viewsection_drag_indicator"),
// Makes element grabbable only if grist is not readonly.
dom.cls('layout_grabbable', (use) => !use(gristDoc.isReadonlyKo)),
!draggable ? dom.style("visibility", "hidden") : null
),
dom.maybe((use) => use(use(viewInstance.viewSection.table).summarySourceTable), () =>
cssSigmaIcon('Pivot', testId('sigma'))),
buildWidgetTitle(vs, options, testId('viewsection-title'), cssTestClick(testId("viewsection-blank"))),
viewInstance.buildTitleControls(),
dom('div.viewsection_buttons',
dom.create(viewSectionMenu, gristDoc, vs)
)
)),
dom.create(filterBar, gristDoc, vs),
dom.maybe<BaseView|null>(vs.viewInstance, (viewInstance) => [
dom('div.view_data_pane_container.flexvbox',
cssResizing.cls('', isResizing),
dom.maybe(viewInstance.disableEditing, () =>
dom('div.disable_viewpane.flexvbox', 'No data')
),
dom.maybe(viewInstance.isTruncated, () =>
dom('div.viewsection_truncated', 'Not all data is shown')
),
dom.cls((use) => 'viewsection_type_' + use(vs.parentKey)),
viewInstance.viewPane,
),
dom.maybe(use => !use(isNarrowScreenObs()), () => viewInstance.selectionSummary?.buildDom()),
]),
dom.on('mousedown', () => { viewModel?.activeSectionId(sectionRowId); }),
);
}
// With new widgetPopup it is hard to click on viewSection without a activating it, hence we
// add a little blank space to use in test.
const cssTestClick = styled(`div`, `
min-width: 2px;
`);
const cssSigmaIcon = styled(icon, `
bottom: 1px;
margin-right: 5px;
background-color: ${theme.lightText}
`);
const cssViewLeaf = styled('div', `
@media ${mediaSmall} {
& {
margin: 4px;
}
}
`);
const cssViewLeafInactive = styled('div', `
@media screen and ${mediaSmall} {
& {
overflow: hidden;
background: repeating-linear-gradient(
-45deg,
${theme.widgetInactiveStripesDark},
${theme.widgetInactiveStripesDark} 10px,
${theme.widgetInactiveStripesLight} 10px,
${theme.widgetInactiveStripesLight} 20px
);
border: 1px solid ${theme.widgetBorder};
border-radius: 4px;
padding: 0 2px;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
&.layout_vbox {
max-width: 32px;
}
&.layout_hbox {
max-height: 32px;
}
& > .viewsection_title.flexhbox {
position: absolute;
}
& > .view_data_pane_container,
& .viewsection_buttons,
& .grist-single-record__menu,
& > .filter_bar {
display: none;
}
}
`);
// z-index ensure it's above the resizer line, since it's hard to grab otherwise
const cssDragIcon = styled(icon, `
visibility: hidden;
--icon-color: ${colors.slate};
top: -1px;
z-index: 100;
.viewsection_title:hover &.layout_grabbable {
visibility: visible;
}
`);
// This class is added while sections are being resized (or otherwise edited), to ensure that the
// content of the section (such as an iframe) doesn't interfere with mouse drag-related events.
// (It assumes that contained elements do not set pointer-events to another value; if that were
// important then we'd need to use an overlay element during dragging.)
const cssResizing = styled('div', `
pointer-events: none;
`);
const cssMiniSection = styled('div.mini_section_container', `
--icon-color: ${colors.lightGreen};
display: flex;
background: ${theme.mainPanelBg};
align-items: center;
padding-right: 8px;
`);
const cssDragHandle = styled('div.draggable-handle', `
display: flex;
padding: 8px;
flex: 1;
padding-right: 16px;
`);