mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 21:54:47 +08:00
Screenshot: https://imgur.com/a/FUh1E91 Bug: 754216 Change-Id: Id7f04637f85ad776e4c61b0cedf500351723342d Reviewed-on: https://chromium-review.googlesource.com/1081677 Commit-Queue: Erik Luo <luoe@chromium.org> Reviewed-by: Dmitry Gozman <dgozman@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#564286} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: d96b9f46eafdd0145393b5fe3e188e9bedb2da72
2048 lines
60 KiB
JavaScript
2048 lines
60 KiB
JavaScript
/*
|
|
* 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.createShadowRoot();
|
|
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)');
|
|
};
|
|
|
|
/**
|
|
* @param {string} localName
|
|
* @param {string} typeExtension
|
|
* @param {!Object} prototype
|
|
* @return {function()}
|
|
* @suppressGlobalPropertiesCheck
|
|
* @template T
|
|
*/
|
|
UI.registerCustomElement = function(localName, typeExtension, prototype) {
|
|
return document.registerElement(typeExtension, {prototype: Object.create(prototype), extends: localName});
|
|
};
|
|
|
|
/**
|
|
* @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('content');
|
|
}
|
|
|
|
/**
|
|
* @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('label', '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('button', 'text-button', {
|
|
/**
|
|
* @this {Element}
|
|
*/
|
|
createdCallback: function() {
|
|
this.type = 'button';
|
|
const root = UI.createShadowRootWithCoreStyles(this, 'ui/textButton.css');
|
|
root.createChild('content');
|
|
},
|
|
|
|
__proto__: HTMLButtonElement.prototype
|
|
});
|
|
|
|
UI.registerCustomElement('label', 'dt-radio', {
|
|
/**
|
|
* @this {Element}
|
|
*/
|
|
createdCallback: function() {
|
|
this.radioElement = this.createChild('input', 'dt-radio-button');
|
|
this.radioElement.type = 'radio';
|
|
const root = UI.createShadowRootWithCoreStyles(this, 'ui/radioButton.css');
|
|
root.createChild('content').select = '.dt-radio-button';
|
|
root.createChild('content');
|
|
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('label', '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('content');
|
|
},
|
|
|
|
/**
|
|
* @param {string} type
|
|
* @this {Element}
|
|
*/
|
|
set type(type) {
|
|
this._iconElement.setIconType(type);
|
|
},
|
|
|
|
__proto__: HTMLLabelElement.prototype
|
|
});
|
|
|
|
UI.registerCustomElement('label', '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('label', '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('content');
|
|
},
|
|
|
|
/**
|
|
* @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) => text.trimMiddle(width));
|
|
};
|
|
|
|
/**
|
|
* @param {!CanvasRenderingContext2D} context
|
|
* @param {string} text
|
|
* @param {number} maxWidth
|
|
* @return {string}
|
|
*/
|
|
UI.trimTextEnd = function(context, text, maxWidth) {
|
|
return UI.trimText(context, text, maxWidth, (text, width) => text.trimEnd(width));
|
|
};
|
|
|
|
/**
|
|
* @param {!CanvasRenderingContext2D} context
|
|
* @param {string} text
|
|
* @return {number}
|
|
*/
|
|
UI.measureTextWidth = function(context, text) {
|
|
const maxCacheableLength = 200;
|
|
if (text.length > maxCacheableLength)
|
|
return context.measureText(text).width;
|
|
|
|
let widthCache = UI.measureTextWidth._textWidthCache;
|
|
if (!widthCache) {
|
|
widthCache = new Map();
|
|
UI.measureTextWidth._textWidthCache = widthCache;
|
|
}
|
|
const font = context.font;
|
|
let textWidths = widthCache.get(font);
|
|
if (!textWidths) {
|
|
textWidths = new Map();
|
|
widthCache.set(font, textWidths);
|
|
}
|
|
let width = textWidths.get(text);
|
|
if (!width) {
|
|
width = context.measureText(text).width;
|
|
textWidths.set(text, width);
|
|
}
|
|
return width;
|
|
};
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
UI.ThemeSupport = class {
|
|
/**
|
|
* @param {!Common.Setting} setting
|
|
*/
|
|
constructor(setting) {
|
|
this._themeName = setting.get() || 'default';
|
|
this._themableProperties = new Set([
|
|
'color', 'box-shadow', 'text-shadow', 'outline-color', 'background-image', 'background-color',
|
|
'border-left-color', 'border-right-color', 'border-top-color', 'border-bottom-color', '-webkit-border-image',
|
|
'fill', 'stroke'
|
|
]);
|
|
/** @type {!Map<string, string>} */
|
|
this._cachedThemePatches = new Map();
|
|
this._setting = setting;
|
|
this._customSheets = new Set();
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
hasTheme() {
|
|
return this._themeName !== 'default';
|
|
}
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
themeName() {
|
|
return this._themeName;
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
*/
|
|
injectHighlightStyleSheets(element) {
|
|
this._injectingStyleSheet = true;
|
|
UI.appendStyle(element, 'ui/inspectorSyntaxHighlight.css');
|
|
if (this._themeName === 'dark')
|
|
UI.appendStyle(element, 'ui/inspectorSyntaxHighlightDark.css');
|
|
this._injectingStyleSheet = false;
|
|
}
|
|
|
|
/**
|
|
* @param {!Element|!ShadowRoot} element
|
|
*/
|
|
injectCustomStyleSheets(element) {
|
|
for (const sheet of this._customSheets){
|
|
const styleElement = createElement('style');
|
|
styleElement.type = 'text/css';
|
|
styleElement.textContent = sheet;
|
|
element.appendChild(styleElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} sheetText
|
|
*/
|
|
addCustomStylesheet(sheetText) {
|
|
this._customSheets.add(sheetText);
|
|
}
|
|
|
|
/**
|
|
* @param {!Document} document
|
|
*/
|
|
applyTheme(document) {
|
|
if (!this.hasTheme())
|
|
return;
|
|
|
|
if (this._themeName === 'dark')
|
|
document.documentElement.classList.add('-theme-with-dark-background');
|
|
|
|
const styleSheets = document.styleSheets;
|
|
const result = [];
|
|
for (let i = 0; i < styleSheets.length; ++i)
|
|
result.push(this._patchForTheme(styleSheets[i].href, styleSheets[i]));
|
|
result.push('/*# sourceURL=inspector.css.theme */');
|
|
|
|
const styleElement = createElement('style');
|
|
styleElement.type = 'text/css';
|
|
styleElement.textContent = result.join('\n');
|
|
document.head.appendChild(styleElement);
|
|
}
|
|
|
|
/**
|
|
* @param {string} id
|
|
* @param {string} text
|
|
* @return {string}
|
|
* @suppressGlobalPropertiesCheck
|
|
*/
|
|
themeStyleSheet(id, text) {
|
|
if (!this.hasTheme() || this._injectingStyleSheet)
|
|
return '';
|
|
|
|
let patch = this._cachedThemePatches.get(id);
|
|
if (!patch) {
|
|
const styleElement = createElement('style');
|
|
styleElement.type = 'text/css';
|
|
styleElement.textContent = text;
|
|
document.body.appendChild(styleElement);
|
|
patch = this._patchForTheme(id, styleElement.sheet);
|
|
document.body.removeChild(styleElement);
|
|
}
|
|
return patch;
|
|
}
|
|
|
|
/**
|
|
* @param {string} id
|
|
* @param {!StyleSheet} styleSheet
|
|
* @return {string}
|
|
*/
|
|
_patchForTheme(id, styleSheet) {
|
|
const cached = this._cachedThemePatches.get(id);
|
|
if (cached)
|
|
return cached;
|
|
|
|
try {
|
|
const rules = styleSheet.cssRules;
|
|
const result = [];
|
|
for (let j = 0; j < rules.length; ++j) {
|
|
if (rules[j] instanceof CSSImportRule) {
|
|
result.push(this._patchForTheme(rules[j].styleSheet.href, rules[j].styleSheet));
|
|
continue;
|
|
}
|
|
const output = [];
|
|
const style = rules[j].style;
|
|
const selectorText = rules[j].selectorText;
|
|
for (let i = 0; style && i < style.length; ++i)
|
|
this._patchProperty(selectorText, style, style[i], output);
|
|
if (output.length)
|
|
result.push(rules[j].selectorText + '{' + output.join('') + '}');
|
|
}
|
|
|
|
const fullText = result.join('\n');
|
|
this._cachedThemePatches.set(id, fullText);
|
|
return fullText;
|
|
} catch (e) {
|
|
this._setting.set('default');
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} selectorText
|
|
* @param {!CSSStyleDeclaration} style
|
|
* @param {string} name
|
|
* @param {!Array<string>} output
|
|
*
|
|
* Theming API is primarily targeted at making dark theme look good.
|
|
* - If rule has ".-theme-preserve" in selector, it won't be affected.
|
|
* - One can create specializations for dark themes via body.-theme-with-dark-background selector in host context.
|
|
*/
|
|
_patchProperty(selectorText, style, name, output) {
|
|
if (!this._themableProperties.has(name))
|
|
return;
|
|
|
|
const value = style.getPropertyValue(name);
|
|
if (!value || value === 'none' || value === 'inherit' || value === 'initial' || value === 'transparent')
|
|
return;
|
|
if (name === 'background-image' && value.indexOf('gradient') === -1)
|
|
return;
|
|
|
|
if (selectorText.indexOf('-theme-') !== -1)
|
|
return;
|
|
|
|
let colorUsage = UI.ThemeSupport.ColorUsage.Unknown;
|
|
if (name.indexOf('background') === 0 || name.indexOf('border') === 0)
|
|
colorUsage |= UI.ThemeSupport.ColorUsage.Background;
|
|
if (name.indexOf('background') === -1)
|
|
colorUsage |= UI.ThemeSupport.ColorUsage.Foreground;
|
|
|
|
output.push(name);
|
|
output.push(':');
|
|
const items = value.replace(Common.Color.Regex, '\0$1\0').split('\0');
|
|
for (let i = 0; i < items.length; ++i)
|
|
output.push(this.patchColorText(items[i], colorUsage));
|
|
if (style.getPropertyPriority(name))
|
|
output.push(' !important');
|
|
output.push(';');
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @param {!UI.ThemeSupport.ColorUsage} colorUsage
|
|
* @return {string}
|
|
*/
|
|
patchColorText(text, colorUsage) {
|
|
const color = Common.Color.parse(text);
|
|
if (!color)
|
|
return text;
|
|
const outColor = this.patchColor(color, colorUsage);
|
|
let outText = outColor.asString(null);
|
|
if (!outText)
|
|
outText = outColor.asString(outColor.hasAlpha() ? Common.Color.Format.RGBA : Common.Color.Format.RGB);
|
|
return outText || text;
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Color} color
|
|
* @param {!UI.ThemeSupport.ColorUsage} colorUsage
|
|
* @return {!Common.Color}
|
|
*/
|
|
patchColor(color, colorUsage) {
|
|
const hsla = color.hsla();
|
|
this._patchHSLA(hsla, colorUsage);
|
|
const rgba = [];
|
|
Common.Color.hsl2rgb(hsla, rgba);
|
|
return new Common.Color(rgba, color.format());
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<number>} hsla
|
|
* @param {!UI.ThemeSupport.ColorUsage} colorUsage
|
|
*/
|
|
_patchHSLA(hsla, colorUsage) {
|
|
const hue = hsla[0];
|
|
const sat = hsla[1];
|
|
let lit = hsla[2];
|
|
const alpha = hsla[3];
|
|
|
|
switch (this._themeName) {
|
|
case 'dark':
|
|
const minCap = colorUsage & UI.ThemeSupport.ColorUsage.Background ? 0.14 : 0;
|
|
const maxCap = colorUsage & UI.ThemeSupport.ColorUsage.Foreground ? 0.9 : 1;
|
|
lit = 1 - lit;
|
|
if (lit < minCap * 2)
|
|
lit = minCap + lit / 2;
|
|
else if (lit > 2 * maxCap - 1)
|
|
lit = maxCap - 1 / 2 + lit / 2;
|
|
|
|
break;
|
|
}
|
|
hsla[0] = Number.constrain(hue, 0, 1);
|
|
hsla[1] = Number.constrain(sat, 0, 1);
|
|
hsla[2] = Number.constrain(lit, 0, 1);
|
|
hsla[3] = Number.constrain(alpha, 0, 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @enum {number}
|
|
*/
|
|
UI.ThemeSupport.ColorUsage = {
|
|
Unknown: 0,
|
|
Foreground: 1 << 0,
|
|
Background: 1 << 1,
|
|
};
|
|
|
|
/**
|
|
* @param {string} article
|
|
* @param {string} title
|
|
* @return {!Element}
|
|
*/
|
|
UI.createDocumentationLink = function(article, title) {
|
|
return UI.XLink.create('https://developers.google.com/web/tools/chrome-devtools/' + article, title);
|
|
};
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @return {!Promise<?Image>}
|
|
*/
|
|
UI.loadImage = function(url) {
|
|
return new Promise(fulfill => {
|
|
const image = new Image();
|
|
image.addEventListener('load', () => fulfill(image));
|
|
image.addEventListener('error', () => fulfill(null));
|
|
image.src = url;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {?string} data
|
|
* @return {!Promise<?Image>}
|
|
*/
|
|
UI.loadImageFromData = function(data) {
|
|
return data ? UI.loadImage('data:image/jpg;base64,' + data) : Promise.resolve(null);
|
|
};
|
|
|
|
/** @type {!UI.ThemeSupport} */
|
|
UI.themeSupport;
|
|
|
|
/**
|
|
* @param {function(!File)} callback
|
|
* @return {!Node}
|
|
*/
|
|
UI.createFileSelectorElement = function(callback) {
|
|
const fileSelectorElement = createElement('input');
|
|
fileSelectorElement.type = 'file';
|
|
fileSelectorElement.style.display = 'none';
|
|
fileSelectorElement.setAttribute('tabindex', -1);
|
|
fileSelectorElement.onchange = onChange;
|
|
function onChange(event) {
|
|
callback(fileSelectorElement.files[0]);
|
|
}
|
|
return fileSelectorElement;
|
|
};
|
|
|
|
/**
|
|
* @const
|
|
* @type {number}
|
|
*/
|
|
UI.MaxLengthForDisplayedURLs = 150;
|
|
|
|
UI.MessageDialog = class {
|
|
/**
|
|
* @param {string} message
|
|
* @param {!Document|!Element=} where
|
|
* @return {!Promise}
|
|
*/
|
|
static async show(message, where) {
|
|
const dialog = new UI.Dialog();
|
|
dialog.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
|
|
dialog.setDimmed(true);
|
|
const shadowRoot = UI.createShadowRootWithCoreStyles(dialog.contentElement, 'ui/confirmDialog.css');
|
|
const content = shadowRoot.createChild('div', 'widget');
|
|
await new Promise(resolve => {
|
|
const okButton = UI.createTextButton(Common.UIString('OK'), resolve, '', true);
|
|
content.createChild('div', 'message').createChild('span').textContent = message;
|
|
content.createChild('div', 'button').appendChild(okButton);
|
|
dialog.setOutsideClickCallback(event => {
|
|
event.consume();
|
|
resolve();
|
|
});
|
|
dialog.show(where);
|
|
okButton.focus();
|
|
});
|
|
dialog.hide();
|
|
}
|
|
};
|
|
|
|
UI.ConfirmDialog = class {
|
|
/**
|
|
* @param {string} message
|
|
* @param {!Document|!Element=} where
|
|
* @return {!Promise<boolean>}
|
|
*/
|
|
static async show(message, where) {
|
|
const dialog = new UI.Dialog();
|
|
dialog.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
|
|
dialog.setDimmed(true);
|
|
const shadowRoot = UI.createShadowRootWithCoreStyles(dialog.contentElement, 'ui/confirmDialog.css');
|
|
const content = shadowRoot.createChild('div', 'widget');
|
|
content.createChild('div', 'message').createChild('span').textContent = message;
|
|
const buttonsBar = content.createChild('div', 'button');
|
|
const result = await new Promise(resolve => {
|
|
buttonsBar.appendChild(UI.createTextButton(Common.UIString('OK'), () => resolve(true), '', true));
|
|
buttonsBar.appendChild(UI.createTextButton(Common.UIString('Cancel'), () => resolve(false)));
|
|
dialog.setOutsideClickCallback(event => {
|
|
event.consume();
|
|
resolve(false);
|
|
});
|
|
dialog.show(where);
|
|
});
|
|
dialog.hide();
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {!UI.ToolbarToggle} toolbarButton
|
|
* @return {!Element}
|
|
*/
|
|
UI.createInlineButton = function(toolbarButton) {
|
|
const element = createElement('span');
|
|
const shadowRoot = UI.createShadowRootWithCoreStyles(element, 'ui/inlineButton.css');
|
|
element.classList.add('inline-button');
|
|
const toolbar = new UI.Toolbar('');
|
|
toolbar.appendToolbarItem(toolbarButton);
|
|
shadowRoot.appendChild(toolbar.element);
|
|
return element;
|
|
};
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @param {number} maxLength
|
|
* @return {!DocumentFragment}
|
|
*/
|
|
UI.createExpandableText = function(text, maxLength) {
|
|
const fragment = createDocumentFragment();
|
|
fragment.textContent = text.slice(0, maxLength);
|
|
const hiddenText = text.slice(maxLength);
|
|
|
|
const expandButton = fragment.createChild('span', 'expandable-inline-button');
|
|
expandButton.setAttribute('data-text', ls`Show ${Number.withThousandsSeparator(hiddenText.length)} more`);
|
|
expandButton.addEventListener('click', () => {
|
|
if (expandButton.parentElement)
|
|
expandButton.parentElement.insertBefore(createTextNode(hiddenText), expandButton);
|
|
expandButton.remove();
|
|
});
|
|
|
|
const copyButton = fragment.createChild('span', 'expandable-inline-button');
|
|
copyButton.setAttribute('data-text', ls`Copy`);
|
|
copyButton.addEventListener('click', () => {
|
|
InspectorFrontendHost.copyText(text);
|
|
});
|
|
return fragment;
|
|
};
|