312 lines
11 KiB
JavaScript
312 lines
11 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
var SOURCE = 'kandev-inspector';
|
|
var DRAG_THRESHOLD = 5;
|
|
var Z = 2147483640;
|
|
|
|
var active = false;
|
|
var mouseDownAt = null;
|
|
var dragRect = null;
|
|
var hoverEl = null;
|
|
var counter = 0;
|
|
|
|
var hoverOverlay = null;
|
|
var dragOverlay = null;
|
|
var popup = null;
|
|
var pendingAnnotation = null;
|
|
|
|
function send(type, payload) {
|
|
try {
|
|
window.parent.postMessage({ source: SOURCE, type: type, payload: payload }, '*');
|
|
} catch (e) {}
|
|
}
|
|
|
|
function getSelector(el) {
|
|
var parts = [];
|
|
var cur = el;
|
|
while (cur && cur.nodeType === 1 && parts.length < 5) {
|
|
var tag = cur.tagName.toLowerCase();
|
|
var id = cur.getAttribute && cur.getAttribute('id');
|
|
if (id) { parts.unshift(tag + '#' + CSS.escape(id)); break; }
|
|
var cls = '';
|
|
if (cur.classList && cur.classList.length) {
|
|
cls = Array.from(cur.classList).slice(0, 2)
|
|
.map(function (c) { return '.' + CSS.escape(c); }).join('');
|
|
}
|
|
var part = tag + cls;
|
|
var parent = cur.parentElement;
|
|
if (parent) {
|
|
var sibs = Array.from(parent.children).filter(function (c) { return c.tagName === cur.tagName; });
|
|
if (sibs.length > 1) part += ':nth-of-type(' + (sibs.indexOf(cur) + 1) + ')';
|
|
}
|
|
parts.unshift(part);
|
|
cur = parent;
|
|
}
|
|
return parts.join(' > ');
|
|
}
|
|
|
|
function captureElement(el) {
|
|
if (!el || el.nodeType !== 1) return null;
|
|
var cls = '';
|
|
if (el.className && typeof el.className === 'string') {
|
|
cls = el.className.trim().split(/\s+/).slice(0, 3).join(' ') || '';
|
|
}
|
|
return {
|
|
tag: el.tagName.toLowerCase(),
|
|
id: el.id || undefined,
|
|
classes: cls || undefined,
|
|
role: el.getAttribute('role') || undefined,
|
|
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
text: ((el.textContent || '').replace(/\s+/g, ' ').trim()).slice(0, 120) || undefined,
|
|
selector: getSelector(el),
|
|
};
|
|
}
|
|
|
|
function elementsInRect(rect) {
|
|
var out = [];
|
|
var all = document.querySelectorAll('body *');
|
|
for (var i = 0; i < all.length && out.length < 5; i++) {
|
|
var el = all[i];
|
|
if (el === hoverOverlay || el === dragOverlay || el === popup) continue;
|
|
if (popup && popup.contains(el)) continue;
|
|
var r = el.getBoundingClientRect();
|
|
if (r.width < 4 || r.height < 4) continue;
|
|
if (r.left >= rect.x && r.top >= rect.y
|
|
&& r.right <= rect.x + rect.w && r.bottom <= rect.y + rect.h) {
|
|
out.push(captureElement(el));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function makeOverlay(borderColor, bg) {
|
|
var d = document.createElement('div');
|
|
d.style.cssText = 'position:fixed;pointer-events:none;box-sizing:border-box;display:none;z-index:' + Z + ';'
|
|
+ 'border:2px solid ' + borderColor + ';background:' + bg + ';';
|
|
document.body.appendChild(d);
|
|
return d;
|
|
}
|
|
|
|
function ensureOverlays() {
|
|
if (!hoverOverlay) hoverOverlay = makeOverlay('#6366f1', 'rgba(99,102,241,0.10)');
|
|
if (!dragOverlay) dragOverlay = makeOverlay('#f59e0b', 'rgba(245,158,11,0.15)');
|
|
}
|
|
|
|
function positionOverlay(o, x, y, w, h) {
|
|
o.style.display = 'block';
|
|
o.style.left = x + 'px';
|
|
o.style.top = y + 'px';
|
|
o.style.width = w + 'px';
|
|
o.style.height = h + 'px';
|
|
}
|
|
|
|
function hideOverlay(o) { if (o) o.style.display = 'none'; }
|
|
|
|
function placeMarker(num, x, y) {
|
|
var m = document.createElement('div');
|
|
m.setAttribute('data-kandev-marker', String(num));
|
|
m.style.cssText = 'position:fixed;left:' + (x - 12) + 'px;top:' + (y - 12) + 'px;width:24px;height:24px;'
|
|
+ 'border-radius:50%;background:#6366f1;color:#fff;font:600 12px/24px system-ui,sans-serif;'
|
|
+ 'text-align:center;z-index:' + Z + ';pointer-events:none;box-shadow:0 1px 4px rgba(0,0,0,0.3);';
|
|
m.textContent = String(num);
|
|
document.body.appendChild(m);
|
|
}
|
|
|
|
function clearMarkers() {
|
|
var nodes = document.querySelectorAll('[data-kandev-marker]');
|
|
for (var i = 0; i < nodes.length; i++) nodes[i].parentNode.removeChild(nodes[i]);
|
|
counter = 0;
|
|
}
|
|
|
|
function closePopup() {
|
|
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
popup = null;
|
|
pendingAnnotation = null;
|
|
}
|
|
|
|
function currentPagePath() {
|
|
var path = location.pathname;
|
|
var prefix = window.__kandevProxyPrefix;
|
|
if (typeof prefix === 'string' && prefix) {
|
|
if (path === prefix || path.indexOf(prefix + '/') === 0) {
|
|
path = path.slice(prefix.length) || '/';
|
|
}
|
|
}
|
|
return path + location.search;
|
|
}
|
|
|
|
function openCommentPopup(anchorX, anchorY, onSubmit) {
|
|
var keep = pendingAnnotation;
|
|
closePopup();
|
|
pendingAnnotation = keep;
|
|
popup = document.createElement('div');
|
|
popup.style.cssText = 'position:fixed;left:' + anchorX + 'px;top:' + anchorY + 'px;z-index:' + (Z + 1) + ';'
|
|
+ 'background:#fff;border:1px solid #e5e7eb;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.15);'
|
|
+ 'padding:8px;width:280px;font-family:system-ui,sans-serif;';
|
|
var ta = document.createElement('textarea');
|
|
ta.placeholder = 'Describe the change (Enter to save, Esc to cancel)';
|
|
ta.style.cssText = 'width:100%;min-height:60px;border:1px solid #e5e7eb;border-radius:4px;padding:6px;'
|
|
+ 'font:13px system-ui,sans-serif;resize:vertical;outline:none;box-sizing:border-box;';
|
|
var row = document.createElement('div');
|
|
row.style.cssText = 'display:flex;gap:6px;justify-content:flex-end;margin-top:6px;';
|
|
var cancel = document.createElement('button');
|
|
cancel.textContent = 'Cancel';
|
|
cancel.style.cssText = 'padding:4px 10px;border:1px solid #e5e7eb;background:#fff;border-radius:4px;cursor:pointer;font:12px system-ui,sans-serif;';
|
|
var save = document.createElement('button');
|
|
save.textContent = 'Save';
|
|
save.style.cssText = 'padding:4px 10px;border:0;background:#6366f1;color:#fff;border-radius:4px;cursor:pointer;font:12px system-ui,sans-serif;';
|
|
|
|
cancel.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); closePopup(); });
|
|
save.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); onSubmit(ta.value.trim()); });
|
|
ta.addEventListener('keydown', function (e) {
|
|
e.stopPropagation();
|
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSubmit(ta.value.trim()); }
|
|
if (e.key === 'Escape') { e.preventDefault(); closePopup(); }
|
|
});
|
|
|
|
popup.appendChild(ta);
|
|
row.appendChild(cancel); row.appendChild(save);
|
|
popup.appendChild(row);
|
|
document.body.appendChild(popup);
|
|
ta.focus();
|
|
}
|
|
|
|
function commitAnnotation(comment) {
|
|
if (!pendingAnnotation) return;
|
|
counter += 1;
|
|
var a = pendingAnnotation;
|
|
a.id = 'a-' + Date.now() + '-' + counter;
|
|
a.comment = comment || '';
|
|
placeMarker(counter, a.markerX, a.markerY);
|
|
// Note: we do NOT set `a.number` here. The React parent assigns numbers
|
|
// so they remain monotonic across iframe refreshes.
|
|
send('annotation-added', a);
|
|
pendingAnnotation = null;
|
|
closePopup();
|
|
}
|
|
|
|
function startPending(kind, rect, element, elements, markerX, markerY) {
|
|
pendingAnnotation = {
|
|
kind: kind,
|
|
rect: rect,
|
|
element: element || null,
|
|
elements: elements || null,
|
|
markerX: markerX,
|
|
markerY: markerY,
|
|
pagePath: currentPagePath(),
|
|
};
|
|
var popupX = Math.min(markerX + 14, window.innerWidth - 296);
|
|
var popupY = Math.min(markerY + 14, window.innerHeight - 140);
|
|
openCommentPopup(popupX, popupY, commitAnnotation);
|
|
}
|
|
|
|
function onMouseDown(e) {
|
|
if (popup && popup.contains(e.target)) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
mouseDownAt = { x: e.clientX, y: e.clientY };
|
|
dragRect = null;
|
|
}
|
|
|
|
function onMouseMove(e) {
|
|
if (popup && popup.contains(e.target)) return;
|
|
if (mouseDownAt) {
|
|
var dx = e.clientX - mouseDownAt.x;
|
|
var dy = e.clientY - mouseDownAt.y;
|
|
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
|
|
dragRect = {
|
|
x: Math.min(mouseDownAt.x, e.clientX),
|
|
y: Math.min(mouseDownAt.y, e.clientY),
|
|
w: Math.abs(dx),
|
|
h: Math.abs(dy),
|
|
};
|
|
hideOverlay(hoverOverlay);
|
|
positionOverlay(dragOverlay, dragRect.x, dragRect.y, dragRect.w, dragRect.h);
|
|
}
|
|
return;
|
|
}
|
|
var el = e.target;
|
|
if (el === hoverOverlay || el === dragOverlay || (popup && popup.contains(el))) return;
|
|
hoverEl = el;
|
|
var r = el.getBoundingClientRect();
|
|
positionOverlay(hoverOverlay, r.left, r.top, r.width, r.height);
|
|
}
|
|
|
|
function onMouseUp(e) {
|
|
if (popup && popup.contains(e.target)) return;
|
|
var wasDragging = !!dragRect;
|
|
var rect = dragRect;
|
|
var down = mouseDownAt;
|
|
mouseDownAt = null;
|
|
dragRect = null;
|
|
hideOverlay(dragOverlay);
|
|
|
|
if (wasDragging && rect && rect.w > DRAG_THRESHOLD && rect.h > DRAG_THRESHOLD) {
|
|
var inside = elementsInRect(rect);
|
|
startPending('area', rect, null, inside, rect.x + rect.w, rect.y);
|
|
return;
|
|
}
|
|
if (down) {
|
|
var clickEl = document.elementFromPoint(down.x, down.y);
|
|
if (!clickEl || clickEl === hoverOverlay || clickEl === dragOverlay) return;
|
|
if (popup && popup.contains(clickEl)) return;
|
|
var el = captureElement(clickEl);
|
|
startPending('pin', null, el, null, down.x, down.y);
|
|
}
|
|
}
|
|
|
|
function onClickCapture(e) {
|
|
if (popup && popup.contains(e.target)) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
}
|
|
|
|
function onKeyDown(e) {
|
|
if (popup) return;
|
|
if (e.key === 'Escape') { e.preventDefault(); setActive(false); send('inspect-exited', {}); }
|
|
}
|
|
|
|
function setActive(isOn) {
|
|
if (isOn === active) return;
|
|
active = isOn;
|
|
if (isOn) {
|
|
ensureOverlays();
|
|
document.body.style.cursor = 'crosshair';
|
|
document.addEventListener('mousedown', onMouseDown, true);
|
|
document.addEventListener('mousemove', onMouseMove, true);
|
|
document.addEventListener('mouseup', onMouseUp, true);
|
|
document.addEventListener('click', onClickCapture, true);
|
|
document.addEventListener('keydown', onKeyDown, true);
|
|
} else {
|
|
document.removeEventListener('mousedown', onMouseDown, true);
|
|
document.removeEventListener('mousemove', onMouseMove, true);
|
|
document.removeEventListener('mouseup', onMouseUp, true);
|
|
document.removeEventListener('click', onClickCapture, true);
|
|
document.removeEventListener('keydown', onKeyDown, true);
|
|
document.body.style.cursor = '';
|
|
hideOverlay(hoverOverlay);
|
|
hideOverlay(dragOverlay);
|
|
closePopup();
|
|
}
|
|
}
|
|
|
|
function removeMarker(num) {
|
|
if (num == null) return;
|
|
var nodes = document.querySelectorAll('[data-kandev-marker="' + String(num) + '"]');
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
if (nodes[i].parentNode) nodes[i].parentNode.removeChild(nodes[i]);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('message', function (e) {
|
|
if (!e.data || e.data.source !== SOURCE) return;
|
|
if (e.data.type === 'toggle-inspect') setActive(!!(e.data.payload && e.data.payload.active));
|
|
if (e.data.type === 'clear-annotations') clearMarkers();
|
|
if (e.data.type === 'remove-marker') removeMarker(e.data.payload && e.data.payload.number);
|
|
});
|
|
|
|
send('inspector-ready', {});
|
|
})();
|