UNPKG

@agility/web-studio-sdk

Version:

Standard Development Kit used to enable Web Studio features in Agility CMS

650 lines (640 loc) 20.9 kB
// src/util/initCSSAndPreviewPanel.ts var initCSSAndPreviewPanel = () => { const previewBar = document.body.querySelector( '[data-agility-previewbar="true"]' ); if (previewBar) { previewBar.style.display = "none"; } if (!document.body.classList.contains("agility-live-preview")) { document.body.classList.add("agility-live-preview"); const cssLink = document.createElement("link"); cssLink.href = "https://unpkg.com/@agility/web-studio-sdk@latest/dist/web-studio.css"; cssLink.rel = "stylesheet"; cssLink.type = "text/css"; document.head.appendChild(cssLink); } }; // src/util/getGuid.ts var getGuid = (file) => { const guid = document.body.getAttribute("data-agility-guid"); if (!guid) { console.error( `%cWeb Studio SDK - Error In:${file} no guid found on body element. Make sure your body element is set up like this: <body data-agility-guid='{{agilityguid}}'>` ); return null; } return guid; }; // src/util/invokeFrameEvent.ts var invokeFrameEvent = (messageType, arg) => { const agilityGuid = getGuid(`invoke frame event`); window.parent.postMessage( { source: "agility-preview-center", guid: agilityGuid, messageType, arg }, "*" ); }; // src/util/frameEvents.ts var dispatchReadyEvent = ({ windowWidth, windowHeight, url, windowScrollableHeight, hasComponentDecorators, hasFieldDecorators, hasPageDecorators }) => { invokeFrameEvent("ready", { windowWidth, windowHeight, url, windowScrollableHeight, hasComponentDecorators, hasFieldDecorators, hasPageDecorators }); }; var dispatchNavigationEvent = (args) => { invokeFrameEvent("navigation", args); }; var dispatchEditComponentEvent = ({ contentID, pageID }) => { invokeFrameEvent("edit-component", { contentID, pageID }); }; var dispatchEditFieldEvent = ({ fieldName, contentID, pageID }) => { invokeFrameEvent("edit-field", { fieldName, contentID, pageID }); }; var dispatchScrollEvent = ({ scrollY, scrollX, windowWidth, windowHeight }) => { invokeFrameEvent("sdk-scroll", { scrollX, scrollY, windowWidth, windowHeight }); }; var dispatchWindowResizeEvent = ({ windowWidth, windowHeight }) => { invokeFrameEvent("sdk-window-resize", { windowWidth, windowHeight }); }; var dispatchSetCommentCoordsEvent = ({ percentageOffsetX, percentageOffsetY, uniqueSelector, elementIndex, isDragEndEvent, threadId, calcFallbackX, calcFallbackY, originX, originY }) => { invokeFrameEvent(isDragEndEvent ? "set-comment-coords-on-drag-end" : "set-comment-coords", { percentageOffsetX, percentageOffsetY, uniqueSelector, elementIndex, isDragEndEvent, threadId, calcFallbackX, calcFallbackY, originX, originY }); }; var dispatchCommentDictionaryUpdatedEvent = ({ updatedCommentDictionary }) => { invokeFrameEvent("comment-dictionary-updated", { updatedCommentDictionary }); }; var dispatchDecoratorMapUpdatedEvent = ({ decoratorMap }) => { invokeFrameEvent("decorator-map-updated", { decoratorMap }); }; // src/util/initComponents.ts var initComponents = () => { const pages = document.querySelectorAll("[data-agility-page]"); pages.forEach((page) => { const components = page.querySelectorAll("[data-agility-component]"); const pageID = page.getAttribute("data-agility-page"); components.forEach((component) => { const contentID = component.getAttribute("data-agility-component"); if (!component.classList.contains("agility-component")) { component.classList.add("agility-component"); const divCompEdit = document.createElement("button"); divCompEdit.classList.add("agility-component-edit"); divCompEdit.setAttribute("type", "button"); divCompEdit.setAttribute("title", "Edit"); divCompEdit.addEventListener("click", (e) => { if (!contentID) return; e.preventDefault(); e.stopPropagation(); dispatchEditComponentEvent({ contentID: parseInt(contentID), pageID: pageID ? parseInt(pageID) : -1 }); }); component.appendChild(divCompEdit); const imgEdit = document.createElement("img"); imgEdit.src = "https://cdn.aglty.io/content-manager/images/studio-edit.svg"; imgEdit.alt = "Edit"; divCompEdit.appendChild(imgEdit); component.querySelectorAll("[data-agility-field]").forEach((field) => { field.classList.add("agility-field"); const fieldName = field.getAttribute("data-agility-field"); const divFieldEdit = document.createElement("button"); divFieldEdit.classList.add("agility-field-edit"); divFieldEdit.setAttribute("type", "button"); divFieldEdit.setAttribute("title", "Edit"); divFieldEdit.addEventListener("click", (e) => { if (!contentID || !fieldName) return; e.preventDefault(); e.stopPropagation(); dispatchEditFieldEvent({ contentID: parseInt(contentID), fieldName, pageID: pageID ? parseInt(pageID) : -1 }); }); field.appendChild(divFieldEdit); const imgEditField = document.createElement("img"); imgEditField.src = "https://cdn.aglty.io/content-manager/images/studio-edit.svg"; imgEditField.alt = "Edit"; divFieldEdit.appendChild(imgEditField); }); } }); }); }; // src/util/applyContentItem.ts var applyContentItem = (contentItem) => { const components = document.querySelectorAll("[data-agility-component]"); components.forEach((component) => { const contentID = parseInt( component.getAttribute("data-agility-component") || "" ); if (contentID !== contentItem.contentID) return; component.querySelectorAll("[data-agility-field]").forEach((field) => { const fieldName = field.getAttribute("data-agility-field") || ""; const fieldNameInContentItem = Object.keys(contentItem.values).find( (key) => key.toLowerCase() === fieldName.toLowerCase() ); const fieldValue = contentItem.values[fieldNameInContentItem || ""]; const fieldEditButton = field.querySelector(".agility-field-edit"); if (typeof fieldValue === "string") { if (fieldValue && fieldValue.startsWith("<a ") && fieldValue.endsWith("</a>")) { field.innerHTML = fieldValue; } else { if (field.hasAttribute("data-agility-html")) { field.innerHTML = fieldValue; } else { field.textContent = fieldValue; } } if (!fieldEditButton) { console.warn("No edit button found for field", fieldName); return; } field.appendChild(fieldEditButton); } else if (fieldValue.url) { const img = field.querySelector( "img:not(.agility-field-edit img)" ); if (img && img.src) { field.querySelectorAll("source").forEach((source) => { const oldSrc = source.srcset; const oldSrcParts = oldSrc.split("?"); const newSrc2 = fieldValue.url + "?" + oldSrcParts[1]; source.srcset = newSrc2; }); const currentSrc = img.src; const currentSrcParts = currentSrc.split("?"); const newSrc = fieldValue.url + "?" + currentSrcParts[1]; img.loading = "eager"; img.alt = fieldValue.label; img.src = newSrc; } } else { console.log( "%cWeb Studio SDK\n Cannot apply field value of field", "font-weight: bold", fieldName, "value: ", fieldValue ); } }); }); }; // src/util/commentUtils.ts function getDeepestElementAtCoordinates(element, x, y) { function findDeepestElement(element2, x2, y2) { const rect = element2.getBoundingClientRect(); const style = window.getComputedStyle(element2); const marginTop = parseFloat(style.marginTop); const marginRight = parseFloat(style.marginRight); const marginBottom = parseFloat(style.marginBottom); const marginLeft = parseFloat(style.marginLeft); const adjustedRect = { left: rect.left - marginLeft, right: rect.right + marginRight, top: rect.top - marginTop, bottom: rect.bottom + marginBottom }; const isInside = x2 >= adjustedRect.left && x2 <= adjustedRect.right && y2 >= adjustedRect.top && y2 <= adjustedRect.bottom; if (!isInside) return null; for (let i = 0; i < element2.children.length; i++) { const child = element2.children[i]; const deepestChild = findDeepestElement(child, x2, y2); if (deepestChild) { return deepestChild; } } return element2; } return findDeepestElement(element, x, y); } function escapeCSSIdentifier(ident) { return ident.replace(/^(\d)/, "\\3$1 ").replace(/^(-\d)/, "\\$1").replace(/([^\x00-\x7F]|[!"#$%&'()*+,.\/:;<=>?@[\]^`{|}~])/g, "\\$&"); } function getSelectorIndex(sel, x, y) { const elements = Array.from(document.querySelectorAll(sel)); const index = elements.findIndex((el) => { const rect = el.getBoundingClientRect(); const style = getComputedStyle(el); const marginTop = parseFloat(style.marginTop); const marginRight = parseFloat(style.marginRight); const marginBottom = parseFloat(style.marginBottom); const marginLeft = parseFloat(style.marginLeft); const marginRect = { top: rect.top - marginTop, right: rect.right + marginRight, bottom: rect.bottom + marginBottom, left: rect.left - marginLeft }; return marginRect.left <= x && x <= marginRect.right && marginRect.top <= y && y <= marginRect.bottom; }); return index; } function getUniqueSelector(element) { let path = []; while (element.parentElement) { let selector = element.tagName.toLowerCase(); if (element.id) { selector += `#${escapeCSSIdentifier(element.id)}`; } else if (element.className) { let classNames = element.className.toString().split(" ").filter(Boolean).map((cls) => escapeCSSIdentifier(cls)).join("."); if (classNames) { selector += `.${classNames}`; } } path.unshift(selector); element = element.parentElement; } return path.join(" > "); } function getRelativePercentage(deepestEle, x, y) { if (!deepestEle) return null; const rect = deepestEle.getBoundingClientRect(); const relativeX = x - rect.left; const relativeY = y - rect.top; const percentageX = relativeX / rect.width * 100; const percentageY = relativeY / rect.height * 100; return { percentageX: Math.max(0, Math.min(100, percentageX)), // Ensure value is between 0 and 100 percentageY: Math.max(0, Math.min(100, percentageY)) // Ensure value is between 0 and 100 }; } function getAbsolutePositionFromPercentage(deepestEle, percentageX, percentageY) { if (!deepestEle || percentageY === null || percentageY === void 0 || percentageX === null || percentageX === void 0) return null; const rect = deepestEle.getBoundingClientRect(); const style = getComputedStyle(deepestEle); const marginTop = parseFloat(style.marginTop); const marginLeft = parseFloat(style.marginLeft); const scrollX = document.documentElement.scrollLeft; const scrollY = document.documentElement.scrollTop; const x = rect.left + marginLeft + percentageX / 100 * rect.width + scrollX; const y = rect.top + marginTop + percentageY / 100 * rect.height + scrollY; return { x, y }; } // src/util/generateDecoratorMap.ts var generateDecoratorMap = () => { const fieldMap = /* @__PURE__ */ new Map(); const components = document.querySelectorAll("[data-agility-component]"); if (!components.length) { return; } components.forEach((component) => { const contentID = parseInt( component.getAttribute("data-agility-component") || "" ); if (!contentID) { return; } const fields = component.querySelectorAll("[data-agility-field]"); if (!fields.length) { return; } let fieldNames = []; fields.forEach((field) => { const fieldName = field.getAttribute("data-agility-field") || ""; if (!fieldName) { return; } fieldNames.push(fieldName); }); fieldMap.set(contentID, fieldNames); }); return fieldMap; }; // src/util/initializePreview.ts var throttle = (func, limit) => { let lastFunc; let lastRan; return function(...args) { const context = this; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(function() { if (Date.now() - lastRan >= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; }; var initializePreview = ({ setIsInitialized: setIsInitialized2 }) => { if (!window.parent || !window.parent.postMessage) return; if (window.self === window.top) return; setIsInitialized2(true); const agilityGuid = getGuid("initialize preview"); let resizeTimeout; window.addEventListener("resize", () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { const args = { windowHeight: window.innerHeight, windowWidth: window.innerWidth, windowScrollableHeight: document.documentElement.scrollHeight }; dispatchWindowResizeEvent(args); }, 250); }); const throttledScrollHandler = throttle(() => { const args = { windowScrollableHeight: document.documentElement.scrollHeight, windowHeight: window.innerHeight, windowWidth: window.innerWidth, scrollY: window.scrollY, scrollX: window.scrollX }; dispatchScrollEvent(args); }, 10); window.addEventListener("scroll", throttledScrollHandler); window.addEventListener("message", ({ data }) => { const { source, messageType, guid, arg } = data; if (source !== "agility-instance" || guid !== agilityGuid) return; switch (messageType) { case "ready": initCSSAndPreviewPanel(); initComponents(); const decoratorMap = generateDecoratorMap(); if (decoratorMap) { dispatchDecoratorMapUpdatedEvent({ decoratorMap }); } break; case "get-comment-coords": const { originX, originY, calcFallbackX, calcFallbackY, isDragEndEvent, threadId } = arg; const element = document.elementFromPoint(originX, originY); if (!element) { console.warn("No element found at the specified coordinates."); return; } const deepestEle = getDeepestElementAtCoordinates( element, originX, originY ); if (!deepestEle) { console.warn("Deepest element could not be found"); return; } const uniqueSelector = getUniqueSelector(deepestEle); const eleIndex = getSelectorIndex(uniqueSelector, originX, originY); const percentageCoords = getRelativePercentage( deepestEle, originX, originY ); dispatchSetCommentCoordsEvent({ uniqueSelector, percentageOffsetX: percentageCoords?.percentageX, percentageOffsetY: percentageCoords?.percentageY, elementIndex: eleIndex, isDragEndEvent: !!isDragEndEvent, threadId, originX, originY, calcFallbackX, calcFallbackY }); case "update-comment-dictionary": { const { commentDictionary } = arg; const updatedCommentDictionary = {}; if (!commentDictionary) { console.warn("Web Studio SDK - no comments to update"); return; } for (const [key, value] of Object.entries( commentDictionary )) { const { percentageOffsetX, percentageOffsetY, uniqueSelector: uniqueSelector2, elementIndex } = value; updatedCommentDictionary[key] = value; if (!uniqueSelector2) { console.warn( "Web Studio SDK - could not find the unique selector for this comment" ); } else { let element2 = null; if (elementIndex === null || elementIndex === void 0 || elementIndex === -1) { element2 = document.querySelector(uniqueSelector2); } else { const elements = document.querySelectorAll(uniqueSelector2); element2 = elements[elementIndex]; } if (element2) { const coords = getAbsolutePositionFromPercentage( element2, percentageOffsetX, percentageOffsetY ); if (coords) { updatedCommentDictionary[key] = { ...updatedCommentDictionary[key], x: coords.x, y: coords.y }; } else { console.warn( "Web Studio SDK - could not find the absolute position from percentage" ); } } } } dispatchCommentDictionaryUpdatedEvent({ updatedCommentDictionary }); } case "content-change": { const contentItem = arg; applyContentItem(contentItem); break; } case "refresh": setTimeout(() => { location.replace(location.href); }, 1e3); break; default: console.warn( "%cWeb Studio SDK\n Unknown message type on website:", "font-weight: bold", messageType, arg ); break; } }); const windowWidth = window.innerWidth; const windowHeight = window.outerHeight; const hasPageDecorators = document.querySelector("[data-agility-page]"); const hasComponentDecorators = document.querySelector( "[data-agility-component]" ); const hasFieldDecorators = document.querySelector("[data-agility-field]"); dispatchReadyEvent({ windowWidth, windowHeight, hasPageDecorators: !!hasPageDecorators, hasComponentDecorators: !!hasComponentDecorators, hasFieldDecorators: !!hasFieldDecorators, windowScrollableHeight: document.documentElement.scrollHeight }); }; // src/index.ts var isInitialized = false; var setIsInitialized = (state) => { isInitialized = state; startObserver(); }; var isDocumentReady = false; var setDocumentReady = (state) => { isDocumentReady = state; startObserver(); }; var onLocationChange = () => { const agilityPageIDElem = document.querySelector("[data-agility-page]"); const agilityDynamicContentElem = document.querySelector( "[data-agility-dynamic-content]" ); let pageID = -1; let contentID = -1; if (agilityPageIDElem) { pageID = parseInt(agilityPageIDElem.getAttribute("data-agility-page") || ""); } if (isNaN(pageID) || pageID < 1) { console.warn( "%cWeb Studio SDK\n - no pageID found on the `data-agility-page` element. \nMake sure you have an element set up like this: data-agility-page='{{agilitypageid}}' .", "font-weight: bold" ); return; } if (agilityDynamicContentElem) { contentID = agilityDynamicContentElem.getAttribute( "data-agility-dynamic-content" ); } let fullUrl = location.href; if (fullUrl.indexOf("?") > -1) { fullUrl = fullUrl.substring(0, fullUrl.indexOf("?")); } const args = { url: fullUrl, pageID, contentID, windowScrollableHeight: document.documentElement.scrollHeight, windowHeight: window.innerHeight }; dispatchNavigationEvent(args); initComponents(); const decoratorMap = generateDecoratorMap(); if (decoratorMap) { dispatchDecoratorMapUpdatedEvent({ decoratorMap }); } }; var startObserver = () => { if (!isDocumentReady || !isInitialized) return; const observer = new MutationObserver((target, options) => { if (isInitialized) { setTimeout(() => { onLocationChange(); }, 100); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [ "data-agility-page", "data-agility-dynamic-content", "data-agility-guid", "data-agility-field", "data-agility-component", "data-agility-html", "data-agility-previewbar" ] }); }; if (document.readyState === "complete" || document.readyState === "interactive") { setDocumentReady(true); } document.onreadystatechange = () => { if (document.readyState === "interactive" || document.readyState === "complete") { setDocumentReady(true); } }; initializePreview({ setIsInitialized });