diff --git a/src/Model/TemplateEngine/Decorator/InspectorHints.php b/src/Model/TemplateEngine/Decorator/InspectorHints.php index 0e511fa..c74199e 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHints.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHints.php @@ -71,7 +71,7 @@ public function render(BlockInterface $block, $templateFile, array $dictionary = } /** - * Inject data-mageforge-* attributes into HTML for inspector + * Inject MageForge inspector comment markers into HTML * * @param string $html * @param BlockInterface $block @@ -97,25 +97,30 @@ private function injectInspectorAttributes(string $html, BlockInterface $block, $blockAlias = $this->getBlockAlias($block); $isOverride = $this->isTemplateOverride($templateFile, $moduleName) ? '1' : '0'; - // Build data attributes - $dataAttributes = sprintf( - 'data-mageforge-template="%s" data-mageforge-block="%s" data-mageforge-module="%s" data-mageforge-id="%s" data-mageforge-viewmodel="%s" data-mageforge-parent="%s" data-mageforge-alias="%s" data-mageforge-override="%s"', - htmlspecialchars($relativeTemplatePath, ENT_QUOTES, 'UTF-8'), - htmlspecialchars($blockClass, ENT_QUOTES, 'UTF-8'), - htmlspecialchars($moduleName, ENT_QUOTES, 'UTF-8'), - htmlspecialchars($wrapperId, ENT_QUOTES, 'UTF-8'), - htmlspecialchars($viewModel, ENT_QUOTES, 'UTF-8'), - htmlspecialchars($parentBlock, ENT_QUOTES, 'UTF-8'), - htmlspecialchars($blockAlias, ENT_QUOTES, 'UTF-8'), - htmlspecialchars($isOverride, ENT_QUOTES, 'UTF-8') - ); - - // Wrap content with data attributes using display:contents to avoid layout issues + // Build metadata as JSON + $metadata = [ + 'id' => $wrapperId, + 'template' => $relativeTemplatePath, + 'block' => $blockClass, + 'module' => $moduleName, + 'viewModel' => $viewModel, + 'parent' => $parentBlock, + 'alias' => $blockAlias, + 'override' => $isOverride, + ]; + + // JSON encode with proper escaping for HTML comments + $jsonMetadata = json_encode($metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // Escape any comment terminators in JSON to prevent breaking out of comment + $jsonMetadata = str_replace('-->', '-->', $jsonMetadata); + + // Wrap content with comment markers $wrappedHtml = sprintf( - '
%s
', - htmlspecialchars($wrapperId, ENT_QUOTES, 'UTF-8'), - $dataAttributes, - $html + "\n%s\n", + $jsonMetadata, + $html, + $wrapperId ); return $wrappedHtml; diff --git a/src/view/frontend/web/js/inspector.js b/src/view/frontend/web/js/inspector.js index 153ae95..60ea1be 100644 --- a/src/view/frontend/web/js/inspector.js +++ b/src/view/frontend/web/js/inspector.js @@ -32,6 +32,10 @@ document.addEventListener('alpine:init', () => { this.mouseMoveHandler = (e) => this.handleMouseMove(e); this.clickHandler = (e) => this.handleClick(e); + // Cache for block detection + this.cachedBlocks = null; + this.lastBlocksCacheTime = 0; + this.setupKeyboardShortcuts(); this.createHighlightBox(); this.createInfoBadge(); @@ -41,6 +45,138 @@ document.addEventListener('alpine:init', () => { this.$dispatch('mageforge:inspector:init'); }, + /** + * Parse MageForge comment markers in DOM + */ + parseCommentMarker(comment) { + const text = comment.textContent.trim(); + + // Check if it's a start marker + if (text.startsWith('MAGEFORGE_START ')) { + const jsonStr = text.substring('MAGEFORGE_START '.length); + try { + // Unescape any escaped comment terminators + const unescapedJson = jsonStr.replace(/-->/g, '-->'); + return { + type: 'start', + data: JSON.parse(unescapedJson) + }; + } catch (e) { + console.error('Failed to parse MageForge start marker:', e); + return null; + } + } + + // Check if it's an end marker + if (text.startsWith('MAGEFORGE_END ')) { + const id = text.substring('MAGEFORGE_END '.length).trim(); + return { + type: 'end', + id: id + }; + } + + return null; + }, + + /** + * Find all MageForge block regions in DOM + */ + findAllMageForgeBlocks() { + const blocks = []; + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_COMMENT, + null + ); + + const stack = []; + let comment; + + while ((comment = walker.nextNode())) { + const parsed = this.parseCommentMarker(comment); + + if (!parsed) continue; + + if (parsed.type === 'start') { + stack.push({ + startComment: comment, + data: parsed.data, + elements: [] + }); + } else if (parsed.type === 'end' && stack.length > 0) { + const currentBlock = stack[stack.length - 1]; + if (currentBlock.data.id === parsed.id) { + currentBlock.endComment = comment; + + // Collect all elements between start and end comments + currentBlock.elements = this.getElementsBetweenComments( + currentBlock.startComment, + currentBlock.endComment + ); + + blocks.push(currentBlock); + stack.pop(); + } + } + } + + return blocks; + }, + + /** + * Get all elements between two comment nodes + */ + getElementsBetweenComments(startComment, endComment) { + const elements = []; + let node = startComment.nextSibling; + + while (node && node !== endComment) { + if (node.nodeType === Node.ELEMENT_NODE) { + elements.push(node); + // Also add all descendants + elements.push(...node.querySelectorAll('*')); + } + node = node.nextSibling; + } + + return elements; + }, + + /** + * Find MageForge block data for a given element + */ + findBlockForElement(element) { + // Cache blocks for performance + if (!this.cachedBlocks || Date.now() - this.lastBlocksCacheTime > 1000) { + this.cachedBlocks = this.findAllMageForgeBlocks(); + this.lastBlocksCacheTime = Date.now(); + } + + let closestBlock = null; + let closestDepth = -1; + + // Find the deepest (most specific) block containing this element + for (const block of this.cachedBlocks) { + if (block.elements.includes(element)) { + // Calculate depth (how many ancestors between element and body) + let depth = 0; + let node = element; + while (node && node !== document.body) { + depth++; + node = node.parentElement; + } + + if (depth > closestDepth) { + closestBlock = block; + closestDepth = depth; + } + } + } + + return closestBlock; + }, + /** * Setup keyboard shortcuts */ @@ -279,19 +415,19 @@ document.addEventListener('alpine:init', () => { } document.removeEventListener('mousemove', this.mouseMoveHandler); - + // Keep click handler active if pinned (for click-outside detection) if (!this.isPinned) { document.removeEventListener('click', this.clickHandler, false); } - + document.body.style.cursor = ''; - + // Only hide if not pinned if (!this.isPinned) { this.hideHighlight(); } - + this.hoveredElement = null; this.lastBadgeUpdate = 0; }, @@ -399,20 +535,25 @@ document.addEventListener('alpine:init', () => { /** * Unpin and close the badge - // Remove click handler now that we're unpinned - document.removeEventListener('click', this.clickHandler, false); */ unpinBadge() { this.isPinned = false; this.hideHighlight(); this.selectedElement = null; + + // Remove click handler + document.removeEventListener('click', this.clickHandler, false); + + // Reactivate picker if inspector is still open + if (this.isOpen) { + this.activatePicker(); + } }, /** * Find nearest inspectable element */ findInspectableElement(target) { - // Return the target element itself, or filter out inspector elements if (!target) return null; // Skip inspector's own elements @@ -425,27 +566,19 @@ document.addEventListener('alpine:init', () => { return null; } - return target; - }, - - /** - * Find parent element with Magento template data - */ - findParentWithTemplateData(element) { - let parent = element.parentElement; - let maxDepth = 10; - - while (parent && maxDepth > 0) { - if (parent.hasAttribute && parent.hasAttribute('data-mageforge-template')) { - return parent; - } - parent = parent.parentElement; - maxDepth--; + // Check if this element is part of a MageForge block + const block = this.findBlockForElement(target); + if (block) { + // Attach block data to element for easy access + target._mageforgeBlockData = block.data; + return target; } return null; }, + + /** * Show highlight overlay on element */ @@ -517,16 +650,22 @@ document.addEventListener('alpine:init', () => { * Build badge content with element metadata */ buildBadgeContent(element) { - const data = { - template: element.getAttribute('data-mageforge-template') || '', - blockClass: element.getAttribute('data-mageforge-block') || '', - module: element.getAttribute('data-mageforge-module') || '', - viewModel: element.getAttribute('data-mageforge-viewmodel') || '', - parentBlock: element.getAttribute('data-mageforge-parent') || '', - blockAlias: element.getAttribute('data-mageforge-alias') || '', - isOverride: element.getAttribute('data-mageforge-override') === '1' + const data = element._mageforgeBlockData || { + template: '', + block: '', + module: '', + viewModel: '', + parent: '', + alias: '', + override: '0' }; + // Convert override string to boolean and add aliases for compatibility + data.isOverride = data.override === '1'; + data.blockClass = data.block; + data.parentBlock = data.parent; + data.blockAlias = data.alias; + // Clear badge this.infoBadge.innerHTML = ''; @@ -724,29 +863,44 @@ document.addEventListener('alpine:init', () => { * Render structure tab when element has no direct template data */ renderStructureWithParentData(container, element) { - const parentWithData = this.findParentWithTemplateData(element); + // Try to find parent element with block data + let parent = element.parentElement; + let parentBlock = null; + let maxDepth = 10; - if (parentWithData) { - this.renderInheritedStructure(container, element, parentWithData); - } else { - this.renderNoTemplateData(container, element); + while (parent && maxDepth > 0) { + parentBlock = this.findBlockForElement(parent); + if (parentBlock) { + this.renderInheritedStructure(container, element, parentBlock); + return; + } + parent = parent.parentElement; + maxDepth--; } + + this.renderNoTemplateData(container, element); }, /** * Render inherited structure from parent element */ - renderInheritedStructure(container, element, parentWithData) { - const parentData = { - template: parentWithData.getAttribute('data-mageforge-template') || '', - blockClass: parentWithData.getAttribute('data-mageforge-block') || '', - module: parentWithData.getAttribute('data-mageforge-module') || '', - viewModel: parentWithData.getAttribute('data-mageforge-viewmodel') || '', - parentBlock: parentWithData.getAttribute('data-mageforge-parent') || '', - blockAlias: parentWithData.getAttribute('data-mageforge-alias') || '', - isOverride: parentWithData.getAttribute('data-mageforge-override') === '1' + renderInheritedStructure(container, element, parentBlock) { + const parentData = parentBlock.data || { + template: '', + block: '', + module: '', + viewModel: '', + parent: '', + alias: '', + override: '0' }; + // Convert to expected format + parentData.blockClass = parentData.block; + parentData.parentBlock = parentData.parent; + parentData.blockAlias = parentData.alias; + parentData.isOverride = parentData.override === '1'; + // Inheritance note const inheritanceNote = document.createElement('div'); inheritanceNote.style.cssText = ` @@ -1347,9 +1501,18 @@ document.addEventListener('alpine:init', () => { * Update panel with element data */ updatePanelData(element) { - this.panelData.template = element.getAttribute('data-mageforge-template') || 'N/A'; - this.panelData.block = element.getAttribute('data-mageforge-block') || 'N/A'; - this.panelData.module = element.getAttribute('data-mageforge-module') || 'N/A'; + const data = element._mageforgeBlockData; + + if (!data) { + this.panelData.template = 'N/A'; + this.panelData.block = 'N/A'; + this.panelData.module = 'N/A'; + return; + } + + this.panelData.template = data.template || 'N/A'; + this.panelData.block = data.block || 'N/A'; + this.panelData.module = data.module || 'N/A'; }, })); });