feat: 支持 Shadow DOM 模式并增强widget移动端显示 (#317)
* feat: 支持 Shadow DOM 模式并增强widget移动端显示 * chore: bump version to v0.9.61
This commit is contained in:
+155
-65
@@ -17,87 +17,177 @@
|
||||
popupTitle = "Need help?",
|
||||
popupSubtitle = "Our staff are always ready to help!",
|
||||
popupImage = "",
|
||||
popupClosable = "true"
|
||||
popupClosable = "true",
|
||||
useShadowDom = "true"
|
||||
} = d.currentScript.dataset;
|
||||
const _src = d.currentScript.src;
|
||||
const wrapper = d.createElement("iframe");
|
||||
wrapper.id = "VOCECHAT_WIDGET";
|
||||
const origin = new URL(_src).origin;
|
||||
|
||||
// 如果有提示框内容,关闭状态的宽度和高度需要更大
|
||||
const hasTooltip = popupTitle || popupSubtitle;
|
||||
const actualCloseWidth = hasTooltip ? 320 : closeWidth;
|
||||
const actualCloseHeight = hasTooltip ? 120 : closeHeight;
|
||||
|
||||
const styles = {
|
||||
const baseStyles = {
|
||||
position: "fixed",
|
||||
borderRadius: "8px",
|
||||
right: "16px",
|
||||
[position]: "16px", // [position] = [right] or [left]
|
||||
bottom: "16px",
|
||||
border: "none",
|
||||
zIndex: 9999,
|
||||
boxShadow: `rgb(0 0 0 / 25%) 0px 25px 50px -12px`
|
||||
zIndex: 9999
|
||||
};
|
||||
Object.assign(wrapper.style, styles);
|
||||
wrapper.src = `${
|
||||
new URL(_src).origin
|
||||
}/widget.html?id=${id}&host=${hostId}&autoReg=${autoReg}&token=${loginToken}&themeColor=${encodeURIComponent(
|
||||
themeColor
|
||||
)}&from=${encodeURIComponent(location.hostname)}&welcome=${encodeURIComponent(welcome)}&title=${encodeURIComponent(title)}&logo=${encodeURIComponent(logo)}&popupTitle=${encodeURIComponent(popupTitle)}&popupSubtitle=${encodeURIComponent(popupSubtitle)}&popupImage=${encodeURIComponent(popupImage)}&popupClosable=${popupClosable}`;
|
||||
wrapper.width = actualCloseWidth;
|
||||
wrapper.height = actualCloseHeight;
|
||||
wrapper.frameborder = 0;
|
||||
wrapper.referrerPolicy = "unsafe-url";
|
||||
w.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
const { data: CMD } = event;
|
||||
switch (CMD) {
|
||||
case "OPEN":
|
||||
wrapper.setAttribute("width", openWidth);
|
||||
wrapper.setAttribute("height", openHeight);
|
||||
break;
|
||||
case "CLOSE":
|
||||
wrapper.setAttribute("width", actualCloseWidth);
|
||||
wrapper.setAttribute("height", actualCloseHeight);
|
||||
break;
|
||||
case "RELOAD_WITH_OPEN":
|
||||
{
|
||||
const url = new URL(wrapper.src);
|
||||
url.searchParams.append("open", new Date().getTime());
|
||||
console.log("new src", url.href);
|
||||
wrapper.src = url.href;
|
||||
|
||||
if (useShadowDom === "true") {
|
||||
// Shadow DOM 模式 - 直接注入内容
|
||||
const container = d.createElement("div");
|
||||
container.id = "VOCECHAT_WIDGET_CONTAINER";
|
||||
Object.assign(container.style, baseStyles);
|
||||
// 不限制容器大小,让内容自己决定
|
||||
container.style.pointerEvents = "none"; // 容器本身不响应点击,让点击穿透
|
||||
|
||||
const shadowRoot = container.attachShadow({ mode: "open" });
|
||||
|
||||
// 创建根容器
|
||||
const rootDiv = d.createElement("div");
|
||||
rootDiv.id = "root";
|
||||
rootDiv.style.background = "transparent";
|
||||
rootDiv.style.pointerEvents = "auto"; // 内容可以响应点击
|
||||
|
||||
// 创建 modal 容器
|
||||
const modalDiv = d.createElement("div");
|
||||
modalDiv.id = "root-modal";
|
||||
|
||||
shadowRoot.appendChild(rootDiv);
|
||||
shadowRoot.appendChild(modalDiv);
|
||||
|
||||
d.body.appendChild(container);
|
||||
|
||||
// 先加载 asset manifest 获取正确的文件路径
|
||||
fetch(`${origin}/asset-manifest.json`)
|
||||
.then(res => res.json())
|
||||
.then(manifest => {
|
||||
// 加载样式
|
||||
const styleLink = d.createElement("link");
|
||||
styleLink.rel = "stylesheet";
|
||||
styleLink.href = `${origin}${manifest.files["widget.css"]}`;
|
||||
shadowRoot.appendChild(styleLink);
|
||||
|
||||
// 加载并执行 widget 脚本
|
||||
const script = d.createElement("script");
|
||||
script.src = `${origin}${manifest.files["widget.js"]}`;
|
||||
script.onload = () => {
|
||||
// 脚本加载完成后,需要手动初始化 React 应用到 shadow DOM
|
||||
const params = new URLSearchParams({
|
||||
id,
|
||||
host: hostId,
|
||||
autoReg,
|
||||
token: loginToken,
|
||||
themeColor,
|
||||
from: location.hostname,
|
||||
welcome,
|
||||
title,
|
||||
logo,
|
||||
popupTitle,
|
||||
popupSubtitle,
|
||||
popupImage,
|
||||
popupClosable,
|
||||
embed: "true",
|
||||
closeWidth,
|
||||
closeHeight,
|
||||
openWidth,
|
||||
openHeight
|
||||
});
|
||||
|
||||
// 将参数传递给 shadow DOM 内的应用
|
||||
if (w.VoceChatWidgetInit) {
|
||||
w.VoceChatWidgetInit(shadowRoot, params.toString());
|
||||
}
|
||||
};
|
||||
shadowRoot.appendChild(script);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to load widget manifest:", err);
|
||||
});
|
||||
|
||||
// 暴露全局函数用于打开 widget(简化版,只触发事件)
|
||||
w.VoceChatWidget = {
|
||||
open: function() {
|
||||
const event = new CustomEvent("vocechat-widget-open");
|
||||
shadowRoot.dispatchEvent(event);
|
||||
},
|
||||
close: function() {
|
||||
const event = new CustomEvent("vocechat-widget-close");
|
||||
shadowRoot.dispatchEvent(event);
|
||||
},
|
||||
toggle: function() {
|
||||
const event = new CustomEvent("vocechat-widget-toggle");
|
||||
shadowRoot.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// 传统 iframe 模式
|
||||
const wrapper = d.createElement("iframe");
|
||||
wrapper.id = "VOCECHAT_WIDGET";
|
||||
Object.assign(wrapper.style, baseStyles);
|
||||
wrapper.src = `${origin}/widget.html?id=${id}&host=${hostId}&autoReg=${autoReg}&token=${loginToken}&themeColor=${encodeURIComponent(
|
||||
themeColor
|
||||
)}&from=${encodeURIComponent(location.hostname)}&welcome=${encodeURIComponent(welcome)}&title=${encodeURIComponent(title)}&logo=${encodeURIComponent(logo)}&popupTitle=${encodeURIComponent(popupTitle)}&popupSubtitle=${encodeURIComponent(popupSubtitle)}&popupImage=${encodeURIComponent(popupImage)}&popupClosable=${popupClosable}`;
|
||||
wrapper.width = actualCloseWidth;
|
||||
wrapper.height = actualCloseHeight;
|
||||
wrapper.frameborder = 0;
|
||||
wrapper.referrerPolicy = "unsafe-url";
|
||||
|
||||
w.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
const { data: CMD } = event;
|
||||
switch (CMD) {
|
||||
case "OPEN":
|
||||
wrapper.setAttribute("width", openWidth);
|
||||
wrapper.setAttribute("height", openHeight);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
d.body.appendChild(wrapper);
|
||||
break;
|
||||
case "CLOSE":
|
||||
wrapper.setAttribute("width", actualCloseWidth);
|
||||
wrapper.setAttribute("height", actualCloseHeight);
|
||||
break;
|
||||
case "RELOAD_WITH_OPEN":
|
||||
{
|
||||
const url = new URL(wrapper.src);
|
||||
url.searchParams.append("open", new Date().getTime());
|
||||
console.log("new src", url.href);
|
||||
wrapper.src = url.href;
|
||||
wrapper.setAttribute("width", openWidth);
|
||||
wrapper.setAttribute("height", openHeight);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
d.body.appendChild(wrapper);
|
||||
|
||||
// 暴露全局函数用于打开 widget
|
||||
w.VoceChatWidget = {
|
||||
open: function() {
|
||||
wrapper.setAttribute("width", openWidth);
|
||||
wrapper.setAttribute("height", openHeight);
|
||||
wrapper.contentWindow?.postMessage("OPEN_FROM_PARENT", "*");
|
||||
},
|
||||
close: function() {
|
||||
wrapper.setAttribute("width", actualCloseWidth);
|
||||
wrapper.setAttribute("height", actualCloseHeight);
|
||||
wrapper.contentWindow?.postMessage("CLOSE_FROM_PARENT", "*");
|
||||
},
|
||||
toggle: function() {
|
||||
const currentWidth = wrapper.getAttribute("width");
|
||||
if (currentWidth == actualCloseWidth) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
// 暴露全局函数用于打开 widget
|
||||
w.VoceChatWidget = {
|
||||
open: function() {
|
||||
wrapper.setAttribute("width", openWidth);
|
||||
wrapper.setAttribute("height", openHeight);
|
||||
wrapper.contentWindow?.postMessage("OPEN_FROM_PARENT", "*");
|
||||
},
|
||||
close: function() {
|
||||
wrapper.setAttribute("width", actualCloseWidth);
|
||||
wrapper.setAttribute("height", actualCloseHeight);
|
||||
wrapper.contentWindow?.postMessage("CLOSE_FROM_PARENT", "*");
|
||||
},
|
||||
toggle: function() {
|
||||
const currentWidth = wrapper.getAttribute("width");
|
||||
if (currentWidth == actualCloseWidth) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
})(window, document);
|
||||
|
||||
Reference in New Issue
Block a user