@agility/web-studio-sdk
Version:
Standard Development Kit used to enable Web Studio features in Agility CMS
650 lines (640 loc) • 20.9 kB
JavaScript
// 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 });