|
|
/*
|
|
|
* vdev-select.js
|
|
|
* Copyright (C) 2025 veypi <i@veypi.com>
|
|
|
*
|
|
|
* Distributed under terms of the MIT license.
|
|
|
*/
|
|
|
|
|
|
const copyToClipboard = async (text) => {
|
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
|
return navigator.clipboard.writeText(text)
|
|
|
}
|
|
|
prompt('http环境无法自动复制,请手动复制内容到剪贴板:', text)
|
|
|
return new Promise((resolve) => { })
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Div Selector Plugin
|
|
|
* 用于选择页面div元素并高亮显示的插件
|
|
|
*/
|
|
|
class DivSelectorPlugin {
|
|
|
postMessage = (typ, args) => { }
|
|
|
constructor(options = {}) {
|
|
|
this.options = {
|
|
|
highlightColor: '#007bff',
|
|
|
overlayColor: 'rgba(0, 123, 255, 0.1)',
|
|
|
borderWidth: '2px',
|
|
|
zIndex: 10000,
|
|
|
showTagName: true,
|
|
|
...options
|
|
|
};
|
|
|
|
|
|
this.isActive = false;
|
|
|
this.selectedElement = null;
|
|
|
this.currentHoverElement = null;
|
|
|
this.overlay = null;
|
|
|
this.selectedOverlay = null;
|
|
|
this.tooltip = null;
|
|
|
this.selectedTooltip = null;
|
|
|
this.actionPanel = null;
|
|
|
this.originalCursor = '';
|
|
|
|
|
|
this.init();
|
|
|
}
|
|
|
|
|
|
init() {
|
|
|
this.createStyles();
|
|
|
this.bindEvents();
|
|
|
}
|
|
|
|
|
|
createStyles() {
|
|
|
const style = document.createElement('style');
|
|
|
style.textContent = `
|
|
|
.div-selector-overlay {
|
|
|
position: fixed;
|
|
|
pointer-events: none;
|
|
|
border: ${this.options.borderWidth} solid ${this.options.highlightColor};
|
|
|
background: ${this.options.overlayColor};
|
|
|
z-index: ${this.options.zIndex};
|
|
|
transition: all 0.2s ease;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.div-selector-selected {
|
|
|
position: fixed;
|
|
|
pointer-events: none;
|
|
|
border: ${this.options.borderWidth} solid #28a745;
|
|
|
background: rgba(40, 167, 69, 0.1);
|
|
|
z-index: ${this.options.zIndex - 1};
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.div-selector-tooltip {
|
|
|
position: fixed;
|
|
|
background: rgba(51, 51, 51, 0.95);
|
|
|
cursor:pointer;
|
|
|
color: white;
|
|
|
padding: 8px 12px;
|
|
|
border-radius: 6px;
|
|
|
font-size: 12px;
|
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
|
z-index: ${this.options.zIndex + 1};
|
|
|
user-select: none;
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
|
backdrop-filter: blur(4px);
|
|
|
}
|
|
|
|
|
|
.div-selector-tooltip.hover {
|
|
|
background: rgba(0, 123, 255, 0.9);
|
|
|
}
|
|
|
|
|
|
.div-selector-tooltip.selected {
|
|
|
background: rgba(40, 167, 69, 0.9);
|
|
|
border-color: rgba(40, 167, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
.div-selector-actions {
|
|
|
position: fixed;
|
|
|
background: white;
|
|
|
border: 1px solid #ddd;
|
|
|
border-radius: 8px;
|
|
|
padding: 8px;
|
|
|
z-index: ${this.options.zIndex + 2};
|
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
|
display: flex;
|
|
|
gap: 6px;
|
|
|
min-width: 30px;
|
|
|
backdrop-filter: blur(8px);
|
|
|
}
|
|
|
|
|
|
.div-selector-btn {
|
|
|
padding: 4px 6px;
|
|
|
border: 1px solid #ddd;
|
|
|
background: white;
|
|
|
border-radius: 6px;
|
|
|
cursor: pointer;
|
|
|
font-size: 12px;
|
|
|
transition: all 0.2s ease;
|
|
|
text-align: left;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.div-selector-btn:hover {
|
|
|
background: #f8f9fa;
|
|
|
transform: translateX(2px);
|
|
|
}
|
|
|
|
|
|
.div-selector-btn.danger {
|
|
|
color: #dc3545;
|
|
|
border-color: #dc3545;
|
|
|
}
|
|
|
|
|
|
.div-selector-btn.danger:hover {
|
|
|
background: #dc3545;
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.div-selector-btn.primary {
|
|
|
background: #007bff;
|
|
|
color: white;
|
|
|
border-color: #007bff;
|
|
|
}
|
|
|
|
|
|
.div-selector-btn.primary:hover {
|
|
|
background: #0056b3;
|
|
|
}
|
|
|
|
|
|
.div-selector-active {
|
|
|
cursor: crosshair !important;
|
|
|
}
|
|
|
`;
|
|
|
document.head.appendChild(style);
|
|
|
}
|
|
|
|
|
|
bindEvents() {
|
|
|
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
|
this.handleClick = this.handleClick.bind(this);
|
|
|
this.handleScroll = this.handleScroll.bind(this);
|
|
|
this.handleResize = this.handleResize.bind(this);
|
|
|
}
|
|
|
|
|
|
activate() {
|
|
|
if (this.isActive) return;
|
|
|
|
|
|
this.isActive = true;
|
|
|
this.originalCursor = document.body.style.cursor;
|
|
|
document.body.classList.add('div-selector-active');
|
|
|
|
|
|
document.addEventListener('mousemove', this.handleMouseMove);
|
|
|
document.addEventListener('click', this.handleClick);
|
|
|
window.addEventListener('scroll', this.handleScroll);
|
|
|
window.addEventListener('resize', this.handleResize);
|
|
|
|
|
|
this.createOverlay();
|
|
|
this.createTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
deactivate() {
|
|
|
if (!this.isActive) return;
|
|
|
|
|
|
this.isActive = false;
|
|
|
document.body.style.cursor = this.originalCursor;
|
|
|
document.body.classList.remove('div-selector-active');
|
|
|
|
|
|
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
|
document.removeEventListener('click', this.handleClick);
|
|
|
window.removeEventListener('scroll', this.handleScroll);
|
|
|
window.removeEventListener('resize', this.handleResize);
|
|
|
|
|
|
this.removeOverlay();
|
|
|
this.removeTooltip();
|
|
|
// this.clearSelection();
|
|
|
|
|
|
console.log('Div Selector Plugin deactivated.');
|
|
|
}
|
|
|
|
|
|
handleMouseMove(e) {
|
|
|
if (!this.isActive) return;
|
|
|
|
|
|
const element = document.elementFromPoint(e.clientX, e.clientY);
|
|
|
if (!element) return;
|
|
|
|
|
|
// 忽略插件自身的元素
|
|
|
if (this.isPluginElement(element)) return;
|
|
|
|
|
|
// 查找最近的div元素
|
|
|
const divElement = element.tagName === 'DIV' ? element : element.closest('div,[vref]');
|
|
|
if (!divElement || this.isPluginElement(divElement)) return;
|
|
|
|
|
|
// 如果已选中元素,不要高亮显示它
|
|
|
if (this.selectedElement === divElement) return;
|
|
|
|
|
|
this.currentHoverElement = divElement;
|
|
|
this.highlightElement(divElement);
|
|
|
this.updateHoverTooltip(divElement);
|
|
|
}
|
|
|
|
|
|
handleClick(e) {
|
|
|
if (!this.isActive) return;
|
|
|
|
|
|
const element = document.elementFromPoint(e.clientX, e.clientY);
|
|
|
if (!element) return;
|
|
|
|
|
|
// 忽略插件自身的元素
|
|
|
if (this.isPluginElement(element)) return;
|
|
|
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
|
|
|
const divElement = element.tagName === 'DIV' ? element : element.closest('div');
|
|
|
if (!divElement || this.isPluginElement(divElement)) return;
|
|
|
|
|
|
this.selectElement(divElement);
|
|
|
this.deactivate()
|
|
|
}
|
|
|
|
|
|
handleScroll() {
|
|
|
if (this.selectedElement) {
|
|
|
this.updateSelectedOverlay();
|
|
|
this.updateSelectedTooltip();
|
|
|
this.updateActionPanelPosition();
|
|
|
}
|
|
|
if (this.currentHoverElement && !this.selectedElement) {
|
|
|
this.highlightElement(this.currentHoverElement);
|
|
|
this.updateHoverTooltip(this.currentHoverElement);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
handleResize() {
|
|
|
if (this.selectedElement) {
|
|
|
this.updateSelectedOverlay();
|
|
|
this.updateSelectedTooltip();
|
|
|
this.updateActionPanelPosition();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
isPluginElement(element) {
|
|
|
return element.closest('.div-selector-overlay, .div-selector-selected, .div-selector-tooltip, .div-selector-actions');
|
|
|
}
|
|
|
|
|
|
createOverlay() {
|
|
|
this.overlay = document.createElement('div');
|
|
|
this.overlay.className = 'div-selector-overlay';
|
|
|
this.overlay.style.display = 'none';
|
|
|
document.body.appendChild(this.overlay);
|
|
|
}
|
|
|
|
|
|
createSelectedOverlay() {
|
|
|
this.selectedOverlay = document.createElement('div');
|
|
|
this.selectedOverlay.className = 'div-selector-selected';
|
|
|
document.body.appendChild(this.selectedOverlay);
|
|
|
}
|
|
|
|
|
|
createTooltip() {
|
|
|
this.tooltip = document.createElement('div');
|
|
|
this.tooltip.className = 'div-selector-tooltip hover';
|
|
|
this.tooltip.style.display = 'none';
|
|
|
document.body.appendChild(this.tooltip);
|
|
|
}
|
|
|
|
|
|
createSelectedTooltip() {
|
|
|
this.selectedTooltip = document.createElement('div');
|
|
|
this.selectedTooltip.className = 'div-selector-tooltip selected';
|
|
|
document.body.appendChild(this.selectedTooltip);
|
|
|
}
|
|
|
|
|
|
highlightElement(element) {
|
|
|
if (!this.overlay) return;
|
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
|
this.overlay.style.display = 'block';
|
|
|
this.overlay.style.left = rect.left + 'px';
|
|
|
this.overlay.style.top = rect.top + 'px';
|
|
|
this.overlay.style.width = rect.width + 'px';
|
|
|
this.overlay.style.height = rect.height + 'px';
|
|
|
}
|
|
|
|
|
|
updateHoverTooltip(element) {
|
|
|
if (!this.tooltip) return;
|
|
|
|
|
|
const elementInfo = this.getElementInfo(element);
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
|
this.tooltip.textContent = elementInfo;
|
|
|
this.tooltip.style.display = 'block';
|
|
|
|
|
|
// 计算tooltip位置 - 左上角,稍微偏移避免遮挡
|
|
|
let left = rect.left - 8;
|
|
|
let top = rect.top - this.tooltip.offsetHeight - 8;
|
|
|
|
|
|
// 确保不超出视窗
|
|
|
if (left < 0) left = 8;
|
|
|
if (top < 0) top = rect.top + 8;
|
|
|
|
|
|
this.tooltip.style.left = left + 'px';
|
|
|
this.tooltip.style.top = top + 'px';
|
|
|
}
|
|
|
|
|
|
selectElement(element) {
|
|
|
// 清除之前的选择
|
|
|
this.clearSelection();
|
|
|
|
|
|
this.selectedElement = element;
|
|
|
|
|
|
// 创建选中状态的覆盖层和tooltip
|
|
|
this.createSelectedOverlay();
|
|
|
this.createSelectedTooltip();
|
|
|
this.updateSelectedOverlay();
|
|
|
this.updateSelectedTooltip();
|
|
|
|
|
|
// 隐藏悬停效果
|
|
|
if (this.overlay) {
|
|
|
this.overlay.style.display = 'none';
|
|
|
}
|
|
|
if (this.tooltip) {
|
|
|
this.tooltip.style.display = 'none';
|
|
|
}
|
|
|
|
|
|
// 显示操作面板
|
|
|
this.showActionPanel();
|
|
|
|
|
|
console.log('Element selected:', element);
|
|
|
}
|
|
|
|
|
|
updateSelectedOverlay() {
|
|
|
if (!this.selectedOverlay || !this.selectedElement) return;
|
|
|
|
|
|
const rect = this.selectedElement.getBoundingClientRect();
|
|
|
|
|
|
this.selectedOverlay.style.left = rect.left + 'px';
|
|
|
this.selectedOverlay.style.top = rect.top + 'px';
|
|
|
this.selectedOverlay.style.width = rect.width + 'px';
|
|
|
this.selectedOverlay.style.height = rect.height + 'px';
|
|
|
}
|
|
|
|
|
|
updateSelectedTooltip() {
|
|
|
if (!this.selectedTooltip || !this.selectedElement) return;
|
|
|
|
|
|
const elementInfo = this.getElementInfo(this.selectedElement);
|
|
|
let vref = this.getFilePath(this.selectedElement)
|
|
|
const rect = this.selectedElement.getBoundingClientRect();
|
|
|
this.selectedTooltip.addEventListener('click', e => {
|
|
|
if (vref) {
|
|
|
this.postMessage('fs-open', vref)
|
|
|
}
|
|
|
e.preventDefault()
|
|
|
e.stopPropagation()
|
|
|
})
|
|
|
|
|
|
this.selectedTooltip.textContent = `✓ ${elementInfo}`;
|
|
|
|
|
|
// 计算tooltip位置 - 选中框的左上角
|
|
|
let left = rect.left - 8;
|
|
|
let top = rect.top - this.selectedTooltip.offsetHeight - 8;
|
|
|
|
|
|
// 确保不超出视窗
|
|
|
if (left < 0) left = 8;
|
|
|
if (top < 0) top = rect.top + 8;
|
|
|
|
|
|
this.selectedTooltip.style.left = left + 'px';
|
|
|
this.selectedTooltip.style.top = top + 'px';
|
|
|
}
|
|
|
|
|
|
showActionPanel() {
|
|
|
this.removeActionPanel();
|
|
|
|
|
|
const rect = this.selectedElement.getBoundingClientRect();
|
|
|
|
|
|
this.actionPanel = document.createElement('div');
|
|
|
this.actionPanel.className = 'div-selector-actions';
|
|
|
|
|
|
// 计算位置 - 放在右上角
|
|
|
const panelWidth = 180; // 预估宽度
|
|
|
const panelHeight = 40; // 预估高度
|
|
|
|
|
|
let left = rect.right - panelWidth;
|
|
|
let top = rect.top + 6;
|
|
|
|
|
|
// 确保不超出视窗
|
|
|
if (left + panelWidth > window.innerWidth) {
|
|
|
left = rect.left - panelWidth - 8;
|
|
|
}
|
|
|
if (left < 0) left = 8;
|
|
|
|
|
|
if (top + panelHeight > window.innerHeight) {
|
|
|
top = window.innerHeight - panelHeight - 8;
|
|
|
}
|
|
|
if (top < 0) top = 8;
|
|
|
|
|
|
this.actionPanel.style.left = left + 'px';
|
|
|
this.actionPanel.style.top = top + 'px';
|
|
|
|
|
|
// 创建操作按钮
|
|
|
const buttons = [
|
|
|
{ text: '📋', action: () => this.copySelector(this.selectedElement) },
|
|
|
// { text: '🔍', action: () => this.copyXPath(this.selectedElement) },
|
|
|
// { text: '👁️', action: () => this.viewStyles(this.selectedElement) },
|
|
|
// { text: '📍', action: () => this.clearSelection() },
|
|
|
{ text: 'x', action: () => this.clearSelection() },
|
|
|
];
|
|
|
|
|
|
buttons.forEach(btn => {
|
|
|
const button = document.createElement('button');
|
|
|
button.className = `div-selector-btn ${btn.class || ''}`;
|
|
|
button.textContent = btn.text;
|
|
|
button.onclick = btn.action;
|
|
|
this.actionPanel.appendChild(button);
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(this.actionPanel);
|
|
|
}
|
|
|
|
|
|
updateActionPanelPosition() {
|
|
|
if (!this.actionPanel || !this.selectedElement) return;
|
|
|
|
|
|
const rect = this.selectedElement.getBoundingClientRect();
|
|
|
const panelWidth = this.actionPanel.offsetWidth;
|
|
|
const panelHeight = this.actionPanel.offsetHeight;
|
|
|
|
|
|
let left = rect.right - panelWidth + 8;
|
|
|
let top = rect.top - 8;
|
|
|
|
|
|
// 确保不超出视窗
|
|
|
if (left + panelWidth > window.innerWidth) {
|
|
|
left = rect.left - panelWidth - 8;
|
|
|
}
|
|
|
if (left < 0) left = 8;
|
|
|
|
|
|
if (top + panelHeight > window.innerHeight) {
|
|
|
top = window.innerHeight - panelHeight - 8;
|
|
|
}
|
|
|
if (top < 0) top = 8;
|
|
|
|
|
|
this.actionPanel.style.left = left + 'px';
|
|
|
this.actionPanel.style.top = top + 'px';
|
|
|
}
|
|
|
|
|
|
clearSelection() {
|
|
|
this.selectedElement = null;
|
|
|
|
|
|
if (this.selectedOverlay) {
|
|
|
this.selectedOverlay.remove();
|
|
|
this.selectedOverlay = null;
|
|
|
}
|
|
|
|
|
|
if (this.selectedTooltip) {
|
|
|
this.selectedTooltip.remove();
|
|
|
this.selectedTooltip = null;
|
|
|
}
|
|
|
|
|
|
this.removeActionPanel();
|
|
|
|
|
|
console.log('Selection cleared');
|
|
|
}
|
|
|
|
|
|
getElementInfo(element) {
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
let vref = element.getAttribute('vref')
|
|
|
if (!vref) {
|
|
|
vref = element.closest('[vref]').getAttribute('vref')
|
|
|
vref += "." + element.tagName.toLowerCase();
|
|
|
}
|
|
|
return `${vref} (${Math.round(rect.width)}×${Math.round(rect.height)})`;
|
|
|
}
|
|
|
|
|
|
getFilePath(element) {
|
|
|
let vref = element.getAttribute('vref')
|
|
|
if (!vref) {
|
|
|
vref = element.closest('[vref]').getAttribute('vref')
|
|
|
}
|
|
|
if (vref) {
|
|
|
vref = '/ui' + vref + '.html'
|
|
|
}
|
|
|
return vref
|
|
|
}
|
|
|
|
|
|
copySelector(element) {
|
|
|
let vref = this.getFilePath(element)
|
|
|
copyToClipboard(vref).then(() => {
|
|
|
console.log('CSS Selector copied:', selector);
|
|
|
this.showNotification('📋 CSS选择器已复制到剪贴板');
|
|
|
});
|
|
|
}
|
|
|
|
|
|
copyXPath(element) {
|
|
|
const xpath = this.generateXPath(element);
|
|
|
copyToClipboard(xpath).then(() => {
|
|
|
console.log('XPath copied:', xpath);
|
|
|
this.showNotification('🔍 XPath已复制到剪贴板');
|
|
|
});
|
|
|
}
|
|
|
|
|
|
viewStyles(element) {
|
|
|
const styles = window.getComputedStyle(element);
|
|
|
const importantStyles = [
|
|
|
'display', 'position', 'width', 'height', 'margin', 'padding',
|
|
|
'background-color', 'color', 'font-size', 'border', 'z-index', 'opacity'
|
|
|
];
|
|
|
|
|
|
let styleInfo = '📊 重要样式属性:\n\n';
|
|
|
importantStyles.forEach(prop => {
|
|
|
styleInfo += `${prop}: ${styles.getPropertyValue(prop)}\n`;
|
|
|
});
|
|
|
|
|
|
alert(styleInfo);
|
|
|
}
|
|
|
|
|
|
|
|
|
showNotification(message) {
|
|
|
const notification = document.createElement('div');
|
|
|
notification.style.cssText = `
|
|
|
position: fixed;
|
|
|
top: 20px;
|
|
|
right: 20px;
|
|
|
background: linear-gradient(135deg, #28a745, #20c997);
|
|
|
color: white;
|
|
|
padding: 12px 18px;
|
|
|
border-radius: 8px;
|
|
|
z-index: ${this.options.zIndex + 10};
|
|
|
font-size: 14px;
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
|
transform: translateX(100%);
|
|
|
transition: transform 0.3s ease;
|
|
|
`;
|
|
|
notification.textContent = message;
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
// 动画显示
|
|
|
setTimeout(() => {
|
|
|
notification.style.transform = 'translateX(0)';
|
|
|
}, 100);
|
|
|
|
|
|
// 自动隐藏
|
|
|
setTimeout(() => {
|
|
|
notification.style.transform = 'translateX(100%)';
|
|
|
setTimeout(() => {
|
|
|
notification.remove();
|
|
|
}, 300);
|
|
|
}, 2500);
|
|
|
}
|
|
|
|
|
|
generateXPath(element) {
|
|
|
if (element.id) {
|
|
|
return `//*[@id="${element.id}"]`;
|
|
|
}
|
|
|
|
|
|
const path = [];
|
|
|
while (element && element.nodeType === Node.ELEMENT_NODE) {
|
|
|
let index = 0;
|
|
|
let hasFollowingSiblings = false;
|
|
|
let hasPrecedingSiblings = false;
|
|
|
|
|
|
for (let sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
|
|
|
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
|
|
|
hasPrecedingSiblings = true;
|
|
|
index++;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for (let sibling = element.nextSibling; sibling && !hasFollowingSiblings; sibling = sibling.nextSibling) {
|
|
|
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
|
|
|
hasFollowingSiblings = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const tagName = element.nodeName.toLowerCase();
|
|
|
const pathIndex = (hasPrecedingSiblings || hasFollowingSiblings) ? `[${index + 1}]` : '';
|
|
|
path.unshift(tagName + pathIndex);
|
|
|
element = element.parentNode;
|
|
|
if (element === document.body) break;
|
|
|
}
|
|
|
|
|
|
return path.length ? '/' + path.join('/') : null;
|
|
|
}
|
|
|
|
|
|
removeOverlay() {
|
|
|
if (this.overlay) {
|
|
|
this.overlay.remove();
|
|
|
this.overlay = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
removeTooltip() {
|
|
|
if (this.tooltip) {
|
|
|
this.tooltip.remove();
|
|
|
this.tooltip = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
removeActionPanel() {
|
|
|
if (this.actionPanel) {
|
|
|
this.actionPanel.remove();
|
|
|
this.actionPanel = null;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export default DivSelectorPlugin
|