Files
upstream-affine/blocksuite/affine/components/src/portal/helper.ts
upstream-mirror 702cc698a7
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
snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
2026-09-16 04:01:20 +05:00

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 };
}