UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

1,653 lines (1,471 loc) 62.9 kB
/* * Copyright (C) 2011 Google Inc. All rights reserved. * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com). * Copyright (C) 2009 Joseph Pecoraro * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ UI.highlightedSearchResultClassName = 'highlighted-search-result'; UI.highlightedCurrentSearchResultClassName = 'current-search-result'; /** * @param {!Element} element * @param {?function(!MouseEvent): boolean} elementDragStart * @param {function(!MouseEvent)} elementDrag * @param {?function(!MouseEvent)} elementDragEnd * @param {?string} cursor * @param {?string=} hoverCursor * @param {number=} startDelay */ UI.installDragHandle = function( element, elementDragStart, elementDrag, elementDragEnd, cursor, hoverCursor, startDelay) { /** * @param {!Event} event */ function onMouseDown(event) { const dragHandler = new UI.DragHandler(); const dragStart = dragHandler.elementDragStart.bind( dragHandler, element, elementDragStart, elementDrag, elementDragEnd, cursor, event); if (startDelay) startTimer = setTimeout(dragStart, startDelay); else dragStart(); } function onMouseUp() { if (startTimer) clearTimeout(startTimer); startTimer = null; } let startTimer; element.addEventListener('mousedown', onMouseDown, false); if (startDelay) element.addEventListener('mouseup', onMouseUp, false); if (hoverCursor !== null) element.style.cursor = hoverCursor || cursor || ''; }; /** * @param {!Element} targetElement * @param {?function(!MouseEvent):boolean} elementDragStart * @param {function(!MouseEvent)} elementDrag * @param {?function(!MouseEvent)} elementDragEnd * @param {?string} cursor * @param {!Event} event */ UI.elementDragStart = function(targetElement, elementDragStart, elementDrag, elementDragEnd, cursor, event) { const dragHandler = new UI.DragHandler(); dragHandler.elementDragStart(targetElement, elementDragStart, elementDrag, elementDragEnd, cursor, event); }; /** * @unrestricted */ UI.DragHandler = class { constructor() { this._elementDragMove = this._elementDragMove.bind(this); this._elementDragEnd = this._elementDragEnd.bind(this); this._mouseOutWhileDragging = this._mouseOutWhileDragging.bind(this); } _createGlassPane() { this._glassPaneInUse = true; if (!UI.DragHandler._glassPaneUsageCount++) { UI.DragHandler._glassPane = new UI.GlassPane(); UI.DragHandler._glassPane.setPointerEventsBehavior(UI.GlassPane.PointerEventsBehavior.BlockedByGlassPane); UI.DragHandler._glassPane.show(UI.DragHandler._documentForMouseOut); } } _disposeGlassPane() { if (!this._glassPaneInUse) return; this._glassPaneInUse = false; if (--UI.DragHandler._glassPaneUsageCount) return; UI.DragHandler._glassPane.hide(); delete UI.DragHandler._glassPane; delete UI.DragHandler._documentForMouseOut; } /** * @param {!Element} targetElement * @param {?function(!MouseEvent):boolean} elementDragStart * @param {function(!MouseEvent)} elementDrag * @param {?function(!MouseEvent)} elementDragEnd * @param {?string} cursor * @param {!Event} event */ elementDragStart(targetElement, elementDragStart, elementDrag, elementDragEnd, cursor, event) { // Only drag upon left button. Right will likely cause a context menu. So will ctrl-click on mac. if (event.button || (Host.isMac() && event.ctrlKey)) return; if (this._elementDraggingEventListener) return; if (elementDragStart && !elementDragStart(/** @type {!MouseEvent} */ (event))) return; const targetDocument = event.target.ownerDocument; this._elementDraggingEventListener = elementDrag; this._elementEndDraggingEventListener = elementDragEnd; console.assert( (UI.DragHandler._documentForMouseOut || targetDocument) === targetDocument, 'Dragging on multiple documents.'); UI.DragHandler._documentForMouseOut = targetDocument; this._dragEventsTargetDocument = targetDocument; try { this._dragEventsTargetDocumentTop = targetDocument.defaultView.top.document; } catch (e) { this._dragEventsTargetDocumentTop = this._dragEventsTargetDocument; } targetDocument.addEventListener('mousemove', this._elementDragMove, true); targetDocument.addEventListener('mouseup', this._elementDragEnd, true); targetDocument.addEventListener('mouseout', this._mouseOutWhileDragging, true); if (targetDocument !== this._dragEventsTargetDocumentTop) this._dragEventsTargetDocumentTop.addEventListener('mouseup', this._elementDragEnd, true); if (typeof cursor === 'string') { this._restoreCursorAfterDrag = restoreCursor.bind(this, targetElement.style.cursor); targetElement.style.cursor = cursor; targetDocument.body.style.cursor = cursor; } /** * @param {string} oldCursor * @this {UI.DragHandler} */ function restoreCursor(oldCursor) { targetDocument.body.style.removeProperty('cursor'); targetElement.style.cursor = oldCursor; this._restoreCursorAfterDrag = null; } event.preventDefault(); } _mouseOutWhileDragging() { this._unregisterMouseOutWhileDragging(); this._createGlassPane(); } _unregisterMouseOutWhileDragging() { if (!UI.DragHandler._documentForMouseOut) return; UI.DragHandler._documentForMouseOut.removeEventListener('mouseout', this._mouseOutWhileDragging, true); } _unregisterDragEvents() { if (!this._dragEventsTargetDocument) return; this._dragEventsTargetDocument.removeEventListener('mousemove', this._elementDragMove, true); this._dragEventsTargetDocument.removeEventListener('mouseup', this._elementDragEnd, true); if (this._dragEventsTargetDocument !== this._dragEventsTargetDocumentTop) this._dragEventsTargetDocumentTop.removeEventListener('mouseup', this._elementDragEnd, true); delete this._dragEventsTargetDocument; delete this._dragEventsTargetDocumentTop; } /** * @param {!Event} event */ _elementDragMove(event) { if (event.buttons !== 1) { this._elementDragEnd(event); return; } if (this._elementDraggingEventListener(/** @type {!MouseEvent} */ (event))) this._cancelDragEvents(event); } /** * @param {!Event} event */ _cancelDragEvents(event) { this._unregisterDragEvents(); this._unregisterMouseOutWhileDragging(); if (this._restoreCursorAfterDrag) this._restoreCursorAfterDrag(); this._disposeGlassPane(); delete this._elementDraggingEventListener; delete this._elementEndDraggingEventListener; } /** * @param {!Event} event */ _elementDragEnd(event) { const elementDragEnd = this._elementEndDraggingEventListener; this._cancelDragEvents(/** @type {!MouseEvent} */ (event)); event.preventDefault(); if (elementDragEnd) elementDragEnd(/** @type {!MouseEvent} */ (event)); } }; UI.DragHandler._glassPaneUsageCount = 0; /** * @param {?Node=} node * @return {boolean} */ UI.isBeingEdited = function(node) { if (!node || node.nodeType !== Node.ELEMENT_NODE) return false; let element = /** {!Element} */ (node); if (element.classList.contains('text-prompt') || element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') return true; if (!UI.__editingCount) return false; while (element) { if (element.__editing) return true; element = element.parentElementOrShadowHost(); } return false; }; /** * @return {boolean} * @suppressGlobalPropertiesCheck */ UI.isEditing = function() { if (UI.__editingCount) return true; const focused = document.deepActiveElement(); if (!focused) return false; return focused.classList.contains('text-prompt') || focused.nodeName === 'INPUT' || focused.nodeName === 'TEXTAREA'; }; /** * @param {!Element} element * @param {boolean} value * @return {boolean} */ UI.markBeingEdited = function(element, value) { if (value) { if (element.__editing) return false; element.classList.add('being-edited'); element.__editing = true; UI.__editingCount = (UI.__editingCount || 0) + 1; } else { if (!element.__editing) return false; element.classList.remove('being-edited'); delete element.__editing; --UI.__editingCount; } return true; }; // Avoids Infinity, NaN, and scientific notation (e.g. 1e20), see crbug.com/81165. UI._numberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/; UI.StyleValueDelimiters = ' \xA0\t\n"\':;,/()'; /** * @param {!Event} event * @return {?string} */ UI._valueModificationDirection = function(event) { let direction = null; if (event.type === 'mousewheel') { // When shift is pressed while spinning mousewheel, delta comes as wheelDeltaX. if (event.wheelDeltaY > 0 || event.wheelDeltaX > 0) direction = 'Up'; else if (event.wheelDeltaY < 0 || event.wheelDeltaX < 0) direction = 'Down'; } else { if (event.key === 'ArrowUp' || event.key === 'PageUp') direction = 'Up'; else if (event.key === 'ArrowDown' || event.key === 'PageDown') direction = 'Down'; } return direction; }; /** * @param {string} hexString * @param {!Event} event * @return {?string} */ UI._modifiedHexValue = function(hexString, event) { const direction = UI._valueModificationDirection(event); if (!direction) return null; const mouseEvent = /** @type {!MouseEvent} */ (event); const number = parseInt(hexString, 16); if (isNaN(number) || !isFinite(number)) return null; const hexStrLen = hexString.length; const channelLen = hexStrLen / 3; // Colors are either rgb or rrggbb. if (channelLen !== 1 && channelLen !== 2) return null; // Precision modifier keys work with both mousewheel and up/down keys. // When ctrl is pressed, increase R by 1. // When shift is pressed, increase G by 1. // When alt is pressed, increase B by 1. // If no shortcut keys are pressed then increase hex value by 1. // Keys can be pressed together to increase RGB channels. e.g trying different shades. let delta = 0; if (UI.KeyboardShortcut.eventHasCtrlOrMeta(mouseEvent)) delta += Math.pow(16, channelLen * 2); if (mouseEvent.shiftKey) delta += Math.pow(16, channelLen); if (mouseEvent.altKey) delta += 1; if (delta === 0) delta = 1; if (direction === 'Down') delta *= -1; // Increase hex value by 1 and clamp from 0 ... maxValue. const maxValue = Math.pow(16, hexStrLen) - 1; const result = Number.constrain(number + delta, 0, maxValue); // Ensure the result length is the same as the original hex value. let resultString = result.toString(16).toUpperCase(); for (let i = 0, lengthDelta = hexStrLen - resultString.length; i < lengthDelta; ++i) resultString = '0' + resultString; return resultString; }; /** * @param {number} number * @param {!Event} event * @param {number=} modifierMultiplier * @return {?number} */ UI._modifiedFloatNumber = function(number, event, modifierMultiplier) { const direction = UI._valueModificationDirection(event); if (!direction) return null; const mouseEvent = /** @type {!MouseEvent} */ (event); // Precision modifier keys work with both mousewheel and up/down keys. // When ctrl is pressed, increase by 100. // When shift is pressed, increase by 10. // When alt is pressed, increase by 0.1. // Otherwise increase by 1. let delta = 1; if (UI.KeyboardShortcut.eventHasCtrlOrMeta(mouseEvent)) delta = 100; else if (mouseEvent.shiftKey) delta = 10; else if (mouseEvent.altKey) delta = 0.1; if (direction === 'Down') delta *= -1; if (modifierMultiplier) delta *= modifierMultiplier; // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns. // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1. const result = Number((number + delta).toFixed(6)); if (!String(result).match(UI._numberRegex)) return null; return result; }; /** * @param {string} wordString * @param {!Event} event * @param {function(string, number, string):string=} customNumberHandler * @return {?string} */ UI.createReplacementString = function(wordString, event, customNumberHandler) { let prefix; let suffix; let number; let replacementString = null; let matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString); if (matches && matches.length) { prefix = matches[1]; suffix = matches[3]; number = UI._modifiedHexValue(matches[2], event); if (number !== null) replacementString = prefix + number + suffix; } else { matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString); if (matches && matches.length) { prefix = matches[1]; suffix = matches[3]; number = UI._modifiedFloatNumber(parseFloat(matches[2]), event); if (number !== null) { replacementString = customNumberHandler ? customNumberHandler(prefix, number, suffix) : prefix + number + suffix; } } } return replacementString; }; /** * @param {!Event} event * @param {!Element} element * @param {function(string,string)=} finishHandler * @param {function(string)=} suggestionHandler * @param {function(string, number, string):string=} customNumberHandler * @return {boolean} */ UI.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler) { /** * @return {?Range} * @suppressGlobalPropertiesCheck */ function createRange() { return document.createRange(); } const arrowKeyOrMouseWheelEvent = (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.type === 'mousewheel'); const pageKeyPressed = (event.key === 'PageUp' || event.key === 'PageDown'); if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed) return false; const selection = element.getComponentSelection(); if (!selection.rangeCount) return false; const selectionRange = selection.getRangeAt(0); if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element)) return false; const originalValue = element.textContent; const wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, UI.StyleValueDelimiters, element); const wordString = wordRange.toString(); if (suggestionHandler && suggestionHandler(wordString)) return false; const replacementString = UI.createReplacementString(wordString, event, customNumberHandler); if (replacementString) { const replacementTextNode = createTextNode(replacementString); wordRange.deleteContents(); wordRange.insertNode(replacementTextNode); const finalSelectionRange = createRange(); finalSelectionRange.setStart(replacementTextNode, 0); finalSelectionRange.setEnd(replacementTextNode, replacementString.length); selection.removeAllRanges(); selection.addRange(finalSelectionRange); event.handled = true; event.preventDefault(); if (finishHandler) finishHandler(originalValue, replacementString); return true; } return false; }; /** * @param {number} ms * @param {number=} precision * @return {string} */ Number.preciseMillisToString = function(ms, precision) { precision = precision || 0; const format = '%.' + precision + 'f\xa0ms'; return Common.UIString(format, ms); }; /** @type {!Common.UIStringFormat} */ UI._microsFormat = new Common.UIStringFormat('%.0f\xa0\u03bcs'); /** @type {!Common.UIStringFormat} */ UI._subMillisFormat = new Common.UIStringFormat('%.2f\xa0ms'); /** @type {!Common.UIStringFormat} */ UI._millisFormat = new Common.UIStringFormat('%.0f\xa0ms'); /** @type {!Common.UIStringFormat} */ UI._secondsFormat = new Common.UIStringFormat('%.2f\xa0s'); /** @type {!Common.UIStringFormat} */ UI._minutesFormat = new Common.UIStringFormat('%.1f\xa0min'); /** @type {!Common.UIStringFormat} */ UI._hoursFormat = new Common.UIStringFormat('%.1f\xa0hrs'); /** @type {!Common.UIStringFormat} */ UI._daysFormat = new Common.UIStringFormat('%.1f\xa0days'); /** * @param {number} ms * @param {boolean=} higherResolution * @return {string} */ Number.millisToString = function(ms, higherResolution) { if (!isFinite(ms)) return '-'; if (ms === 0) return '0'; if (higherResolution && ms < 0.1) return UI._microsFormat.format(ms * 1000); if (higherResolution && ms < 1000) return UI._subMillisFormat.format(ms); if (ms < 1000) return UI._millisFormat.format(ms); const seconds = ms / 1000; if (seconds < 60) return UI._secondsFormat.format(seconds); const minutes = seconds / 60; if (minutes < 60) return UI._minutesFormat.format(minutes); const hours = minutes / 60; if (hours < 24) return UI._hoursFormat.format(hours); const days = hours / 24; return UI._daysFormat.format(days); }; /** * @param {number} seconds * @param {boolean=} higherResolution * @return {string} */ Number.secondsToString = function(seconds, higherResolution) { if (!isFinite(seconds)) return '-'; return Number.millisToString(seconds * 1000, higherResolution); }; /** * @param {number} bytes * @return {string} */ Number.bytesToString = function(bytes) { if (bytes < 1024) return Common.UIString('%.0f\xa0B', bytes); const kilobytes = bytes / 1024; if (kilobytes < 100) return Common.UIString('%.1f\xa0KB', kilobytes); if (kilobytes < 1024) return Common.UIString('%.0f\xa0KB', kilobytes); const megabytes = kilobytes / 1024; if (megabytes < 100) return Common.UIString('%.1f\xa0MB', megabytes); else return Common.UIString('%.0f\xa0MB', megabytes); }; /** * @param {number} num * @return {string} */ Number.withThousandsSeparator = function(num) { let str = num + ''; const re = /(\d+)(\d{3})/; while (str.match(re)) str = str.replace(re, '$1\xa0$2'); // \xa0 is a non-breaking space return str; }; /** * @param {string} format * @param {?ArrayLike} substitutions * @return {!Element} */ UI.formatLocalized = function(format, substitutions) { const formatters = {s: substitution => substitution}; /** * @param {!Element} a * @param {string|!Element} b * @return {!Element} */ function append(a, b) { a.appendChild(typeof b === 'string' ? createTextNode(b) : b); return a; } return String.format(Common.UIString(format), substitutions, formatters, createElement('span'), append) .formattedResult; }; /** * @return {string} */ UI.openLinkExternallyLabel = function() { return Common.UIString('Open in new tab'); }; /** * @return {string} */ UI.copyLinkAddressLabel = function() { return Common.UIString('Copy link address'); }; /** * @return {string} */ UI.anotherProfilerActiveLabel = function() { return Common.UIString('Another profiler is already active'); }; /** * @param {string|undefined} description * @return {string} */ UI.asyncStackTraceLabel = function(description) { if (description) { if (description === 'Promise.resolve') description = Common.UIString('Promise resolved'); else if (description === 'Promise.reject') description = Common.UIString('Promise rejected'); return description + ' ' + Common.UIString('(async)'); } return Common.UIString('Async Call'); }; /** * @param {!Element} element */ UI.installComponentRootStyles = function(element) { UI.appendStyle(element, 'ui/inspectorCommon.css'); UI.themeSupport.injectHighlightStyleSheets(element); UI.themeSupport.injectCustomStyleSheets(element); element.classList.add('platform-' + Host.platform()); // Detect overlay scrollbar enable by checking for nonzero scrollbar width. if (!Host.isMac() && UI.measuredScrollbarWidth(element.ownerDocument) === 0) element.classList.add('overlay-scrollbar-enabled'); }; /** * @param {?Document} document * @return {number} */ UI.measuredScrollbarWidth = function(document) { if (typeof UI._measuredScrollbarWidth === 'number') return UI._measuredScrollbarWidth; if (!document) return 16; const scrollDiv = document.createElement('div'); scrollDiv.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll;'); document.body.appendChild(scrollDiv); UI._measuredScrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; document.body.removeChild(scrollDiv); return UI._measuredScrollbarWidth; }; /** * @param {!Element} element * @param {string=} cssFile * @return {!DocumentFragment} */ UI.createShadowRootWithCoreStyles = function(element, cssFile) { const shadowRoot = element.attachShadow({ mode: 'open' }); UI.appendStyle(shadowRoot, 'ui/inspectorCommon.css'); UI.themeSupport.injectHighlightStyleSheets(shadowRoot); UI.themeSupport.injectCustomStyleSheets(shadowRoot); if (cssFile) UI.appendStyle(shadowRoot, cssFile); shadowRoot.addEventListener('focus', UI._focusChanged.bind(UI), true); return shadowRoot; }; /** * @param {!Document} document * @param {!Event} event */ UI._windowFocused = function(document, event) { if (event.target.document.nodeType === Node.DOCUMENT_NODE) document.body.classList.remove('inactive'); UI._keyboardFocus = true; const listener = () => { const activeElement = document.deepActiveElement(); if (activeElement) activeElement.removeAttribute('data-keyboard-focus'); UI._keyboardFocus = false; }; document.defaultView.requestAnimationFrame(() => { UI._keyboardFocus = false; document.removeEventListener('mousedown', listener, true); }); document.addEventListener('mousedown', listener, true); }; /** * @param {!Document} document * @param {!Event} event */ UI._windowBlurred = function(document, event) { if (event.target.document.nodeType === Node.DOCUMENT_NODE) document.body.classList.add('inactive'); }; /** * @param {!Event} event */ UI._focusChanged = function(event) { const document = event.target && event.target.ownerDocument; const element = document ? document.deepActiveElement() : null; UI.Widget.focusWidgetForNode(element); UI.XWidget.focusWidgetForNode(element); if (!UI._keyboardFocus) return; element.setAttribute('data-keyboard-focus', 'true'); element.addEventListener('blur', () => element.removeAttribute('data-keyboard-focus'), {once: true, capture: true}); }; /** * @unrestricted */ UI.ElementFocusRestorer = class { /** * @param {!Element} element */ constructor(element) { this._element = element; this._previous = element.ownerDocument.deepActiveElement(); element.focus(); } restore() { if (!this._element) return; if (this._element.hasFocus() && this._previous) this._previous.focus(); this._previous = null; this._element = null; } }; /** * @param {!Element} element * @param {number} offset * @param {number} length * @param {!Array.<!Object>=} domChanges * @return {?Element} */ UI.highlightSearchResult = function(element, offset, length, domChanges) { const result = UI.highlightSearchResults(element, [new TextUtils.SourceRange(offset, length)], domChanges); return result.length ? result[0] : null; }; /** * @param {!Element} element * @param {!Array.<!TextUtils.SourceRange>} resultRanges * @param {!Array.<!Object>=} changes * @return {!Array.<!Element>} */ UI.highlightSearchResults = function(element, resultRanges, changes) { return UI.highlightRangesWithStyleClass(element, resultRanges, UI.highlightedSearchResultClassName, changes); }; /** * @param {!Element} element * @param {string} className */ UI.runCSSAnimationOnce = function(element, className) { function animationEndCallback() { element.classList.remove(className); element.removeEventListener('webkitAnimationEnd', animationEndCallback, false); } if (element.classList.contains(className)) element.classList.remove(className); element.addEventListener('webkitAnimationEnd', animationEndCallback, false); element.classList.add(className); }; /** * @param {!Element} element * @param {!Array.<!TextUtils.SourceRange>} resultRanges * @param {string} styleClass * @param {!Array.<!Object>=} changes * @return {!Array.<!Element>} */ UI.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes) { changes = changes || []; const highlightNodes = []; const textNodes = element.childTextNodes(); const lineText = textNodes .map(function(node) { return node.textContent; }) .join(''); const ownerDocument = element.ownerDocument; if (textNodes.length === 0) return highlightNodes; const nodeRanges = []; let rangeEndOffset = 0; for (let i = 0; i < textNodes.length; ++i) { const range = {}; range.offset = rangeEndOffset; range.length = textNodes[i].textContent.length; rangeEndOffset = range.offset + range.length; nodeRanges.push(range); } let startIndex = 0; for (let i = 0; i < resultRanges.length; ++i) { const startOffset = resultRanges[i].offset; const endOffset = startOffset + resultRanges[i].length; while (startIndex < textNodes.length && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset) startIndex++; let endIndex = startIndex; while (endIndex < textNodes.length && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset) endIndex++; if (endIndex === textNodes.length) break; const highlightNode = ownerDocument.createElement('span'); highlightNode.className = styleClass; highlightNode.textContent = lineText.substring(startOffset, endOffset); const lastTextNode = textNodes[endIndex]; const lastText = lastTextNode.textContent; lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset); changes.push({node: lastTextNode, type: 'changed', oldText: lastText, newText: lastTextNode.textContent}); if (startIndex === endIndex) { lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode); changes.push({node: highlightNode, type: 'added', nextSibling: lastTextNode, parent: lastTextNode.parentElement}); highlightNodes.push(highlightNode); const prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset)); lastTextNode.parentElement.insertBefore(prefixNode, highlightNode); changes.push({node: prefixNode, type: 'added', nextSibling: highlightNode, parent: lastTextNode.parentElement}); } else { const firstTextNode = textNodes[startIndex]; const firstText = firstTextNode.textContent; const anchorElement = firstTextNode.nextSibling; firstTextNode.parentElement.insertBefore(highlightNode, anchorElement); changes.push( {node: highlightNode, type: 'added', nextSibling: anchorElement, parent: firstTextNode.parentElement}); highlightNodes.push(highlightNode); firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset); changes.push({node: firstTextNode, type: 'changed', oldText: firstText, newText: firstTextNode.textContent}); for (let j = startIndex + 1; j < endIndex; j++) { const textNode = textNodes[j]; const text = textNode.textContent; textNode.textContent = ''; changes.push({node: textNode, type: 'changed', oldText: text, newText: textNode.textContent}); } } startIndex = endIndex; nodeRanges[startIndex].offset = endOffset; nodeRanges[startIndex].length = lastTextNode.textContent.length; } return highlightNodes; }; UI.applyDomChanges = function(domChanges) { for (let i = 0, size = domChanges.length; i < size; ++i) { const entry = domChanges[i]; switch (entry.type) { case 'added': entry.parent.insertBefore(entry.node, entry.nextSibling); break; case 'changed': entry.node.textContent = entry.newText; break; } } }; UI.revertDomChanges = function(domChanges) { for (let i = domChanges.length - 1; i >= 0; --i) { const entry = domChanges[i]; switch (entry.type) { case 'added': entry.node.remove(); break; case 'changed': entry.node.textContent = entry.oldText; break; } } }; /** * @param {!Element} element * @param {?Element=} containerElement * @return {!UI.Size} */ UI.measurePreferredSize = function(element, containerElement) { const oldParent = element.parentElement; const oldNextSibling = element.nextSibling; containerElement = containerElement || element.ownerDocument.body; containerElement.appendChild(element); element.positionAt(0, 0); const result = element.getBoundingClientRect(); element.positionAt(undefined, undefined); if (oldParent) oldParent.insertBefore(element, oldNextSibling); else element.remove(); return new UI.Size(result.width, result.height); }; /** * @unrestricted */ UI.InvokeOnceHandlers = class { /** * @param {boolean} autoInvoke */ constructor(autoInvoke) { this._handlers = null; this._autoInvoke = autoInvoke; } /** * @param {!Object} object * @param {function()} method */ add(object, method) { if (!this._handlers) { this._handlers = new Map(); if (this._autoInvoke) this.scheduleInvoke(); } let methods = this._handlers.get(object); if (!methods) { methods = new Set(); this._handlers.set(object, methods); } methods.add(method); } /** * @suppressGlobalPropertiesCheck */ scheduleInvoke() { if (this._handlers) requestAnimationFrame(this._invoke.bind(this)); } _invoke() { const handlers = this._handlers; this._handlers = null; const keys = handlers.keysArray(); for (let i = 0; i < keys.length; ++i) { const object = keys[i]; const methods = handlers.get(object).valuesArray(); for (let j = 0; j < methods.length; ++j) methods[j].call(object); } } }; UI._coalescingLevel = 0; UI._postUpdateHandlers = null; UI.startBatchUpdate = function() { if (!UI._coalescingLevel++) UI._postUpdateHandlers = new UI.InvokeOnceHandlers(false); }; UI.endBatchUpdate = function() { if (--UI._coalescingLevel) return; UI._postUpdateHandlers.scheduleInvoke(); UI._postUpdateHandlers = null; }; /** * @param {!Object} object * @param {function()} method */ UI.invokeOnceAfterBatchUpdate = function(object, method) { if (!UI._postUpdateHandlers) UI._postUpdateHandlers = new UI.InvokeOnceHandlers(true); UI._postUpdateHandlers.add(object, method); }; /** * @param {!Window} window * @param {!Function} func * @param {!Array.<{from:number, to:number}>} params * @param {number} duration * @param {function()=} animationComplete * @return {function()} */ UI.animateFunction = function(window, func, params, duration, animationComplete) { const start = window.performance.now(); let raf = window.requestAnimationFrame(animationStep); function animationStep(timestamp) { const progress = Number.constrain((timestamp - start) / duration, 0, 1); func(...params.map(p => p.from + (p.to - p.from) * progress)); if (progress < 1) raf = window.requestAnimationFrame(animationStep); else if (animationComplete) animationComplete(); } return () => window.cancelAnimationFrame(raf); }; /** * @unrestricted */ UI.LongClickController = class extends Common.Object { /** * @param {!Element} element * @param {function(!Event)} callback */ constructor(element, callback) { super(); this._element = element; this._callback = callback; this._enable(); } reset() { if (this._longClickInterval) { clearInterval(this._longClickInterval); delete this._longClickInterval; } } _enable() { if (this._longClickData) return; const boundMouseDown = mouseDown.bind(this); const boundMouseUp = mouseUp.bind(this); const boundReset = this.reset.bind(this); this._element.addEventListener('mousedown', boundMouseDown, false); this._element.addEventListener('mouseout', boundReset, false); this._element.addEventListener('mouseup', boundMouseUp, false); this._element.addEventListener('click', boundReset, true); this._longClickData = {mouseUp: boundMouseUp, mouseDown: boundMouseDown, reset: boundReset}; /** * @param {!Event} e * @this {UI.LongClickController} */ function mouseDown(e) { if (e.which !== 1) return; const callback = this._callback; this._longClickInterval = setTimeout(callback.bind(null, e), 200); } /** * @param {!Event} e * @this {UI.LongClickController} */ function mouseUp(e) { if (e.which !== 1) return; this.reset(); } } dispose() { if (!this._longClickData) return; this._element.removeEventListener('mousedown', this._longClickData.mouseDown, false); this._element.removeEventListener('mouseout', this._longClickData.reset, false); this._element.removeEventListener('mouseup', this._longClickData.mouseUp, false); this._element.addEventListener('click', this._longClickData.reset, true); delete this._longClickData; } }; /** * @param {!Document} document * @param {!Common.Setting} themeSetting */ UI.initializeUIUtils = function(document, themeSetting) { document.body.classList.toggle('inactive', !document.hasFocus()); document.defaultView.addEventListener('focus', UI._windowFocused.bind(UI, document), false); document.defaultView.addEventListener('blur', UI._windowBlurred.bind(UI, document), false); document.addEventListener('focus', UI._focusChanged.bind(UI), true); document.addEventListener('keydown', event => { UI._keyboardFocus = true; document.defaultView.requestAnimationFrame(() => void(UI._keyboardFocus = false)); }, true); if (!UI.themeSupport) UI.themeSupport = new UI.ThemeSupport(themeSetting); UI.themeSupport.applyTheme(document); const body = /** @type {!Element} */ (document.body); UI.appendStyle(body, 'ui/inspectorStyle.css'); UI.GlassPane.setContainer(/** @type {!Element} */ (document.body)); }; /** * @param {string} name * @return {string} */ UI.beautifyFunctionName = function(name) { return name || Common.UIString('(anonymous)'); }; var customElements = {} /** * @param {string} localName * @param {string} typeExtension * @param {!Object} prototype * @return {function()} * @suppressGlobalPropertiesCheck * @template T */ UI.registerCustomElement = function(localName, typeExtension, prototype) { if (document.registerElement) { return document.registerElement(typeExtension, { prototype: Object.create(prototype), extends: localName }); } else { customElements[typeExtension] = { name: localName, prototype: prototype } function ElementConstructor () { const element = document.createElement(typeExtension) return element } Object.defineProperty(ElementConstructor.prototype = prototype, 'constructor', { value: ElementConstructor }) if (!document.__createElement) { document.__createElement = document.createElement Object.defineProperty(document, 'createElement', { configurable: true, value: function value (name, options) { var is = (options && options.is) || name var info = customElements[is] var element = document.__createElement(info ? info.name : is) if (info) { Object.setPrototypeOf(element, info.prototype) element.setAttribute('is', is) } if (typeof element.createdCallback === 'function') { element.createdCallback() } element.__setAttribute = element.setAttribute element.setAttribute = function(key, val) { if (typeof element.attributeChangedCallback === 'function') { var odlVal = element.getAttribute(key) element.__setAttribute(key, val) element.attributeChangedCallback(key, odlVal, val) } else { element.__setAttribute(key, val) } } return element } }) } return ElementConstructor } }; /** * @param {string} text * @param {function(!Event)=} clickHandler * @param {string=} className * @param {boolean=} primary * @return {!Element} */ UI.createTextButton = function(text, clickHandler, className, primary) { const element = createElementWithClass('button', className || '', 'text-button'); element.textContent = text; if (primary) element.classList.add('primary-button'); if (clickHandler) element.addEventListener('click', clickHandler, false); return element; }; /** * @param {string=} className * @param {string=} type * @return {!Element} */ UI.createInput = function(className, type) { const element = createElementWithClass('input', className || ''); element.spellcheck = false; element.classList.add('harmony-input'); if (type) element.type = type; return element; }; /** * @param {string} name * @param {string} title * @param {boolean=} checked * @return {!Element} */ UI.createRadioLabel = function(name, title, checked) { const element = createElement('label', 'dt-radio'); element.radioElement.name = name; element.radioElement.checked = !!checked; element.createTextChild(title); return element; }; /** * @param {string} title * @param {string} iconClass * @return {!Element} */ UI.createLabel = function(title, iconClass) { const element = createElement('label', 'dt-icon-label'); element.createChild('span').textContent = title; element.type = iconClass; return element; }; /** * @return {!Element} * @param {number} min * @param {number} max * @param {number} tabIndex */ UI.createSliderLabel = function(min, max, tabIndex) { const element = createElement('label', 'dt-slider'); element.sliderElement.min = min; element.sliderElement.max = max; element.sliderElement.step = 1; element.sliderElement.tabIndex = tabIndex; return element; }; /** * @param {!Node} node * @param {string} cssFile * @suppressGlobalPropertiesCheck */ UI.appendStyle = function(node, cssFile) { const content = Runtime.cachedResources[cssFile] || ''; if (!content) console.error(cssFile + ' not preloaded. Check module.json'); let styleElement = createElement('style'); styleElement.type = 'text/css'; styleElement.textContent = content; node.appendChild(styleElement); const themeStyleSheet = UI.themeSupport.themeStyleSheet(cssFile, content); if (themeStyleSheet) { styleElement = createElement('style'); styleElement.type = 'text/css'; styleElement.textContent = themeStyleSheet + '\n' + Runtime.resolveSourceURL(cssFile + '.theme'); node.appendChild(styleElement); } }; /** * @extends {HTMLLabelElement} */ UI.CheckboxLabel = class extends HTMLLabelElement { constructor() { super(); /** @type {!DocumentFragment} */ this._shadowRoot; /** @type {!HTMLInputElement} */ this.checkboxElement; /** @type {!Element} */ this.textElement; throw new Error('Checkbox must be created via factory method.'); } /** * @override */ createdCallback() { UI.CheckboxLabel._lastId = (UI.CheckboxLabel._lastId || 0) + 1; const id = 'ui-checkbox-label' + UI.CheckboxLabel._lastId; this._shadowRoot = UI.createShadowRootWithCoreStyles(this, 'ui/checkboxTextLabel.css'); this.checkboxElement = /** @type {!HTMLInputElement} */ (this._shadowRoot.createChild('input')); this.checkboxElement.type = 'checkbox'; this.checkboxElement.setAttribute('id', id); this.textElement = this._shadowRoot.createChild('label', 'dt-checkbox-text'); this.textElement.setAttribute('for', id); this._shadowRoot.createChild('slot'); } /** * @param {string=} title * @param {boolean=} checked * @param {string=} subtitle * @return {!UI.CheckboxLabel} */ static create(title, checked, subtitle) { if (!UI.CheckboxLabel._constructor) UI.CheckboxLabel._constructor = UI.registerCustomElement('span', 'dt-checkbox', UI.CheckboxLabel.prototype); const element = /** @type {!UI.CheckboxLabel} */ (new UI.CheckboxLabel._constructor()); element.checkboxElement.checked = !!checked; if (title !== undefined) { element.textElement.textContent = title; if (subtitle !== undefined) element.textElement.createChild('div', 'dt-checkbox-subtitle').textContent = subtitle; } return element; } /** * @param {string} color * @this {Element} */ set backgroundColor(color) { this.checkboxElement.classList.add('dt-checkbox-themed'); this.checkboxElement.style.backgroundColor = color; } /** * @param {string} color * @this {Element} */ set checkColor(color) { this.checkboxElement.classList.add('dt-checkbox-themed'); const stylesheet = createElement('style'); stylesheet.textContent = 'input.dt-checkbox-themed:checked:after { background-color: ' + color + '}'; this._shadowRoot.appendChild(stylesheet); } /** * @param {string} color * @this {Element} */ set borderColor(color) { this.checkboxElement.classList.add('dt-checkbox-themed'); this.checkboxElement.style.borderColor = color; } }; (function() { UI.registerCustomElement('div', 'text-button', { /** * @this {Element} */ createdCallback: function() { this.type = 'button'; const root = UI.createShadowRootWithCoreStyles(this, 'ui/textButton.css'); root.createChild('slot'); }, __proto__: HTMLButtonElement.prototype }); UI.registerCustomElement('span', 'dt-radio', { /** * @this {Element} */ createdCallback: function() { this.radioElement = this.createChild('input', 'dt-radio-button'); this.radioElement.slot = 'dt-radio-button'; this.radioElement.type = 'radio'; const root = UI.createShadowRootWithCoreStyles(this, 'ui/radioButton.css'); root.createChild('slot').name = 'dt-radio-button'; root.createChild('slot'); this.addEventListener('click', radioClickHandler, false); }, __proto__: HTMLLabelElement.prototype }); /** * @param {!Event} event * @suppressReceiverCheck * @this {Element} */ function radioClickHandler(event) { if (this.radioElement.checked || this.radioElement.disabled) return; this.radioElement.checked = true; this.radioElement.dispatchEvent(new Event('change')); } UI.registerCustomElement('span', 'dt-icon-label', { /** * @this {Element} */ createdCallback: function() { const root = UI.createShadowRootWithCoreStyles(this); this._iconElement = UI.Icon.create(); this._iconElement.style.setProperty('margin-right', '4px'); root.appendChild(this._iconElement); root.createChild('slot'); }, /** * @param {string} type * @this {Element} */ set type(type) { this._iconElement.setIconType(type); }, __proto__: HTMLLabelElement.prototype }); UI.registerCustomElement('span', 'dt-slider', { /** * @this {Element} */ createdCallback: function() { const root = UI.createShadowRootWithCoreStyles(this, 'ui/slider.css'); this.sliderElement = createElementWithClass('input', 'dt-range-input'); this.sliderElement.type = 'range'; root.appendChild(this.sliderElement); }, /** * @param {number} amount * @this {Element} */ set value(amount) { this.sliderElement.value = amount; }, /** * @this {Element} */ get value() { return this.sliderElement.value; }, __proto__: HTMLLabelElement.prototype }); UI.registerCustomElement('span', 'dt-small-bubble', { /** * @this {Element} */ createdCallback: function() { const root = UI.createShadowRootWithCoreStyles(this, 'ui/smallBubble.css'); this._textElement = root.createChild('div'); this._textElement.className = 'info'; this._textElement.createChild('slot'); }, /** * @param {string} type * @this {Element} */ set type(type) { this._textElement.className = type; }, __proto__: HTMLLabelElement.prototype }); UI.registerCustomElement('div', 'dt-close-button', { /** * @this {Element} */ createdCallback: function() { const root = UI.createShadowRootWithCoreStyles(this, 'ui/closeButton.css'); this._buttonElement = root.createChild('div', 'close-button'); const regularIcon = UI.Icon.create('smallicon-cross', 'default-icon'); this._hoverIcon = UI.Icon.create('mediumicon-red-cross-hover', 'hover-icon'); this._activeIcon = UI.Icon.create('mediumicon-red-cross-active', 'active-icon'); this._buttonElement.appendChild(regularIcon); this._buttonElement.appendChild(this._hoverIcon); this._buttonElement.appendChild(this._activeIcon); }, /** * @param {boolean} gray * @this {Element} */ set gray(gray) { if (gray) { this._hoverIcon.setIconType('mediumicon-gray-cross-hover'); this._activeIcon.setIconType('mediumicon-gray-cross-active'); } else { this._hoverIcon.setIconType('mediumicon-red-cross-hover'); this._activeIcon.setIconType('mediumicon-red-cross-active'); } }, __proto__: HTMLDivElement.prototype }); })(); /** * @param {!Element} input * @param {function(string)} apply * @param {function(string):boolean} validate * @param {boolean} numeric * @param {number=} modifierMultiplier * @return {function(string)} */ UI.bindInput = function(input, apply, validate, numeric, modifierMultiplier) { input.addEventListener('change', onChange, false); input.addEventListener('input', onInput, false); input.addEventListener('keydown', onKeyDown, false); input.addEventListener('focus', input.select.bind(input), false); function onInput() { input.classList.toggle('error-input', !validate(input.value)); } function onChange() { const valid = validate(input.value); input.classList.toggle('error-input', !valid); if (valid) apply(input.value); } /** * @param {!Event} event */ function onKeyDown(event) { if (isEnterKey(event)) { if (validate(input.value)) apply(input.value); event.preventDefault(); return; } if (!numeric) return; const value = UI._modifiedFloatNumber(parseFloat(input.value), event, modifierMultiplier); const stringValue = value ? String(value) : ''; if (!validate(stringValue) || !value) return; input.value = stringValue; apply(input.value); event.preventDefault(); } /** * @param {string} value */ function setValue(value) { if (value === input.value) return; const valid = validate(value); input.classList.toggle('error-input', !valid); input.value = value; } return setValue; }; /** * @param {!CanvasRenderingContext2D} context * @param {string} text * @param {number} maxWidth * @param {function(string, number):string} trimFunction * @return {string} */ UI.trimText = function(context, text, maxWidth, trimFunction) { const maxLength = 200; if (maxWidth <= 10) return ''; if (text.length > maxLength) text = trimFunction(text, maxLength); const textWidth = UI.measureTextWidth(context, text); if (textWidth <= maxWidth) return text; let l = 0; let r = text.length; let lv = 0; let rv = textWidth; while (l < r && lv !== rv && lv !== maxWidth) { const m = Math.ceil(l + (r - l) * (maxWidth - lv) / (rv - lv)); const mv = UI.measureTextWidth(context, trimFunction(text, m)); if (mv <= maxWidth) { l = m; lv = mv; } else { r = m - 1; rv = mv; } } text = trimFunction(text, l); return text !== '\u2026' ? text : ''; }; /** * @param {!CanvasRenderingContext2D} context * @param {string} text * @param {number} maxWidth * @return {string} */ UI.trimTextMiddle = function(context, text, maxWidth) { return UI.trimText(context, text, maxWidth, (text, width) =>