mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) support ?embed=true and &style=light for a clean embed experience
Summary: This adds query parameters useful for tailoring the Grist experience, with an eye to embedding. Setting `style=light` removes side and top bars, as a first pass at a focused view of a single document page (this would benefit from refining). Setting `embed=true` has no significant effect just yet other than it restricts document access to viewer at most (this can be overridden by specifying `/m/default`). Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2585
This commit is contained in:
140
app/client/lib/UrlState.ts
Normal file
140
app/client/lib/UrlState.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Generic support of observable state represented by the current page's URL. The state is
|
||||
* initialized on first use, and updated on navigation events, such as Back/Forward button clicks,
|
||||
* and on calls to pushUrl().
|
||||
*
|
||||
* Application-specific module should instantiate UrlState with the desired way to encode state in
|
||||
* URLs. Other code may then use the UrlState object exposed by that app-specific module.
|
||||
*
|
||||
* UrlState also provides functions to navigate: makeUrl(), pushUrl(), and setLinkUrl(). The
|
||||
* preferred option is to use actual <a> links for navigation, creating them like so:
|
||||
*
|
||||
* import {urlState} from '...appUrlState';
|
||||
* dom('a', urlState().setLinkUrl({org: 'foo'}))
|
||||
* dom('a', urlState().setLinkUrl({docPage: pageId}))
|
||||
*
|
||||
* These will set actual hrefs (e.g. allowing links to be opened in a new tab), and also will
|
||||
* intercept clicks and update history (using pushUrl()) without reloading the page.
|
||||
*/
|
||||
import * as log from 'app/client/lib/log';
|
||||
import {BaseObservable, Disposable, dom, DomElementMethod, observable} from 'grainjs';
|
||||
|
||||
export interface UrlStateSpec<IUrlState> {
|
||||
encodeUrl(state: IUrlState, baseLocation: Location | URL): string;
|
||||
decodeUrl(location: Location | URL): IUrlState;
|
||||
updateState(prevState: IUrlState, newState: IUrlState): IUrlState;
|
||||
|
||||
// If present, the return value is checked by pushUrl() to decide if we can stay on the page or
|
||||
// need to load the new URL. The new URL is always loaded if origin changes.
|
||||
needPageLoad(prevState: IUrlState, newState: IUrlState): boolean;
|
||||
|
||||
// Give the implementation a chance to complete outstanding work, e.g. if there is unsaved
|
||||
// data in the page state that would get destroyed.
|
||||
delayPushUrl(prevState: IUrlState, newState: IUrlState): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state of a page in browser history, as encoded in window.location URL.
|
||||
*/
|
||||
export class UrlState<IUrlState> extends Disposable {
|
||||
// Current state. This gets initialized in the constructor, and updated on navigation events.
|
||||
public state = observable<IUrlState>(this._getState());
|
||||
|
||||
constructor(private _window: HistWindow, private _stateImpl: UrlStateSpec<IUrlState>) {
|
||||
super();
|
||||
|
||||
// Create a hook for navigation. It's exposed on the window for overriding in tests.
|
||||
if (!_window._urlStateLoadPage) {
|
||||
_window._urlStateLoadPage = (href) => { _window.location.href = href; };
|
||||
}
|
||||
|
||||
// On navigation events, update our current state, including the observables.
|
||||
this.autoDispose(dom.onElem(this._window, 'popstate', (ev) => this.loadState()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new history entry (navigable with Back/Forward buttons), encoding the given state
|
||||
* in the URL. This is similar to navigating to a new URL, but does not reload the page.
|
||||
*/
|
||||
public async pushUrl(urlState: IUrlState, options: {replace?: boolean, avoidReload?: boolean} = {}) {
|
||||
const prevState = this.state.get();
|
||||
const newState = this._stateImpl.updateState(prevState, urlState);
|
||||
const newUrl = this._stateImpl.encodeUrl(newState, this._window.location);
|
||||
|
||||
// Don't create a new history entry if nothing changed as it would only be annoying.
|
||||
if (newUrl === this._window.location.href) { return; }
|
||||
|
||||
const oldOrigin = this._window.location.origin;
|
||||
const newOrigin = new URL(newUrl).origin;
|
||||
|
||||
// We can only pushState() without reloading the page if going to a same-origin URL.
|
||||
const samePage = (oldOrigin === newOrigin &&
|
||||
(options.avoidReload || !this._stateImpl.needPageLoad(prevState, newState)));
|
||||
|
||||
if (samePage) {
|
||||
await this._stateImpl.delayPushUrl(prevState, newState);
|
||||
if (options.replace) {
|
||||
this._window.history.replaceState(null, '', newUrl);
|
||||
} else {
|
||||
this._window.history.pushState(null, '', newUrl);
|
||||
}
|
||||
// pushState/replaceState above do not trigger 'popstate' event, so we call loadState() manually.
|
||||
this.loadState();
|
||||
} else {
|
||||
this._window._urlStateLoadPage!(newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URL (e.g. to use in a link's href) encoding the given state. The `use` argument
|
||||
* allows for this to be used in a computed, and is used by setLinkUrl().
|
||||
*/
|
||||
public makeUrl(urlState: IUrlState, use: UseCB = unwrap): string {
|
||||
const fullState = this._stateImpl.updateState(use(this.state), urlState);
|
||||
return this._stateImpl.encodeUrl(fullState, this._window.location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies to an <a> element to create a smart link, e.g. dom('a', setLinkUrl({ws: wsId})). It
|
||||
* both sets the href (e.g. to allow the link to be opened to a new tab), AND intercepts plain
|
||||
* clicks on it to "follow" the link without reloading the page.
|
||||
*/
|
||||
public setLinkUrl(urlState: IUrlState): DomElementMethod[] {
|
||||
return [
|
||||
dom.attr('href', (use) => this.makeUrl(urlState, use)),
|
||||
dom.on('click', (ev) => {
|
||||
// Only override plain-vanilla clicks.
|
||||
if (ev.shiftKey || ev.metaKey || ev.ctrlKey || ev.altKey) { return; }
|
||||
ev.preventDefault();
|
||||
return this.pushUrl(urlState);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the state from the current URL. This shouldn't normally need to get called. It's called
|
||||
* automatically when needed. It's also used by tests.
|
||||
*/
|
||||
public loadState() {
|
||||
log.debug(`loadState ${this._window.location.href}`);
|
||||
this.state.set(this._getState());
|
||||
}
|
||||
|
||||
private _getState(): IUrlState {
|
||||
return this._stateImpl.decodeUrl(this._window.location);
|
||||
}
|
||||
}
|
||||
|
||||
// This is what we expect from the global Window object. Tests may override with a mock.
|
||||
export interface HistWindow extends EventTarget {
|
||||
history: History;
|
||||
location: Location;
|
||||
|
||||
// This is a hook we create, to allow stubbing or overriding in tests.
|
||||
_urlStateLoadPage?: (href: string) => void;
|
||||
}
|
||||
|
||||
// The type of a 'use' callback as used in a computed(). It's what makes a computed subscribe to
|
||||
// its dependencies. The unwrap() helper allows using a dependency without any subscribing.
|
||||
type UseCB = <T>(obs: BaseObservable<T>) => T;
|
||||
const unwrap: UseCB = (obs) => obs.get();
|
||||
15
app/client/lib/log.ts
Normal file
15
app/client/lib/log.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Client-side debug logging.
|
||||
* At the moment this simply logs to the browser console, but it's still useful to have dedicated
|
||||
* methods to allow collecting them in the future, or silencing them in production or in mocha
|
||||
* tests.
|
||||
*/
|
||||
|
||||
export type LogMethod = (message: string, ...args: any[]) => void;
|
||||
|
||||
// tslint:disable:no-console
|
||||
export const debug: LogMethod = console.debug.bind(console);
|
||||
export const info: LogMethod = console.info.bind(console);
|
||||
export const log: LogMethod = console.log.bind(console);
|
||||
export const warn: LogMethod = console.warn.bind(console);
|
||||
export const error: LogMethod = console.error.bind(console);
|
||||
Reference in New Issue
Block a user