(function () { "use strict"; const IS_FIREFOX = navigator.userAgent.indexOf("Firefox") !== -1; class OwlDevtoolsGlobalHook { constructor() { // The set of apps exposed by owl this.apps = window.__OWL_DEVTOOLS__.apps; // Class definition of an owl Fiber this.Fiber = window.__OWL_DEVTOOLS__.Fiber; // Same but for RootFiber this.RootFiber = window.__OWL_DEVTOOLS__.RootFiber; // This is for retrocompatibility purposes since new versions of owl should always expose toRaw and reactive // in __OWL_DEVTOOLS__ this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw; this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive; // Set to keep track of the HTML elements we added to the page this.addedElements = []; // To keep track of the succession order of the render events this.eventId = 0; // Set to keep track of the frame on which this script is loaded this.frame = "top"; // Allows to launch a message each time an iframe html element is added to the page const iFrameObserver = new MutationObserver(function (mutationsList) { mutationsList.forEach(function (mutation) { mutation.addedNodes.forEach(function (addedNode) { if (addedNode.tagName == "IFRAME") { // Ensure it is not an empty iframe element if (addedNode.contentDocument) { /* * This message is intercepted by the content script which relays it to the background script which relays it to the devtools app. * This process may seem long and indirect but is necessary. This applies to all window.top.postMessage methods in this file. * More information in the docs: https://developer.chrome.com/docs/extensions/mv3/devtools/#evaluated-scripts-to-devtools */ window.top.postMessage({ source: "owl-devtools", type: "NewIFrame", data: addedNode.contentDocument.location.href, }); } } }); }); }); iFrameObserver.observe(document.body, { subtree: true, childList: true }); this.patchAppsSetMethods(); this.recordEvents = false; this.traceRenderings = false; this.traceSubscriptions = false; this.requestedFrame = false; this.eventsBatch = []; // Object which defines how different types of data should be displayed when passed to the devtools this.serializer = { // Defines how leaf object nodes should be displayed in the extension when inside a bigger structure // This is also used by default when it is a standalone item (like functions, numbers, strings, etc) // The asConstructorName parameter can be passed to change the display of objects and functions to // be their constructor name (useful for prototype, map and set display) serializeItem(value, asConstructorName = false) { if (typeof value === "array") { return "Array(" + value.length + ")"; } else if (typeof value === "object") { if (value == null) { return "null"; } if (asConstructorName) { return value.constructor.name; } return "{...}"; } else if (typeof value === "undefined") { return "undefined"; } else if (typeof value === "string") { return JSON.stringify(value); } else if (typeof value === "function") { if (asConstructorName) { return value.constructor.name; } let functionString = value.toString(); if (functionString.startsWith("class")) { return "class " + value.name; } else { // This replaces the body of any function with curly braces by ... const regex = /\{(?![^()]*\))(.*?)\}(?![^{}]*\})/s; return functionString.replace(regex, "{...}"); } } else { let valueAsString = value.toString(); if (asConstructorName && valueAsString.length > 10) { valueAsString = valueAsString.substring(0, 8) + "..."; } return valueAsString; } }, array(arr) { const result = []; let length = 0; for (const item of arr) { if (length > 25) { result.push("..."); break; } const element = this.serializeItem(item); length += element.length; result.push(element); } return "[" + result.join(", ") + "]"; }, object(obj) { const result = []; let length = 0; for (const [key, value] of Object.entries(obj)) { if (length > 25) { result.push("..."); break; } const element = key + ": " + this.serializeItem(value); length += element.length; result.push(element); } for (const key of Object.getOwnPropertySymbols(obj)) { if (length > 25) { result.push("..."); break; } const element = key.toString() + ": " + this.serializeItem(obj[key]); length += element.length; result.push(element); } return "{" + result.join(", ") + "}"; }, map(obj) { const result = []; let length = 0; for (const [key, value] of obj.entries()) { if (length > 25) { result.push("..."); break; } const element = this.serializeItem(key, true) + " => " + this.serializeItem(value, true); length += element.length; result.push(element); } return "Map(" + obj.size + "){" + result.join(", ") + "}"; }, ["map entry"](obj) { return ( "{" + this.serializeItem(obj[0], true) + " => " + this.serializeItem(obj[1], true) + "}" ); }, set(obj) { const result = []; let length = 0; for (const value of obj) { if (length > 25) { result.push("..."); break; } const element = this.serializeItem(value, false); length += element.length; result.push(element); } return "Set(" + obj.size + "){" + result.join(", ") + "}"; }, // Returns a shortened string version of a collection of properties for display purposes // Shortcut for serializeItem when it is a standalone item serializeContent(item, type) { if (this[type]) { return this[type](item); } return this.serializeItem(item); }, }; } initDevtools(frame = "top") { if (!this.devtoolsInit) { this.frame = frame; const self = this; // Flush the events batcher when a root render is completed const original_Complete = self.RootFiber.prototype.complete; self.RootFiber.prototype.complete = function () { original_Complete.call(this, ...arguments); const path = self.getComponentPath(this.node); //Add a functionnality to the complete function which sends a message to the window every time it is triggered. window.top.postMessage({ source: "owl-devtools", type: "Complete", data: path, origin: { frame: self.frame }, }); if (self.recordEvents) { window.top.postMessage({ source: "owl-devtools", type: "Event", data: self.eventsBatch, }); self.eventsBatch = []; } }; this.devtoolsInit = true; } } // Modify the methods of the apps set in order to send a message each time it is modified. patchAppsSetMethods() { const originalAdd = this.apps.add; const originalDelete = this.apps.delete; this.apps.add = function () { originalAdd.call(this, ...arguments); window.top.postMessage({ source: "owl-devtools", type: "RefreshApps", }); }; this.apps.delete = function () { originalDelete.call(this, ...arguments); window.top.postMessage({ source: "owl-devtools", type: "RefreshApps", }); }; } // Modify methods of each app so that it triggers messages on each flush and component render patchAppMethods() { let app; for (const appItem of this.apps) { if (appItem.root) { app = appItem; } } if (!app.root) { return; } const self = this; const originalFlush = app.scheduler.constructor.prototype.flush; let inFlush = false; let _render = false; app.scheduler.constructor.prototype.flush = function () { // Used to know when a render is triggered inside the flush method or not inFlush = true; originalFlush.call(this, ...arguments); inFlush = false; }; const originalRender = self.Fiber.prototype.render; self.Fiber.prototype.render = function () { const id = self.eventId++; _render = false; let flushed = false; // We know if a render comes from flush before calling the render method if (this instanceof self.RootFiber && inFlush) { flushed = true; } // patch the renderFn function from the node to only measure the time // spent in the template, not in additional work, such as flushing scheduler const node = this.node; const nodeRenderFn = node.renderFn; let time = 0; this.node.renderFn = function () { const before = performance.now(); const result = nodeRenderFn.call(this); time = performance.now() - before; // unpatch the node render function node.renderFn = nodeRenderFn; return result; }; originalRender.call(this, ...arguments); if (_render && self.traceRenderings && this instanceof self.RootFiber) { console.groupCollapsed(`Rendering <${this.node.name}>`); console.trace(); console.groupEnd(); } if (self.recordEvents) { const path = self.getComponentPath(this.node); // if the render comes from flush if (flushed) { self.eventsBatch.push({ type: "render (flushed)", component: this.node.name, key: this.node.parentKey ? this.node.parentKey : "", path: path, time: time, id: id, }); // if _render is called, it is a proper render (and not a delayed one) } else if (_render) { // A render on a RootFiber is a root render and can propagate other renders to its children if (this instanceof self.RootFiber) { self.eventsBatch.push({ type: this.deep ? "render (deep)" : "render", component: this.node.name, key: this.node.parentKey ? this.node.parentKey : "", path: path, time: time, id: id, }); // if the node status is NEW, the node has been created just before rendering } else if (this.node.status === 0) { self.eventsBatch.push({ type: "create", component: this.node.name, key: this.node.parentKey ? this.node.parentKey : "", path: path, time: time, id: id, }); // else it is an update } else { self.eventsBatch.push({ type: "update", component: this.node.name, key: this.node.parentKey ? this.node.parentKey : "", path: path, time: time, id: id, }); } // _render has not been called so it is a delayed render that could be flushed later on } else { self.eventsBatch.push({ type: "render (delayed)", component: this.node.name, key: this.node.parentKey ? this.node.parentKey : "", path: path, time: time, id: id, }); } } }; const original_Render = self.Fiber.prototype._render; self.Fiber.prototype._render = function () { _render = true; original_Render.call(this, ...arguments); }; // Signals when a component is destroyed const originalDestroy = app.root.constructor.prototype._destroy; app.root.constructor.prototype._destroy = function () { if (self.recordEvents) { const path = self.getComponentPath(this); const event = { type: "destroy", component: this.name, key: this.parentKey, path: path, time: 0, id: self.eventId++, }; self.eventsBatch.push(event); const before = performance.now(); originalDestroy.call(this, ...arguments); event.time = performance.now() - before; } else { originalDestroy.call(this, ...arguments); } }; } // patch reactivity system to activate subscription tracing patchReactivity() { // i am simultaneously proud and ashamed of this code... const WeakMapGet = WeakMap.prototype.get; const MapGet = Map.prototype.get; const SetAdd = Set.prototype.add; const self = this; let callbacksToTargets; let targetToKeysToCallbacks; // Step 1: extract internal values from owl const obj = self.reactive({}, () => {}); let count = 0; WeakMap.prototype.get = function () { count++; if (count === 1) { targetToKeysToCallbacks = this; } if (count === 3) { callbacksToTargets = this; WeakMap.prototype.get = WeakMapGet; } return WeakMapGet.call(this, ...arguments); }; obj.a; // Step 2: patch these objects let key; function getKey(_key) { key = _key; return MapGet.call(this, _key); } targetToKeysToCallbacks.get = function (target) { const keyToCallbacks = WeakMapGet.call(this, target); if (keyToCallbacks) { keyToCallbacks.get = getKey; } return keyToCallbacks; }; function addTarget(target) { // HERE if (self.traceSubscriptions && !this.has(target)) { console.groupCollapsed(`Observing:`, key, target); console.trace(); console.groupEnd(); } return SetAdd.call(this, target); } callbacksToTargets.get = function (callback) { const targets = WeakMapGet.call(this, callback); if (targets) { targets.add = addTarget; } return targets; }; } toggleTracing(value) { if (value) { this.patchAppMethods(); this.patchAppMethods = () => {}; // to only patch once } this.traceRenderings = value; return this.traceRenderings; } toggleSubscriptionTracing(value) { if (value) { this.patchReactivity(); this.patchReactivity = () => {}; // to only patch once } this.traceSubscriptions = value; return value; } // Enables/disables the recording of the render/destroy events based on value toggleEventsRecording(value, index) { if (value) { this.patchAppMethods(); this.patchAppMethods = () => {}; // to only patch once } this.recordEvents = value; this.eventId = index; return this.recordEvents; } // Reset the event ids (the events on devtools side will be cleared at the same time) resetEvents() { this.eventId = 0; } // Get the urls of all iframes present on the page getIFrameUrls() { let frames = []; for (const frame of document.querySelectorAll("iframe")) { frames.push(frame.contentDocument.location.href); } return frames; } // Draws a highlighting rectangle on the specified html element and displays its dimensions and the specified name in a box highlightElements(elements, name) { this.removeHighlights(); if (elements.length === 0) { return; } let minTop = Number.MAX_SAFE_INTEGER; let minLeft = Number.MAX_SAFE_INTEGER; let maxBottom = Number.MIN_SAFE_INTEGER; let maxRight = Number.MIN_SAFE_INTEGER; for (const element of elements) { let rect; // Since this function accepts text nodes, we need to create a range to get the position and dimensions if (element instanceof Text) { const range = document.createRange(); range.selectNode(element); rect = range.getBoundingClientRect(); } else { rect = element.getBoundingClientRect(); } const top = rect.top; const left = rect.left; const bottom = top + rect.height; const right = left + rect.width; minTop = Math.min(minTop, top); minLeft = Math.min(minLeft, left); maxBottom = Math.max(maxBottom, bottom); maxRight = Math.max(maxRight, right); const width = right - left; const height = bottom - top; if (element instanceof HTMLElement) { const marginTop = parseInt(getComputedStyle(element).marginTop); const marginRight = parseInt(getComputedStyle(element).marginRight); const marginBottom = parseInt(getComputedStyle(element).marginBottom); const marginLeft = parseInt(getComputedStyle(element).marginLeft); const paddingTop = parseInt(getComputedStyle(element).paddingTop); const paddingRight = parseInt(getComputedStyle(element).paddingRight); const paddingBottom = parseInt(getComputedStyle(element).paddingBottom); const paddingLeft = parseInt(getComputedStyle(element).paddingLeft); const highlight = document.createElement("div"); highlight.style.top = `${top}px`; highlight.style.left = `${left}px`; highlight.style.width = `${width}px`; highlight.style.height = `${height}px`; highlight.style.boxSizing = "border-box"; highlight.style.position = "fixed"; highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)"; highlight.style.borderStyle = "solid"; highlight.style.borderWidth = `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px`; highlight.style.borderColor = "rgba(65, 196, 68, 0.4)"; highlight.style.zIndex = "10000"; highlight.style.pointerEvents = "none"; document.body.appendChild(highlight); this.addedElements.push(highlight); const highlightMargins = document.createElement("div"); highlightMargins.style.top = `${top - marginTop}px`; highlightMargins.style.left = `${left - marginLeft}px`; highlightMargins.style.width = `${width + marginLeft + marginRight}px`; highlightMargins.style.height = `${height + marginBottom + marginTop}px`; highlightMargins.style.position = "fixed"; highlightMargins.style.borderStyle = "solid"; highlightMargins.style.boxSizing = "border-box"; highlightMargins.style.borderWidth = `${marginTop}px ${marginRight}px ${marginBottom}px ${marginLeft}px`; highlightMargins.style.borderColor = "rgba(241, 179, 121, 0.4)"; highlightMargins.style.zIndex = "10000"; highlightMargins.style.pointerEvents = "none"; document.body.appendChild(highlightMargins); this.addedElements.push(highlightMargins); // In case the element is a range because a text node was passed } else { const highlight = document.createElement("div"); highlight.style.top = `${top}px`; highlight.style.left = `${left}px`; highlight.style.width = `${width}px`; highlight.style.height = `${height}px`; highlight.style.boxSizing = "border-box"; highlight.style.position = "fixed"; highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)"; highlight.style.zIndex = "10000"; highlight.style.pointerEvents = "none"; document.body.appendChild(highlight); this.addedElements.push(highlight); } } const width = maxRight - minLeft; const height = maxBottom - minTop; const detailsBox = document.createElement("div"); detailsBox.className = "owl-devtools-detailsBox"; detailsBox.innerHTML = `
${name}
${width.toFixed( 2 )}px x ${height.toFixed(2)}px
`; detailsBox.style.position = "fixed"; detailsBox.style.backgroundColor = "black"; detailsBox.style.padding = "5px"; detailsBox.style.zIndex = "10000"; detailsBox.style.pointerEvents = "none"; detailsBox.style.display = "inline"; document.body.appendChild(detailsBox); this.addedElements.push(detailsBox); const viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); const viewportHeight = Math.max( document.documentElement.clientHeight, window.innerHeight || 0 ); const detailsBoxRect = detailsBox.getBoundingClientRect(); let detailsBoxTop = minTop + height + 5; let detailsBoxLeft = minLeft; if (detailsBoxTop + detailsBoxRect.height > viewportHeight) { detailsBoxTop = minTop - detailsBoxRect.height - 5; if (detailsBoxTop < 0) { detailsBoxTop = minTop; } } if (detailsBoxLeft + detailsBoxRect.width > viewportWidth) { detailsBoxLeft = minLeft - detailsBoxRect.width - 5; } detailsBox.style.top = `${detailsBoxTop}px`; detailsBox.style.left = `${detailsBoxLeft + 5}px`; } // Remove all elements drawn by the HighlightElement function removeHighlights = () => { for (const el of this.addedElements) { el.remove(); } this.addedElements = []; }; // Identify the hovered component based on the corresponding DOM element and send the Select message // when the target changes HTMLSelector = (ev) => { const target = ev.target; if (!this.currentSelectedElement || !target.isEqualNode(this.currentSelectedElement)) { const path = this.getElementPath(target); this.highlightComponent(path); this.currentSelectedElement = target; window.top.postMessage({ source: "owl-devtools", type: "SelectElement", data: path, }); } }; // Activate the HTML selector tool enableHTMLSelector() { document.addEventListener("mouseover", this.HTMLSelector, { capture: true }); document.addEventListener("click", this.disableHTMLSelector, { capture: true }); document.addEventListener("mouseout", this.removeHighlights, { capture: true }); } // Diasble the HTML selector tool disableHTMLSelector = (ev = undefined) => { if (ev) { if (!ev.isTrusted) { return; } ev.stopPropagation(); ev.preventDefault(); } this.removeHighlights(); document.removeEventListener("mouseover", this.HTMLSelector, { capture: true }); document.removeEventListener("click", this.disableHTMLSelector, { capture: true }); document.removeEventListener("mouseout", this.removeHighlights, { capture: true }); window.top.postMessage({ source: "owl-devtools", type: "StopSelector", }); }; // Returns the object specified by the path starting from the topParent object getObject(topParent, path) { let obj = topParent; if (path.length === 0) { return obj; } for (const key of path) { switch (key.type) { case "prototype": obj = Object.getPrototypeOf(obj); break; // We will be handling maps and sets entries as arrays to keep things stable case "set entries": case "map entries": obj = [...obj]; break; // Kind of tricky structure-wise but the set entry already is the object itself so this is // only useful for display purpose in the devtools. case "set value": obj = obj; break; // A map entry is a two items array with the key and the value so indexes 0 and 1 case "map key": obj = obj[0]; break; case "map value": obj = obj[1]; break; case "getter": // When the item is a getter, get its value. try { obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj); } catch (e) { obj = e.toString(); } break; case "prototype getter": case "set entry": case "map entry": case "item": // When the item is a symbol, we make sure it is the right one by using the index on the // Object.getOwnPropertySymbols array. if (key.hasOwnProperty("symbolIndex") && key.symbolIndex >= 0) { const symbol = Object.getOwnPropertySymbols(obj)[key.symbolIndex]; if (symbol) { obj = obj[symbol]; } } else if (Object.getOwnPropertyDescriptor(obj, key.value)?.hasOwnProperty("get")) { try { obj = Object.getOwnPropertyDescriptor(obj, key.value).get; } catch (e) { obj = e.toString(); } } else { try { obj = obj[key.value]; } catch (e) { obj = e.toString(); } } } if (obj) { obj = this.toRaw(obj); } } return obj; } // Returns the asked property given its global path getObjectProperty(path) { // Just return the corresponding app if path is of length 1 if (path.length === 1) { return [...this.apps][path[0]]; } // Path to the component node is only strings, becomes objects for properties const index = path.findIndex((key) => typeof key !== "string"); const componentNode = this.getComponentNode(path.slice(0, index)); const obj = this.getObject(componentNode, path.slice(index)); return obj; } // Returns a modified version of an object node that has compatible format with the devtools ObjectTreeElement component // parentObj is the parent of the node, key is the path key of the node, type is where the object belongs to (props, env, instance, ...), // oldBranch is supposedly the same final node in the previous version of the tree and oldTree is the complete old component details serializeObjectChild(parentObj, key, depth, type, path, oldBranch, oldTree) { let obj; let child = { depth: depth, toggled: false, objectType: type, hasChildren: false, }; if (oldBranch?.toggled) { child.toggled = true; } child.path = path.concat([key]); child.children = []; switch (key.type) { case "prototype": child.name = "[[Prototype]]"; child.contentType = "object"; child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true); child.hasChildren = true; if (!oldTree && type === "env") { child.toggled = true; } break; case "set entries": case "map entries": child.name = "[[Entries]]"; child.content = ""; child.contentType = "array"; child.hasChildren = true; break; case "set value": child.name = "value"; obj = parentObj; break; case "map key": child.name = "key"; obj = parentObj[0]; break; case "map value": child.name = "value"; obj = parentObj[1]; break; case "set entry": case "map entry": case "item": if (Object.getOwnPropertyDescriptor(parentObj, key.value)?.hasOwnProperty("get")) { child.name = "get " + key.value; obj = Object.getOwnPropertyDescriptor(parentObj, key.value).get; } else { child.name = key.value; if (typeof key.value === "symbol") { child.name = key.value.toString(); obj = parentObj[key.value]; child.path[child.path.length - 1].symbolIndex = Object.getOwnPropertySymbols( parentObj ).findIndex((sym) => sym === key.value); child.path[child.path.length - 1].value = key.value.toString(); } else { obj = parentObj[key.value]; } } if (key.type === "map entry") { child.contentType = "array"; child.hasChildren = obj.length > 0; child.content = this.serializer.serializeContent(obj, key.type); } break; } if (!child.contentType) { if (obj === null) { child.content = "null"; child.contentType = "object"; child.hasChildren = false; } else if (obj === undefined) { child.content = "undefined"; child.contentType = "undefined"; child.hasChildren = false; } else { obj = this.toRaw(obj); switch (true) { case obj instanceof Map: child.contentType = "map"; child.hasChildren = true; break; case obj instanceof Set: child.contentType = "set"; child.hasChildren = true; break; case obj instanceof Array: child.contentType = "array"; child.hasChildren = obj.length > 0; break; case typeof obj === "function": child.contentType = "function"; child.hasChildren = true; break; case obj instanceof Object: child.contentType = "object"; child.hasChildren = Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length; break; default: child.contentType = typeof obj; child.hasChildren = false; } if (key.type === "set entry") { child.content = this.serializer.serializeItem(obj, true); } else { child.content = this.serializer.serializeContent(obj, child.contentType); } } } if (child.toggled) { child.children = this.loadObjectChildren( child.path, child.depth, child.contentType, child.objectType, oldTree ); } this.addHighlightedKeys(child); return child; } // returns the serialized node corresponding to its path in the serialized tree getObjectInOldTree(oldTree, completePath, objType) { const objPathIndex = completePath.findIndex((key) => typeof key !== "string"); let path = completePath.slice(objPathIndex); let obj; if (objType === "subscription") { const subscriptionPath = completePath.slice(0, objPathIndex + 3); obj = oldTree.subscriptions.children.find( (child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath) ).target; path = path.slice(3); } else { // Everything here is in component if it is not an app so remove this key of the path in the former case if (objPathIndex > 1) { path.shift(); } // the value is either "props" or "env" here if (objType !== "instance") { obj = oldTree[path[0].value].children; path.shift(); // there is nothing otherwise but extension side it is in instance } else { obj = oldTree.instance.children; } // the first element here is directly in an array instead of a children array obj = obj[path[0].childIndex]; path.shift(); } // just follow the indexes in the rest of the path for (const key of path) { obj = obj.children[key.childIndex]; } return obj; } // Returns a serialized version of the children properties of the specified component's property given its path. loadObjectChildren(path, depth, contentType, objType, oldTree) { const children = []; depth = depth + 1; let obj = this.getObjectProperty(path); let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType); if (!obj) { return []; } let index = 0; const lastKey = path.at(-1); let prototype; if (lastKey.type.includes("entry")) { if (lastKey.type === "map entry") { const mapKey = this.serializeObjectChild( obj, { type: "map key", childIndex: 0 }, depth, objType, path, oldBranch?.children[0], oldTree ); children.push(mapKey); const mapValue = this.serializeObjectChild( obj, { type: "map value", childIndex: 1 }, depth, objType, path, oldBranch?.children[1], oldTree ); children.push(mapValue); } else if (lastKey.type === "set entry") { const setValue = this.serializeObjectChild( obj, { type: "set value", childIndex: 0 }, depth, objType, path, oldBranch?.children[0], oldTree ); children.push(setValue); } return children; } switch (contentType) { case "array": if (lastKey.type.includes("entries")) { for (; index < obj.length; index++) { const child = this.serializeObjectChild( obj, { type: lastKey.type.replace("entries", "entry"), value: index, childIndex: children.length, }, depth, objType, path, oldBranch?.children[index], oldTree ); if (child) { children.push(child); } } } else { for (; index < obj.length; index++) { const child = this.serializeObjectChild( obj, { type: "item", value: index, childIndex: children.length }, depth, objType, path, oldBranch?.children[index], oldTree ); if (child) { children.push(child); } } } break; case "set": case "map": const entries = this.serializeObjectChild( obj, { type: contentType + " entries", value: "[[Entries]]", childIndex: children.length }, depth, objType, path, oldBranch?.children[index], oldTree ); if (entries) { children.push(entries); index++; } Reflect.ownKeys(obj).forEach((key) => { const child = this.serializeObjectChild( obj, { type: "item", value: key, childIndex: children.length }, depth, objType, path, oldBranch?.children[index], oldTree ); if (child) { children.push(child); } index++; }); break; case "object": case "function": Reflect.ownKeys(obj).forEach((key) => { if ( key !== "__proto__" && Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get") ) { let child = { name: key, depth: depth, toggled: false, objectType: objType, path: [...path, { type: "getter", value: key, childIndex: children.length }], contentType: "getter", content: "(...)", hasChildren: false, children: [], }; children.push(child); } const child = this.serializeObjectChild( obj, { type: "item", value: key, childIndex: children.length }, depth, objType, path, oldBranch?.children[index], oldTree ); if (child) children.push(child); index++; }); } let proto = Object.getPrototypeOf(obj); while (proto) { Reflect.ownKeys(proto).forEach((key) => { if ( key !== "__proto__" && Object.getOwnPropertyDescriptor(proto, key).hasOwnProperty("get") ) { let child = { name: key, depth: depth, toggled: false, objectType: objType, path: [ ...path, { type: "prototype getter", value: key, childIndex: children.length }, ], contentType: "getter", content: "(...)", hasChildren: false, children: [], }; children.push(child); } }); proto = Object.getPrototypeOf(proto); } if (obj.__proto__) { prototype = this.serializeObjectChild( obj, { type: "prototype", childIndex: children.length }, depth, objType, path, oldBranch?.children.at(-1), oldTree ); children.push(prototype); } return children; } // Returns the Component node given its path and the root component node getComponentNode(path) { // The node is an app and not a component if (path.length === 1) { return [...this.apps][path[0]]; } // The second element in the path will always be the root of the app let node = [...this.apps][path[0]]?.root; if (!node) { return null; } for (let i = 2; i < path.length; i++) { // From this point onwards, it is an object path inside the component node if (typeof path[i] !== "string") { break; } if (node.children.hasOwnProperty(path[i])) { node = node.children[path[i]]; } else { return null; } } return node; } // Apply manual render to the specified component refreshComponent(path) { const componentNode = this.getComponentNode(path); componentNode.render(); } // Returns the component's details given its path getComponentDetails(path = null, oldTree = null) { let component = {}; if (!path) { path = this.getElementPath($0); } component.path = path; let node = this.getComponentNode(path) || [...this.apps].find((app) => app.root)?.root; if (!node) { return null; } // A path with only the app index indicates that the component is an App instead const isApp = path.length === 1; if (isApp) { component.version = node.__proto__.constructor?.version ? node.__proto__.constructor.version : "<2.0.8"; } // Load props of the component const props = isApp ? node.props : node.component.props; component.props = { toggled: oldTree ? oldTree.props.toggled : true, children: [] }; component.name = isApp ? node?.name ? `App (${node.name})` : "App " + (Number(path[0]) + 1) : node.component.constructor.name; const propsPath = isApp ? [...path, { type: "item", value: "props" }] : [...path, { type: "item", value: "component" }, { type: "item", value: "props" }]; Reflect.ownKeys(props).forEach((key) => { let oldBranch = oldTree?.props.children[component.props.children.length]; const property = this.serializeObjectChild( props, { type: "item", value: key, childIndex: component.props.children.length }, 0, "props", propsPath, oldBranch, oldTree ); if (property) { component.props.children.push(property); } }); // Load env of the component const env = isApp ? node.env : node.component.env; component.env = { toggled: oldTree ? oldTree.env.toggled : false, children: [] }; const envPath = isApp ? [...path, { type: "item", value: "env" }] : [...path, { type: "item", value: "component" }, { type: "item", value: "env" }]; Reflect.ownKeys(env).forEach((key) => { let oldBranch = oldTree?.env.children[component.env.children.length]; const envElement = this.serializeObjectChild( env, { type: "item", value: key, childIndex: component.env.children.length }, 0, "env", envPath, oldBranch, oldTree ); if (envElement) { component.env.children.push(envElement); } }); // Load env getters let obj = Object.getPrototypeOf(env); Reflect.ownKeys(obj).forEach((key) => { if ( key !== "__proto__" && Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get") ) { let child = { name: key, depth: 0, toggled: false, objectType: "env", path: [ ...envPath, { type: "prototype getter", value: key, childIndex: component.env.children.length }, ], contentType: "getter", content: "(...)", hasChildren: false, children: [], }; component.env.children.push(child); } }); const envPrototype = this.serializeObjectChild( env, { type: "prototype", childIndex: component.env.children.length }, 0, "env", envPath, oldTree?.env[component.env.children.length], oldTree ); component.env.children.push(envPrototype); // Load instance of the component const instance = isApp ? node : node.component; component.instance = { toggled: oldTree ? oldTree.instance.toggled : true, children: [] }; const instancePath = isApp ? path : [...path, { type: "item", value: "component" }]; Reflect.ownKeys(instance).forEach((key) => { if (!["env", "props"].includes(key)) { let oldBranch = oldTree?.instance.children[component.instance.children.length]; const instanceElement = this.serializeObjectChild( instance, { type: "item", value: key, childIndex: component.instance.children.length }, 0, "instance", instancePath, oldBranch, oldTree ); if (instanceElement) { component.instance.children.push(instanceElement); } } }); // Load instance getters obj = Object.getPrototypeOf(instance); while (obj) { Reflect.ownKeys(obj).forEach((key) => { if ( key !== "__proto__" && Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get") ) { let child = { name: key, depth: 0, toggled: false, objectType: "instance", path: [ ...instancePath, { type: "prototype getter", value: key, childIndex: component.instance.children.length, }, ], contentType: "getter", content: "(...)", hasChildren: false, children: [], }; component.instance.children.push(child); } }); obj = Object.getPrototypeOf(obj); } const instancePrototype = this.serializeObjectChild( instance, { type: "prototype", childIndex: component.instance.children.length }, 0, "instance", instancePath, oldTree?.instance[component.instance.children.length], oldTree ); component.instance.children.push(instancePrototype); // Load subscriptions of the component if (isApp) { component.subscriptions = { toggled: oldTree ? oldTree.subscriptions.toggled : true, children: [], }; } else { const rawSubscriptions = this.topLevelSubscriptions(node); component.subscriptions = { toggled: oldTree ? oldTree.subscriptions.toggled : true, children: [], }; rawSubscriptions.forEach((rawSubscription) => { let subscription = { target: { name: this.targetName(rawSubscription.target, node), contentType: typeof rawSubscription.target === "object" ? Array.isArray(rawSubscription.target) ? "array" : "object" : rawSubscription.target, depth: 0, path: [ ...path, { type: "item", value: "subscriptions" }, { type: "item", value: rawSubscription.index }, { type: "item", value: "target" }, ], toggled: false, objectType: "subscription", }, }; if ( oldTree && oldTree.subscriptions.children[rawSubscription.index] && oldTree.subscriptions.children[rawSubscription.index].target.toggled ) { subscription.target.toggled = true; } if (rawSubscription.target == null) { if (subscription.target.contentType === "undefined") { subscription.target.content = "undefined"; } else { subscription.target.content = "null"; } subscription.target.hasChildren = false; } else { subscription.target.content = this.serializer.serializeContent( rawSubscription.target, subscription.target.contentType ); subscription.target.hasChildren = subscription.target.contentType === "object" ? Object.keys(rawSubscription.target).length > 0 : subscription.target.contentType === "array" ? rawSubscription.target.length > 0 : false; } subscription.target.children = []; if (subscription.target.toggled) { subscription.target.children = this.loadObjectChildren( subscription.target.path, subscription.target.depth, subscription.target.contentType, subscription.target.objectType, oldTree ); } this.addHighlightedKeys(subscription.target); component.subscriptions.children.push(subscription); }); } return component; } // Replace the content of a parsed getter object with the result of the corresponding get method loadGetterContent(getter) { let obj = this.getObjectProperty(getter.path); if (obj == null) { if (typeof obj === "undefined") { getter.content = "undefined"; getter.contentType = "undefined"; } else { getter.content = "null"; getter.contentType = "object"; } getter.hasChildren = false; } else { obj = this.toRaw(obj); switch (true) { case obj instanceof Map: getter.contentType = "map"; getter.hasChildren = obj.size > 0; break; case obj instanceof Set: getter.contentType = "set"; getter.hasChildren = obj.size > 0; break; case obj instanceof Array: getter.contentType = "array"; getter.hasChildren = obj.length > 0; break; case typeof obj === "function": getter.contentType = "function"; getter.hasChildren = Reflect.ownKeys(obj).length > 0; break; case obj instanceof Object: getter.contentType = "object"; getter.hasChildren = Reflect.ownKeys(obj).length > 0; break; default: getter.contentType = typeof obj; getter.hasChildren = false; } getter.content = this.serializer.serializeContent(obj, getter.contentType); } return getter; } // Gives the DOM elements which correspond to the given component node getDOMElementsRecursive(node) { if (node.hasOwnProperty("bdom")) { return this.getDOMElementsRecursive(node.bdom); } if (node.hasOwnProperty("content")) { return this.getDOMElementsRecursive(node.content); } if (node.hasOwnProperty("el")) { if (node.el instanceof HTMLElement || node.el instanceof Text) { return [node.el]; } } if (node.hasOwnProperty("child")) { return this.getDOMElementsRecursive(node.child); } if (node.hasOwnProperty("children") && node.children.length > 0) { let elements = []; for (const child of node.children) { if (child) { elements = elements.concat(this.getDOMElementsRecursive(child)); } } if (elements.length > 0) { return elements; } } if (node.hasOwnProperty("parentEl")) { if (node.parentEl instanceof HTMLElement) { return [node.parentEl]; } } return []; } // Triggers the highlight effect around the specified component. highlightComponent(path) { // Try to highlight the root component of the app if function called on an app if (path.length === 1) { path.push("root"); } let component = this.getComponentNode(path); if (!component) { return; } const elements = this.getDOMElementsRecursive(component); this.highlightElements(elements, component.component.constructor.name); } // Edit a reactive state property with the provided value of the given component (path) and the subscription path editObject(path, value, objectType) { if (value === "undefined") { value = undefined; } else { try { value = JSON.parse(value); } catch (e) { console.warn("Could not evaluate user property\n", e); return; } } const item = path.pop(); const obj = this.getObjectProperty(path); const key = item.hasOwnProperty("symbolIndex") ? Object.getOwnPropertySymbols(obj)[item.symbolIndex] : item.value; if (!obj) { return; } if (objectType === "subscription") { this.reactive(obj)[key] = value; } else { obj[key] = value; if (objectType === "props" || objectType === "instance") { const component = this.getComponentNode(path); if (component.__proto__.hasOwnProperty("render")) { this.getComponentNode(path).render(); } else { component.root.render(); } } else if (objectType === "env") { [...this.apps][path[0]].root.render(true); } } } // Recursively checks if the given html element corresponds to a component in the components tree. // Immediatly returns the path of the first component which matches the element searchElement(node, path, element) { if (!node?.bdom) { return null; } const results = this.getDOMElementsRecursive(node); let hasElementInChildren = false; for (const result of results) { if (result.isEqualNode(element)) { return path; } if (result.contains(element)) { hasElementInChildren = true; } } if (hasElementInChildren) { for (const [key, child] of Object.entries(node.children)) { const result = this.searchElement(child, path.concat([key]), element); if (result) { return result; } } } return null; } // Returns the path to the component which is currently being inspected getElementPath(element) { if (element) { // Create an array with the html element and all its successive parents const parentsList = [element]; if (element.tagName !== "BODY") { while (element && element?.parentElement?.tagName !== "BODY") { element = element.parentElement; parentsList.push(element); } } const appsArray = [...this.apps]; // Try to find a correspondance between the elements in the array and the owl component, stops at first result found for (const elem of parentsList) { for (const [index, app] of appsArray.entries()) { const inspectedPath = this.searchElement(app.root, ["root"], elem); if (inspectedPath) { inspectedPath.unshift(index.toString()); return inspectedPath; } } } } // If nothing was found, return the path of the first root component found in the apps const appIndex = [...this.apps].findIndex((app) => app.root); return [appIndex.toString(), "root"]; } // Returns the tree of components of the inspected page in a parsed format // Use inspectedPath to specify the path of the selected component getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) { const appsArray = [...this.apps]; const trees = appsArray.map((app, index) => { let oldTree; if (oldTrees) { oldTree = oldTrees[index]; } let appNode = {}; appNode = { name: app?.name ? `App (${app.name})` : "App " + (index + 1), path: [index.toString()], key: "", depth: 0, toggled: true, selected: false, highlighted: false, version: app.__proto__.constructor?.version ? app.__proto__.constructor.version : "<2.0.8", children: [], }; if (app.root) { const root = { name: app.root.component.constructor.name, path: [index.toString(), "root"], key: "", depth: 1, toggled: true, selected: false, highlighted: false, }; if (oldTree) { appNode.toggled = oldTree.toggled; oldTree = oldTree.children[0]; } if (oldTree) { root.toggled = oldTree.toggled; } // If no path is provided, it defaults to the target of the inspect element action if (!inspectedPath) { inspectedPath = this.getElementPath($0); } if (inspectedPath.join("/") === index.toString()) { appNode.selected = true; root.highlighted = true; } else if (inspectedPath.join("/") === index.toString() + "/root") { root.selected = true; } root.children = this.fillTree(app.root, root, inspectedPath.join("/"), oldTree); appNode.children.push(root); } return appNode; }); const component = this.getComponentDetails(inspectedPath, oldDetails); return trees ? [trees, component] : []; } // Recursively fills the components tree as a parsed version fillTree(appNode, treeNode, inspectedPathString, oldBranch) { const children = []; for (const [key, appChild] of Object.entries(appNode.children)) { let child = { name: appChild.component.constructor.name, key: key, depth: treeNode.depth + 1, toggled: true, selected: false, highlighted: false, }; child.path = treeNode.path.concat([child.key]); let oldChild = null; if (oldBranch) { const searchResult = oldBranch.children.find((o) => o.key === key); if (searchResult) { oldChild = searchResult; child.toggled = oldChild.toggled; } } const childPathString = child.path.join("/"); if (childPathString === inspectedPathString) { child.selected = true; } else if (childPathString.includes(inspectedPathString)) { child.highlighted = true; } child.children = this.fillTree(appChild, child, inspectedPathString, oldChild); children.push(child); } return children; } // Returns the path of the given component node getComponentPath(componentNode) { let path = []; if (componentNode.parentKey) { path = [componentNode.parentKey]; while (componentNode.parent && componentNode.parent.parentKey) { componentNode = componentNode.parent; path.unshift(componentNode.parentKey); } } path.unshift("root"); const appsArray = [...this.apps]; let index = appsArray.findIndex((app) => app === componentNode.app); path.unshift(index.toString()); return path; } // Store the object into a temp window variable and log it to the console sendObjectToConsole(path) { const obj = this.getObjectProperty(path); let index = 1; while (window["temp" + index] !== undefined) { index++; } window["temp" + index] = obj; console.log("temp" + index + " = ", window["temp" + index]); } // Inspect the DOM of the component in the elements tab of the devtools inspectComponentDOM(path) { const componentNode = this.getComponentNode(path); const elements = this.getDOMElementsRecursive(componentNode); if (IS_FIREFOX) { window.$temp = elements[0]; } else { inspect(elements[0]); } } // Inspect the DOM of the component in the elements tab of the devtools inspectComponentCompiledTemplate(path) { const componentNode = this.getComponentNode(path); const template = componentNode.component.constructor.template; if (IS_FIREFOX) { window.$temp = componentNode.app.templates[template]; } else { inspect(componentNode.app.templates[template]); } } // Inspect source code of the component (corresponds to inspecting its constructor) inspectComponentRawTemplate(path) { const componentNode = this.getComponentNode(path); const template = componentNode.component.constructor.template; const templateNode = componentNode.app.rawTemplates[template]; console.log(templateNode); } // Inspect source code of the function given by its path inspectFunctionSource(path) { const obj = this.getObjectProperty(path); if (IS_FIREFOX) { window.$temp = obj; } else { inspect(obj); } } targetName(target, node) { // check on component const { component } = node; for (const [key, value] of Object.entries(component)) { if (target === this.toRaw(value)) { return key; } } // check on props for (const [key, value] of Object.entries(component.props)) { if (target === this.toRaw(value)) { return `props.${key}`; } } return "[unknown]"; } /** * Removes subscriptions that are a direct child of another subscription: * they will be reachable from the top level by expanding observed keys. * * @param {ComponentNode} node * @returns {{ keys: PropertyKey[], target: unknown}[]} the top level * subscriptions of the node */ topLevelSubscriptions(node) { const subscriptions = node.subscriptions.map((s, index) => ({ ...s, index })); const topLevelValues = new Set(Object.values(node.component).map((o) => this.toRaw(o))); const toOmit = new Set( subscriptions .flatMap(({ keys, target }) => keys.map((k) => this.toRaw(target[k]))) .filter((obj) => !topLevelValues.has(obj)) ); return subscriptions.filter(({ target }) => !toOmit.has(target)); } addHighlightedKeys(child) { const { path } = child; const subscriptionIndex = path.findIndex((item) => typeof item !== "string"); if (path[subscriptionIndex]?.value === "subscriptions") { const node = this.getComponentNode(path.slice(0, subscriptionIndex)); // Add observed keys const targetToKeys = new Map(node.subscriptions.map(({ keys, target }) => [target, keys])); const target = this.getObjectProperty(child.path); child.keys = targetToKeys.get(target)?.map((k) => String(k)); } } } function checkOwlStatus() { let owlStatus = 2; if (!window.__OWL__DEVTOOLS_GLOBAL_HOOK__) { if (window.__OWL_DEVTOOLS__ || window.owl?.App) { owlStatus = 1; } else { // It seems that sending a 0 results in undefined at the other end for some reason owlStatus = -1; } } window.postMessage({ source: "owl-devtools", type: "owlStatus", data: owlStatus }); } if (!window.__OWL_DEVTOOLS__) { let val; const descriptor = Object.getOwnPropertyDescriptor(window, "__OWL_DEVTOOLS__") || { get() { return val; }, set(value) { val = value; }, }; Object.defineProperty(window, "__OWL_DEVTOOLS__", { get() { return descriptor.get.call(this); }, set(value) { descriptor.set.call(this, value); if (value?.Fiber !== undefined) { window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook(); } window.top.postMessage({ source: "owl-devtools", type: "FrameReady" }); checkOwlStatus(); }, }); // Do note that the reload message is not sent on the top window so that it is not intercepted when originating // from an iframe window.postMessage({ source: "owl-devtools", type: "Reload" }); } else if ( window.__OWL_DEVTOOLS__?.Fiber !== undefined && !window.__OWL__DEVTOOLS_GLOBAL_HOOK__ ) { window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook(); window.postMessage({ source: "owl-devtools", type: "Reload" }); } // Listener that checks whether owl is available on the page and if it has the right version window.addEventListener( "message", function (event) { if (event.data.source === "owl-devtools-background" && event.data.type === "checkOwlStatus") { checkOwlStatus(); } }, false ); checkOwlStatus(); // Indicates whether the scripts loaded successfully or not (only useful when loaded with eval in iframes) return window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined; })();