mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Implement 'Print widget' option to print individual view sections.
Summary: - Supports multi-page printing with some aggressive css overrides. - Relies on a new function implemented by grist-plugin-api to print a multi-page CustomView. - Renders all rows for printing for scrolly-based views. Test Plan: Doesn't seem possible to do a selenium test for printing. Tested manually on Chrome, Firefox, and Safari. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2635
This commit is contained in:
parent
d2ad5edc46
commit
99ab09651e
@ -230,6 +230,9 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
||||
this.autoDispose(this.viewSection.activeSortSpec.subscribe(() => this.setCursorPos({rowIndex: 0})));
|
||||
|
||||
this.copySelection = ko.observable(null);
|
||||
|
||||
// Whether parts needed for printing should be rendered now.
|
||||
this._isPrinting = ko.observable(false);
|
||||
}
|
||||
Base.setBaseFor(BaseView);
|
||||
_.extend(Base.prototype, BackboneEvents);
|
||||
@ -612,6 +615,13 @@ BaseView.prototype.onResize = function() {
|
||||
BaseView.prototype.onRowResize = function(rowModels) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Called before and after printing this section.
|
||||
*/
|
||||
BaseView.prototype.prepareToPrint = function(onOff) {
|
||||
this._isPrinting(onOff);
|
||||
};
|
||||
|
||||
/**
|
||||
* Called to obtain the rowModel for the given rowId. Returns a rowModel if it belongs to the
|
||||
* section and is rendered, otherwise returns null.
|
||||
|
@ -89,6 +89,12 @@ export class CustomView extends Disposable {
|
||||
this.autoDispose(this.cursor.rowIndex.subscribe(this._updateCursor));
|
||||
}
|
||||
|
||||
public async triggerPrint() {
|
||||
if (!this.isDisposed() && this._rpc) {
|
||||
return await this._rpc.callRemoteFunc("print");
|
||||
}
|
||||
}
|
||||
|
||||
private _updateView(dataChange: boolean) {
|
||||
if (this.isDisposed()) { return; }
|
||||
if (this._rpc) {
|
||||
|
@ -79,10 +79,12 @@
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.detailview_record_detail.active {
|
||||
/* highlight active record in Card List by overlaying the active-section highlight */
|
||||
margin-left: -3px;
|
||||
border-left: 3px solid var(--grist-color-light-green);
|
||||
@media not print {
|
||||
.detailview_record_detail.active {
|
||||
/* highlight active record in Card List by overlaying the active-section highlight */
|
||||
margin-left: -3px;
|
||||
border-left: 3px solid var(--grist-color-light-green);
|
||||
}
|
||||
}
|
||||
|
||||
/*** single record ***/
|
||||
|
@ -4,6 +4,7 @@ var ko = require('knockout');
|
||||
var dom = require('app/client/lib/dom');
|
||||
var kd = require('app/client/lib/koDom');
|
||||
var koDomScrolly = require('app/client/lib/koDomScrolly');
|
||||
const {renderAllRows} = require('app/client/components/Printing');
|
||||
|
||||
require('app/client/lib/koUtil'); // Needed for subscribeInit.
|
||||
|
||||
@ -282,6 +283,11 @@ DetailView.prototype.buildDom = function() {
|
||||
}),
|
||||
koDomScrolly.scrolly(this.viewData, {fitToWidth: true},
|
||||
row => this.makeRecord(row)),
|
||||
|
||||
kd.maybe(this._isPrinting, () =>
|
||||
renderAllRows(this.tableModel, this.sortedRows.getKoArray().peek(), row =>
|
||||
this.makeRecord(row))
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return dom(
|
||||
|
@ -71,6 +71,7 @@
|
||||
left: 0px;
|
||||
overflow: hidden;
|
||||
width: 4rem; /* Also should match width for .gridview_header_corner, and the overlay elements */
|
||||
flex: none;
|
||||
|
||||
border-bottom: 1px solid var(--grist-color-dark-grey);
|
||||
background-color: var(--grist-color-light-grey);
|
||||
@ -89,6 +90,15 @@
|
||||
.gridview_data_row_num {
|
||||
background-color: var(--grist-color-light-grey) !important;
|
||||
}
|
||||
.gridview_header_backdrop_top {
|
||||
display: none;
|
||||
}
|
||||
.column_name.mod-add-column {
|
||||
display: none;
|
||||
}
|
||||
.gridview_data_header {
|
||||
background-color: var(--grist-color-light-grey) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========= Overlay styles ========== */
|
||||
|
@ -21,6 +21,7 @@ var BaseView = require('./BaseView');
|
||||
var selector = require('./Selector');
|
||||
var CopySelection = require('./CopySelection');
|
||||
|
||||
const {renderAllRows} = require('app/client/components/Printing');
|
||||
const {reportError} = require('app/client/models/AppModel');
|
||||
|
||||
// Grist UI Components
|
||||
@ -831,127 +832,132 @@ GridView.prototype.buildDom = function() {
|
||||
) //end hbox
|
||||
), // END COL HEADER BOX
|
||||
|
||||
koDomScrolly.scrolly(data, { paddingBottom: 80, paddingRight: 28 }, function(row) {
|
||||
// TODO. There are several ways to implement a cursor; similar concerns may arise
|
||||
// when implementing selection and cell editor.
|
||||
// (1) Class on 'div.field.field_clip'. Fewest elements, seems possibly best for
|
||||
// performance. Problem is: it's impossible to get cursor exactly right with a
|
||||
// one-sided border. Attaching a cursor as additional element inside the cell
|
||||
// truncates the cursor to the cell's inside because of 'overflow: hidden'.
|
||||
// (2) 'div.field' with 'div.field_clip' inside, on which a class is toggled. This
|
||||
// works well. The only concern is whether this slows down rendering. Would be
|
||||
// good to measure and compare rendering speed.
|
||||
// Related: perhaps the fastest rendering would be for a table.
|
||||
// (3) Separate element attached to the row, absolutely positioned at left
|
||||
// position and width of the selected cell. This works too. Requires
|
||||
// maintaining a list of leftOffsets (or measuring the cell's), and feels less
|
||||
// clean and more complicated than (2).
|
||||
|
||||
// IsRowActive and isCellActive are a significant optimization. IsRowActive is called
|
||||
// for all rows when cursor.rowIndex changes, but the value only changes for two of the
|
||||
// rows. IsCellActive is only subscribed to columns for the active row. This way, when
|
||||
// the cursor moves, there are (rows+2*columns) calls rather than rows*columns.
|
||||
var isRowActive = ko.computed(() => row._index() === self.cursor.rowIndex());
|
||||
return dom('div.gridview_row',
|
||||
dom.autoDispose(isRowActive),
|
||||
|
||||
// rowid dom
|
||||
dom('div.gridview_data_row_num',
|
||||
dom('div.gridview_data_row_info',
|
||||
kd.toggleClass('linked_dst', () => {
|
||||
// Must ensure that linkedRowId is not null to avoid drawing on rows whose
|
||||
// row ids are null.
|
||||
return self.linkedRowId() && self.linkedRowId() === row.getRowId();
|
||||
})
|
||||
),
|
||||
kd.text(function() { return row._index() + 1; }),
|
||||
|
||||
kd.scope(row._validationFailures, function(failures) {
|
||||
if (!row._isAddRow() && failures.length > 0) {
|
||||
return dom('div.validation_error_number', failures.length,
|
||||
kd.attr('title', function() {
|
||||
return "Validation failed: " +
|
||||
failures.map(function(val) { return val.name(); }).join(", ");
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
kd.toggleClass('selected', () =>
|
||||
!row._isAddRow() && self.cellSelector.isRowSelected(row._index())),
|
||||
dom.on('contextmenu', ev => self.maybeSelectRow(ev.currentTarget, row.getRowId())),
|
||||
menu(ctl => RowContextMenu({
|
||||
disableInsert: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.tableModel.tableMetaRow.onDemand()),
|
||||
disableDelete: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.getSelection().onlyAddRowSelected()),
|
||||
isViewSorted: self.viewSection.activeSortSpec.peek().length > 0,
|
||||
}), { trigger: ['contextmenu'] }),
|
||||
),
|
||||
|
||||
|
||||
dom('div.record',
|
||||
kd.toggleClass('record-add', row._isAddRow),
|
||||
kd.style('borderLeftWidth', v.borderWidthPx),
|
||||
kd.style('borderBottomWidth', v.borderWidthPx),
|
||||
//These are grabbed from v.optionsObj at start of GridView buildDom
|
||||
kd.toggleClass('record-hlines', vHorizontalGridlines),
|
||||
kd.toggleClass('record-vlines', vVerticalGridlines),
|
||||
kd.toggleClass('record-zebra', vZebraStripes),
|
||||
// even by 1-indexed rownum, so +1 (makes more sense for user-facing display stuff)
|
||||
kd.toggleClass('record-even', () => (row._index()+1) % 2 === 0 ),
|
||||
|
||||
self.comparison ? kd.cssClass(() => {
|
||||
var rowId = row.id();
|
||||
if (rightAddRows.has(rowId)) { return 'diff-remote'; }
|
||||
else if (rightRemoveRows.has(rowId)) { return 'diff-parent'; }
|
||||
else if (leftAddRows.has(rowId)) { return 'diff-local'; }
|
||||
return '';
|
||||
}) : null,
|
||||
|
||||
kd.foreach(v.viewFields(), function(field) {
|
||||
// Whether the cell has a cursor (possibly in an inactive view section).
|
||||
var isCellSelected = ko.computed(() =>
|
||||
isRowActive() && field._index() === self.cursor.fieldIndex());
|
||||
|
||||
// Whether the cell is active: has the cursor in the active section.
|
||||
var isCellActive = ko.computed(() => isCellSelected() && v.hasFocus());
|
||||
|
||||
// Whether the cell is part of an active copy-paste operation.
|
||||
var isCopyActive = ko.computed(function() {
|
||||
return self.copySelection() &&
|
||||
self.copySelection().isCellSelected(row.id(), field.colId());
|
||||
});
|
||||
var fieldBuilder = self.fieldBuilders.at(field._index());
|
||||
var isSelected = ko.computed(() => {
|
||||
return !row._isAddRow() &&
|
||||
!self.cellSelector.isCurrentSelectType(selector.NONE) &&
|
||||
ko.unwrap(self.isColSelected.at(field._index())) &&
|
||||
self.cellSelector.isRowSelected(row._index());
|
||||
});
|
||||
return dom(
|
||||
'div.field',
|
||||
kd.toggleClass('scissors', isCopyActive),
|
||||
dom.autoDispose(isCopyActive),
|
||||
dom.autoDispose(isCellSelected),
|
||||
dom.autoDispose(isCellActive),
|
||||
dom.autoDispose(isSelected),
|
||||
kd.style('width', field.widthPx),
|
||||
//TODO: Ensure that fields in a row resize when
|
||||
//a cell in that row becomes larger
|
||||
kd.style('borderRightWidth', v.borderWidthPx),
|
||||
kd.style('color', field.textColor),
|
||||
// If making a comparison, use the background exclusively for
|
||||
// marking that up.
|
||||
self.comparison ? null : kd.style('background-color', () => (row._isAddRow() || isSelected()) ? '' : field.fillColor()),
|
||||
|
||||
kd.toggleClass('selected', isSelected),
|
||||
fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}) //end scrolly
|
||||
koDomScrolly.scrolly(data, { paddingBottom: 80, paddingRight: 28 }, renderRow),
|
||||
|
||||
kd.maybe(this._isPrinting, () =>
|
||||
renderAllRows(this.tableModel, this.sortedRows.getKoArray().peek(), renderRow)
|
||||
),
|
||||
) // end scrollpane
|
||||
);// END MAIN VIEW BOX
|
||||
|
||||
function renderRow(row) {
|
||||
// TODO. There are several ways to implement a cursor; similar concerns may arise
|
||||
// when implementing selection and cell editor.
|
||||
// (1) Class on 'div.field.field_clip'. Fewest elements, seems possibly best for
|
||||
// performance. Problem is: it's impossible to get cursor exactly right with a
|
||||
// one-sided border. Attaching a cursor as additional element inside the cell
|
||||
// truncates the cursor to the cell's inside because of 'overflow: hidden'.
|
||||
// (2) 'div.field' with 'div.field_clip' inside, on which a class is toggled. This
|
||||
// works well. The only concern is whether this slows down rendering. Would be
|
||||
// good to measure and compare rendering speed.
|
||||
// Related: perhaps the fastest rendering would be for a table.
|
||||
// (3) Separate element attached to the row, absolutely positioned at left
|
||||
// position and width of the selected cell. This works too. Requires
|
||||
// maintaining a list of leftOffsets (or measuring the cell's), and feels less
|
||||
// clean and more complicated than (2).
|
||||
|
||||
// IsRowActive and isCellActive are a significant optimization. IsRowActive is called
|
||||
// for all rows when cursor.rowIndex changes, but the value only changes for two of the
|
||||
// rows. IsCellActive is only subscribed to columns for the active row. This way, when
|
||||
// the cursor moves, there are (rows+2*columns) calls rather than rows*columns.
|
||||
var isRowActive = ko.computed(() => row._index() === self.cursor.rowIndex());
|
||||
return dom('div.gridview_row',
|
||||
dom.autoDispose(isRowActive),
|
||||
|
||||
// rowid dom
|
||||
dom('div.gridview_data_row_num',
|
||||
dom('div.gridview_data_row_info',
|
||||
kd.toggleClass('linked_dst', () => {
|
||||
// Must ensure that linkedRowId is not null to avoid drawing on rows whose
|
||||
// row ids are null.
|
||||
return self.linkedRowId() && self.linkedRowId() === row.getRowId();
|
||||
})
|
||||
),
|
||||
kd.text(function() { return row._index() + 1; }),
|
||||
|
||||
kd.scope(row._validationFailures, function(failures) {
|
||||
if (!row._isAddRow() && failures.length > 0) {
|
||||
return dom('div.validation_error_number', failures.length,
|
||||
kd.attr('title', function() {
|
||||
return "Validation failed: " +
|
||||
failures.map(function(val) { return val.name(); }).join(", ");
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
kd.toggleClass('selected', () =>
|
||||
!row._isAddRow() && self.cellSelector.isRowSelected(row._index())),
|
||||
dom.on('contextmenu', ev => self.maybeSelectRow(ev.currentTarget, row.getRowId())),
|
||||
menu(ctl => RowContextMenu({
|
||||
disableInsert: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.tableModel.tableMetaRow.onDemand()),
|
||||
disableDelete: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.getSelection().onlyAddRowSelected()),
|
||||
isViewSorted: self.viewSection.activeSortSpec.peek().length > 0,
|
||||
}), { trigger: ['contextmenu'] }),
|
||||
),
|
||||
|
||||
|
||||
dom('div.record',
|
||||
kd.toggleClass('record-add', row._isAddRow),
|
||||
kd.style('borderLeftWidth', v.borderWidthPx),
|
||||
kd.style('borderBottomWidth', v.borderWidthPx),
|
||||
//These are grabbed from v.optionsObj at start of GridView buildDom
|
||||
kd.toggleClass('record-hlines', vHorizontalGridlines),
|
||||
kd.toggleClass('record-vlines', vVerticalGridlines),
|
||||
kd.toggleClass('record-zebra', vZebraStripes),
|
||||
// even by 1-indexed rownum, so +1 (makes more sense for user-facing display stuff)
|
||||
kd.toggleClass('record-even', () => (row._index()+1) % 2 === 0 ),
|
||||
|
||||
self.comparison ? kd.cssClass(() => {
|
||||
var rowId = row.id();
|
||||
if (rightAddRows.has(rowId)) { return 'diff-remote'; }
|
||||
else if (rightRemoveRows.has(rowId)) { return 'diff-parent'; }
|
||||
else if (leftAddRows.has(rowId)) { return 'diff-local'; }
|
||||
return '';
|
||||
}) : null,
|
||||
|
||||
kd.foreach(v.viewFields(), function(field) {
|
||||
// Whether the cell has a cursor (possibly in an inactive view section).
|
||||
var isCellSelected = ko.computed(() =>
|
||||
isRowActive() && field._index() === self.cursor.fieldIndex());
|
||||
|
||||
// Whether the cell is active: has the cursor in the active section.
|
||||
var isCellActive = ko.computed(() => isCellSelected() && v.hasFocus());
|
||||
|
||||
// Whether the cell is part of an active copy-paste operation.
|
||||
var isCopyActive = ko.computed(function() {
|
||||
return self.copySelection() &&
|
||||
self.copySelection().isCellSelected(row.id(), field.colId());
|
||||
});
|
||||
var fieldBuilder = self.fieldBuilders.at(field._index());
|
||||
var isSelected = ko.computed(() => {
|
||||
return !row._isAddRow() &&
|
||||
!self.cellSelector.isCurrentSelectType(selector.NONE) &&
|
||||
ko.unwrap(self.isColSelected.at(field._index())) &&
|
||||
self.cellSelector.isRowSelected(row._index());
|
||||
});
|
||||
return dom(
|
||||
'div.field',
|
||||
kd.toggleClass('scissors', isCopyActive),
|
||||
dom.autoDispose(isCopyActive),
|
||||
dom.autoDispose(isCellSelected),
|
||||
dom.autoDispose(isCellActive),
|
||||
dom.autoDispose(isSelected),
|
||||
kd.style('width', field.widthPx),
|
||||
//TODO: Ensure that fields in a row resize when
|
||||
//a cell in that row becomes larger
|
||||
kd.style('borderRightWidth', v.borderWidthPx),
|
||||
kd.style('color', field.textColor),
|
||||
// If making a comparison, use the background exclusively for
|
||||
// marking that up.
|
||||
self.comparison ? null : kd.style('background-color', () => (row._isAddRow() || isSelected()) ? '' : field.fillColor()),
|
||||
|
||||
kd.toggleClass('selected', isSelected),
|
||||
fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @inheritdoc */
|
||||
|
@ -690,4 +690,9 @@ const cssViewContentPane = styled('div', `
|
||||
position: relative;
|
||||
min-width: 240px;
|
||||
margin: 12px;
|
||||
@media print {
|
||||
& {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
60
app/client/components/Printing.css
Normal file
60
app/client/components/Printing.css
Normal file
@ -0,0 +1,60 @@
|
||||
@media print {
|
||||
/* Various style overrides needed to print a single section (page widget). */
|
||||
|
||||
.print-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.print-parent {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
height: max-content !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.print-widget {
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
.print-widget .viewsection_title {
|
||||
display: none !important;
|
||||
}
|
||||
.print-widget .view_data_pane_container {
|
||||
border: none !important;
|
||||
}
|
||||
.print-widget .viewsection_content {
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
.print-widget .detailview_single {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.print-widget .gridview_data_pane {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
height: max-content !important;
|
||||
overflow: visible !important;
|
||||
border-top: 1px solid var(--grist-color-dark-grey);
|
||||
border-left: 1px solid var(--grist-color-dark-grey);
|
||||
}
|
||||
|
||||
.print-widget .gridview_data_scroll {
|
||||
position: relative !important;
|
||||
height: max-content !important;
|
||||
}
|
||||
|
||||
.print-widget .scrolly_outer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.print-widget .custom_view {
|
||||
height: calc(100vh - 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@media not print {
|
||||
.print-all-rows {
|
||||
display: none;
|
||||
}
|
||||
}
|
108
app/client/components/Printing.ts
Normal file
108
app/client/components/Printing.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import {CustomView} from 'app/client/components/CustomView';
|
||||
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||
import * as DataTableModel from 'app/client/models/DataTableModel';
|
||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {dom} from 'grainjs';
|
||||
|
||||
type RowId = number|'new';
|
||||
|
||||
/**
|
||||
* Print the specified viewSection (aka page widget). We use the existing view instance rather
|
||||
* than render a new one, since it may have state local to this instance view, such as current
|
||||
* filters.
|
||||
*
|
||||
* Views get a chance to render things specially for printing (which is needed when they use
|
||||
* scrolly for normal rendering).
|
||||
*
|
||||
* To let an existing view print across multiple pages, we can't have it nested in a flexbox or a
|
||||
* div with 'height: 100%'. We achieve it by forcing all parents of our view to have a simple
|
||||
* layout. This is potentially fragile.
|
||||
*/
|
||||
export async function printViewSection(layout: any, viewSection: ViewSectionRec) {
|
||||
const viewInstance = viewSection.viewInstance.peek();
|
||||
const sectionElem = viewInstance?.viewPane?.closest('.viewsection_content');
|
||||
if (!sectionElem) {
|
||||
throw new Error("No page widget to print");
|
||||
}
|
||||
if (viewInstance instanceof CustomView) {
|
||||
try {
|
||||
await viewInstance.triggerPrint();
|
||||
return;
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(`Failed to trigger print in CustomView: ${e}`);
|
||||
// continue on to trying to print from outside, which should work OK for a single page.
|
||||
}
|
||||
}
|
||||
|
||||
function prepareToPrint(onOff: boolean) {
|
||||
// Hide all layout boxes that do NOT contain the section to be printed.
|
||||
layout.forEachBox((box: any) => {
|
||||
if (!box.dom.contains(sectionElem)) {
|
||||
box.dom.classList.toggle('print-hide', onOff);
|
||||
}
|
||||
});
|
||||
|
||||
// Mark the section to be printed.
|
||||
sectionElem.classList.toggle('print-widget', onOff);
|
||||
|
||||
// Let the view instance update its rendering, e.g. to render all rows when scrolly is in use.
|
||||
viewInstance?.prepareToPrint(onOff);
|
||||
|
||||
// If .print-all-rows element is present (created for scrolly-based views), use it as the
|
||||
// start element for the loop below, to ensure it's rendered flexbox-free.
|
||||
const keyElem = sectionElem.querySelector('.print-all-rows') || sectionElem;
|
||||
|
||||
// Go through all parents of the element to be printed. For @media print, we override their
|
||||
// layout in a heavy-handed way, forcing them all to be non-flexbox and sized to content,
|
||||
// since our normal flexbox-based layout is sized to screen and would not print multiple pages.
|
||||
let elem = keyElem.parentElement;
|
||||
while (elem) {
|
||||
elem.classList.toggle('print-parent', onOff);
|
||||
elem = elem.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
const sub1 = dom.onElem(window, 'beforeprint', () => prepareToPrint(true));
|
||||
const sub2 = dom.onElem(window, 'afterprint', () => {
|
||||
sub1.dispose();
|
||||
sub2.dispose();
|
||||
// To debug printing, set window.debugPringint=1 in the console, then print a section, dismiss
|
||||
// the print dialog, switch to "@media print" emulation, and you can explore the styles. You'd
|
||||
// need to reload the page to do it again.
|
||||
if (!(window as any).debugPrinting) {
|
||||
prepareToPrint(false);
|
||||
}
|
||||
});
|
||||
|
||||
window.print();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Produces a div with all requested rows using the same renderRow() function as used with scrolly
|
||||
* for dynamically rendered views. This is used for printing, so these rows do not subscribe to
|
||||
* data.
|
||||
*
|
||||
* To avoid creating a lot of subscriptions when rendering rows this way, we render one DOM row at
|
||||
* a time, copy the produced HTML, and dispose the produced DOM.
|
||||
*/
|
||||
export function renderAllRows(
|
||||
tableModel: DataTableModel, rowIds: RowId[], renderRow: (r: DataRowModel) => Element,
|
||||
) {
|
||||
const rowModel = tableModel.createFloatingRowModel(null) as DataRowModel;
|
||||
const html: string[] = [];
|
||||
rowIds.forEach((rowId, index) => {
|
||||
if (rowId !== 'new') {
|
||||
rowModel._index(index);
|
||||
rowModel.assign(rowId);
|
||||
const elem = renderRow(rowModel);
|
||||
html.push(elem.outerHTML);
|
||||
dom.domDispose(elem);
|
||||
}
|
||||
});
|
||||
rowModel.dispose();
|
||||
const result = dom('div.print-all-rows');
|
||||
result.innerHTML = html.join("\n");
|
||||
return result;
|
||||
}
|
@ -7,6 +7,7 @@ import * as GridView from 'app/client/components/GridView';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {Layout} from 'app/client/components/Layout';
|
||||
import {LayoutEditor} from 'app/client/components/LayoutEditor';
|
||||
import {printViewSection} from 'app/client/components/Printing';
|
||||
import {Delay} from 'app/client/lib/Delay';
|
||||
import {createObsArray} from 'app/client/lib/koArrayWrap';
|
||||
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
@ -124,6 +125,7 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
|
||||
deleteSection: () => { this._removeViewSection(this.viewModel.activeSectionId()); },
|
||||
nextSection: () => { this._otherSection(+1); },
|
||||
prevSection: () => { this._otherSection(-1); },
|
||||
printSection: () => { printViewSection(this._layout, this.viewModel.activeSection()).catch(reportError); },
|
||||
};
|
||||
this.autoDispose(commands.createGroup(commandGroup, this, true));
|
||||
}
|
||||
|
@ -92,6 +92,11 @@ exports.groups = [{
|
||||
name: 'dataSelectionTabOpen',
|
||||
keys: [],
|
||||
desc: 'Shortcut to data selection tab'
|
||||
},
|
||||
{
|
||||
name: 'printSection',
|
||||
keys: [],
|
||||
desc: 'Print currently selected page widget',
|
||||
}
|
||||
]
|
||||
}, {
|
||||
|
1
app/client/declarations.d.ts
vendored
1
app/client/declarations.d.ts
vendored
@ -68,6 +68,7 @@ declare module "app/client/components/BaseView" {
|
||||
public buildTitleControls(): DomArg;
|
||||
public getLoadingDonePromise(): Promise<void>;
|
||||
public onResize(): void;
|
||||
public prepareToPrint(onOff: boolean): void;
|
||||
}
|
||||
export = BaseView;
|
||||
}
|
||||
|
@ -199,6 +199,12 @@ const cssTopHeader = styled('div', `
|
||||
const cssResizeFlexVHandle = styled(resizeFlexVHandle, `
|
||||
--resize-handle-color: ${colors.mediumGrey};
|
||||
--resize-handle-highlight: ${colors.lightGreen};
|
||||
|
||||
@media print {
|
||||
& {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`);
|
||||
const cssResizeDisabledBorder = styled('div', `
|
||||
flex: none;
|
||||
|
@ -9,17 +9,19 @@ import {dom} from 'grainjs';
|
||||
*/
|
||||
export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionRec, isReadonly: boolean) {
|
||||
return [
|
||||
menuItemCmd(allCommands.printSection, 'Print widget', testId('print-section')),
|
||||
dom.maybe((use) => ['detail', 'single'].includes(use(viewSection.parentKey)), () =>
|
||||
menuItemCmd(allCommands.editLayout, 'Edit Card Layout',
|
||||
dom.cls('disabled', isReadonly))),
|
||||
|
||||
menuDivider(),
|
||||
menuItemCmd(allCommands.viewTabOpen, 'Widget options', testId('widget-options')),
|
||||
menuItemCmd(allCommands.sortFilterTabOpen, 'Advanced Sort & Filter'),
|
||||
menuItemCmd(allCommands.dataSelectionTabOpen, 'Data selection'),
|
||||
|
||||
menuDivider(),
|
||||
menuItemCmd(allCommands.deleteSection, 'Delete widget',
|
||||
dom.cls('disabled', viewModel.viewSections().peekLength <= 1 || isReadonly),
|
||||
testId('section-delete')),
|
||||
menuDivider(),
|
||||
|
||||
menuItemCmd(allCommands.viewTabOpen, 'Widget options', testId('widget-options')),
|
||||
menuItemCmd(allCommands.sortFilterTabOpen, 'Advanced Sort & Filter'),
|
||||
menuItemCmd(allCommands.dataSelectionTabOpen, 'Data selection')
|
||||
];
|
||||
}
|
||||
|
@ -45,6 +45,12 @@ const cssMenuElem = styled('div', `
|
||||
z-index: 999;
|
||||
--weaseljs-selected-background-color: ${vars.primaryBg};
|
||||
--weaseljs-menu-item-padding: 8px 24px;
|
||||
|
||||
@media print {
|
||||
& {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const menuItemStyle = `
|
||||
|
@ -150,6 +150,11 @@ if (typeof window !== 'undefined') {
|
||||
rpc.setSendMessage(msg => window.parent.postMessage(msg, "*"));
|
||||
window.onmessage = (e: MessageEvent) => rpc.receiveMessage(e.data);
|
||||
}
|
||||
|
||||
// Allow outer Grist application to trigger printing. This is similar to using
|
||||
// iframe.contentWindow.print(), but that call does not work cross-domain.
|
||||
rpc.registerFunc("print", () => window.print());
|
||||
|
||||
} else if (typeof process === 'undefined') {
|
||||
// Web worker. We can't really bring in the types for WebWorker (available with --lib flag)
|
||||
// without conflicting with a regular window, so use just use `self as any` here.
|
||||
|
Loading…
Reference in New Issue
Block a user