@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
296 lines • 13.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOMSmartLinkManager = void 0;
const configuration_1 = require("../utils/configuration");
const customElements_1 = require("../utils/customElements");
const errors_1 = require("../utils/errors");
const link_1 = require("../utils/link");
const messageValidation_1 = require("../utils/messageValidation");
const components_1 = require("../web-components/components");
const IFrameCommunicatorTypes_1 = require("./IFrameCommunicatorTypes");
const Logger_1 = require("./Logger");
const SmartLinkRenderer_1 = require("./SmartLinkRenderer");
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 crashes
typeof window === "undefined" ||
typeof MutationObserver === "undefined" ||
typeof IntersectionObserver === "undefined") {
throw new 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 ?? !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