diff --git a/src/TwigHooks/assets/admin/entrypoint.js b/src/TwigHooks/assets/admin/entrypoint.js new file mode 100644 index 00000000..5de4a6d1 --- /dev/null +++ b/src/TwigHooks/assets/admin/entrypoint.js @@ -0,0 +1,222 @@ +import '../../../../assets/admin/entrypoint'; +import { createPopper } from '@popperjs/core'; + +// Globalny przełącznik mechanizmu tooltipów +let tooltipEnabled = false; + +// Nasłuchujemy skrótu klawiszowego (Ctrl/Cmd+Shift+K) do włączania/wyłączania tooltipów +document.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'k') { + tooltipEnabled = !tooltipEnabled; + console.log(`Tooltips are now ${tooltipEnabled ? 'enabled' : 'disabled'}`); + // Jeśli wyłączamy tooltipy, usuwamy wszystkie widoczne tooltipy... + if (!tooltipEnabled) { + document.querySelectorAll('.custom-tooltip').forEach(t => t.remove()); + // ...oraz usuwamy obramowania ze wszystkich elementów [data-hook] + document.querySelectorAll('[data-hook]').forEach(el => { + el.style.boxShadow = ''; + }); + } else { + // Gdy tooltipy zostały włączone, sprawdzamy elementy, które są już hoverowane + document.querySelectorAll('[data-hook]').forEach(el => { + if (el.matches(':hover')) { + // Wywołujemy syntetyczne zdarzenie, aby natychmiast pokazać tooltip + el.dispatchEvent(new Event('mouseenter')); + } + }); + } + } +}); + +// Numer do przydzielenia unikalnego ID tooltipowi +let nextTooltipId = 0; +// Globalna tablica przechowująca pozycje już wyrenderowanych tooltipów +const renderedTooltips = []; + +/** + * Generuje losowy kolor RGB w przedziale 50-205 dla każdego kanału. + */ +function generateRGBColor() { + const r = Math.floor(Math.random() * 156 + 50); + const g = Math.floor(Math.random() * 156 + 50); + const b = Math.floor(Math.random() * 156 + 50); + return `rgb(${r}, ${g}, ${b})`; +} + +/** + * Pobiera kolor tooltipa z atrybutu lub generuje go automatycznie, + * zapisując wynik w atrybucie, by przy kolejnych wywołaniach użyć tego samego. + */ +function getTooltipColor(el) { + let color = el.getAttribute('data-tooltip-color'); + if (!color) { + color = generateRGBColor(); + el.setAttribute('data-tooltip-color', color); + } + return color; +} + +/** + * Oblicza kontrastujący kolor tekstu (biały lub czarny) na podstawie podanego koloru tła. + */ +function getContrastingTextColor(rgbStr) { + const matches = rgbStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (!matches) return '#fff'; + const r = parseInt(matches[1], 10); + const g = parseInt(matches[2], 10); + const b = parseInt(matches[3], 10); + // Prosty wzór na jasność – im jaśniejszy, tym lepszy kontrast osiągniemy czarnym tekstem + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + return brightness > 128 ? '#000' : '#fff'; +} + +/** + * Sprawdza, czy tooltip, którego pozycję pobieramy (rect), koliduje + * z którymś z już wyrenderowanych tooltipów. Jeśli tak – dodaje dodatkowy offset. + */ +function adjustTooltipPosition(tooltip) { + const rect = tooltip.getBoundingClientRect(); + let additionalOffset = { x: 0, y: 0 }; + + renderedTooltips.forEach(existing => { + if (Math.abs(existing.top - rect.top) < 20 && Math.abs(existing.left - rect.left) < 20) { + additionalOffset.x += 20; + additionalOffset.y += 20; + } + }); + + if (additionalOffset.x || additionalOffset.y) { + tooltip.style.transform = `translate(${additionalOffset.x}px, ${additionalOffset.y}px)`; + } + + const newRect = tooltip.getBoundingClientRect(); + renderedTooltips.push({ id: tooltip.dataset.tooltipId, top: newRect.top, left: newRect.left }); +} + +/** + * Usuwa pozycję tooltipa z globalnej tablicy po jego usunięciu. + */ +function removeTooltipPosition(tooltipId) { + const index = renderedTooltips.findIndex(entry => entry.id === tooltipId); + if (index !== -1) { + renderedTooltips.splice(index, 1); + } +} + +document.querySelectorAll('[data-hook]').forEach(el => { + let tooltip; + let hideTimeout; + + function showTooltip() { + // Jeśli mechanizm tooltipów jest wyłączony, nie robimy nic + if (!tooltipEnabled) return; + + // Anulujemy ewentualne ukrywanie + clearTimeout(hideTimeout); + + // Jeżeli tooltip jeszcze nie istnieje, go tworzymy + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.className = 'custom-tooltip'; + tooltip.dataset.tooltipId = nextTooltipId++; + + // Pobieramy dane z atrybutów + const hook = el.getAttribute('data-hook') || 'N/A'; + const hookable = el.getAttribute('data-hookable') || 'N/A'; + + // Ładne formatowanie zawartości tooltipa + tooltip.innerHTML = ` +
+
+ Hook: + ${hook} +
+
+ Hookable: + ${hookable} +
+
+ + `; + + // Ustawiamy kolor tooltipa i obramowania + const color = getTooltipColor(el); + tooltip.style.backgroundColor = color; + tooltip.style.border = `2px solid ${color}`; + tooltip.style.boxShadow = `0 0 5px ${color}`; + // Ustawiamy kontrastujący kolor tekstu + tooltip.style.color = getContrastingTextColor(color); + + document.body.appendChild(tooltip); + + // Inicjalizacja Popper.js – dynamiczne pozycjonowanie + createPopper(el, tooltip, { + placement: 'bottom', + modifiers: [ + { + name: 'offset', + options: { offset: [0, 8] }, + }, + { + name: 'preventOverflow', + options: { boundary: 'viewport' }, + }, + { + name: 'flip', + options: { fallbackPlacements: ['top', 'right', 'left'] }, + }, + ], + }); + + // Po krótkim czasie korygujemy pozycję tooltipa + setTimeout(() => { + adjustTooltipPosition(tooltip); + }, 50); + + // Tooltip nie znika natychmiast – gdy kursor wejdzie na tooltip, przerywamy ukrywanie + tooltip.addEventListener('mouseenter', () => { + clearTimeout(hideTimeout); + tooltip.style.opacity = '1'; + }); + tooltip.addEventListener('mouseleave', hideTooltip); + + // Obsługa kopiowania – kliknięcie na element z klasą "copy" + tooltip.querySelectorAll('.copy').forEach(span => { + span.style.cursor = 'pointer'; + span.addEventListener('click', (e) => { + e.stopPropagation(); + const textToCopy = span.textContent; + navigator.clipboard.writeText(textToCopy).then(() => { + const copiedDiv = tooltip.querySelector('.tooltip-copied'); + if (copiedDiv) { + copiedDiv.textContent = `Skopiowano ${span.getAttribute('data-copy').toUpperCase()}`; + copiedDiv.style.display = 'block'; + setTimeout(() => { + copiedDiv.style.display = 'none'; + }, 1500); + } + }).catch(err => { + console.error('Error copying text: ', err); + }); + }); + }); + } + tooltip.style.opacity = '1'; + const color = getTooltipColor(el); + el.style.boxShadow = `0 0 0 3px ${color}, 0 0 10px 3px ${color}`; + } + + function hideTooltip() { + hideTimeout = setTimeout(() => { + if (tooltip) { + removeTooltipPosition(tooltip.dataset.tooltipId); + tooltip.remove(); + tooltip = null; + } + el.style.boxShadow = ''; + }, 500); + } + + el.addEventListener('mouseenter', showTooltip); + el.addEventListener('mouseleave', hideTooltip); +}); diff --git a/src/TwigHooks/assets/admin/styles/app.scss b/src/TwigHooks/assets/admin/styles/app.scss new file mode 100644 index 00000000..516950a8 --- /dev/null +++ b/src/TwigHooks/assets/admin/styles/app.scss @@ -0,0 +1,49 @@ +[data-hook] { + position: relative; + transition: box-shadow 0.3s ease-in-out; +} + +/* Styl tooltipa generowanego przez Popper.js */ +.custom-tooltip { + position: absolute; + width: fit-content; + height: fit-content; + z-index: 9999; + padding: 2px 2px; + border-radius: 6px; + white-space: pre-wrap; + font-size: 1em; + transition: opacity 0.3s ease-in-out; + opacity: 0; + user-select: text; + + .tooltip-section { + display: flex; + flex-direction: column; + gap: 1px; + } + + .tooltip-line { + line-height: 1; + } + + .tooltip-line strong { + margin-right: 3px; + } + + .tooltip-config { + margin: 0; + padding-left: 2px; + list-style-type: disc; + } + + .tooltip-copied { + margin-top: 6px; + font-size: 0.85em; + color: #fff; + background: rgba(0, 0, 0, 0.6); + padding: 3px 6px; + border-radius: 4px; + text-align: center; + } +} diff --git a/src/TwigHooks/assets/shop/entrypoint.js b/src/TwigHooks/assets/shop/entrypoint.js new file mode 100644 index 00000000..e69de29b diff --git a/src/TwigHooks/assets/shop/styles/app.scss b/src/TwigHooks/assets/shop/styles/app.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php b/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php index 8c2733b6..b8e3ecf6 100644 --- a/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php +++ b/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php @@ -24,9 +24,7 @@ public function __construct(private readonly HookRendererInterface $innerRendere public function render(array $hookNames, array $hookContext = []): string { $renderedParts = []; - $renderedParts[] = $this->getOpeningDebugComment($hookNames); $renderedParts[] = trim($this->innerRenderer->render($hookNames, $hookContext)); - $renderedParts[] = $this->getClosingDebugComment($hookNames); return implode(\PHP_EOL, $renderedParts); } diff --git a/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php b/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php index 240a63cb..ec281762 100644 --- a/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php +++ b/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php @@ -27,10 +27,7 @@ public function __construct(private readonly HookableRendererInterface $innerRen public function render(AbstractHookable $hookable, HookableMetadata $metadata): string { - $renderedParts = []; - $renderedParts[] = $this->getOpeningDebugComment($hookable); - $renderedParts[] = trim($this->innerRenderer->render($hookable, $metadata)); - $renderedParts[] = $this->getClosingDebugComment($hookable); + $renderedParts[] = $this->wrapWithDebugAttributes(trim($this->innerRenderer->render($hookable, $metadata)), $hookable); return implode(\PHP_EOL, $renderedParts); } @@ -70,4 +67,15 @@ private function getClosingDebugComment(AbstractHookable $hookable): string $hookable->priority(), ); } + + private function wrapWithDebugAttributes(string $content, AbstractHookable $hookable): string + { + $debugAttributes = sprintf(' data-hook="%s" data-hookable="%s" data-hookable-config="%s"', + htmlspecialchars($hookable->hookName, ENT_QUOTES), + htmlspecialchars($hookable->name, ENT_QUOTES), + $hookable->priority() + ); + + return preg_replace('/<(?!\/)(\w+)([^>]*)>/', '<$1$2' . $debugAttributes . '>', $content, 1); + } } diff --git a/src/TwigHooks/tests/Application/templates/admin/javascripts.html.twig b/src/TwigHooks/tests/Application/templates/admin/javascripts.html.twig new file mode 100644 index 00000000..f6071973 --- /dev/null +++ b/src/TwigHooks/tests/Application/templates/admin/javascripts.html.twig @@ -0,0 +1 @@ +{{ encore_entry_script_tags('app-admin-entry', null, 'app.admin') }} diff --git a/src/TwigHooks/tests/Application/templates/admin/stylesheets.html.twig b/src/TwigHooks/tests/Application/templates/admin/stylesheets.html.twig new file mode 100644 index 00000000..d1a2b97c --- /dev/null +++ b/src/TwigHooks/tests/Application/templates/admin/stylesheets.html.twig @@ -0,0 +1 @@ +{{ encore_entry_link_tags('app-admin-styles', null, 'app.admin') }} diff --git a/src/TwigHooks/tests/Application/templates/shop/scripts.html.twig b/src/TwigHooks/tests/Application/templates/shop/scripts.html.twig new file mode 100644 index 00000000..a659c2c1 --- /dev/null +++ b/src/TwigHooks/tests/Application/templates/shop/scripts.html.twig @@ -0,0 +1 @@ +{{ encore_entry_script_tags('app-shop-entry', null, 'app.shop') }} diff --git a/src/TwigHooks/tests/Application/templates/shop/stylesheets.html.twig b/src/TwigHooks/tests/Application/templates/shop/stylesheets.html.twig new file mode 100644 index 00000000..e67708b4 --- /dev/null +++ b/src/TwigHooks/tests/Application/templates/shop/stylesheets.html.twig @@ -0,0 +1 @@ +{{ encore_entry_link_tags('app-shop-styles', null, 'app.shop') }}