diff --git a/README.md b/README.md index 9270d5d..bded9fd 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Features: - **Does not affect structure** or existing styles 👌 - Detects **Knockout** components and templates 🤜 +- Detects **jQuery** widget registration and usage 💲 - Finds Magento **mage-init scripts** within templates/layouts 📌 - Uses dev-tools like **element picker** to select elements 🔫 - Prints **browseable structure** and internal informations in console 👀 diff --git a/view/base/web/js/LayoutHints.js b/view/base/web/js/LayoutHints.js index fe658a8..d23cc6f 100644 --- a/view/base/web/js/LayoutHints.js +++ b/view/base/web/js/LayoutHints.js @@ -1,21 +1,26 @@ define([ './highlights', + './debug/JQueryWidgetDebugger', './debug/KnockoutDebugger', './debug/MageLayoutDebugger', './debug/MageInitDebugger' -], function (highlights, KnockoutDebugger, MageLayoutDebugger, MageInitDebugger) { +], function (highlights, JQueryWidgetDebugger, KnockoutDebugger, MageLayoutDebugger, MageInitDebugger) { var colors = { - blue4: "36 47 155", - blue3: "100 111 212", - blue2: "155 163 235", - blue1: "219 223 253", - navy: "42 37 80", - brown: "84 18 18", + blue5: "36 80 190", + blue4: "36 47 155", + blue3: "100 111 212", + blue2: "155 163 235", + blue1: "219 223 253", + navy: "42 37 80", + brown: "84 18 18", + orange: "225 70 0" } - var labelStyle = "padding-left: 3px; padding-right: 3px; border-radius: 3px; margin-right: .5em; display: inline-block; cursor: pointer;" + var labelStyle = "padding-left: 3px; padding-right: 3px; border-radius: 3px; margin-right: .5em; display: inline-block; font-weight: bold; cursor: pointer;" + var labelStyleAqua = `${labelStyle} background: rgb(${colors.blue5}); color: white;` var labelStyleBlue = `${labelStyle} background: rgb(${colors.blue4}); color: white;` var labelStyleNavy = `${labelStyle} background: rgb(${colors.navy}); color: white;` var labelStyleBrown = `${labelStyle} background: rgb(${colors.brown}); color: white;` + var labelStyleOrange = `${labelStyle} background: rgb(${colors.orange}); color: white;` return class LayoutHints { constructor (mageLayoutTree, initOptions) { @@ -27,28 +32,33 @@ define([ this.debuggers = {} - this.debuggers.mageInit = new MageInitDebugger(this) - this.debuggers.mageLayout = new MageLayoutDebugger( - this, mageLayoutTree, { largerFontSize: "1.25em", - labelStyleBlue, labelStyleNavy, labelStyleBrown, blockEditUrl: initOptions.blockEditUrl, - mageInitDebugger: this.debuggers.mageInit } ) - this.debuggers.knockout = new KnockoutDebugger( - this, - { - largerFontSize: "1.25em", - labelStyles: labelStyleBrown, - } - ) + this.debuggers.mageInit = new MageInitDebugger({ + largerFontSize: "1.25em", + }) + + this.debuggers.jqueryWidget = new JQueryWidgetDebugger({ + largerFontSize: "1.25em", + }) + + this.debuggers.knockout = new KnockoutDebugger(this, { + largerFontSize: "1.25em", + labelStyles: labelStyleBrown, + }) + + this.debuggers.mageInit.badgeStyle = labelStyleBlue + this.debuggers.mageLayout.badgeStyle = labelStyleOrange + this.debuggers.jqueryWidget.badgeStyle = labelStyleAqua + this.debuggers.knockout.badgeStyle = labelStyleBrown // set up basic highlight styles document.documentElement.style.setProperty('--hl-bg', `rgb(${colors.blue1} / .85)`) @@ -67,7 +77,6 @@ define([ } this.highlight(inspectable, { printOnClick: false }) - this.consolePrint(inspectable) } findInspectable (element) { @@ -79,11 +88,7 @@ define([ continue } - var inspectable = typeDebugger.getInspectable(element) - inspectable.element = element - inspectable.type = type - - return inspectable + return element } } while (element = element.parentElement) } @@ -110,42 +115,94 @@ define([ this.highlight(closestHighlightable) } - highlight (data, options) { - if (!this.debuggers[data.type] || typeof this.debuggers[data.type].highlight !== 'function') { - throw new Error(`Cannot highlight element of type ${data.type}`) - } + highlight (element) { + let elementsData = new Map() - return this.debuggers[data.type].highlight(data, options) - } + // collect data from debuggers + for (let type in this.debuggers) { + const typeDebugger = this.debuggers[type] + + if (typeof typeDebugger.isInspectable !== 'function' + || typeof typeDebugger.getHighlightsData !== 'function' + || !typeDebugger.isInspectable(element) + ) { + continue + } + + const highlights = typeDebugger.getHighlightsData(element) + + for (const highlight of highlights) { + const highlightedEl = highlight.element || element - consolePrint (data, options) { - if (!this.debuggers[data.type] || typeof this.debuggers[data.type].consolePrint !== 'function') { - throw new Error(`Cannot consolePrint element of type ${data.type}`) + const elementData = elementsData.get(highlightedEl) || [] + elementData.push(Object.assign(highlight, { type })) + + elementsData.set(highlightedEl, elementData) + } } - return this.debuggers[data.type].consolePrint(data, options) + for (const [highlightedEl, highlightData] of elementsData) { + let content = '' + + for (let highlight of highlightData) { + content += `
` + + for (var badge of highlight?.badges || []) { + content += `${badge}` + } + + content += `@ ${highlight.type}` + + if (highlight.content) { + content += `
${highlight.content}
` + } + + content += `
` + } + + const highlightOverlay = highlights.create(highlightedEl, content) + + if (highlightedEl === element) { + highlightOverlay.addEventListener("click", event => this.onClickHighlight(element, event)) + highlightOverlay.addEventListener("contextmenu", event => this.onRightClickHighlight(element, event)) + } + } } - onClickHighlight (e) { - if (e.target.tagName === 'A') { + onClickHighlight (element, event) { + if (event.target.tagName === 'A') { return } - e.preventDefault() + console.group("Inspecting: ", element) + + for (var type in this.debuggers) { + if (typeof this.debuggers[type].consolePrint === 'function') { + try { + this.debuggers[type].consolePrint(element) + } catch (e) { + console.error(e) + } + } + } + + console.groupEnd() + + event.preventDefault() highlights.clear() this.removeMouseTracker() } - onRightClickHighlight (inspectable, event) { + onRightClickHighlight (element, event) { event.preventDefault() highlights.clear() this.removeMouseTracker() - if (!inspectable.element.parentElement) { + if (!element.parentElement) { return } - var parentInspectable = this.findInspectable(inspectable.element.parentElement) + var parentInspectable = this.findInspectable(element.parentElement) if (!parentInspectable) { return diff --git a/view/base/web/js/debug/JQueryWidgetDebugger.js b/view/base/web/js/debug/JQueryWidgetDebugger.js new file mode 100644 index 0000000..fa06101 --- /dev/null +++ b/view/base/web/js/debug/JQueryWidgetDebugger.js @@ -0,0 +1,104 @@ +define(['jquery', '../highlights', 'jquery-ui-modules/widget'], function ($, highlights) { + const registeredWidgets = [] + const initialized = [] + + $.widget = function (_super) { + const func = function () { + if (!registeredWidgets.includes(arguments[0])) { + registeredWidgets.push(arguments[0]) + } + + _super.apply(this, arguments) + } + + Object.assign(func, _super) + + return func + }($.widget) + + $.Widget = class extends $.Widget { + _createWidget() { + initialized.push({ widget: this.widgetName, element: arguments[1], options: arguments[0] }) + super._createWidget(...arguments) + } + } + + return class JQueryWidgetDebugger { + constructor (options = {}) { + this.largerFontSize = options.largerFontSize || '1em' + } + + isInspectable (element) { + return !!initialized.find(i => i.element === element) + } + + getInspectable (element) { + const inspectable = initialized.find(i => i.element === element) + + if (!inspectable) { + throw new Error("This element is not inspectable!") + } + + return inspectable + } + + getHighlightsData (element) { + const elementInitialized = initialized.filter(i => i.element === element) + + const highlightsData = elementInitialized.map(i => { + return { + badges: ["$." + i.widget], + content: `
${JSON.stringify(i.options, null, 2)}
` + } + }) + + if (element === document.body) { + const unusedWidgets = this.filterUnusedWidgets() + + highlightsData.push({ + badges: [`${unusedWidgets.length} Unused jQuery Widgets`], + content: `
${unusedWidgets.join('; ')}
` + }) + } + + return highlightsData + } + + consolePrint (element) { + const inits = initialized.filter(i => i.element === element) + + if (!inits.length === 0) { + return + } + + inits.forEach(initData => { + console.group( + `%c$.${initData.widget}`, + `${this.badgeStyle || ''}; font-size: ${this.largerFontSize}`, + ) + console.log("Options: ", initData.options) + console.groupEnd() + }) + + if (document.body === element) { + console.log("Unused jQuery Widgets: ", this.filterUnusedWidgets()) + } + } + + filterUnusedWidgets () { + return registeredWidgets.filter(widgetName => { + const parts = widgetName.split('.') + const name = parts[parts.length - 1] + + return !initialized.find(init => init.widget === name) + }) + } + + dump () { + console.log("JQuery Registered Widgets: ", registeredWidgets) + console.log("JQuery Widget Usage: ", initialized) + + console.log("Unused widgets: ", this.filterUnusedWidgets()) + } + } +}) diff --git a/view/base/web/js/debug/KnockoutDebugger.js b/view/base/web/js/debug/KnockoutDebugger.js index 38e41ab..a3986e9 100644 --- a/view/base/web/js/debug/KnockoutDebugger.js +++ b/view/base/web/js/debug/KnockoutDebugger.js @@ -19,8 +19,7 @@ define(['knockout', '../highlights'], function (ko, highlights) { constructor (layoutHints, options = {}) { this.layoutHints = layoutHints - this.labelStyles = options.labelStyles || '' - this.largerFontSize = options.largerFontSize || '' + this.largerFontSize = options.largerFontSize || '1em' this.initTemplateCollector() } @@ -83,24 +82,24 @@ define(['knockout', '../highlights'], function (ko, highlights) { throw new Error("This element is not inspectable!") } - var context = ko.contextFor(element) - var templates = new Set(this.templates.get(element)) + const context = ko.contextFor(element) + const templates = new Set(this.templates.get(element)) while (ko.contextFor(element.parentElement) === context) { element = element.parentElement // merge templates if (this.templates.has(element)) { - for (var template of this.templates.get(element)) { + for (let template of this.templates.get(element)) { templates.add(template) } } } - var parent = element.parentElement - var elements = [] + const parent = element.parentElement + const elements = [] - for (var child of parent.children) { + for (let child of parent.children) { if (ko.contextFor(child) === context) { elements.push(child) } @@ -109,38 +108,33 @@ define(['knockout', '../highlights'], function (ko, highlights) { return { element, elements, context, templates } } - highlight (data, { printOnClick = true } = {}) { - var $data = data.context.$data + getHighlightsData (element) { + const inspectable = this.getInspectable(element) + const $data = inspectable.context.$data - var content - var names = getNames(data) - - content = `

- KO - ${names.map(n => `${n}`).join(' ')} -

` + let content = '' if ($data.component) { - content += `

${$data.component}

` + content += `
Comp: ${$data.component}
` } if ($data.template) { - content += `

${$data.template}

` + content += `
Templ: ${$data.template}
` } - for (var el of data.elements) { - var highlightEl = highlights.create(el, content) - - if (printOnClick) { - highlightEl.addEventListener("click", () => this.consolePrint(data)) - } + return [{ + badges: getNames(inspectable), + content + }] + } - highlightEl.addEventListener("click", event => this.layoutHints.onClickHighlight(event)) - highlightEl.addEventListener("contextmenu", event => this.layoutHints.onRightClickHighlight(data, event)) + consolePrint (element, { groupPrefix = "", collapse = false } = {}) { + if (!this.isInspectable(element)) { + return } - } - consolePrint (data, { groupPrefix = "", collapse = false, withParent = true } = {}) { + const data = this.getInspectable(element) + var context = data.context var $data = context.$data @@ -149,15 +143,13 @@ define(['knockout', '../highlights'], function (ko, highlights) { if (collapse) { console.groupCollapsed( - `${groupPrefix}%cKO%c${nameString}`, - this.labelStyles, - ...names.map(n => this.labelStyles) + `${groupPrefix}%c${nameString}`, + ...names.map(n => this.badgeStyle) ) } else { console.group( - `${groupPrefix}%cKO%c${nameString}`, - `${this.labelStyles}; font-size: ${this.largerFontSize};`, - ...names.map(n => `${this.labelStyles}; font-size: ${this.largerFontSize};`) + `${groupPrefix}%c${nameString}`, + ...names.map(n => `${this.badgeStyle}; font-size: ${this.largerFontSize};`) ) } @@ -198,14 +190,6 @@ define(['knockout', '../highlights'], function (ko, highlights) { console.log(`KO Template ref:\n%c${ref}`, `font-size: ${this.largerFontSize}; font-weight: bold;`); } - - if (withParent) { - var parent = this.layoutHints.findInspectable(data.element.parentElement) - - if (parent) { - this.layoutHints.consolePrint(parent, { groupPrefix: "Parent: ", collapse: true, withParent: true, withChildren: false }) - } - } } console.log("Elements:\n", ...data.elements) diff --git a/view/base/web/js/debug/MageInitDebugger.js b/view/base/web/js/debug/MageInitDebugger.js index 2ea5cdd..47b5fff 100644 --- a/view/base/web/js/debug/MageInitDebugger.js +++ b/view/base/web/js/debug/MageInitDebugger.js @@ -1,9 +1,9 @@ define([], function () { return class MageInitDebugger { - constructor (layoutHints) { - this.layoutHints = layoutHints - this.inits = new Map() + constructor (options = {}) { + this.largerFontSize = options.largerFontSize || '1em' + this.inits = new Map() this.collectInits() } @@ -23,9 +23,9 @@ define([], function () { ? this.inits.get(targetElement) : [] - var mageInit = script + var mageInit = JSON.parse(script ? el.innerHTML - : el.dataset.mageInit + : el.dataset.mageInit) elementInits.push({ el: targetElement, @@ -37,6 +37,67 @@ define([], function () { } } + isInspectable (element) { + return this.inits.has(element) + } + + getHighlightsData (element) { + const highlightData = [] + + const init = this.inits.get(element) + const badges = [`${init.length} inits`] + + let insideInits = 0 + let headInits = 0 + + for (var [initElement, initConfigs] of this.inits) { + if (element.contains(initElement) && initElement !== element) { + insideInits += initConfigs.length + } + + if (element === document.body && document.head.contains(initElement)) { + headInits += initConfigs.length + } + } + + if (insideInits > 0) { + badges.push(`${insideInits} inside`) + } + + if (headInits > 0) { + badges.push(`${headInits} head inits`) + } + + return [{ badges }] + } + + consolePrint (element) { + const inits = this.inits.get(element) + + if (!inits) { + return + } + + inits.forEach(init => { + const initType = init.script + ? '