UNPKG

@contentstack/live-preview-utils

Version:

Contentstack provides the Live Preview SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane.

526 lines (523 loc) 22.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/livePreview/editButton/editButton.ts var editButton_exports = {}; __export(editButton_exports, { LivePreviewEditButton: () => LivePreviewEditButton, createMultipleEditButton: () => createMultipleEditButton, createSingularEditButton: () => createSingularEditButton, doesEditButtonExist: () => doesEditButtonExist, getEditButtonPosition: () => getEditButtonPosition, isPointerWithinEditButtonSafeZone: () => isPointerWithinEditButtonSafeZone, shouldRenderEditButton: () => shouldRenderEditButton, toggleEditButtonElement: () => toggleEditButtonElement }); module.exports = __toCommonJS(editButton_exports); var import_signals = require("@preact/signals"); var import_inIframe = require("../../common/inIframe.cjs"); var import_configManager = __toESM(require("../../configManager/configManager.cjs"), 1); var import_cslp = require("../../cslp/index.cjs"); var import_editButton = require("./editButton.style.cjs"); var import_logger = require("../../logger/logger.cjs"); var import_types = require("../../types/types.cjs"); var import_livePreviewEventManager = __toESM(require("../eventManager/livePreviewEventManager.cjs"), 1); var import_editButton2 = require("./editButton.constant.cjs"); var import_utils = require("../../utils/index.cjs"); function calculateEditButtonPosition(currentHoveredElement, cslpButtonPosition) { const editButtonPosition = { upperBoundOfTooltip: 0, leftBoundOfTooltip: 0 }; const currentRectOfElement = currentHoveredElement.getBoundingClientRect(); try { const buttonMeasurementValues = { width: 72, halfWidth: 36, height: 40, basicMargin: 5, widthWithMargin: 77 }; switch (cslpButtonPosition) { case "top-center": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.top - buttonMeasurementValues.height; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.width / 2 - buttonMeasurementValues.halfWidth + currentRectOfElement.left; break; case "top-right": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.top - buttonMeasurementValues.height; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.right - buttonMeasurementValues.width; break; case "right": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.top - buttonMeasurementValues.basicMargin; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.right + buttonMeasurementValues.basicMargin; break; case "bottom": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.bottom + buttonMeasurementValues.basicMargin; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.left - buttonMeasurementValues.basicMargin; break; case "bottom-left": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.bottom + buttonMeasurementValues.basicMargin; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.left - buttonMeasurementValues.basicMargin; break; case "bottom-center": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.bottom + buttonMeasurementValues.basicMargin; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.width / 2 - buttonMeasurementValues.halfWidth + currentRectOfElement.left; break; case "bottom-right": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.bottom + buttonMeasurementValues.basicMargin; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.right - buttonMeasurementValues.width; break; case "left": editButtonPosition.upperBoundOfTooltip = currentRectOfElement.top - buttonMeasurementValues.basicMargin; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.left - buttonMeasurementValues.widthWithMargin; break; // default position => top, top-left or any other string default: editButtonPosition.upperBoundOfTooltip = currentRectOfElement.top - buttonMeasurementValues.height; editButtonPosition.leftBoundOfTooltip = currentRectOfElement.left - buttonMeasurementValues.basicMargin; break; } return editButtonPosition; } catch (error) { import_logger.PublicLogger.error(error); return editButtonPosition; } } var createSingularEditButton = (editCallback) => { const singularEditButton = document.createElement("div"); singularEditButton.classList.add("cslp-tooltip-child", "singular"); singularEditButton.setAttribute( "data-test-id", "cslp-singular-edit-button" ); singularEditButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8.1 3.5L0.3 11.3C0.1 11.5 0 11.7 0 12V15C0 15.6 0.4 16 1 16H4C4.3 16 4.5 15.9 4.7 15.7L12.5 7.9L8.1 3.5Z" fill="#718096"></path> <path d="M15.7 3.3L12.7 0.3C12.3 -0.1 11.7 -0.1 11.3 0.3L9.5 2.1L13.9 6.5L15.7 4.7C16.1 4.3 16.1 3.7 15.7 3.3Z" fill="#718096"></path> </svg>Edit`; singularEditButton.addEventListener("click", editCallback); return singularEditButton; }; var createMultipleEditButton = (editCallback, linkCallback) => { const multipleEditButton = document.createElement("div"); multipleEditButton.classList.add("cslp-tooltip-child"); multipleEditButton.setAttribute("data-title", "Edit"); multipleEditButton.setAttribute( "data-test-id", "cslp-multiple-edit-button" ); multipleEditButton.innerHTML = ` <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8.1 3.5L0.3 11.3C0.1 11.5 0 11.7 0 12V15C0 15.6 0.4 16 1 16H4C4.3 16 4.5 15.9 4.7 15.7L12.5 7.9L8.1 3.5Z" fill="#718096"></path> <path d="M15.7 3.3L12.7 0.3C12.3 -0.1 11.7 -0.1 11.3 0.3L9.5 2.1L13.9 6.5L15.7 4.7C16.1 4.3 16.1 3.7 15.7 3.3Z" fill="#718096"></path> </svg>`; multipleEditButton.addEventListener("click", editCallback); const multipleExternalLinkButton = document.createElement("div"); multipleExternalLinkButton.classList.add("cslp-tooltip-child"); multipleExternalLinkButton.setAttribute("data-title", "Go to link"); multipleExternalLinkButton.setAttribute( "data-test-id", "cslp-multiple-external-link-button" ); multipleExternalLinkButton.innerHTML = ` <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M2.66654 2.66758H13.3332V13.3342H6.66654V16.0009H13.3332C14.0405 16.0009 14.7187 15.72 15.2188 15.2199C15.7189 14.7198 15.9999 14.0415 15.9999 13.3342V2.66758C15.9999 1.96034 15.7189 1.28206 15.2188 0.781964C14.7187 0.281867 14.0405 0.000915527 13.3332 0.000915527H2.66654C1.9593 0.000915527 1.28102 0.281867 0.780927 0.781964C0.280829 1.28206 -0.00012207 1.96034 -0.00012207 2.66758V9.33425H2.66654V2.66758Z" fill="#718096" /> <path d="M6.94263 7.05734L0.999958 13L2.88529 14.8853L8.82796 8.94267L10.8853 11V5.00001H4.88529L6.94263 7.05734Z" fill="#718096" /> </svg>`; multipleExternalLinkButton.addEventListener("click", linkCallback); const multipleEditFragment = document.createDocumentFragment(); multipleEditFragment.appendChild(multipleEditButton); multipleEditFragment.appendChild(multipleExternalLinkButton); const multipleDiv = document.createElement("div"); multipleDiv.appendChild(multipleEditFragment); multipleDiv.classList.add((0, import_editButton.cslpTagStyles)()["multiple"]); return multipleDiv; }; function getEditButtonPosition(currentHoveredElement, defaultPosition) { if (!currentHoveredElement) return { upperBoundOfTooltip: 0, leftBoundOfTooltip: 0 }; const cslpButtonPosition = currentHoveredElement.getAttribute( "data-cslp-button-position" ); if (cslpButtonPosition) { return calculateEditButtonPosition( currentHoveredElement, cslpButtonPosition ); } return calculateEditButtonPosition( currentHoveredElement, defaultPosition || "top" ); } function shouldRenderEditButton() { const config = import_configManager.default.get(); if (!config.editButton.enable) { if (config.editButton.enable === void 0) import_logger.PublicLogger.error( "enable key is required inside editButton object" ); return false; } try { const currentLocation = new URL(window.location.href); const cslpButtonQueryValue = currentLocation.searchParams.get("cslp-buttons"); if (cslpButtonQueryValue !== null && config.editButton.includeByQueryParameter !== false) return cslpButtonQueryValue === "false" ? false : true; } catch (error) { import_logger.PublicLogger.error(error); } const iFrameCheck = (0, import_inIframe.inIframe)(); if (!iFrameCheck && config.editButton.exclude?.find( (exclude) => exclude === "outsideLivePreviewPortal" )) { return false; } if (iFrameCheck && config.editButton.exclude?.find( (exclude) => exclude === "insideLivePreviewPortal" )) { return false; } else if (iFrameCheck) { if (config.windowType === "builder") { return false; } return true; } return true; } function toggleEditButtonElement() { const render = shouldRenderEditButton(); const exists = doesEditButtonExist(); if (render && !exists) { LivePreviewEditButton.livePreviewEditButton = new LivePreviewEditButton(); } else if (!render && exists) { LivePreviewEditButton.livePreviewEditButton?.destroy(); } } function doesEditButtonExist() { return document.getElementById(import_editButton2.EDIT_BUTTON_TOOLTIP_ID) !== null; } var LivePreviewEditButton = class { constructor() { this.tooltip = null; this.typeOfCurrentChild = "singular"; this.tooltipChild = { singular: null, multiple: null }; this.createCslpTooltip = this.createCslpTooltip.bind(this); this.updateTooltipPosition = this.updateTooltipPosition.bind(this); this.addEditStyleOnHover = this.addEditStyleOnHover.bind(this); this.scrollHandler = this.scrollHandler.bind(this); this.generateRedirectUrl = this.generateRedirectUrl.bind(this); this.linkClickHandler = this.linkClickHandler.bind(this); this.destroy = this.destroy.bind(this); if (this.createCslpTooltip()) { this.updateTooltipPosition(); window.addEventListener("scroll", this.updateTooltipPosition); window.addEventListener("mouseover", this.addEditStyleOnHover); } } createCslpTooltip() { const editButton = import_configManager.default.get().editButton; if (!document.getElementById(import_editButton2.EDIT_BUTTON_TOOLTIP_ID) && editButton.enable && shouldRenderEditButton()) { const tooltip = document.createElement("button"); this.tooltip = tooltip; this.tooltip.classList.add((0, import_editButton.cslpTagStyles)()["cslp-tooltip"]); this.tooltip.setAttribute("data-test-id", "cs-cslp-tooltip"); this.tooltip.id = import_editButton2.EDIT_BUTTON_TOOLTIP_ID; window.document.body.insertAdjacentElement( "beforeend", this.tooltip ); this.tooltipChild.singular = createSingularEditButton( this.scrollHandler ); this.tooltipChild.multiple = createMultipleEditButton( this.scrollHandler, this.linkClickHandler ); this.tooltip.appendChild(this.tooltipChild.singular); return true; } return false; } updateTooltipPosition() { if (!document.getElementById("cslp-tooltip")) { this.createCslpTooltip(); } const editButton = import_configManager.default.get().editButton; const elements = import_configManager.default.get().elements; if (!elements.highlightedElement || !this.tooltip) return false; const currentRectOfElement = elements.highlightedElement.getBoundingClientRect(); const currentRectOfParentOfElement = this.tooltip.parentElement?.getBoundingClientRect(); if (currentRectOfElement && currentRectOfParentOfElement) { const editButtonPosition = getEditButtonPosition( elements.highlightedElement, editButton.position ); let upperBoundOfTooltip = editButtonPosition.upperBoundOfTooltip; const leftBoundOfTooltip = editButtonPosition.leftBoundOfTooltip; if (upperBoundOfTooltip < 0) { if (currentRectOfElement.top < 0) upperBoundOfTooltip = currentRectOfElement.top; else upperBoundOfTooltip = 0; } this.tooltip.style.top = upperBoundOfTooltip + "px"; this.tooltip.style.zIndex = elements.highlightedElement.style.zIndex || "200"; this.tooltip.style.left = leftBoundOfTooltip + "px"; if (this.tooltipChild.singular && this.tooltipChild.multiple) { if (elements.highlightedElement.hasAttribute("href") && this.typeOfCurrentChild !== "multiple") { this.tooltip.innerHTML = ""; this.tooltip.appendChild(this.tooltipChild.multiple); this.typeOfCurrentChild = "multiple"; } else if (this.typeOfCurrentChild !== "singular") { this.tooltip.innerHTML = ""; this.tooltip.appendChild(this.tooltipChild.singular); this.typeOfCurrentChild = "singular"; } } return true; } return false; } addEditStyleOnHover(e) { const updateStyles = this.shouldUpdateStyle(e); const shouldRedraw = typeof updateStyles === "undefined" ? true : updateStyles; if (!shouldRedraw) { return; } const updateTooltipPosition = ({ cslpTag, highlightedElement }) => { if (this.updateTooltipPosition()) { this.tooltip?.setAttribute("current-data-cslp", cslpTag); this.tooltip?.setAttribute( "current-href", highlightedElement.getAttribute("href") ?? "" ); } }; const editButton = import_configManager.default.get().editButton; const windowType = import_configManager.default.get().windowType; if ((windowType === import_types.ILivePreviewWindowType.PREVIEW || windowType === import_types.ILivePreviewWindowType.INDEPENDENT) && editButton.enable) { (0, import_cslp.addCslpOutline)(e, updateTooltipPosition); } } shouldUpdateStyle(event) { const editButtonPos = import_configManager.default.get().editButton.position; const editButtonDomRect = this.tooltip?.getBoundingClientRect(); return isPointerWithinEditButtonSafeZone({ event, editButtonPos, editButtonDomRect }); } scrollHandler() { if (!this.tooltip) return; const cslpTag = this.tooltip.getAttribute("current-data-cslp"); if (cslpTag) { const { content_type_uid, entry_uid, locale, variant, fieldPathWithIndex } = (0, import_cslp.extractDetailsFromCslp)(cslpTag); if ((0, import_inIframe.inIframe)()) { import_livePreviewEventManager.default?.send("scroll", { field: fieldPathWithIndex, content_type_uid, entry_uid, variant, locale }); } else { try { const redirectUrl = this.generateRedirectUrl( content_type_uid, locale, entry_uid, variant, fieldPathWithIndex ); window.open(redirectUrl, "_blank"); } catch (error) { import_logger.PublicLogger.error(error); } } } } /** * Generates the redirect URL for editing a specific entry in the Live Preview SDK. * @param content_type_uid - The UID of the content type. * @param locale - The locale of the entry (default: "en-us"). * @param entry_uid - The UID of the entry. * @param preview_field - The field to be previewed. * @returns The redirect URL for editing the entry. */ generateRedirectUrl(content_type_uid, locale = "en-us", entry_uid, variant, preview_field) { const config = import_configManager.default.get(); if (!config.stackDetails.apiKey) { throw `To use edit tags, you must provide the stack API key. Specify the API key while initializing the Live Preview SDK. ContentstackLivePreview.init({ ..., stackDetails: { apiKey: 'your-api-key' }, ... })`; } if (!config.stackDetails.environment) { throw `To use edit tags, you must provide the preview environment. Specify the preview environment while initializing the Live Preview SDK. ContentstackLivePreview.init({ ..., stackDetails: { environment: 'Your-environment' }, ... })`; } const protocol = String(config.clientUrlParams.protocol); const host = String(config.clientUrlParams.host); const port = String(config.clientUrlParams.port); const environment = String(config.stackDetails.environment); const branch = String(config.stackDetails.branch || "main"); let urlHash = `!/stack/${config.stackDetails.apiKey}/content-type/${content_type_uid}/${locale ?? "en-us"}/entry/${entry_uid}`; if (variant) { urlHash += `/variant/${variant}/edit`; } else { urlHash += `/edit`; } const url = new URL(`${protocol}://${host}`); url.port = port; url.hash = urlHash; if (config.stackDetails.branch) { url.searchParams.append("branch", branch); } url.searchParams.append("preview-field", preview_field); url.searchParams.append("preview-locale", locale ?? "en-us"); url.searchParams.append("preview-environment", environment); return `${url.origin}/${url.hash}${url.search}`; } linkClickHandler() { if (!this.tooltip) return; const hrefAttribute = this.tooltip.getAttribute("current-href"); if (hrefAttribute) { window.location.assign(hrefAttribute); } } /** * Destroys the edit button by removing event listeners and removing the tooltip. */ destroy() { window.removeEventListener("scroll", this.updateTooltipPosition); window.removeEventListener("mouseover", this.addEditStyleOnHover); this.tooltip?.remove(); } }; LivePreviewEditButton.livePreviewEditButton = null; (0, import_signals.effect)(function handleWindowTypeChange() { if (typeof window === "undefined") return; import_configManager.default.get().windowType; if (LivePreviewEditButton && !(0, import_utils.isOpeningInTimeline)()) { toggleEditButtonElement(); } }); function isPointerWithinEditButtonSafeZone({ event, editButtonDomRect, editButtonPos }) { const SAFE_ZONE_RATIO = 0.1; const MAX_SAFE_ZONE_DISTANCE = 30; if (!editButtonDomRect || !editButtonPos) { return void 0; } if (!(editButtonDomRect.x > 0) || !(editButtonDomRect.y > 0)) { return void 0; } const isTop = editButtonPos.includes("top"); const isLeft = editButtonPos.includes("left"); const isBottom = editButtonPos.includes("bottom"); const isVertical = isTop || isBottom; const cslpElement = event.composedPath().find((target) => { const element2 = target; if (element2.nodeName === "BODY") { return false; } if (typeof element2?.hasAttribute !== "function") { return false; } return element2.hasAttribute("data-cslp"); }); if (!cslpElement) { return void 0; } const element = cslpElement; const elementRect = element.getBoundingClientRect(); let safeZoneDistance = isVertical ? ( // if vertical positioning ("top"/"bottom") // button is rendered along the width elementRect.width * SAFE_ZONE_RATIO ) : ( // button is rendered along the height elementRect.height * SAFE_ZONE_RATIO ); safeZoneDistance = safeZoneDistance > MAX_SAFE_ZONE_DISTANCE ? MAX_SAFE_ZONE_DISTANCE : safeZoneDistance; const tooltipX2 = editButtonDomRect.x + editButtonDomRect.width; const tooltipY2 = editButtonDomRect.y + editButtonDomRect.height; const safeX1 = editButtonDomRect.x - safeZoneDistance; const safeX2 = tooltipX2 + safeZoneDistance; const safeY1 = editButtonDomRect.y - safeZoneDistance; const safeY2 = tooltipY2 + safeZoneDistance; if (isTop || isBottom) { const verticalSafeDistance = isTop ? Math.abs(tooltipY2 - event.clientY) : Math.abs(editButtonDomRect.y - event.clientY); const isInSafeZone = event.clientX > safeX1 && event.clientX < safeX2 && verticalSafeDistance < safeZoneDistance; if (isInSafeZone) { return false; } } else { const horizontalSafeDistance = isLeft ? Math.abs(tooltipX2 - event.clientX) : Math.abs(editButtonDomRect.x - event.clientX); const isInSafeZone = event.clientY > safeY1 && event.clientY < safeY2 && horizontalSafeDistance < safeZoneDistance; if (isInSafeZone) { return false; } } return true; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { LivePreviewEditButton, createMultipleEditButton, createSingularEditButton, doesEditButtonExist, getEditButtonPosition, isPointerWithinEditButtonSafeZone, shouldRenderEditButton, toggleEditButtonElement }); //# sourceMappingURL=editButton.cjs.map