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';
},
}));
});