mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add support for editing on mobile.
Summary: - Add custom handling for dblclick on mobile, to allow focusing editor. - In place of Clipboard.js, use a FocusLayer with document.body as the default focus element. - Set maximum-scale on iOS viewport to prevent auto-zoom. - Reposition the editor on window resize when editing a cell, which is a normal occurrence on Android when virtual keyboard is shown. - Add Save/Cancel icon-buttons next to cell editor on mobile. Test Plan: Tested manually on Safari / FF on iPhone, and on Chrome on Android emulator. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2721
This commit is contained in:
@@ -72,6 +72,7 @@ class FocusLayerManager extends Disposable {
|
||||
}
|
||||
|
||||
public addLayer(layer: FocusLayer) {
|
||||
this.getCurrentLayer()?.onDefaultBlur();
|
||||
this._focusLayers.push(layer);
|
||||
// Move the focus to the new layer. Not just grabFocus, because if the focus is on the previous
|
||||
// layer's defaultFocusElem, the new layer might consider it "allowed" and never get the focus.
|
||||
@@ -102,6 +103,7 @@ class FocusLayerManager extends Disposable {
|
||||
this._timeoutId = null;
|
||||
const layer = this.getCurrentLayer();
|
||||
if (!layer || document.activeElement === layer.defaultFocusElem) {
|
||||
layer?.onDefaultFocus();
|
||||
return;
|
||||
}
|
||||
// If the window doesn't have focus, don't rush to grab it, or we can interfere with focus
|
||||
@@ -111,10 +113,10 @@ class FocusLayerManager extends Disposable {
|
||||
}
|
||||
if (document.activeElement && layer.allowFocus(document.activeElement)) {
|
||||
watchElementForBlur(document.activeElement, () => this.grabFocus());
|
||||
layer.onDefaultBlur?.();
|
||||
layer.onDefaultBlur();
|
||||
} else {
|
||||
layer.defaultFocusElem.focus();
|
||||
layer.onDefaultFocus?.();
|
||||
layer.onDefaultFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,15 +132,16 @@ export class FocusLayer extends Disposable implements FocusLayerOptions {
|
||||
|
||||
public defaultFocusElem: HTMLElement;
|
||||
public allowFocus: (elem: Element) => boolean;
|
||||
public onDefaultFocus?: () => void;
|
||||
public onDefaultBlur?: () => void;
|
||||
public _onDefaultFocus?: () => void;
|
||||
public _onDefaultBlur?: () => void;
|
||||
private _isDefaultFocused: boolean|null = null;
|
||||
|
||||
constructor(options: FocusLayerOptions) {
|
||||
super();
|
||||
this.defaultFocusElem = options.defaultFocusElem;
|
||||
this.allowFocus = options.allowFocus;
|
||||
this.onDefaultFocus = options.onDefaultFocus;
|
||||
this.onDefaultBlur = options.onDefaultBlur;
|
||||
this._onDefaultFocus = options.onDefaultFocus;
|
||||
this._onDefaultBlur = options.onDefaultBlur;
|
||||
|
||||
const managerRefCount = this.autoDispose(_focusLayerManager.use(null));
|
||||
const manager = managerRefCount.get();
|
||||
@@ -146,6 +149,20 @@ export class FocusLayer extends Disposable implements FocusLayerOptions {
|
||||
this.onDispose(() => manager.removeLayer(this));
|
||||
this.autoDispose(dom.onElem(this.defaultFocusElem, 'blur', () => manager.grabFocus()));
|
||||
}
|
||||
|
||||
public onDefaultFocus() {
|
||||
// Only trigger onDefaultFocus() callback when the focus status actually changed.
|
||||
if (this._isDefaultFocused) { return; }
|
||||
this._isDefaultFocused = true;
|
||||
this._onDefaultFocus?.();
|
||||
}
|
||||
|
||||
public onDefaultBlur() {
|
||||
// Only trigger onDefaultBlur() callback when the focus status actually changed.
|
||||
if (this._isDefaultFocused === false) { return; }
|
||||
this._isDefaultFocused = false;
|
||||
this._onDefaultBlur?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,3 +11,10 @@ export function isDesktop() {
|
||||
const platformType = getParser().getPlatformType();
|
||||
return (!platformType || platformType === 'desktop');
|
||||
}
|
||||
|
||||
// Returns whether the browser is on mobile iOS.
|
||||
// This is used in particular in viewport.ts to set maximum-scale=1 (to prevent iOS auto-zoom when
|
||||
// an input is focused, without preventing manual pinch-to-zoom).
|
||||
export function isIOS() {
|
||||
return navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
|
||||
}
|
||||
|
||||
37
app/client/lib/dblclick.ts
Normal file
37
app/client/lib/dblclick.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {dom, EventCB} from 'grainjs';
|
||||
|
||||
const DOUBLE_TAP_INTERVAL_MS = 500;
|
||||
|
||||
/**
|
||||
* Helper to handle 'dblclick' events on either browser or mobile.
|
||||
*
|
||||
* This is equivalent to a 'dblclick' handler when touch events are not supported. When they are,
|
||||
* the callback will be called on second touch within a short time of a first one. (In that case,
|
||||
* preventDefault() prevents a 'dblclick' event from being emulated.)
|
||||
*
|
||||
* Background: though mobile browsers we care about already generate 'click' and 'dblclick' events
|
||||
* in response to touch events, it doesn't seem to be treated as a direct user interaction. E.g.
|
||||
* double-click to edit a cell should focus the editor and open the mobile keyboard, but a
|
||||
* JS-issued focus() call only works when triggered by a direct user interaction, and synthesized
|
||||
* dblclick doesn't seem to do that.
|
||||
*
|
||||
* Helpful links on emulated (synthesized) events:
|
||||
* - https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent
|
||||
* - https://github.com/w3c/pointerevents/issues/171
|
||||
*/
|
||||
export function onDblClickMatchElem(elem: EventTarget, selector: string, callback: EventCB): void {
|
||||
dom.onMatchElem(elem, selector, 'dblclick', (ev, _elem) => {
|
||||
callback(ev, _elem);
|
||||
});
|
||||
|
||||
let lastTapTime = 0;
|
||||
dom.onMatchElem(elem, selector, 'touchend', (ev, _elem) => {
|
||||
const currentTime = Date.now();
|
||||
const tapLength = currentTime - lastTapTime;
|
||||
lastTapTime = currentTime;
|
||||
if (tapLength < DOUBLE_TAP_INTERVAL_MS && tapLength > 0) {
|
||||
ev.preventDefault();
|
||||
callback(ev, _elem);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user