(core) Cleanup removing some old unused files, fixing logo.css, and removing #grist-app.

Summary:
- Move logo.css to core, since it's not included otherwise
- Remove unused old DocList and ViewLinker files.
- Remove #grist-app div that was only serving to supply a background

Test Plan: No changes of behavior, existing tests should pass.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2634
pull/6/head
Dmitry S 4 years ago
parent bd6a54e901
commit d2ad5edc46

@ -13,15 +13,12 @@
--color-link-bright: orange;
--color-start-page-bg: #f0f0f0;
--color-doclist-bg: #fcfcfc;
--color-navbar-bg: var(--color-logo-bg);
--color-navbar-btn-bg: #fefefe;
--color-navbar-btn-bg-hover: #f6f6f6;
--color-navbar-btn-disabled: #ccc;
--color-header-doclist: #6d6dde;
--color-tab-bar-bg: #d6d6d6;
--color-border-light: #ddd;
@ -35,8 +32,6 @@
--color-btn-accept: #3eda2c;
--layout-top-spacer: 20px;
--layout-doclist-header-height: 42px;
--layout-doclist-height: calc(100vh - 2*var(--layout-top-spacer));
--color-list-row-hover: #f0f0f0;

@ -1,4 +1,4 @@
/* global $, window, document */
/* global $, window */
const {App} = require('./ui/App');
@ -12,7 +12,7 @@ const ko = require('knockout');
setupKoDisposal(ko);
$(function() {
window.gristApp = App.create(null, document.getElementById('grist-app'));
window.gristApp = App.create(null);
// Set from the login tests to stub and un-stub functions during execution.
window.loginTestSandbox = null;

@ -1,302 +0,0 @@
#grist-app {
background: url('img/gplaypattern.png');
}
.start-doclist {
position: relative;
margin: 0 auto;
padding: 0;
top: var(--layout-top-spacer);
height: calc(100% - 2*var(--layout-top-spacer));
width: 90vw;
min-width: 600px;
max-width: 900px;
background-color: var(--color-doclist-bg);
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.5);
overflow: hidden;
}
.header-actions {
background-color: var(--color-logo-bg);
text-align: center;
}
.header-actions-logo {
height: var(--layout-doclist-header-height);
padding: .5rem;
background: url('img/logo-grist-transp.png');
background-repeat: no-repeat;
background-size: contain;
background-position: center;
cursor: pointer;
}
.footer-actions {
margin: 1rem auto;
text-align: center;
}
.header-doclist {
height: var(--layout-doclist-header-height);
min-height: var(--layout-doclist-header-height);
background: linear-gradient(to bottom, #f9f9f9, #f0f0f0);
align-items: center;
}
.doclist-link {
margin: .5rem;
font-weight: 600;
color: var(--color-header-doclist);
}
.doclist-section {
height: 100%;
}
.section-sidepane-wrapper {
width: 180px;
min-width: 180px;
border-right: 1px solid var(--color-border-light);
}
.section-sidepane {
margin: .5rem auto;
padding: 0;
text-align: center;
align-items: center;
}
.section-sidepane.b-actions {
margin: .5rem 2rem;
align-items: stretch;
}
.section-doclist {
height: var(--layout-doclist-height);
}
.doclist-body-wrapper {
margin-top: 2px;
height: calc(100% - 2px - var(--layout-doclist-header-height));
width: 100%;
overflow: auto;
}
.g-btn.mod-doclist__sidepane {
border-style: solid;
border-width: 1px 1px 1px 4px;
font-weight: 400;
color: #000;
height: 30px;
line-height: 18px;
}
.g-btn.mod-doclist__sidepane:hover {
font-weight: 600;
color: #fff;
}
.g-btn.mod-doclist__sidepane.fit-text {
font-size: 1.0rem;
}
.g-btn.mod-doclist__sidepane.el-login {
border-color: var(--color-btn-login);
background-color: var(--color-btn-login-background);
}
.g-btn.mod-doclist__sidepane.el-login:hover {
background-color: var(--color-btn-login);
}
.g-btn.mod-doclist__sidepane.el-createdoc {
border-color: var(--color-btn-createdoc);
}
.g-btn.mod-doclist__sidepane.el-createdoc:hover {
background-color: var(--color-btn-createdoc);
}
.g-btn.mod-doclist__sidepane.el-editteam {
margin-top: 1rem;
border-color: #7e25ff;
}
.g-btn.mod-doclist__sidepane.el-editteam:hover {
background-color: #7e25ff;
}
.g-btn.mod-doclist__sidepane.el-uploaddoc {
position: relative;
margin-top: 1rem;
border-color: var(--color-btn-uploaddoc);
}
.g-btn.mod-doclist__sidepane.el-uploaddoc:hover {
background-color: var(--color-btn-uploaddoc);
}
.g-btn.mod-doclist__invite {
width: 20px;
height: 20px;
padding: 4px;
}
.doclist-invite {
display: flex;
flex-direction: column;
}
.doclist-invite-bottom-row {
display: inline-block;
font-size: 1.0rem;
color: var(--color-hint-text);
}
.mod-doclist__sidepane.el-divider {
border-bottom: 1px solid var(--color-border-light);
height: 1px;
width: 90%;
margin: 15px auto;
}
.el-uploaddoc__bar {
position: absolute;
height: 100%;
width: 0%;
left: 0px;
top: 0px;
padding-top: 1px;
color: #fff;
background-color: var(--color-btn-uploaddoc);
font-size: 2rem;
transition: width .2s ease-out, opacity .2s;
}
.g-btn {
margin: 0;
padding: .5rem .8rem;
cursor: pointer;
}
.g-list {
list-style-type: none;
margin: 0;
padding: 0;
}
.doclist-logo-img {
width: 90px;
}
.doclist-header {
top: 0;
height: var(--layout-doclist-header-height);
min-height: var(--layout-doclist-header-height);
padding: .2rem 1rem;
align-items: center;
background: linear-gradient(to bottom, #f0f0f0, #e6e6e6);
box-shadow: 0 2px 2px -1px rgba(0,0,0,0.5);
text-decoration: underline;
}
.doclist-row {
padding: 1rem;
border-bottom: 1px solid var(--color-border-light);
transition: background-color .2s;
}
.doclist-row:hover {
background-color: var(--color-list-row-hover);
}
.doclist-sample {
color: var(--color-header-doclist);
}
.doclist-column {
min-width: 120px;
cursor: pointer;
}
.doclist-column.doclist-column__name {
min-width: 160px;
}
.doclist-column.doclist-column__download, .doclist-column.doclist-column__delete {
min-width: 15px;
color: #777;
font-size: 1.0rem;
text-align: right;
margin-left: 10px;
padding: 3px;
}
.doclist-column.mod-header {
padding-left: 1.6rem;
}
.doclist-column__size {
text-align: right;
padding-right: 1rem;
}
.doclist-download_link {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.glyphicon.mod-doclist-column {
padding-right: .5rem;
}
div.uploadAlert {
position: relative;
margin-bottom: 1rem;
}
.gupload {
margin-bottom: 1rem;
}
.gupload_item {
position: relative;
background-color: transparent;
}
.gupload_bar {
position: absolute;
z-index: -1;
left: 0;
top: 0;
height: 100%;
background-color: lightgreen;
}
.gupload_failed {
background-color: #FFA0A0;
}
.gupload_info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

@ -1,331 +0,0 @@
/* global window, $ */
const bluebird = require('bluebird');
const _ = require('underscore');
const ko = require('knockout');
const BackboneEvents = require('backbone').Events;
const gutil = require('app/common/gutil');
const version = require('app/common/version');
const dispose = require('../lib/dispose');
const dom = require('../lib/dom');
const kd = require('../lib/koDom');
const koSession = require('../lib/koSession');
const {IMPORTABLE_EXTENSIONS, selectFiles} = require('../lib/uploads');
const commands = require('./commands');
const {urlState} = require('app/client/models/gristUrlState');
const {showConfirmDialog} = require('./Confirm');
const sortByFuncs = {
'name': (a, b) => a.localeCompare(b),
'mtime': (a, b) => gutil.nativeCompare(a, b),
'size': (a, b) => gutil.nativeCompare(a, b)
};
function DocList(app) {
this.app = app;
this.docListModel = app.docListModel;
this.docs = app.docListModel.docs;
this._docInvites = app.docListModel.docInvites;
// Default sort is by modified time in descending order
// TODO: This would be better yet as a user preference, since this still resets to default for
// each browser window.
this.sortBy = koSession.sessionValue('docListSortBy', 'mtime');
this.sortAsc = koSession.sessionValue('docListSortDir', -1); // 1 for asc, -1 for desc
let compareFunc = this.autoDispose(ko.computed(() => {
let attrib = this.sortBy();
let asc = this.sortAsc();
let compareFunc = sortByFuncs[attrib];
return (a, b) => gutil.nativeCompare(a.tag, b.tag) || compareFunc(a[attrib], b[attrib]) * asc;
}));
this.sortedDocs = this.autoDispose(ko.computed(() =>
this.docs.all().sort(compareFunc())).extend({rateLimit: 0}));
this.docListModel.refreshDocList();
this.autoDispose(this.app.login.isLoggedIn.subscribe(loggedIn => {
// Refresh the doc list to show/hide docs and invites belonging to the logged in user.
this.docListModel.refreshDocList();
}));
}
dispose.makeDisposable(DocList);
_.extend(DocList.prototype, BackboneEvents);
// TODO: Factor out common code from DocList and NavBar (e.g. status indicator)
DocList.prototype.buildDom = function() {
let currentStatus = ko.observable('OK');
this.listenTo(this.app.comm, 'connectionStatus', (message, status) => {
console.warn('connectionStatus', message, status);
currentStatus(status);
});
return this.autoDispose(
dom('div.start-doclist.flexhbox',
dom('section.doclist-section.section-sidepane-wrapper.flexvbox',
parts.sidepane.header(() => this.docListModel.refreshDocList()),
parts.sidepane.connection(currentStatus),
dom('div.section-sidepane.b-actions.flexnone.flexvbox',
kd.maybe(() => this.app.features().signin, () =>
dom('div',
parts.sidepane.loginButton(this.app.login),
dom('div.mod-doclist__sidepane.el-divider')
)
),
parts.sidepane.createDocButton(this.createNewDoc.bind(this)),
window.electronOpenDialog ? parts.sidepane.openDocButton() : null,
parts.sidepane.uploadDocButton(this.uploadNewDoc.bind(this))
),
dom('div.flexitem'),
parts.sidepane.footer(commands.allCommands.help.run)),
dom('section.doclist-section.section-doclist-wrapper.flexitem', this._buildDoclistBody())));
};
DocList.prototype._buildDoclistBody = function() {
return dom('div.section-doclist',
dom('div.doclist-header.flexhbox',
dom('div.doclist-column.doclist-column__name.mod-header.flexitem', 'Name',
parts.doclist.ascDescArrow(this.sortBy, this.sortAsc, 'name'),
dom.on('click', () => updateSort(this.sortBy, this.sortAsc, 'name'))),
dom('div.doclist-column.doclist-column__size', 'Size',
parts.doclist.ascDescArrow(this.sortBy, this.sortAsc, 'size'),
dom.on('click', () => updateSort(this.sortBy, this.sortAsc, 'size'))),
dom('div.doclist-column.doclist-column__modified', 'Modified',
parts.doclist.ascDescArrow(this.sortBy, this.sortAsc, 'mtime'),
dom.on('click', () => updateSort(this.sortBy, this.sortAsc, 'mtime'))),
dom('div.doclist-column.doclist-column__delete')
),
dom('div.doclist-body-wrapper',
dom('div.doclist-body',
kd.scope(this.sortedDocs, docs =>
docs.map(docObj => parts.doclist.listDoc(docObj, this._onClickItem.bind(this),
this._confirmRemoveDoc.bind(this)))),
kd.foreach(this._docInvites, inviteObj =>
parts.doclist.listInvite(inviteObj, this._downloadSharedDoc.bind(this),
this._confirmDeclineInvite.bind(this))))));
};
const parts = {
sidepane: {
connection: (statusObs) =>
dom('div.section-sidepane.b-status.flexhbox',
dom('div.gnotifier', kd.cssClass(() => 'g-status-' + statusObs())),
dom('span', 'Connection: '),
kd.text(statusObs)),
header: (headerCommand) =>
dom('div.header-actions',
dom('div.header-actions-logo', // Logo set in css
{title: `Ver ${version.version} (${version.gitcommit})`},
dom.on('click', headerCommand))),
footer: (helpCommand) =>
dom('div.footer-actions.flexvbox',
dom('a.doclist-link', { href: 'help', target: '_blank' }, 'Help'),
dom('a.doclist-link', { href: 'help/contact-us', target: '_blank'}, 'Contact'),
dom('a.doclist-link', { href: 'https://www.getgrist.com', target: '_blank'}, 'About')),
loginButton: (login) =>
dom('div.g-btn.mod-doclist__sidepane.el-login',
dom.testId('DocList_login'),
kd.toggleClass('fit-text', () => login.isLoggedIn()),
kd.text(login.buttonText),
dom.on('click', e => { login.onClick(e); })),
createDocButton: createNewDoc =>
dom('div.g-btn.mod-doclist__sidepane.el-createdoc',
dom.testId('DocList_createDoc'),
'Create document',
dom.on('click', createNewDoc)),
uploadDocButton: uploadNewDoc => {
let uploadStatusIcons = {
'uploading': 'glyphicon-upload',
'success': 'glyphicon-ok-sign',
'error': 'glyphicon-exclamation-sign'
};
let uploadStatus = ko.observable(null);
let uploadBar;
let uploadProgress = percentage => uploadBar.style.width = percentage + '%';
return dom('div.g-btn.mod-doclist__sidepane.el-uploaddoc',
dom.testId('DocList_uploadBtn'),
uploadBar = dom('div.el-uploaddoc__bar',
kd.show(uploadStatus),
dom('span.glyphicon.glyphicon-upload',
kd.cssClass(() => uploadStatusIcons[uploadStatus()])
)
),
dom('span',
kd.style('visibility', () => uploadStatus() === null ? 'visible' : 'hidden'),
'Import document'
),
dom.on('click', () => uploadNewDoc(uploadStatus, uploadProgress))
);
},
openDocButton: () => dom('div.g-btn.mod-doclist__sidepane.el-uploaddoc',
dom('span', 'Open document'),
dom.on('click', window.electronOpenDialog)
),
},
doclist: {
ascDescArrow: (sortBy, sortAsc, attrib) =>
dom('span', kd.text(() => sortBy() === attrib ? (sortAsc() > 0 ? '▲' : '▼') : '')),
listDoc: (docObj, onClick, onDelete) => {
let mtime = new Date(docObj.mtime);
return dom('div.doclist-row.flexhbox',
kd.toggleClass('doclist-sample', docObj.tag === 'sample'),
dom('div.doclist-column.doclist-column__name.flexitem',
dom('span.glyphicon.glyphicon-file.mod-doclist-column'),
dom('a', {
href: urlState().makeUrl({doc: docObj.name})
},
kd.toggleClass('doclist-sample', docObj.tag === 'sample'),
docObj.tag ? `[${gutil.capitalize(docObj.tag)}] ` : null,
docObj.name,
dom.on('click', (ev) => onClick(ev, docObj))
)
),
dom('div.doclist-column.doclist-column__size', gutil.byteString(docObj.size)),
dom('div.doclist-column.doclist-column__modified',
mtime.toLocaleDateString() + ' ' + mtime.toLocaleTimeString()),
// Show a downlink link for each file in the hosted version, but not in the electron version.
window.isRunningUnderElectron ? null : dom('div.doclist-column.doclist-column__download.glyphicon.glyphicon-download-alt',
dom('a.doclist-download_link', {
href: 'download?' + $.param({doc: docObj.name}),
download: `${docObj.name}.grist`
})
),
dom('div.doclist-column.doclist-column__delete.glyphicon.glyphicon-trash',
dom.on('click', () => onDelete(docObj.name))));
},
listInvite: (docObj, onDownload, onDecline) => {
const name = docObj.senderName;
const email = docObj.senderEmail;
return dom('div.doclist-row.doclist-invite.flexhbox',
dom('div.flexhbox.doclist-invite-top-row',
dom('div.doclist-column.doclist-column__name.flexitem',
dom('span.glyphicon.glyphicon-download-alt.mod-doclist-column'),
dom.on('click', () => onDownload(docObj)),
'[Invite] ',
docObj.name
),
dom('div.doclist-column.doclist-column__size'),
dom('div.doclist-column.doclist-column__modified'),
dom('div.doclist-column.doclist-column__delete.glyphicon.glyphicon-remove',
dom.on('click', () => onDecline(docObj)))
),
dom('div.flexhbox.doclist-invite-bottom-row', 'Sent by: ' + (name ? `${name} (${email})` : email))
);
}
}
};
DocList.prototype._downloadSharedDoc = function(docObj) {
return this.app.comm.downloadSharedDoc(docObj.docId, docObj.name)
.then(() => this.docListModel.refreshDocList());
};
DocList.prototype._confirmDeclineInvite = function(docObj) {
showConfirmDialog(`Ignore invite to ${docObj.name}?`, 'Ignore', () => this._ignoreInvite(docObj));
};
DocList.prototype._confirmRemoveDoc = function(docName) {
if (window.isRunningUnderElectron) {
showConfirmDialog(`Move ${docName} to trash?`, 'Move to trash', () => this.app.comm.deleteDoc(docName, false));
} else {
showConfirmDialog(`Delete ${docName} permanently?`, 'Delete', () => this.app.comm.deleteDoc(docName, true));
}
};
DocList.prototype._ignoreInvite = function(docObj) {
return this.app.comm.ignoreLocalInvite(docObj.docId)
.then(() => this.docListModel.refreshDocList());
};
// commands useable while title is active
DocList.fileEditorCommands = {
accept: function() { this.finishEditingFileName(true); },
cancel: function() { this.finishEditingFileName(false); },
};
DocList.prototype.createNewDoc = function(source, event) {
return this.app.comm.createNewDoc()
.then(docName => urlState().pushUrl({doc: docName}, {avoidReload: true}))
.catch(err => console.error(err));
};
DocList.prototype._onClickItem = function(ev, docObj) {
ev.preventDefault();
if (docObj.tag === 'sample') {
// Note that we don't expect this to fail, so an error will show up in the NotifyBox.
this.app.comm.importSampleDoc(docObj.name)
.then(docName => urlState().pushUrl({doc: docName}, {avoidReload: true}));
return false;
}
// To avoid a page reload, we handle the change of url. Previously we used fragments,
// so the browser knew there was no page change going on. Now, we are using real urls
// and we have to hold the browser's hand.
urlState().pushUrl({doc: docObj.name}, {avoidReload: true});
return false;
};
DocList.prototype.uploadNewDoc = function(uploadStatus, uploadProgress) {
function progress(percent) {
// We use one progress bar to combine the time to upload and the time to import in 40%-60%
// split. TODO The second part needs to be done; currently it stops at 40%, but at least the
// user can tell that there is still something to wait for.
const p = percent * 0.4;
uploadProgress(p);
if (p > 0) { uploadStatus('uploading'); }
}
return bluebird.try(() => {
return selectFiles({multiple: true, extensions: IMPORTABLE_EXTENSIONS},
progress);
})
.then(uploadResult => {
// Put together a text summary of what got uploaded, currently only for debugging.
const summaryList = uploadResult.files.map(f => `${f.origName} (${f.size})`);
console.log('Upload summary:', summaryList.join(", "));
// TODO: This step should include its own progress callback, and the progress indicator should
// combine the two steps, e.g. in 40%-60% split.
return this.app.comm.importDoc(uploadResult.uploadId);
})
.then(docName => {
uploadStatus('success');
return bluebird.delay(500) // Let the user see the OK icon briefly.
.then(() => urlState().pushUrl({doc: docName}, {avoidReload: true}));
})
.catch(err => {
console.error("Upload failed: %s", err);
uploadStatus('error');
return bluebird.delay(2000); // Let the user see the error icon.
})
.finally(() => {
uploadStatus(null);
});
};
function updateSort(sortObs, sortAsc, sortValue) {
let currSort = sortObs();
if (currSort === sortValue) {
sortAsc(-1 * sortAsc());
} else {
sortObs(sortValue);
sortAsc(1);
}
}
module.exports = DocList;

@ -1,334 +0,0 @@
var _ = require('underscore');
var ko = require('knockout');
var gutil = require('app/common/gutil');
var dispose = require('../lib/dispose');
var dom = require('../lib/dom');
var kd = require('../lib/koDom');
var commands = require('./commands');
/**
* Use the browser globals in a way that allows replacing them with mocks in tests.
*/
var G = require('../lib/browserGlobals').get('window', 'document', '$');
//----------------------------------------------------------------------
function ViewLinkerNode(section, column, tableId, primaryTableId) {
this.section = section;
this.sectionRef = section ? section.getRowId() : 0;
this.col = column;
this.colRef = column ? column.getRowId() : 0;
this.tableId = tableId;
this.primaryTableId = primaryTableId;
this.nodeId = this.sectionRef + ':' + this.colRef; // Uniquely identifies the node.
this.linkIconDom = null;
}
ViewLinkerNode.prototype.isLinked = function() {
return this.section && this.section.activeLinkSrcSectionRef() &&
this.section.activeLinkTargetColRef() === this.colRef;
};
ViewLinkerNode.prototype.isValidLinkTo = function(targetNode) {
let tablesMatch = (this.tableId === targetNode.tableId ||
(this.primaryTableId === targetNode.primaryTableId && !this.col && !targetNode.col));
// There are table-to-table links (cursor sync), table-to-column (filter), column-to-table
// (cursor), and column-to-column (filter).
// The table must match.
if (!tablesMatch) { return false; }
// The target must not be already linked.
if (targetNode.section && targetNode.section.activeLinkSrcSectionRef()) {
return false;
}
if (this.section) {
// The link must not create a cycle.
for (let sec = this.section; sec.getRowId(); sec = sec.linkSrcSection()) {
if (targetNode.sectionRef === sec.getRowId()) {
return false;
}
}
}
return true;
};
ViewLinkerNode.prototype.linkCoord = function() {
var rect = this.linkIconDom.getBoundingClientRect();
return {
left: rect.left + rect.width / 2,
top: rect.top + rect.height / 2
};
};
//----------------------------------------------------------------------
/**
* ViewLinker - Builds GUI for linking viewSections in viewModel
*/
function ViewLinker(viewModel) {
this.viewModel = viewModel;
this.viewSections = this.viewModel.viewSections().all();
this.clicked = ko.observable(null);
// For each section, the pair (section, colRef) represents a linkable node for every reference
// column, and for colRef 0 (which corresponds to the special "id" column, or the table itself).
this.allNodes = [];
for (let section of this.viewSections) {
this.allNodes.push(...ViewLinker.createNodes(section, section.table()));
}
this.nodesBySection = _.groupBy(this.allNodes, 'sectionRef');
this.nodesById = _.indexBy(this.allNodes, 'nodeId');
// A list of coordinate data objects for coordinates in no particular order.
this.coordinates = null;
this.boundMouseMove = this.handleMouseMove.bind(this);
this.boundWindowResize = this.handleWindowResize.bind(this);
this.autoDisposeCallback(function() {
G.$(G.window).off('mousemove', this.boundMouseMove);
G.$(G.window).off('resize', this.boundWindowResize);
});
this.canvas = this.autoDispose(dom('canvas.linker_canvas'));
this.buttons = this.autoDispose(
dom('div.linker_save_btns',
dom('div.linker_btn', 'Apply',
dom.on('click', () => {
this.viewModel.isLinking(false);
})
),
dom('div.linker_btn', 'Apply & Save',
dom.on('click', () => {
commands.allCommands.saveLinks.run();
this.viewModel.isLinking(false);
})
),
dom('div.linker_btn', 'Cancel',
dom.on('click', () => {
commands.allCommands.revertLinks.run();
this.viewModel.isLinking(false);
})
)
)
);
dom.id('grist-app').appendChild(this.canvas);
dom.id('view_content').appendChild(this.buttons);
this.ctx = this.canvas.getContext("2d");
// Must monitor link values and redraw arrows on changes since undo-ing/redo-ing may
// affect them.
this.autoDispose(ko.computed(() => {
for (let section of this.viewSections) {
section.linkSrcSectionRef();
section.linkSrcColRef();
section.linkTargetColRef();
}
setTimeout(this.resetArrows.bind(this), 0);
}));
G.$(G.window).on('resize', this.boundWindowResize);
this.handleWindowResize();
}
dispose.makeDisposable(ViewLinker);
ViewLinker.createNodes = function(section, table) {
const nodes = [];
nodes.push(new ViewLinkerNode(section, null, table.tableId(),
table.primaryTableId()));
for (let column of table.columns().peek()) {
let tableId = gutil.removePrefix(column.type(), "Ref:");
if (tableId) {
nodes.push(new ViewLinkerNode(section, column, tableId));
}
}
return nodes;
};
// Fills coordinates object with values and re-renders canvas.
ViewLinker.prototype.resetArrows = function() {
this.resetCoordinates();
this.redrawArrows();
};
ViewLinker.prototype.onClickNode = function(event, node) {
if (!this.clicked()) {
this.clicked(node);
G.$(G.window).on('mousemove', this.boundMouseMove);
} else {
this.connectLink(node);
this.clicked(null);
G.$(G.window).off('mousemove', this.boundMouseMove);
}
};
ViewLinker.prototype.onClickBackground = function(event) {
if (this.clicked()) {
this.clicked(null);
G.$(G.window).off('mousemove', this.boundMouseMove);
this.redrawArrows();
} else {
this.viewModel.isLinking(false);
}
};
ViewLinker.prototype.handleMouseMove = function(event) {
this.redrawArrows();
let coord = this.clicked().linkCoord();
drawArrow(this.ctx, coord.left, coord.top, event.clientX, event.clientY);
this.ctx.stroke();
};
ViewLinker.prototype.handleWindowResize = function(event) {
// Canvas must be scaled up in a particular way for improved resolution.
var w = G.$(G.window);
var windowWidth = w.innerWidth();
var windowHeight = w.innerHeight();
this.ctx.canvas.style.width = windowWidth + 'px';
this.ctx.canvas.style.height = windowHeight + 'px';
this.ctx.canvas.width = windowWidth * 2;
this.ctx.canvas.height = windowHeight * 2;
this.ctx.scale(2, 2);
setTimeout(this.resetArrows.bind(this), 0);
};
// To be called by each viewSection when the ViewLinker is active.
ViewLinker.prototype.buildLeafDom = function(viewSection) {
var sectionRef = viewSection.getRowId();
var mainNode = this.nodesById[sectionRef + ':0'];
var sectionNodes = this.nodesBySection[sectionRef];
var getOtherSections = () => this.clicked() && this.clicked().sectionRef !== sectionRef;
return dom('div.g_record_layout_linking',
dom.on('click', event => { this.onClickBackground(event); }),
dom('div.linker_box',
dom('div.linker_box_section',
dom('div.linker_box_header', 'Scroll',
kd.toggleClass('visible', getOtherSections)
),
this.buildLinkRowDom(viewSection, mainNode)
),
dom('div.linker_box_columns',
dom('div.linker_box_header', 'Filter',
kd.toggleClass('visible', () => getOtherSections() && sectionNodes.length > 1)
),
this.nodesBySection[sectionRef].map(node => {
if (!node.col) { return null; }
return this.buildLinkRowDom(viewSection, node);
})
)
)
);
};
ViewLinker.prototype.buildLinkRowDom = function(viewSection, node) {
var sectionRef = viewSection.getRowId();
var tableId = viewSection.table().tableTitle();
var isAvailable = ko.computed(() => (this.clicked() ?
this.clicked().isValidLinkTo(node) :
// See if there are any nodes this could possibly link to.
this.allNodes.some(tgt => node.isValidLinkTo(tgt))
));
return dom('div.section_link.section_link_' + sectionRef,
dom.autoDispose(isAvailable),
// Prevents closing on link mode when clicking in link boxes.
dom.on('click', event => { event.stopPropagation(); }),
dom('span.view_link.link_' + sectionRef + (node.col ? '_' + node.col.getRowId() : ''),
dom.on('click', event => {
if (isAvailable()) { this.onClickNode(event, node); }
}),
node.linkIconDom =
dom('span.glyphicon.glyphicon-link.view_link_icon',
// Use 'visibility' rather than 'display', to keep size and position available.
kd.style('visibility', () => isAvailable() ? 'visible' : 'hidden'),
kd.toggleClass('selected_link', () => (node.isLinked() || this.clicked() === node))
)
),
dom('span.link_text', tableId + (node.col ? '.' + node.col.colId() : ''),
kd.toggleClass('available_text', isAvailable),
kd.toggleClass('selected_text', () => node.isLinked()),
dom('span.glyphicon.glyphicon-remove.remove_link_icon',
kd.style('visibility', () =>
(node.isLinked() && !this.clicked() ? 'visible' : 'hidden')),
dom.on('click', () => this.removeNodeLinks(node))
)
)
);
};
// Initializes the coordinates array with linked node coordinates.
ViewLinker.prototype.resetCoordinates = function() {
this.coordinates = [];
for (let vs of this.viewSections) {
if (vs.activeLinkSrcSectionRef.peek()) {
let sourceNodeId = vs.activeLinkSrcSectionRef.peek() + ':' + vs.activeLinkSrcColRef.peek();
let targetNodeId = vs.getRowId() + ':' + vs.activeLinkTargetColRef.peek();
this.coordinates.push({
'from': this.nodesById[sourceNodeId].linkCoord(),
'to': this.nodesById[targetNodeId].linkCoord()
});
}
}
};
// Draws all pre-existing arrows on the canvas.
ViewLinker.prototype.redrawArrows = function() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.beginPath();
this.coordinates.forEach(conn =>
drawArrow(this.ctx, conn.from.left, conn.from.top, conn.to.left, conn.to.top));
};
// Saves a new link from the clicked node to 'node' to the local list of links.
ViewLinker.prototype.connectLink = function(node) {
var sourceNode = this.clicked();
var section = node.section;
section.activeLinkSrcSectionRef(sourceNode.sectionRef);
section.activeLinkSrcColRef(sourceNode.colRef);
section.activeLinkTargetColRef(node.colRef);
this.resetArrows();
};
// Removes links from the given node.
ViewLinker.prototype.removeNodeLinks = function(node) {
if (!node.isLinked()) { return; }
var section = node.section;
section.activeLinkSrcSectionRef(0);
section.activeLinkSrcColRef(0);
section.activeLinkTargetColRef(0);
this.resetArrows();
};
// Draws an orange arrow with a black outline in context ctx from (origX, origY) to (x, y).
function drawArrow(ctx, origX, origY, x, y){
ctx.strokeStyle = 'black';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
_drawBasicArrow(ctx, origX, origY, x, y);
ctx.stroke();
ctx.strokeStyle = '#F5A542';
ctx.lineWidth = 3;
_drawBasicArrow(ctx, origX, origY, x, y);
ctx.stroke();
}
// Draws a single arrow with no outline.
function _drawBasicArrow(ctx, origX, origY, x, y) {
var l = 10; // length of arrow head
var angle = Math.atan2(y - origY, x - origX);
ctx.moveTo(x, y);
ctx.lineTo(x - l*Math.cos(angle - Math.PI/5), y - l*Math.sin(angle - Math.PI/5));
ctx.moveTo(x, y);
ctx.lineTo(x - l*Math.cos(angle + Math.PI/5), y - l*Math.sin(angle + Math.PI/5));
ctx.moveTo(origX, origY);
ctx.lineTo(x, y);
}
module.exports = ViewLinker;

@ -128,27 +128,6 @@ declare module "app/client/components/ViewConfigTab" {
export = ViewConfigTab;
}
declare module "app/client/components/ViewLinker" {
import {ViewRec} from "app/client/models/DocModel";
namespace ViewLinker {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class ViewLinkerNode {
public section: any;
public sectionRef: number;
public col: any;
public colRef: number;
public isValidLinkTo(node: ViewLinkerNode): boolean;
}
}
class ViewLinker {
public static create(viewRec: ViewRec): ViewLinker;
public static createNodes(section: any|null, table: any): ViewLinker.ViewLinkerNode[];
}
export = ViewLinker;
}
declare module "app/client/components/commands" {
export class Command {
public name: string;

@ -0,0 +1,69 @@
#grist-logo-wrapper {
position: absolute;
width: 100vw;
height: 100vh;
display: flex;
background-color: var(--color-logo-bg);
}
.grist-logo {
background: var(--color-bg);
margin: auto;
padding: 20px 28px;
}
.grist-logo-grain {
display: inline-block;
width: 42px;
height: 40px;
margin: 1px;
border-radius: 22px 0 18px 0;
}
.grist-logo-grain.grain-flip {
border-radius: 0 22px 0 18px;
}
.grist-logo-grain.grain-empty {
visibility: hidden;
}
.grist-logo-grain.grain-col {
background: var(--color-logo-col);
}
.grist-logo-grain.grain-row {
background: var(--color-logo-row);
}
.grist-logo-grain.grain-cell {
background: var(--color-logo-cell);
}
.grist-logo-grain {
animation: spin-grain 3.2s linear infinite;
}
.grist-logo-grain.grain-2 { animation-delay: .4s; }
.grist-logo-grain.grain-3 { animation-delay: .8s; }
.grist-logo-grain.grain-4 { animation-delay: 1.2s; }
.grist-logo-grain.grain-5 { animation-delay: 0s; }
.grist-logo-grain.grain-6 { animation-delay: .2s; }
.grist-logo-grain.grain-7 { animation-delay: .6s; }
.grist-logo-grain.grain-8 { animation-delay: 1.0s; }
.grist-logo-grain.grain-9 { animation-delay: 1.4s; }
@keyframes spin-grain {
0% {
transform: rotateY(0deg);
}
25% {
transform: rotateY(180deg);
}
50% {
transform: rotateY(0deg);
}
}

@ -9,16 +9,7 @@ body {
font-size: 1.2rem;
margin: 0;
padding: 0;
}
#grist-app {
height: 100%;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
-webkit-flex-wrap: nowrap;
flex-wrap: nowrap;
background: url('img/gplaypattern.png');
}
.g-help {

@ -50,7 +50,7 @@ export class App extends DisposableWithEvents {
// we can choose to refresh the client also.
private _serverVersion: string|null = null;
constructor(private _appDiv: HTMLElement) {
constructor() {
super();
commands.init(); // Initialize the 'commands' module using the default command list.
@ -80,7 +80,7 @@ export class App extends DisposableWithEvents {
G.document.querySelector('#grist-logo-wrapper').remove();
// Help pop-up pane
const helpDiv = this._appDiv.appendChild(
const helpDiv = document.body.appendChild(
dom('div.g-help',
dom.show(isHelpPaneVisible),
dom('table.g-help-table',

@ -38,8 +38,6 @@
</div>
</div>
<div id="grist-app"></div>
<div id="browser-check-problem" style="display: none;">
<div class="browser-check-wrapper">
<div class="browser-check-message">

Loading…
Cancel
Save