Clean deploy: nueva versión
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
export const HOVER_OUTLINE_CLASS = 'inline-editor-hover-outline';
|
||||
export const LOCKED_OUTLINE_CLASS = 'inline-editor-locked-outline';
|
||||
|
||||
/** @param {HTMLElement|HTMLElement[]|null} value @returns {HTMLElement[]} */
|
||||
function toArray(value) {
|
||||
if (!value) return [];
|
||||
return Array.isArray(value) ? value.filter(Boolean) : [value];
|
||||
}
|
||||
|
||||
/** @param {HTMLElement[]} elements @param {string} className */
|
||||
function addClassTo(elements, className) {
|
||||
for (const element of elements) element.classList.add(className);
|
||||
}
|
||||
|
||||
/** @param {HTMLElement[]} elements @param {string} className */
|
||||
function removeClassFrom(elements, className) {
|
||||
for (const element of elements) element.classList.remove(className);
|
||||
}
|
||||
|
||||
/** @type {HTMLElement[]} */
|
||||
let _hoveredElements = [];
|
||||
/** @type {HTMLElement[]} */
|
||||
let _lockedElements = [];
|
||||
let _holdCount = 0;
|
||||
|
||||
/**
|
||||
* Dashed outline for the transient hover. Elements already carrying a locked
|
||||
* outline are skipped so they aren't outlined twice.
|
||||
* @param {HTMLElement|HTMLElement[]} elements
|
||||
*/
|
||||
export function showHoverOutline(elements) {
|
||||
const targets = toArray(elements).filter(element => !_lockedElements.includes(element));
|
||||
if (!targets.length) return;
|
||||
removeClassFrom(_hoveredElements, HOVER_OUTLINE_CLASS);
|
||||
_hoveredElements = targets;
|
||||
addClassTo(_hoveredElements, HOVER_OUTLINE_CLASS);
|
||||
}
|
||||
|
||||
/** Hides the hover outline (no-op while held). */
|
||||
export function hideHoverOutline() {
|
||||
if (_holdCount > 0) return;
|
||||
removeClassFrom(_hoveredElements, HOVER_OUTLINE_CLASS);
|
||||
_hoveredElements = [];
|
||||
}
|
||||
|
||||
export function holdHoverOutline() { _holdCount++; }
|
||||
export function releaseHoverOutline() {
|
||||
_holdCount = Math.max(0, _holdCount - 1);
|
||||
if (_holdCount === 0) hideHoverOutline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Solid outline for a committed selection — a drag preview, a shift+click
|
||||
* group, or the element(s) an open annotation panel targets.
|
||||
* @param {HTMLElement|HTMLElement[]} elements
|
||||
*/
|
||||
export function lockHoverOutline(elements) {
|
||||
const targets = toArray(elements);
|
||||
if (!targets.length) {
|
||||
unlockHoverOutline();
|
||||
return;
|
||||
}
|
||||
removeClassFrom(_lockedElements, LOCKED_OUTLINE_CLASS);
|
||||
_lockedElements = targets;
|
||||
addClassTo(_lockedElements, LOCKED_OUTLINE_CLASS);
|
||||
}
|
||||
|
||||
export function unlockHoverOutline() {
|
||||
removeClassFrom(_lockedElements, LOCKED_OUTLINE_CLASS);
|
||||
_lockedElements = [];
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { SELECTION_RECT_ID, isInsideEditorUi, getEditId } from '../../constants/selectors.js';
|
||||
import { EDIT_BACKGROUND_COLOR, Z_INDEX_EDITOR_OVERLAY, EDIT_BORDER_COLOR } from '../../constants/theme.js';
|
||||
import { MIN_SELECTION_CANDIDATE_SIZE, MAX_SELECTED_ELEMENTS } from '../../constants/layout.js';
|
||||
import { getAppRoot, getOpenModals } from '../../utils/dom-utils.js';
|
||||
import { orderSelection } from '../../state/multi-select-state.js';
|
||||
|
||||
/** Set on `<body>` for the duration of a selection gesture, to keep the browser from painting text over it. */
|
||||
export const NO_TEXT_SELECT_CLASS = 'selection-mode-no-text-select';
|
||||
|
||||
const SELECTION_RECT_STYLES = `
|
||||
#${SELECTION_RECT_ID} {
|
||||
position: fixed;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
background: ${EDIT_BACKGROUND_COLOR};
|
||||
border: 1px solid ${EDIT_BORDER_COLOR};
|
||||
z-index: ${Z_INDEX_EDITOR_OVERLAY};
|
||||
}
|
||||
|
||||
body.${NO_TEXT_SELECT_CLASS}, body.${NO_TEXT_SELECT_CLASS} * {
|
||||
user-select: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
let _rectElement = null;
|
||||
let _stylesInjected = false;
|
||||
|
||||
function ensureStyles() {
|
||||
if (_stylesInjected) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'selection-rect-styles';
|
||||
style.textContent = SELECTION_RECT_STYLES;
|
||||
document.head.appendChild(style);
|
||||
_stylesInjected = true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Geometry
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Normalises two drag corners into a viewport-space rect.
|
||||
* @returns {{x: number, y: number, w: number, h: number}}
|
||||
*/
|
||||
export function getSelectionRect(x1, y1, x2, y2) {
|
||||
return { x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x2 - x1), h: Math.abs(y2 - y1) };
|
||||
}
|
||||
|
||||
function intersects(bounds, rect) {
|
||||
return bounds.right > rect.x && bounds.left < rect.x + rect.w
|
||||
&& bounds.bottom > rect.y && bounds.top < rect.y + rect.h;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Candidate resolution
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* True when the element is a plausible annotation target: visible, big enough to
|
||||
* aim at, and not part of the editor's own UI.
|
||||
* @param {HTMLElement} element
|
||||
* @param {DOMRect} bounds
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isSelectableCandidate(element, bounds) {
|
||||
if (bounds.width < MIN_SELECTION_CANDIDATE_SIZE || bounds.height < MIN_SELECTION_CANDIDATE_SIZE) return false;
|
||||
if (isInsideEditorUi(element)) return false;
|
||||
|
||||
const styles = window.getComputedStyle(element);
|
||||
return styles.display !== 'none' && styles.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
/** Children the band also touches; an empty result means this element is as deep as it goes. */
|
||||
function intersectingChildren(element, rect) {
|
||||
return Array.from(element.children).filter((child) => {
|
||||
const bounds = child.getBoundingClientRect();
|
||||
return isSelectableCandidate(child, bounds) && intersects(bounds, rect);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything the band touches, taking the deepest element on each branch so a
|
||||
* container never comes along with the children that made it a hit.
|
||||
* @param {{x: number, y: number, w: number, h: number}} rect
|
||||
* @returns {HTMLElement[]} Document-ordered, with no member nested in another.
|
||||
*/
|
||||
export function collectSelection(rect) {
|
||||
const selected = [];
|
||||
const openModals = getOpenModals();
|
||||
const roots = openModals.length ? openModals : [getAppRoot()];
|
||||
const queue = roots.flatMap((root) => [...root.children]);
|
||||
|
||||
while (queue.length && selected.length < MAX_SELECTED_ELEMENTS) {
|
||||
const element = queue.shift();
|
||||
const bounds = element.getBoundingClientRect();
|
||||
// No overlap prunes the whole subtree: children sit inside their parent's box.
|
||||
if (!isSelectableCandidate(element, bounds) || !intersects(bounds, rect)) continue;
|
||||
|
||||
// An edit id marks the editor's own annotation target, so stop there rather
|
||||
// than splitting a button or heading into the spans it is built from.
|
||||
const children = getEditId(element) ? [] : intersectingChildren(element, rect);
|
||||
if (children.length) {
|
||||
queue.push(...children);
|
||||
} else {
|
||||
selected.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return orderSelection(selected);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Band rendering
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
export function showSelectionRect(x1, y1, x2, y2) {
|
||||
if (!_rectElement) {
|
||||
ensureStyles();
|
||||
_rectElement = document.createElement('div');
|
||||
_rectElement.id = SELECTION_RECT_ID;
|
||||
document.body.appendChild(_rectElement);
|
||||
}
|
||||
if (!_rectElement.isConnected) document.body.appendChild(_rectElement);
|
||||
|
||||
const rect = getSelectionRect(x1, y1, x2, y2);
|
||||
_rectElement.style.left = `${rect.x}px`;
|
||||
_rectElement.style.top = `${rect.y}px`;
|
||||
_rectElement.style.width = `${rect.w}px`;
|
||||
_rectElement.style.height = `${rect.h}px`;
|
||||
_rectElement.style.display = 'block';
|
||||
}
|
||||
|
||||
export function hideSelectionRect() {
|
||||
if (_rectElement) _rectElement.style.display = 'none';
|
||||
}
|
||||
|
||||
export function destroySelectionRect() {
|
||||
_rectElement?.remove();
|
||||
_rectElement = null;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { getElementType } from '../../utils/dom-utils.js';
|
||||
import { TYPE_TOOLTIP_CURSOR_OFFSET, TOOLTIP_VIEWPORT_MARGIN } from '../../constants/layout.js';
|
||||
import { ELEMENT_TYPE_TRANSLATIONS } from '../../constants/selectors.js';
|
||||
import { showHoverOutline, hideHoverOutline } from './hover-outline.js';
|
||||
import { highlightMarkerForElement, unhighlightAllMarkers } from '../annotation-panel/annotation-markers.js';
|
||||
import { getEditorTranslations } from '../../state/annotation-state.js';
|
||||
|
||||
const ELEMENT_TYPE_TOOLTIP_CLASS = 'element-type-tooltip';
|
||||
|
||||
let typeTooltipElement = null;
|
||||
let displayedType = null;
|
||||
let cachedWidth = 0;
|
||||
let cachedHeight = 0;
|
||||
|
||||
function createTypeTooltip() {
|
||||
if (typeTooltipElement) return;
|
||||
typeTooltipElement = document.createElement('div');
|
||||
typeTooltipElement.className = ELEMENT_TYPE_TOOLTIP_CLASS;
|
||||
document.body.appendChild(typeTooltipElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the tooltip's top-left corner near `(clientX, clientY)`, clamped
|
||||
* per axis so it never crosses within `TOOLTIP_VIEWPORT_MARGIN` of the
|
||||
* viewport edge. The cursor is free to keep moving over the parked tooltip —
|
||||
* it is `pointer-events: none`, so hit-testing resolves straight through it.
|
||||
* @param {number} clientX
|
||||
* @param {number} clientY
|
||||
*/
|
||||
function positionAt(clientX, clientY) {
|
||||
const maxLeft = Math.max(TOOLTIP_VIEWPORT_MARGIN, window.innerWidth - cachedWidth - TOOLTIP_VIEWPORT_MARGIN);
|
||||
const maxTop = Math.max(TOOLTIP_VIEWPORT_MARGIN, window.innerHeight - cachedHeight - TOOLTIP_VIEWPORT_MARGIN);
|
||||
|
||||
const left = Math.min(Math.max(clientX + TYPE_TOOLTIP_CURSOR_OFFSET, TOOLTIP_VIEWPORT_MARGIN), maxLeft);
|
||||
const top = Math.min(Math.max(clientY + TYPE_TOOLTIP_CURSOR_OFFSET, TOOLTIP_VIEWPORT_MARGIN), maxTop);
|
||||
|
||||
typeTooltipElement.style.transform = `translate3d(${left}px, ${top}px, 0)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the type tooltip near the cursor, labelling `element`'s logical type.
|
||||
* @param {HTMLElement} element
|
||||
* @param {number} clientX
|
||||
* @param {number} clientY
|
||||
*/
|
||||
export function showTypeTooltip(element, clientX, clientY) {
|
||||
const type = getElementType(element);
|
||||
if (!typeTooltipElement) createTypeTooltip();
|
||||
if (!typeTooltipElement.isConnected) document.body.appendChild(typeTooltipElement);
|
||||
|
||||
if (type !== displayedType) {
|
||||
displayedType = type;
|
||||
const translations = getEditorTranslations();
|
||||
const translationKey = ELEMENT_TYPE_TRANSLATIONS[type]?.key;
|
||||
typeTooltipElement.textContent = (translationKey && translations[translationKey]) || type;
|
||||
cachedWidth = typeTooltipElement.offsetWidth;
|
||||
cachedHeight = typeTooltipElement.offsetHeight;
|
||||
}
|
||||
|
||||
typeTooltipElement.classList.add('active');
|
||||
positionAt(clientX, clientY);
|
||||
showHoverOutline(element);
|
||||
unhighlightAllMarkers();
|
||||
highlightMarkerForElement(element);
|
||||
}
|
||||
|
||||
/** Hides the type tooltip and its paired hover outline. */
|
||||
export function hideTypeTooltip() {
|
||||
typeTooltipElement?.classList.remove('active');
|
||||
displayedType = null;
|
||||
hideHoverOutline();
|
||||
unhighlightAllMarkers();
|
||||
}
|
||||
Reference in New Issue
Block a user