187 lines
5.7 KiB
JavaScript
187 lines
5.7 KiB
JavaScript
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
const CONTROLLER = '__FUIDT_CONTROLLER__';
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
const ELEMENT_METADATA = '__FUIDT_ELEMENT_METADATA__';
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
const HTML_ELEMENT_REFERENCE = '__FUIDT_HTML_ELEMENT_REFERENCE__';
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
const SERIALIZED_DATA_CHANGE = '__FUIDT_SERIALIZED_DATA_CHANGE__';
|
||
|
|
||
|
/**
|
||
|
* Verifies if a given node is an HTMLElement,
|
||
|
* this method works seamlessly with frames and elements from different documents
|
||
|
*
|
||
|
* This is preferred over simply using `instanceof`.
|
||
|
* Since `instanceof` might be problematic while operating with [multiple realms](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof#instanceof_and_multiple_realms)
|
||
|
*
|
||
|
* @example
|
||
|
* ```ts
|
||
|
* isHTMLElement(event.target) && event.target.focus()
|
||
|
* isHTMLElement(event.target, {constructorName: 'HTMLInputElement'}) && event.target.value // some value
|
||
|
* ```
|
||
|
*
|
||
|
*/
|
||
|
function isHTMLElement(element, options) {
|
||
|
var _typedElement$ownerDo, _options$constructorN;
|
||
|
const typedElement = element;
|
||
|
return Boolean((typedElement == null || (_typedElement$ownerDo = typedElement.ownerDocument) == null ? void 0 : _typedElement$ownerDo.defaultView) && typedElement instanceof typedElement.ownerDocument.defaultView[(_options$constructorN = options == null ? void 0 : options.constructorName) != null ? _options$constructorN : 'HTMLElement']);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
|
||
|
const isHTMLElementWithMetadata = element => Boolean(isHTMLElement(element) && ELEMENT_METADATA in element && element.parentElement !== null);
|
||
|
|
||
|
const createController = defaultView => {
|
||
|
let selectedElement = null;
|
||
|
const observer = new MutationObserver(mutations => {
|
||
|
if (!selectedElement) {
|
||
|
return;
|
||
|
}
|
||
|
for (const mutation of mutations) {
|
||
|
if (mutation.type === 'childList' && Array.from(mutation.removedNodes).includes(selectedElement)) {
|
||
|
controller.withdraw();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
const controller = {
|
||
|
get selectedElement() {
|
||
|
return selectedElement;
|
||
|
},
|
||
|
select: nextSelectedElement => {
|
||
|
if (isHTMLElementWithMetadata(nextSelectedElement)) {
|
||
|
selectedElement = nextSelectedElement;
|
||
|
observer.observe(nextSelectedElement.parentElement, {
|
||
|
childList: true,
|
||
|
subtree: false
|
||
|
});
|
||
|
}
|
||
|
if (selectedElement && nextSelectedElement) {
|
||
|
const metadata = selectedElement[ELEMENT_METADATA];
|
||
|
if (metadata.references.has(nextSelectedElement)) {
|
||
|
return selectedElement;
|
||
|
}
|
||
|
}
|
||
|
controller.withdraw();
|
||
|
return selectedElement;
|
||
|
},
|
||
|
withdraw: () => {
|
||
|
selectedElement = null;
|
||
|
observer.disconnect();
|
||
|
defaultView.postMessage(SERIALIZED_DATA_CHANGE);
|
||
|
}
|
||
|
};
|
||
|
return controller;
|
||
|
};
|
||
|
const injectController = _ref => {
|
||
|
let {
|
||
|
defaultView
|
||
|
} = _ref;
|
||
|
if (!defaultView) {
|
||
|
return;
|
||
|
}
|
||
|
if (!defaultView[CONTROLLER]) {
|
||
|
defaultView[CONTROLLER] = createController(defaultView);
|
||
|
}
|
||
|
};
|
||
|
const getController = targetDocument => {
|
||
|
var _targetDocument$defau, _targetDocument$defau2;
|
||
|
injectController(targetDocument);
|
||
|
return (_targetDocument$defau = (_targetDocument$defau2 = targetDocument.defaultView) == null ? void 0 : _targetDocument$defau2[CONTROLLER]) != null ? _targetDocument$defau : null;
|
||
|
};
|
||
|
|
||
|
const serialize = (data, references) => {
|
||
|
const serializedData = JSON.parse(JSON.stringify(data, (_, value) => {
|
||
|
if (isHTMLElement(value)) return references.add(value);
|
||
|
if (typeof value === 'object' && value && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== Array.prototype) {
|
||
|
if ('toString' in value) {
|
||
|
return value.toString();
|
||
|
}
|
||
|
return undefined;
|
||
|
}
|
||
|
return value;
|
||
|
}));
|
||
|
return serializedData;
|
||
|
};
|
||
|
|
||
|
let counter = 0;
|
||
|
const generateReferenceId = () => HTML_ELEMENT_REFERENCE + ":" + counter++;
|
||
|
const createReferences = () => {
|
||
|
const map = new Map();
|
||
|
const weakMap = new WeakMap();
|
||
|
const references = {
|
||
|
add: element => {
|
||
|
if (weakMap.has(element)) {
|
||
|
// biome-ignore lint/style/noNonNullAssertion: weakMap.has(element) ensures this is not undefined
|
||
|
return weakMap.get(element);
|
||
|
}
|
||
|
const id = generateReferenceId();
|
||
|
map.set(id, element);
|
||
|
weakMap.set(element, id);
|
||
|
return id;
|
||
|
},
|
||
|
get: id => {
|
||
|
const element = map.get(id);
|
||
|
if (element && weakMap.has(element)) {
|
||
|
return element;
|
||
|
}
|
||
|
},
|
||
|
has: element => {
|
||
|
return weakMap.has(element);
|
||
|
}
|
||
|
};
|
||
|
return references;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* devtools middleware
|
||
|
* @public
|
||
|
*/
|
||
|
const devtools = function (targetDocument, middlewareDataCallback) {
|
||
|
if (targetDocument === void 0) {
|
||
|
targetDocument = document;
|
||
|
}
|
||
|
if (middlewareDataCallback === void 0) {
|
||
|
middlewareDataCallback = floatingUIMiddlewareDataCallback;
|
||
|
}
|
||
|
return {
|
||
|
name: '@floating-ui/devtools',
|
||
|
fn: state => {
|
||
|
const {
|
||
|
[ELEMENT_METADATA]: metadata
|
||
|
} = isHTMLElementWithMetadata(state.elements.floating) ? state.elements.floating : Object.assign(state.elements.floating, {
|
||
|
[ELEMENT_METADATA]: {
|
||
|
references: createReferences(),
|
||
|
serializedData: []
|
||
|
}
|
||
|
});
|
||
|
const serializedData = serialize(middlewareDataCallback(state), metadata.references);
|
||
|
metadata.serializedData.unshift(serializedData);
|
||
|
const controller = getController(targetDocument);
|
||
|
if (metadata.serializedData.length > 1 && state.elements.floating === (controller == null ? void 0 : controller.selectedElement)) {
|
||
|
var _targetDocument$defau;
|
||
|
(_targetDocument$defau = targetDocument.defaultView) == null || _targetDocument$defau.postMessage(SERIALIZED_DATA_CHANGE);
|
||
|
}
|
||
|
return {};
|
||
|
}
|
||
|
};
|
||
|
};
|
||
|
const floatingUIMiddlewareDataCallback = state => ({
|
||
|
...state,
|
||
|
type: 'FloatingUIMiddleware'
|
||
|
});
|
||
|
|
||
|
export { devtools, devtools as middleware };
|