UNPKG

@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
"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