mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Polish tutorial popups
Summary: Includes the following changes: * Adds "Click to expand" hover tooltip to all images * Adds support for minimize/maximize by double clicking tutorial popup header * Add New menu (and all other popups) should now persist when user moves tutorial popup * Preserves scrollbar position when minimizing and maximizing tutorial popup * Formula cell editor (and other elements) should now be stacked under tutorial Test Plan: Browser and manual tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3864
This commit is contained in:
parent
f9f212d328
commit
3aac027a13
@ -134,7 +134,7 @@ div.dev_warning {
|
|||||||
display: none;
|
display: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5000;
|
z-index: var(--grist-browser-check-z-index);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
@ -24,15 +24,15 @@ export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
|
|||||||
let inputEl: HTMLInputElement;
|
let inputEl: HTMLInputElement;
|
||||||
setTimeout(() => { inputEl.focus(); inputEl.select(); }, 100);
|
setTimeout(() => { inputEl.focus(); inputEl.select(); }, 100);
|
||||||
|
|
||||||
confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), (
|
confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), {
|
||||||
dom('div', [
|
explanation: dom('div', [
|
||||||
cssField(
|
cssField(
|
||||||
cssLabel("Name"),
|
cssLabel("Name"),
|
||||||
inputEl = cssInput({value: pageName + ' (copy)'}),
|
inputEl = cssInput({value: pageName + ' (copy)'}),
|
||||||
),
|
),
|
||||||
t("Note that this does not copy data, but creates another view of the same data."),
|
t("Note that this does not copy data, but creates another view of the same data."),
|
||||||
])
|
]),
|
||||||
));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeDuplicate(gristDoc: GristDoc, pageId: number, pageName: string = '') {
|
async function makeDuplicate(gristDoc: GristDoc, pageId: number, pageName: string = '') {
|
||||||
|
@ -262,15 +262,17 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
|||||||
t("Error accessing document"),
|
t("Error accessing document"),
|
||||||
t("Reload"),
|
t("Reload"),
|
||||||
async () => window.location.reload(true),
|
async () => window.location.reload(true),
|
||||||
isDocOwner
|
|
||||||
? t("You can try reloading the document, or using recovery mode. " +
|
|
||||||
"Recovery mode opens the document to be fully accessible to " +
|
|
||||||
"owners, and inaccessible to others. It also disables " +
|
|
||||||
"formulas. [{{error}}]", {error: err.message})
|
|
||||||
: isDenied
|
|
||||||
? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message})
|
|
||||||
: t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message}),
|
|
||||||
{
|
{
|
||||||
|
explanation: (
|
||||||
|
isDocOwner
|
||||||
|
? t("You can try reloading the document, or using recovery mode. " +
|
||||||
|
"Recovery mode opens the document to be fully accessible to " +
|
||||||
|
"owners, and inaccessible to others. It also disables " +
|
||||||
|
"formulas. [{{error}}]", {error: err.message})
|
||||||
|
: isDenied
|
||||||
|
? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message})
|
||||||
|
: t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message})
|
||||||
|
),
|
||||||
hideCancel: true,
|
hideCancel: true,
|
||||||
extraButtons: !(isDocOwner && !isDenied) ? null : bigBasicButton(
|
extraButtons: !(isDocOwner && !isDenied) ? null : bigBasicButton(
|
||||||
t("Enter recovery mode"),
|
t("Enter recovery mode"),
|
||||||
|
@ -115,8 +115,12 @@ export class ApiKey extends Disposable {
|
|||||||
confirmModal(
|
confirmModal(
|
||||||
t("Remove API Key"), t("Remove"),
|
t("Remove API Key"), t("Remove"),
|
||||||
() => this._onDelete(),
|
() => this._onDelete(),
|
||||||
t("You're about to delete an API key. This will cause all future requests " +
|
{
|
||||||
"using this API key to be rejected. Do you still want to delete?")
|
explanation: t(
|
||||||
|
"You're about to delete an API key. This will cause all future requests " +
|
||||||
|
"using this API key to be rejected. Do you still want to delete?"
|
||||||
|
),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ body {
|
|||||||
left: 10%;
|
left: 10%;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
z-index: 999;
|
z-index: var(--grist-modal-z-index);
|
||||||
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
|
@ -476,7 +476,8 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs
|
|||||||
function deleteDoc() {
|
function deleteDoc() {
|
||||||
confirmModal(t("Delete {{name}}", {name: doc.name}), t("Delete"),
|
confirmModal(t("Delete {{name}}", {name: doc.name}), t("Delete"),
|
||||||
() => home.deleteDoc(doc.id, false).catch(reportError),
|
() => home.deleteDoc(doc.id, false).catch(reportError),
|
||||||
t("Document will be moved to Trash."));
|
{explanation: t("Document will be moved to Trash.")}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function manageUsers() {
|
async function manageUsers() {
|
||||||
@ -529,7 +530,8 @@ export function makeRemovedDocOptionsMenu(home: HomeModel, doc: Document, worksp
|
|||||||
function hardDeleteDoc() {
|
function hardDeleteDoc() {
|
||||||
confirmModal(t("Permanently Delete \"{{name}}\"?", {name: doc.name}), t("Delete Forever"),
|
confirmModal(t("Permanently Delete \"{{name}}\"?", {name: doc.name}), t("Delete Forever"),
|
||||||
() => home.deleteDoc(doc.id, true).catch(reportError),
|
() => home.deleteDoc(doc.id, true).catch(reportError),
|
||||||
t("Document will be permanently deleted."));
|
{explanation: t("Document will be permanently deleted.")}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -3,9 +3,9 @@ import {urlState} from 'app/client/models/gristUrlState';
|
|||||||
import {renderer} from 'app/client/ui/DocTutorialRenderer';
|
import {renderer} from 'app/client/ui/DocTutorialRenderer';
|
||||||
import {cssPopupBody, FloatingPopup} from 'app/client/ui/FloatingPopup';
|
import {cssPopupBody, FloatingPopup} from 'app/client/ui/FloatingPopup';
|
||||||
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
||||||
import {hoverTooltip} from 'app/client/ui/tooltips';
|
import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips';
|
||||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||||
import {mediaXSmall, theme} from 'app/client/ui2018/cssVars';
|
import {mediaXSmall, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||||
import {confirmModal, modal} from 'app/client/ui2018/modals';
|
import {confirmModal, modal} from 'app/client/ui2018/modals';
|
||||||
@ -25,6 +25,8 @@ interface DocTutorialSlide {
|
|||||||
|
|
||||||
const testId = makeTestId('test-doc-tutorial-');
|
const testId = makeTestId('test-doc-tutorial-');
|
||||||
|
|
||||||
|
const TOOLTIP_KEY = 'docTutorialTooltip';
|
||||||
|
|
||||||
export class DocTutorial extends FloatingPopup {
|
export class DocTutorial extends FloatingPopup {
|
||||||
private _appModel = this._gristDoc.docPageModel.appModel;
|
private _appModel = this._gristDoc.docPageModel.appModel;
|
||||||
private _currentDoc = this._gristDoc.docPageModel.currentDoc.get();
|
private _currentDoc = this._gristDoc.docPageModel.currentDoc.get();
|
||||||
@ -44,7 +46,7 @@ export class DocTutorial extends FloatingPopup {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor(private _gristDoc: GristDoc) {
|
constructor(private _gristDoc: GristDoc) {
|
||||||
super();
|
super({stopClickPropagationOnMove: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
@ -77,7 +79,7 @@ export class DocTutorial extends FloatingPopup {
|
|||||||
|
|
||||||
this._openLightbox((ev.target as HTMLImageElement).src);
|
this._openLightbox((ev.target as HTMLImageElement).src);
|
||||||
}),
|
}),
|
||||||
this._restartGIFs(),
|
this._initializeImages(),
|
||||||
],
|
],
|
||||||
testId('popup-body'),
|
testId('popup-body'),
|
||||||
);
|
);
|
||||||
@ -94,14 +96,17 @@ export class DocTutorial extends FloatingPopup {
|
|||||||
return [
|
return [
|
||||||
cssFooterButtonsLeft(
|
cssFooterButtonsLeft(
|
||||||
cssPopupFooterButton(icon('Undo'),
|
cssPopupFooterButton(icon('Undo'),
|
||||||
hoverTooltip('Restart Tutorial', {key: 'docTutorialTooltip'}),
|
hoverTooltip('Restart Tutorial', {key: TOOLTIP_KEY}),
|
||||||
dom.on('click', () => this._restartTutorial()),
|
dom.on('click', () => this._restartTutorial()),
|
||||||
testId('popup-restart'),
|
testId('popup-restart'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cssProgressBar(
|
cssProgressBar(
|
||||||
range(slides.length).map((i) => cssProgressBarDot(
|
range(slides.length).map((i) => cssProgressBarDot(
|
||||||
{title: slides[i].slideTitle},
|
hoverTooltip(slides[i].slideTitle, {
|
||||||
|
closeOnClick: false,
|
||||||
|
key: TOOLTIP_KEY,
|
||||||
|
}),
|
||||||
cssProgressBarDot.cls('-current', i === slideIndex),
|
cssProgressBarDot.cls('-current', i === slideIndex),
|
||||||
i === slideIndex ? null : dom.on('click', () => this._changeSlide(i)),
|
i === slideIndex ? null : dom.on('click', () => this._changeSlide(i)),
|
||||||
testId(`popup-slide-${i + 1}`),
|
testId(`popup-slide-${i + 1}`),
|
||||||
@ -251,11 +256,19 @@ export class DocTutorial extends FloatingPopup {
|
|||||||
confirmModal(
|
confirmModal(
|
||||||
'Do you want to restart the tutorial? All progress will be lost.',
|
'Do you want to restart the tutorial? All progress will be lost.',
|
||||||
'Restart',
|
'Restart',
|
||||||
doRestart
|
doRestart,
|
||||||
|
{
|
||||||
|
modalOptions: {
|
||||||
|
backerDomArgs: [
|
||||||
|
// Stack modal above the tutorial popup.
|
||||||
|
dom.style('z-index', vars.tutorialModalZIndex.toString()),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _restartGIFs() {
|
private _initializeImages() {
|
||||||
return (element: HTMLElement) => {
|
return (element: HTMLElement) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const imgs = element.querySelectorAll('img');
|
const imgs = element.querySelectorAll('img');
|
||||||
@ -263,6 +276,16 @@ export class DocTutorial extends FloatingPopup {
|
|||||||
// Re-assigning src to itself is a neat way to restart a GIF.
|
// Re-assigning src to itself is a neat way to restart a GIF.
|
||||||
// eslint-disable-next-line no-self-assign
|
// eslint-disable-next-line no-self-assign
|
||||||
img.src = img.src;
|
img.src = img.src;
|
||||||
|
|
||||||
|
setHoverTooltip(img, 'Click to expand', {
|
||||||
|
key: TOOLTIP_KEY,
|
||||||
|
modifiers: {
|
||||||
|
flip: {
|
||||||
|
boundariesElement: 'scrollParent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
placement: 'bottom',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
@ -281,6 +304,11 @@ export class DocTutorial extends FloatingPopup {
|
|||||||
dom.on('click', (ev, elem) => void (ev.target === elem ? ctl.close() : null)),
|
dom.on('click', (ev, elem) => void (ev.target === elem ? ctl.close() : null)),
|
||||||
testId('lightbox'),
|
testId('lightbox'),
|
||||||
];
|
];
|
||||||
|
}, {
|
||||||
|
backerDomArgs: [
|
||||||
|
// Stack modal above the tutorial popup.
|
||||||
|
dom.style('z-index', vars.tutorialModalZIndex.toString()),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {hoverTooltip} from 'app/client/ui/tooltips';
|
import {hoverTooltip} from 'app/client/ui/tooltips';
|
||||||
import {isNarrowScreen, isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars';
|
import {isNarrowScreen, isNarrowScreenObs, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {Disposable, dom, DomArg, DomContents, IDisposable, makeTestId, Observable, styled} from 'grainjs';
|
import {Disposable, dom, DomArg, DomContents, IDisposable, makeTestId, Observable, styled} from 'grainjs';
|
||||||
|
|
||||||
@ -16,11 +16,15 @@ export interface PopupOptions {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
closeButton?: boolean;
|
closeButton?: boolean;
|
||||||
autoHeight?: boolean;
|
autoHeight?: boolean;
|
||||||
|
/** Defaults to false. */
|
||||||
|
stopClickPropagationOnMove?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FloatingPopup extends Disposable {
|
export class FloatingPopup extends Disposable {
|
||||||
protected _isMinimized = Observable.create(this, false);
|
protected _isMinimized = Observable.create(this, false);
|
||||||
|
private _isFinishingMove = false;
|
||||||
private _popupElement: HTMLElement | null = null;
|
private _popupElement: HTMLElement | null = null;
|
||||||
|
private _popupMinimizeButtonElement: HTMLElement | null = null;
|
||||||
|
|
||||||
private _startX: number;
|
private _startX: number;
|
||||||
private _startY: number;
|
private _startY: number;
|
||||||
@ -33,6 +37,25 @@ export class FloatingPopup extends Disposable {
|
|||||||
constructor(protected _options: PopupOptions = {}, private _args: DomArg[] = []) {
|
constructor(protected _options: PopupOptions = {}, private _args: DomArg[] = []) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
if (_options.stopClickPropagationOnMove){
|
||||||
|
// weasel.js registers a 'click' listener that closes any open popups that
|
||||||
|
// are outside the click target. We capture the click event here, stopping
|
||||||
|
// propagation in a few scenarios where closing popups is undesirable.
|
||||||
|
window.addEventListener('click', (ev) => {
|
||||||
|
if (this._isFinishingMove) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._isFinishingMove = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._popupMinimizeButtonElement?.contains(ev.target as Node)) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._minimizeOrMaximize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, {capture: true});
|
||||||
|
}
|
||||||
|
|
||||||
this._handleMouseDown = this._handleMouseDown.bind(this);
|
this._handleMouseDown = this._handleMouseDown.bind(this);
|
||||||
this._handleMouseMove = this._handleMouseMove.bind(this);
|
this._handleMouseMove = this._handleMouseMove.bind(this);
|
||||||
this._handleMouseUp = this._handleMouseUp.bind(this);
|
this._handleMouseUp = this._handleMouseUp.bind(this);
|
||||||
@ -181,6 +204,7 @@ export class FloatingPopup extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleMouseUp() {
|
private _handleMouseUp() {
|
||||||
|
this._isFinishingMove = true;
|
||||||
document.removeEventListener('mousemove', this._handleMouseMove);
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
||||||
document.removeEventListener('mouseup', this._handleMouseUp);
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
||||||
document.body.removeEventListener('mouseleave', this._handleMouseUp);
|
document.body.removeEventListener('mouseleave', this._handleMouseUp);
|
||||||
@ -221,6 +245,11 @@ export class FloatingPopup extends Disposable {
|
|||||||
this._popupElement!.style.top = `${newTop}px`;
|
this._popupElement!.style.top = `${newTop}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _minimizeOrMaximize() {
|
||||||
|
this._isMinimized.set(!this._isMinimized.get());
|
||||||
|
this._repositionPopup();
|
||||||
|
}
|
||||||
|
|
||||||
private _buildPopup() {
|
private _buildPopup() {
|
||||||
const body = cssPopup(
|
const body = cssPopup(
|
||||||
{tabIndex: '-1'},
|
{tabIndex: '-1'},
|
||||||
@ -231,7 +260,8 @@ export class FloatingPopup extends Disposable {
|
|||||||
return cssResizeTopLayer(
|
return cssResizeTopLayer(
|
||||||
cssTopHandle(testId('resize-handle')),
|
cssTopHandle(testId('resize-handle')),
|
||||||
dom.on('mousedown', () => this._resize = true),
|
dom.on('mousedown', () => this._resize = true),
|
||||||
dom.on('dblclick', () => {
|
dom.on('dblclick', (e) => {
|
||||||
|
e.stopImmediatePropagation();
|
||||||
this._popupElement?.style.setProperty('--height', `${POPUP_MAX_HEIGHT}px`);
|
this._popupElement?.style.setProperty('--height', `${POPUP_MAX_HEIGHT}px`);
|
||||||
this._repositionPopup();
|
this._repositionPopup();
|
||||||
})
|
})
|
||||||
@ -262,23 +292,27 @@ export class FloatingPopup extends Disposable {
|
|||||||
}),
|
}),
|
||||||
testId('close'),
|
testId('close'),
|
||||||
),
|
),
|
||||||
cssPopupHeaderButton(
|
this._popupMinimizeButtonElement = cssPopupHeaderButton(
|
||||||
isMinimized ? icon('Maximize'): icon('Minimize'),
|
isMinimized ? icon('Maximize'): icon('Minimize'),
|
||||||
hoverTooltip(isMinimized ? 'Maximize' : 'Minimize', {key: 'docTutorialTooltip'}),
|
hoverTooltip(isMinimized ? 'Maximize' : 'Minimize', {key: 'docTutorialTooltip'}),
|
||||||
dom.on('click', () => {
|
dom.on('click', () => this._minimizeOrMaximize()),
|
||||||
this._isMinimized.set(!this._isMinimized.get());
|
|
||||||
this._repositionPopup();
|
|
||||||
}),
|
|
||||||
testId('minimize-maximize'),
|
testId('minimize-maximize'),
|
||||||
),
|
),
|
||||||
|
// Disable dragging when a button in the header is clicked.
|
||||||
|
dom.on('mousedown', ev => ev.stopPropagation()),
|
||||||
|
dom.on('touchstart', ev => ev.stopPropagation()),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
dom.on('mousedown', this._handleMouseDown),
|
dom.on('mousedown', this._handleMouseDown),
|
||||||
dom.on('touchstart', this._handleTouchStart),
|
dom.on('touchstart', this._handleTouchStart),
|
||||||
|
dom.on('dblclick', () => this._minimizeOrMaximize()),
|
||||||
testId('header'),
|
testId('header'),
|
||||||
),
|
),
|
||||||
dom.maybe(use => !use(this._isMinimized), () => this._buildContent()),
|
cssPopupContent(
|
||||||
|
this._buildContent(),
|
||||||
|
cssPopupContent.cls('-minimized', this._isMinimized),
|
||||||
|
),
|
||||||
() => { window.addEventListener('resize', this._handleWindowResize); },
|
() => { window.addEventListener('resize', this._handleWindowResize); },
|
||||||
dom.onDispose(() => {
|
dom.onDispose(() => {
|
||||||
document.removeEventListener('mousemove', this._handleMouseMove);
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
||||||
@ -344,7 +378,7 @@ const cssPopup = styled('div', `
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 2px solid ${theme.accentBorder};
|
border: 2px solid ${theme.accentBorder};
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
z-index: 999;
|
z-index: ${vars.floatingPopupZIndex};
|
||||||
--height: ${POPUP_MAX_HEIGHT}px;
|
--height: ${POPUP_MAX_HEIGHT}px;
|
||||||
height: ${POPUP_HEIGHT};
|
height: ${POPUP_HEIGHT};
|
||||||
width: ${POPUP_WIDTH};
|
width: ${POPUP_WIDTH};
|
||||||
@ -441,10 +475,10 @@ const cssPopupHeaderButton = styled('div', `
|
|||||||
|
|
||||||
const cssResizeTopLayer = styled('div', `
|
const cssResizeTopLayer = styled('div', `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -6px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 70%;
|
bottom: 28px;
|
||||||
z-index: 500;
|
z-index: 500;
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
`);
|
`);
|
||||||
@ -465,3 +499,14 @@ const cssBottomHandle = styled(cssTopHandle, `
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssPopupContent = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-minimized {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
@ -217,7 +217,7 @@ function workspaceMenu(home: HomeModel, ws: Workspace, renaming: Observable<Work
|
|||||||
function deleteWorkspace() {
|
function deleteWorkspace() {
|
||||||
confirmModal(t("Delete {{workspace}} and all included documents?", {workspace: ws.name}), t("Delete"),
|
confirmModal(t("Delete {{workspace}} and all included documents?", {workspace: ws.name}), t("Delete"),
|
||||||
() => home.deleteWorkspace(ws.id, false),
|
() => home.deleteWorkspace(ws.id, false),
|
||||||
t("Workspace will be moved to Trash."));
|
{explanation: t("Workspace will be moved to Trash.")});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function manageWorkspaceUsers() {
|
async function manageWorkspaceUsers() {
|
||||||
|
@ -59,7 +59,7 @@ export async function replaceTrunkWithFork(user: FullUser|null, doc: Document, a
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError(e); // For example: no write access on trunk.
|
reportError(e); // For example: no write access on trunk.
|
||||||
}
|
}
|
||||||
}, warningText);
|
}, {explanation: warningText});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show message in a modal with a `Sign up` button that redirects to the login page.
|
// Show message in a modal with a `Sign up` button that redirects to the login page.
|
||||||
|
@ -258,13 +258,13 @@ const cssDropdownStatusText = styled('div', `
|
|||||||
color: ${theme.lightText};
|
color: ${theme.lightText};
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// z-index below is set above other assorted children of <body> which include z-index such as 999
|
// z-index below is set above other assorted children of <body>, which includes
|
||||||
// and 1050 (for new-style and old-style modals, for example).
|
// indexes such as 999 for modals.
|
||||||
const cssSnackbarWrapper = styled('div', `
|
const cssSnackbarWrapper = styled('div', `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
z-index: 1100;
|
z-index: ${vars.notificationZIndex};
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -326,7 +326,7 @@ const Container = styled('div', `
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
border: 2px solid ${theme.accentBorder};
|
border: 2px solid ${theme.accentBorder};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
z-index: 1000;
|
z-index: ${vars.onboardingPopupZIndex};
|
||||||
max-width: 490px;
|
max-width: 490px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: ${theme.popupBg};
|
background-color: ${theme.popupBg};
|
||||||
@ -423,7 +423,7 @@ const Overlay = styled('div', `
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 999;
|
z-index: ${vars.onboardingBackdropZIndex};
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
@ -105,9 +105,14 @@ export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOpti
|
|||||||
confirmModal(
|
confirmModal(
|
||||||
`You are about to remove your own access to this ${name}`,
|
`You are about to remove your own access to this ${name}`,
|
||||||
'Remove my access', tryToSaveChanges,
|
'Remove my access', tryToSaveChanges,
|
||||||
'Once you have removed your own access, ' +
|
{
|
||||||
'you will not be able to get it back without assistance ' +
|
explanation: (
|
||||||
`from someone else with sufficient access to the ${name}.`);
|
'Once you have removed your own access, ' +
|
||||||
|
'you will not be able to get it back without assistance ' +
|
||||||
|
`from someone else with sufficient access to the ${name}.`
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
tryToSaveChanges().catch(reportError);
|
tryToSaveChanges().catch(reportError);
|
||||||
}
|
}
|
||||||
|
@ -404,7 +404,7 @@ const cssColumnInfoTooltipButton = styled('div', `
|
|||||||
|
|
||||||
const cssTooltip = styled('div', `
|
const cssTooltip = styled('div', `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5000; /* should be higher than a modal */
|
z-index: ${vars.tooltipZIndex}; /* should be higher than a modal */
|
||||||
background-color: ${theme.tooltipBg};
|
background-color: ${theme.tooltipBg};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 0 2px rgba(0,0,0,0.5);
|
box-shadow: 0 0 2px rgba(0,0,0,0.5);
|
||||||
|
@ -134,6 +134,18 @@ export const vars = {
|
|||||||
logoBg: new CustomProp('logo-bg', '#040404'),
|
logoBg: new CustomProp('logo-bg', '#040404'),
|
||||||
logoSize: new CustomProp('logo-size', '22px 22px'),
|
logoSize: new CustomProp('logo-size', '22px 22px'),
|
||||||
toastBg: new CustomProp('toast-bg', '#040404'),
|
toastBg: new CustomProp('toast-bg', '#040404'),
|
||||||
|
|
||||||
|
/* Z indexes */
|
||||||
|
menuZIndex: new CustomProp('menu-z-index', '999'),
|
||||||
|
modalZIndex: new CustomProp('modal-z-index', '999'),
|
||||||
|
onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'),
|
||||||
|
onboardingPopupZIndex: new CustomProp('onboarding-popup-z-index', '1000'),
|
||||||
|
floatingPopupZIndex: new CustomProp('floating-popup-z-index', '1002'),
|
||||||
|
tutorialModalZIndex: new CustomProp('tutorial-modal-z-index', '1003'),
|
||||||
|
notificationZIndex: new CustomProp('notification-z-index', '1100'),
|
||||||
|
browserCheckZIndex: new CustomProp('browser-check-z-index', '5000'),
|
||||||
|
tooltipZIndex: new CustomProp('tooltip-z-index', '5000'),
|
||||||
|
// TODO: Add properties for remaining hard-coded z-indexes.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,7 +65,7 @@ export const cssMenuElem = styled('div', `
|
|||||||
padding: 8px 0px 16px 0px;
|
padding: 8px 0px 16px 0px;
|
||||||
box-shadow: 0 2px 20px 0 ${theme.menuShadow};
|
box-shadow: 0 2px 20px 0 ${theme.menuShadow};
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
z-index: 999;
|
z-index: ${vars.menuZIndex};
|
||||||
--weaseljs-selected-background-color: ${theme.menuItemSelectedBg};
|
--weaseljs-selected-background-color: ${theme.menuItemSelectedBg};
|
||||||
--weaseljs-menu-item-padding: 8px 24px;
|
--weaseljs-menu-item-padding: 8px 24px;
|
||||||
background-color: ${theme.menuBg};
|
background-color: ${theme.menuBg};
|
||||||
|
@ -137,6 +137,8 @@ export interface IModalOptions {
|
|||||||
noEscapeKey?: boolean;
|
noEscapeKey?: boolean;
|
||||||
// If set, clicking into background does not close dialog.
|
// If set, clicking into background does not close dialog.
|
||||||
noClickAway?: boolean;
|
noClickAway?: boolean;
|
||||||
|
// DOM arguments to pass to the modal backer.
|
||||||
|
backerDomArgs?: DomElementArg[];
|
||||||
// If given, call and wait for this before closing the dialog. If it returns false, don't close.
|
// If given, call and wait for this before closing the dialog. If it returns false, don't close.
|
||||||
// Error also prevents closing, and is reported as an unexpected error.
|
// Error also prevents closing, and is reported as an unexpected error.
|
||||||
beforeClose?: () => Promise<boolean>;
|
beforeClose?: () => Promise<boolean>;
|
||||||
@ -174,7 +176,13 @@ export function modal(
|
|||||||
createFn: (ctl: IModalControl, owner: MultiHolder) => DomElementArg,
|
createFn: (ctl: IModalControl, owner: MultiHolder) => DomElementArg,
|
||||||
options: IModalOptions = {}
|
options: IModalOptions = {}
|
||||||
): void {
|
): void {
|
||||||
const {noEscapeKey, noClickAway, refElement = document.body, variant = 'fade-in'} = options;
|
const {
|
||||||
|
noEscapeKey,
|
||||||
|
noClickAway,
|
||||||
|
refElement = document.body,
|
||||||
|
variant = 'fade-in',
|
||||||
|
backerDomArgs = [],
|
||||||
|
} = options;
|
||||||
|
|
||||||
function doClose() {
|
function doClose() {
|
||||||
if (!modalDom.isConnected) { return; }
|
if (!modalDom.isConnected) { return; }
|
||||||
@ -242,6 +250,7 @@ export function modal(
|
|||||||
return dialogDom;
|
return dialogDom;
|
||||||
}),
|
}),
|
||||||
noClickAway ? null : dom.on('click', () => close()),
|
noClickAway ? null : dom.on('click', () => close()),
|
||||||
|
...backerDomArgs,
|
||||||
);
|
);
|
||||||
|
|
||||||
document.body.appendChild(modalDom);
|
document.body.appendChild(modalDom);
|
||||||
@ -293,7 +302,10 @@ export interface ISaveModalOptions {
|
|||||||
* 3. saveFunc: () => doSomething().catch((e) => { alert("BOOM"); throw new StayOpen(); })
|
* 3. saveFunc: () => doSomething().catch((e) => { alert("BOOM"); throw new StayOpen(); })
|
||||||
* If doSomething fails, an alert is shown, and the dialog stays open.
|
* If doSomething fails, an alert is shown, and the dialog stays open.
|
||||||
*/
|
*/
|
||||||
export function saveModal(createFunc: (ctl: IModalControl, owner: MultiHolder) => ISaveModalOptions) {
|
export function saveModal(
|
||||||
|
createFunc: (ctl: IModalControl, owner: MultiHolder) => ISaveModalOptions,
|
||||||
|
modalOptions?: IModalOptions
|
||||||
|
) {
|
||||||
return modal((ctl, owner) => {
|
return modal((ctl, owner) => {
|
||||||
const options = createFunc(ctl, owner);
|
const options = createFunc(ctl, owner);
|
||||||
|
|
||||||
@ -321,7 +333,14 @@ export function saveModal(createFunc: (ctl: IModalControl, owner: MultiHolder) =
|
|||||||
options.width && cssModalWidth(options.width),
|
options.width && cssModalWidth(options.width),
|
||||||
options.modalArgs,
|
options.modalArgs,
|
||||||
];
|
];
|
||||||
});
|
}, modalOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmModalOptions {
|
||||||
|
explanation?: DomElementArg,
|
||||||
|
hideCancel?: boolean;
|
||||||
|
extraButtons?: DomContents;
|
||||||
|
modalOptions?: IModalOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -333,8 +352,7 @@ export function confirmModal(
|
|||||||
title: DomElementArg,
|
title: DomElementArg,
|
||||||
btnText: DomElementArg,
|
btnText: DomElementArg,
|
||||||
onConfirm: () => Promise<void>,
|
onConfirm: () => Promise<void>,
|
||||||
explanation?: DomElementArg,
|
{explanation, hideCancel, extraButtons, modalOptions}: ConfirmModalOptions = {},
|
||||||
{hideCancel, extraButtons}: {hideCancel?: boolean, extraButtons?: DomContents} = {},
|
|
||||||
): void {
|
): void {
|
||||||
return saveModal((ctl, owner): ISaveModalOptions => ({
|
return saveModal((ctl, owner): ISaveModalOptions => ({
|
||||||
title,
|
title,
|
||||||
@ -344,7 +362,7 @@ export function confirmModal(
|
|||||||
hideCancel,
|
hideCancel,
|
||||||
width: 'normal',
|
width: 'normal',
|
||||||
extraButtons,
|
extraButtons,
|
||||||
}));
|
}), modalOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -592,7 +610,7 @@ const cssModalBacker = styled('div', `
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
z-index: 999;
|
z-index: ${vars.modalZIndex};
|
||||||
background-color: ${theme.modalBackdrop};
|
background-color: ${theme.modalBackdrop};
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
animation-name: ${cssFadeIn};
|
animation-name: ${cssFadeIn};
|
||||||
|
@ -354,7 +354,12 @@ describe('DocTutorial', function () {
|
|||||||
|
|
||||||
it('supports navigating to a specific slide', async function() {
|
it('supports navigating to a specific slide', async function() {
|
||||||
const slide3 = await driver.find('.test-doc-tutorial-popup-slide-3');
|
const slide3 = await driver.find('.test-doc-tutorial-popup-slide-3');
|
||||||
assert.equal(await slide3.getAttribute('title'), 'Adding Columns and Rows');
|
await slide3.mouseMove();
|
||||||
|
await gu.waitToPass(
|
||||||
|
async () => assert.isTrue(await driver.find('.test-tooltip').isDisplayed()),
|
||||||
|
500
|
||||||
|
);
|
||||||
|
assert.equal(await driver.find('.test-tooltip').getText(), 'Adding Columns and Rows');
|
||||||
await slide3.click();
|
await slide3.click();
|
||||||
await driver.find('.test-doc-tutorial-popup-slide-3').click();
|
await driver.find('.test-doc-tutorial-popup-slide-3').click();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@ -368,7 +373,12 @@ describe('DocTutorial', function () {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const slide1 = await driver.find('.test-doc-tutorial-popup-slide-1');
|
const slide1 = await driver.find('.test-doc-tutorial-popup-slide-1');
|
||||||
assert.equal(await slide1.getAttribute('title'), 'Intro');
|
await slide1.mouseMove();
|
||||||
|
await gu.waitToPass(
|
||||||
|
async () => assert.isTrue(await driver.find('.test-tooltip').isDisplayed()),
|
||||||
|
500
|
||||||
|
);
|
||||||
|
assert.equal(await driver.find('.test-tooltip').getText(), 'Intro');
|
||||||
await slide1.click();
|
await slide1.click();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await driver.find('.test-doc-tutorial-popup h1').getText(),
|
await driver.find('.test-doc-tutorial-popup h1').getText(),
|
||||||
@ -392,11 +402,11 @@ describe('DocTutorial', function () {
|
|||||||
assert.isFalse(await driver.find('.test-doc-tutorial-lightbox').isPresent());
|
assert.isFalse(await driver.find('.test-doc-tutorial-lightbox').isPresent());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be minimized and maximized', async function() {
|
it('can be minimized and maximized by clicking a button in the header', async function() {
|
||||||
await driver.find('.test-floating-popup-minimize-maximize').click();
|
await driver.find('.test-floating-popup-minimize-maximize').click();
|
||||||
assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed());
|
assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed());
|
||||||
assert.isFalse(await driver.find('.test-doc-tutorial-popup-body').isPresent());
|
assert.isFalse(await driver.find('.test-doc-tutorial-popup-body').isDisplayed());
|
||||||
assert.isFalse(await driver.find('.test-doc-tutorial-popup-footer').isPresent());
|
assert.isFalse(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed());
|
||||||
|
|
||||||
await driver.find('.test-floating-popup-minimize-maximize').click();
|
await driver.find('.test-floating-popup-minimize-maximize').click();
|
||||||
assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed());
|
assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed());
|
||||||
@ -404,6 +414,18 @@ describe('DocTutorial', function () {
|
|||||||
assert.isTrue(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed());
|
assert.isTrue(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can be minimized and maximized by double clicking the header', async function() {
|
||||||
|
await driver.withActions(a => a.doubleClick(driver.find('.test-floating-popup-title')));
|
||||||
|
assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed());
|
||||||
|
assert.isFalse(await driver.find('.test-doc-tutorial-popup-body').isDisplayed());
|
||||||
|
assert.isFalse(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed());
|
||||||
|
|
||||||
|
await driver.withActions(a => a.doubleClick(driver.find('.test-floating-popup-title')));
|
||||||
|
assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed());
|
||||||
|
assert.isTrue(await driver.find('.test-doc-tutorial-popup-body').isDisplayed());
|
||||||
|
assert.isTrue(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed());
|
||||||
|
});
|
||||||
|
|
||||||
it('does not play an easter egg when opening an anchor link encoded with rr', async function() {
|
it('does not play an easter egg when opening an anchor link encoded with rr', async function() {
|
||||||
await gu.getCell({rowNum: 1, col: 0}).click();
|
await gu.getCell({rowNum: 1, col: 0}).click();
|
||||||
const link = await gu.getAnchor();
|
const link = await gu.getAnchor();
|
||||||
|
Loading…
Reference in New Issue
Block a user