Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
226 lines
6.4 KiB
TypeScript
226 lines
6.4 KiB
TypeScript
import { BlockSuiteError } from '@blocksuite/global/exceptions';
|
|
import {
|
|
autoUpdate,
|
|
computePosition,
|
|
type ComputePositionReturn,
|
|
} from '@floating-ui/dom';
|
|
import { cssVar } from '@toeverything/theme';
|
|
import { render } from 'lit';
|
|
import { Subject } from 'rxjs';
|
|
|
|
import type { AdvancedPortalOptions, PortalOptions } from './types.js';
|
|
|
|
/**
|
|
* Similar to `<blocksuite-portal>`, but only renders once when called.
|
|
*
|
|
* The template should be a **static** template since it will not be re-rendered unless `updatePortal` is called.
|
|
*
|
|
* See {@link Portal} for more details.
|
|
*/
|
|
export function createSimplePortal({
|
|
template,
|
|
container = document.body,
|
|
signal = new AbortController().signal,
|
|
renderOptions,
|
|
shadowDom = true,
|
|
identifyWrapper = true,
|
|
}: PortalOptions) {
|
|
const portalRoot = document.createElement('div');
|
|
if (identifyWrapper) {
|
|
portalRoot.classList.add('blocksuite-portal');
|
|
}
|
|
if (shadowDom) {
|
|
portalRoot.attachShadow({
|
|
mode: 'open',
|
|
...(typeof shadowDom !== 'boolean' ? shadowDom : {}),
|
|
});
|
|
}
|
|
signal.addEventListener('abort', () => {
|
|
portalRoot.remove();
|
|
});
|
|
|
|
const root = shadowDom ? portalRoot.shadowRoot : portalRoot;
|
|
if (!root) {
|
|
throw new BlockSuiteError(
|
|
BlockSuiteError.ErrorCode.ValueNotExists,
|
|
'Failed to create portal root'
|
|
);
|
|
}
|
|
|
|
let updateId = 0;
|
|
const updatePortal: (id: number) => void = id => {
|
|
if (id !== updateId) {
|
|
console.warn(
|
|
'Potentially infinite recursion! Please clean up the old event listeners before `updatePortal`'
|
|
);
|
|
return;
|
|
}
|
|
updateId++;
|
|
const curId = updateId;
|
|
const templateResult =
|
|
template instanceof Function
|
|
? template({ updatePortal: () => updatePortal(curId) })
|
|
: template;
|
|
render(templateResult, root, renderOptions);
|
|
};
|
|
|
|
updatePortal(updateId);
|
|
container.append(portalRoot);
|
|
|
|
// affine's modal will set pointer-events: none to body
|
|
// in order to avoid the issue that the floating element in blocksuite cannot be clicked
|
|
// we add pointer-events: auto here
|
|
portalRoot.style.pointerEvents = 'auto';
|
|
|
|
return portalRoot;
|
|
}
|
|
|
|
/**
|
|
* Where el is the DOM element you'd like to test for visibility
|
|
*/
|
|
function isElementVisible(el: Element) {
|
|
// The API is not stable, so we need to check the existence of the function first
|
|
// See also https://caniuse.com/?search=checkVisibility
|
|
if (el.checkVisibility) {
|
|
// See https://drafts.csswg.org/cssom-view-1/#dom-element-checkvisibility
|
|
return el.checkVisibility();
|
|
}
|
|
// Fallback to the old way
|
|
// Remove this when the `checkVisibility` API is stable
|
|
if (!el.isConnected) return false;
|
|
|
|
if (el instanceof HTMLElement) {
|
|
// See https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
|
|
return !(el.offsetParent === null);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Similar to `createSimplePortal`, but supports auto update position.
|
|
*
|
|
* The template should be a **static** template since it will not be re-rendered.
|
|
*
|
|
* See {@link createSimplePortal} for more details.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* createLitPortal({
|
|
* template: RenameModal({
|
|
* model,
|
|
* abortController: renameAbortController,
|
|
* }),
|
|
* computePosition: {
|
|
* referenceElement: anchor,
|
|
* placement: 'top-end',
|
|
* middleware: [flip(), offset(4)],
|
|
* autoUpdate: true,
|
|
* },
|
|
* abortController: renameAbortController,
|
|
* });
|
|
* ```
|
|
*/
|
|
export function createLitPortal({
|
|
computePosition: positionConfigOrFn,
|
|
abortController,
|
|
closeOnClickAway = false,
|
|
positionStrategy = 'absolute',
|
|
...portalOptions
|
|
}: AdvancedPortalOptions) {
|
|
let positionSlot = new Subject<ComputePositionReturn>();
|
|
const template = portalOptions.template;
|
|
const templateWithPosition =
|
|
template instanceof Function
|
|
? ({ updatePortal }: { updatePortal: () => void }) => {
|
|
// We need to create a new slot for each template, otherwise the slot may be used in the old template
|
|
positionSlot = new Subject<ComputePositionReturn>();
|
|
return template({ updatePortal, positionSlot });
|
|
}
|
|
: template;
|
|
|
|
const portalRoot = createSimplePortal({
|
|
...portalOptions,
|
|
signal: abortController.signal,
|
|
template: templateWithPosition,
|
|
});
|
|
|
|
if (closeOnClickAway) {
|
|
// Avoid triggering click away listener on initial render
|
|
setTimeout(() =>
|
|
document.addEventListener(
|
|
'click',
|
|
e => {
|
|
if (portalRoot.contains(e.target as Node)) return;
|
|
abortController.abort();
|
|
},
|
|
{
|
|
signal: abortController.signal,
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
if (!positionConfigOrFn) {
|
|
return { portal: portalRoot, update: () => {} };
|
|
}
|
|
|
|
const visibility = portalRoot.style.visibility;
|
|
portalRoot.style.visibility = 'hidden';
|
|
portalRoot.style.position = positionStrategy;
|
|
portalRoot.style.left = '0';
|
|
portalRoot.style.top = '0';
|
|
portalRoot.style.zIndex = cssVar('zIndexPopover');
|
|
|
|
Object.assign(portalRoot.style, portalOptions.portalStyles);
|
|
|
|
const computePositionOptions =
|
|
positionConfigOrFn instanceof Function
|
|
? positionConfigOrFn(portalRoot)
|
|
: positionConfigOrFn;
|
|
const { referenceElement, ...options } = computePositionOptions;
|
|
const update = () => {
|
|
if (
|
|
computePositionOptions.abortWhenRefRemoved !== false &&
|
|
referenceElement instanceof Element &&
|
|
!isElementVisible(referenceElement)
|
|
) {
|
|
abortController.abort();
|
|
}
|
|
computePosition(referenceElement, portalRoot, {
|
|
strategy: positionStrategy,
|
|
...options,
|
|
})
|
|
.then(positionReturn => {
|
|
const { x, y } = positionReturn;
|
|
// Use transform maybe cause overlay-mask offset issue
|
|
// portalRoot.style.transform = `translate(${x}px, ${y}px)`;
|
|
portalRoot.style.left = `${x}px`;
|
|
portalRoot.style.top = `${y}px`;
|
|
if (portalRoot.style.visibility === 'hidden') {
|
|
portalRoot.style.visibility = visibility;
|
|
}
|
|
positionSlot.next(positionReturn);
|
|
})
|
|
.catch(console.error);
|
|
};
|
|
if (!computePositionOptions.autoUpdate) {
|
|
update();
|
|
} else {
|
|
const autoUpdateOptions =
|
|
computePositionOptions.autoUpdate === true
|
|
? {}
|
|
: computePositionOptions.autoUpdate;
|
|
const cleanup = autoUpdate(
|
|
referenceElement,
|
|
portalRoot,
|
|
update,
|
|
autoUpdateOptions
|
|
);
|
|
abortController.signal.addEventListener('abort', () => {
|
|
cleanup();
|
|
});
|
|
}
|
|
|
|
return { portal: portalRoot, update };
|
|
}
|