@kontent-ai/smart-link
Version:
Kontent.ai Smart Link SDK allowing to automatically inject [smart links](https://docs.kontent.ai/tutorials/develop-apps/build-strong-foundation/set-up-editing-from-preview#a-using-smart-links) to Kontent.ai according to manually specified [HTML data attri
283 lines • 13 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOMSmartLinkManager = void 0;
const components_1 = require("../web-components/components");
const IFrameCommunicatorTypes_1 = require("./IFrameCommunicatorTypes");
const SmartLinkRenderer_1 = require("./SmartLinkRenderer");
const customElements_1 = require("../utils/customElements");
const messageValidation_1 = require("../utils/messageValidation");
const configuration_1 = require("../utils/configuration");
const link_1 = require("../utils/link");
const Logger_1 = require("./Logger");
const errors_1 = require("../utils/errors");
class DOMSmartLinkManager {
iframeCommunicator;
configuration;
mutationObserver;
intersectionObserver;
renderer;
enabled = false;
renderingTimeoutId = 0;
observedElements = new Set();
visibleElements = new Set();
constructor(iframeCommunicator, configuration) {
this.iframeCommunicator = iframeCommunicator;
this.configuration = configuration;
if (window === undefined || MutationObserver === undefined || IntersectionObserver === undefined) {
throw (0, errors_1.InvalidEnvironmentError)('NodeSmartLinkProvider can only be initialized in a browser environment.');
}
this.mutationObserver = new MutationObserver(this.onDomMutation);
this.intersectionObserver = new IntersectionObserver(this.onElementVisibilityChange);
this.renderer = new SmartLinkRenderer_1.SmartLinkRenderer(this.configuration);
}
toggle = (force) => {
const shouldEnable = force ? force : !this.enabled;
if (shouldEnable) {
this.enable();
}
else {
this.disable();
}
};
enable = () => {
if (this.enabled)
return;
this.startRenderingInterval();
this.listenToGlobalEvents();
this.observeDomMutations();
this.enabled = true;
};
disable = () => {
if (!this.enabled)
return;
this.stopRenderingInterval();
this.unlistenToGlobalEvents();
this.disconnectObservers();
this.renderer.clear();
this.enabled = false;
};
destroy = () => {
this.disable();
this.renderer.destroy();
};
augmentVisibleElements = () => {
requestAnimationFrame(() => {
this.renderer.render(this.visibleElements, this.observedElements);
});
};
/**
* Start an interval rendering (1s) that will re-render highlights for all visible elements using `setInterval`.
* It helps to adjust highlights position even in situations that are currently not supported by
* the SDK (e.g. element position change w/o animations, some infinite animations and other possible unhandled cases)
* for better user experience.
*/
startRenderingInterval = () => {
this.renderingTimeoutId = window.setInterval(this.augmentVisibleElements, 1000);
};
stopRenderingInterval = () => {
if (this.renderingTimeoutId) {
clearInterval(this.renderingTimeoutId);
this.renderingTimeoutId = 0;
}
};
listenToGlobalEvents = () => {
window.addEventListener('ksl:add-button:initial', this.onAddInitialClick, { capture: true });
window.addEventListener('ksl:add-button:action', this.onAddActionClick, { capture: true });
window.addEventListener('ksl:highlight:edit', this.onEditElement, { capture: true });
};
unlistenToGlobalEvents = () => {
window.removeEventListener('ksl:add-button:initial', this.onAddInitialClick, { capture: true });
window.removeEventListener('ksl:add-button:action', this.onAddActionClick, { capture: true });
window.removeEventListener('ksl:highlight:edit', this.onEditElement, { capture: true });
};
observeDomMutations = () => {
if (this.enabled)
return;
this.mutationObserver.observe(window.document.body, {
childList: true,
subtree: true,
});
(0, customElements_1.getAugmentableDescendants)(document, this.configuration).forEach((element) => {
if (element instanceof HTMLElement) {
this.observeElementVisibility(element);
}
});
};
disconnectObservers = () => {
this.mutationObserver.disconnect();
this.intersectionObserver.disconnect();
this.observedElements.forEach((element) => {
this.unobserveElementVisibility(element);
});
this.observedElements = new Set();
this.visibleElements = new Set();
};
observeElementVisibility = (element) => {
if (this.observedElements.has(element))
return;
this.intersectionObserver.observe(element);
this.observedElements.add(element);
};
unobserveElementVisibility = (element) => {
if (!this.observedElements.has(element))
return;
this.intersectionObserver.unobserve(element);
this.observedElements.delete(element);
this.visibleElements.delete(element);
};
onDomMutation = (mutations) => {
const relevantMutations = mutations.filter(isRelevantMutation);
for (const mutation of relevantMutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement))
continue;
if ((0, customElements_1.isElementAugmentable)(node, this.configuration)) {
this.observeElementVisibility(node);
}
for (const element of (0, customElements_1.getAugmentableDescendants)(node, this.configuration)) {
if (!(element instanceof HTMLElement))
continue;
this.observeElementVisibility(element);
}
}
for (const node of mutation.removedNodes) {
if (!(node instanceof HTMLElement))
continue;
if ((0, customElements_1.isElementAugmentable)(node, this.configuration)) {
this.unobserveElementVisibility(node);
}
for (const element of (0, customElements_1.getAugmentableDescendants)(node, this.configuration)) {
if (!(element instanceof HTMLElement))
continue;
this.unobserveElementVisibility(element);
}
}
}
if (relevantMutations.length > 0) {
this.augmentVisibleElements();
}
};
onElementVisibilityChange = (entries) => {
const filteredEntries = entries.filter((entry) => entry.target instanceof HTMLElement);
for (const entry of filteredEntries) {
const target = entry.target;
if (entry.isIntersecting) {
this.visibleElements.add(target);
}
else {
this.visibleElements.delete(target);
}
}
if (filteredEntries.length > 0) {
this.augmentVisibleElements();
}
};
onEditElement = (event) => {
const isInsideWebSpotlight = (0, configuration_1.isInsideWebSpotlightPreviewIFrame)(this.configuration);
const { data, targetNode } = event.detail;
const messageMetadata = {
elementRect: targetNode.getBoundingClientRect(),
};
const validationResult = (0, messageValidation_1.validateEditButtonMessageData)(data, this.configuration);
if (validationResult.type === 'error') {
(0, Logger_1.logError)(`Required attribute was not found: ${validationResult.missing[0]}. Skipped parsing these attributes: ${validationResult.missing.slice(1).join(', ')}`);
(0, Logger_1.logError)('Debug info: ', validationResult.debug);
(0, Logger_1.logError)('Skipping edit button click');
return;
}
if (validationResult.type === 'element') {
if (isInsideWebSpotlight) {
this.iframeCommunicator.sendMessage(IFrameCommunicatorTypes_1.IFrameMessageType.ElementClicked, validationResult.data, messageMetadata);
}
else {
const link = (0, link_1.buildKontentElementLink)({
environmentId: validationResult.data.projectId,
languageCodename: validationResult.data.languageCodename,
itemId: validationResult.data.itemId,
elementCodename: validationResult.data.elementCodename,
});
window.open(link, '_blank');
}
}
else if (validationResult.type === 'contentComponent') {
if (isInsideWebSpotlight) {
this.iframeCommunicator.sendMessage(IFrameCommunicatorTypes_1.IFrameMessageType.ContentComponentClicked, validationResult.data, messageMetadata);
}
else {
(0, Logger_1.logWarn)('Edit buttons for content components are only functional inside Web Spotlight.');
}
}
else {
if (isInsideWebSpotlight) {
this.iframeCommunicator.sendMessage(IFrameCommunicatorTypes_1.IFrameMessageType.ContentItemClicked, validationResult.data, messageMetadata);
}
else {
const link = (0, link_1.buildKontentItemLink)({
environmentId: validationResult.data.projectId,
languageCodename: validationResult.data.languageCodename,
itemId: validationResult.data.itemId,
});
window.open(link, '_blank');
}
}
};
onAddInitialClick = (event) => {
const isInsideWebSpotlight = (0, configuration_1.isInsideWebSpotlightPreviewIFrame)(this.configuration);
const { eventData, onResolve, onReject } = event.detail;
const { data, targetNode } = eventData;
const messageMetadata = {
elementRect: targetNode.getBoundingClientRect(),
};
console.log('data', data);
const messageDataValidation = (0, messageValidation_1.validateAddInitialMessageData)(data, this.configuration);
if (!messageDataValidation.success) {
const [first, ...rest] = messageDataValidation.missing;
onReject({
message: `Could not find required data attribute: '${first}'.${rest.length > 0 ? ` All attributes higher in hierarchy were never searched for: [${rest.join(', ')}]` : ''}`,
});
return;
}
if (!isInsideWebSpotlight) {
onReject({ message: 'Add buttons are only functional inside Web Spotlight' });
return;
}
this.iframeCommunicator.sendMessageWithResponse(IFrameCommunicatorTypes_1.IFrameMessageType.AddInitial, messageDataValidation.data, (response) => {
if (!response || response.elementType === IFrameCommunicatorTypes_1.AddButtonElementType.Unknown) {
console.log('response', JSON.stringify(response));
return onReject({ message: 'Something went wrong' });
}
return onResolve(response);
}, messageMetadata);
};
onAddActionClick = (event) => {
const isInsideWebSpotlight = (0, configuration_1.isInsideWebSpotlightPreviewIFrame)(this.configuration);
const { data, targetNode } = event.detail;
const messageMetadata = {
elementRect: targetNode.getBoundingClientRect(),
};
const messageDataValidation = (0, messageValidation_1.validateAddInitialMessageData)(data, this.configuration);
if (!messageDataValidation.success) {
return;
}
if (!isInsideWebSpotlight) {
(0, Logger_1.logWarn)('Add buttons are only functional inside Web Spotlight.');
return;
}
this.iframeCommunicator.sendMessage(IFrameCommunicatorTypes_1.IFrameMessageType.AddAction, {
...messageDataValidation.data,
action: data.action,
}, messageMetadata);
};
}
exports.DOMSmartLinkManager = DOMSmartLinkManager;
const isRelevantMutation = (mutation) => {
const isTypeRelevant = mutation.type === 'childList';
const isTargetRelevant = mutation.target instanceof HTMLElement && !(0, components_1.isElementWebComponent)(mutation.target);
if (!isTypeRelevant || !isTargetRelevant) {
return false;
}
const hasRelevantAddedNodes = Array.from(mutation.addedNodes).some((node) => node instanceof HTMLElement && !(0, components_1.isElementWebComponent)(node));
const hasRelevantRemovedNodes = Array.from(mutation.removedNodes).some((node) => node instanceof HTMLElement && !(0, components_1.isElementWebComponent)(node));
return hasRelevantAddedNodes || hasRelevantRemovedNodes;
};
//# sourceMappingURL=DOMSmartLinkManager.js.map