UNPKG

debug-server-next

Version:

Dev server for hippy-core.

480 lines (479 loc) 17.4 kB
/* * Copyright (C) 2009 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 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. * * Neither the name of Google Inc. 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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT * OWNER OR 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. */ import * as Host from '../../core/host/host.js'; import * as Root from '../../core/root/root.js'; import { ActionRegistry } from './ActionRegistry.js'; import { ShortcutRegistry } from './ShortcutRegistry.js'; import { SoftContextMenu } from './SoftContextMenu.js'; import { deepElementFromEvent } from './UIUtils.js'; export class Item { _type; _label; _disabled; _checked; _contextMenu; _id; _customElement; _shortcut; constructor(contextMenu, type, label, disabled, checked) { this._type = type; this._label = label; this._disabled = disabled; this._checked = checked; this._contextMenu = contextMenu; this._id = undefined; if (type === 'item' || type === 'checkbox') { this._id = contextMenu ? contextMenu._nextId() : 0; } } id() { if (this._id === undefined) { throw new Error('Tried to access a ContextMenu Item ID but none was set.'); } return this._id; } type() { return this._type; } isEnabled() { return !this._disabled; } setEnabled(enabled) { this._disabled = !enabled; } _buildDescriptor() { switch (this._type) { case 'item': { const result = { type: 'item', id: this._id, label: this._label, enabled: !this._disabled, checked: undefined, subItems: undefined, }; if (this._customElement) { const resultAsSoftContextMenuItem = result; resultAsSoftContextMenuItem.element = this._customElement; } if (this._shortcut) { const resultAsSoftContextMenuItem = result; resultAsSoftContextMenuItem.shortcut = this._shortcut; } return result; } case 'separator': { return { type: 'separator', id: undefined, label: undefined, enabled: undefined, checked: undefined, subItems: undefined, }; } case 'checkbox': { return { type: 'checkbox', id: this._id, label: this._label, checked: Boolean(this._checked), enabled: !this._disabled, subItems: undefined, }; } } throw new Error('Invalid item type:' + this._type); } setShortcut(shortcut) { this._shortcut = shortcut; } } export class Section { _contextMenu; _items; constructor(contextMenu) { this._contextMenu = contextMenu; this._items = []; } appendItem(label, handler, disabled) { const item = new Item(this._contextMenu, 'item', label, disabled); this._items.push(item); if (this._contextMenu) { this._contextMenu._setHandler(item.id(), handler); } return item; } appendCustomItem(element) { const item = new Item(this._contextMenu, 'item', '<custom>'); item._customElement = element; this._items.push(item); return item; } appendSeparator() { const item = new Item(this._contextMenu, 'separator'); this._items.push(item); return item; } appendAction(actionId, label, optional) { const action = ActionRegistry.instance().action(actionId); if (!action) { if (!optional) { console.error(`Action ${actionId} was not defined`); } return; } if (!label) { label = action.title(); } const result = this.appendItem(label, action.execute.bind(action)); const shortcut = ShortcutRegistry.instance().shortcutTitleForAction(actionId); if (shortcut) { result.setShortcut(shortcut); } } appendSubMenuItem(label, disabled) { const item = new SubMenu(this._contextMenu, label, disabled); item._init(); this._items.push(item); return item; } appendCheckboxItem(label, handler, checked, disabled) { const item = new Item(this._contextMenu, 'checkbox', label, disabled, checked); this._items.push(item); if (this._contextMenu) { this._contextMenu._setHandler(item.id(), handler); } return item; } } export class SubMenu extends Item { _sections; _sectionList; constructor(contextMenu, label, disabled) { super(contextMenu, 'subMenu', label, disabled); this._sections = new Map(); this._sectionList = []; } _init() { ContextMenu._groupWeights.forEach(name => this.section(name)); } section(name) { let section = name ? this._sections.get(name) : null; if (!section) { section = new Section(this._contextMenu); if (name) { this._sections.set(name, section); this._sectionList.push(section); } else { this._sectionList.splice(ContextMenu._groupWeights.indexOf('default'), 0, section); } } return section; } headerSection() { return this.section('header'); } newSection() { return this.section('new'); } revealSection() { return this.section('reveal'); } clipboardSection() { return this.section('clipboard'); } editSection() { return this.section('edit'); } debugSection() { return this.section('debug'); } viewSection() { return this.section('view'); } defaultSection() { return this.section('default'); } saveSection() { return this.section('save'); } footerSection() { return this.section('footer'); } _buildDescriptor() { const result = { type: 'subMenu', label: this._label, enabled: !this._disabled, subItems: [], id: undefined, checked: undefined, }; const nonEmptySections = this._sectionList.filter(section => Boolean(section._items.length)); for (const section of nonEmptySections) { for (const item of section._items) { if (!result.subItems) { result.subItems = []; } result.subItems.push(item._buildDescriptor()); } if (section !== nonEmptySections[nonEmptySections.length - 1]) { if (!result.subItems) { result.subItems = []; } result.subItems.push({ type: 'separator', id: undefined, subItems: undefined, checked: undefined, enabled: undefined, label: undefined, }); } } return result; } appendItemsAtLocation(location) { const items = getRegisteredItems(); items.sort((firstItem, secondItem) => { const order1 = firstItem.order || 0; const order2 = secondItem.order || 0; return order1 - order2; }); for (const item of items) { if (item.experiment && !Root.Runtime.experiments.isEnabled(item.experiment)) { continue; } const itemLocation = item.location; const actionId = item.actionId; if (!itemLocation || !itemLocation.startsWith(location + '/')) { continue; } const section = itemLocation.substr(location.length + 1); if (!section || section.includes('/')) { continue; } if (actionId) { this.section(section).appendAction(actionId); } } } static _uniqueSectionName = 0; } export class ContextMenu extends SubMenu { _contextMenu; _defaultSection; _pendingPromises; _pendingTargets; _event; _useSoftMenu; _x; _y; _handlers; _id; _softMenu; constructor(event, useSoftMenu, x, y) { super(null); const mouseEvent = event; this._contextMenu = this; super._init(); this._defaultSection = this.defaultSection(); this._pendingPromises = []; this._pendingTargets = []; this._event = mouseEvent; this._useSoftMenu = Boolean(useSoftMenu); this._x = x === undefined ? mouseEvent.x : x; this._y = y === undefined ? mouseEvent.y : y; this._handlers = new Map(); this._id = 0; const target = deepElementFromEvent(event); if (target) { this.appendApplicableItems(target); } } static initialize() { Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(Host.InspectorFrontendHostAPI.Events.SetUseSoftMenu, setUseSoftMenu); function setUseSoftMenu(event) { ContextMenu._useSoftMenu = event.data; } } static installHandler(doc) { doc.body.addEventListener('contextmenu', handler, false); function handler(event) { const contextMenu = new ContextMenu(event); contextMenu.show(); } } _nextId() { return this._id++; } async show() { ContextMenu._pendingMenu = this; this._event.consume(true); const loadedProviders = await Promise.all(this._pendingPromises); // After loading all providers, the contextmenu might be hidden again, so bail out. if (ContextMenu._pendingMenu !== this) { return; } ContextMenu._pendingMenu = null; for (let i = 0; i < loadedProviders.length; ++i) { const providers = loadedProviders[i]; const target = this._pendingTargets[i]; for (const provider of providers) { provider.appendApplicableItems(this._event, this, target); } } this._pendingPromises = []; this._pendingTargets = []; this._innerShow(); } discard() { if (this._softMenu) { this._softMenu.discard(); } } _innerShow() { const menuObject = this._buildMenuDescriptors(); const eventTarget = this._event.target; if (!eventTarget) { return; } const ownerDocument = eventTarget.ownerDocument; if (this._useSoftMenu || ContextMenu._useSoftMenu || Host.InspectorFrontendHost.InspectorFrontendHostInstance.isHostedMode()) { this._softMenu = new SoftContextMenu(menuObject, this._itemSelected.bind(this)); this._softMenu.show(ownerDocument, new AnchorBox(this._x, this._y, 0, 0)); } else { Host.InspectorFrontendHost.InspectorFrontendHostInstance.showContextMenuAtPoint(this._x, this._y, menuObject, ownerDocument); function listenToEvents() { Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(Host.InspectorFrontendHostAPI.Events.ContextMenuCleared, this._menuCleared, this); Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(Host.InspectorFrontendHostAPI.Events.ContextMenuItemSelected, this._onItemSelected, this); } // showContextMenuAtPoint call above synchronously issues a clear event for previous context menu (if any), // so we skip it before subscribing to the clear event. queueMicrotask(listenToEvents.bind(this)); } } setX(x) { this._x = x; } setY(y) { this._y = y; } _setHandler(id, handler) { if (handler) { this._handlers.set(id, handler); } } _buildMenuDescriptors() { return /** @type {!Array.<!Host.InspectorFrontendHostAPI.ContextMenuDescriptor|!SoftContextMenuDescriptor>} */ super ._buildDescriptor() .subItems; } _onItemSelected(event) { this._itemSelected(event.data); } _itemSelected(id) { const handler = this._handlers.get(id); if (handler) { handler.call(this); } this._menuCleared(); } _menuCleared() { Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.removeEventListener(Host.InspectorFrontendHostAPI.Events.ContextMenuCleared, this._menuCleared, this); Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.removeEventListener(Host.InspectorFrontendHostAPI.Events.ContextMenuItemSelected, this._onItemSelected, this); } containsTarget(target) { return this._pendingTargets.indexOf(target) >= 0; } appendApplicableItems(target) { this._pendingPromises.push(loadApplicableRegisteredProviders(target)); this._pendingTargets.push(target); } static _pendingMenu = null; static _useSoftMenu = false; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/naming-convention static _groupWeights = ['header', 'new', 'reveal', 'edit', 'clipboard', 'debug', 'view', 'default', 'save', 'footer']; } const registeredProviders = []; export function registerProvider(registration) { registeredProviders.push(registration); } async function loadApplicableRegisteredProviders(target) { return Promise.all(registeredProviders.filter(isProviderApplicableToContextTypes).map(registration => registration.loadProvider())); function isProviderApplicableToContextTypes(providerRegistration) { if (!Root.Runtime.Runtime.isDescriptorEnabled({ experiment: providerRegistration.experiment, condition: undefined })) { return false; } if (!providerRegistration.contextTypes) { return true; } for (const contextType of providerRegistration.contextTypes()) { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error if (target instanceof contextType) { return true; } } return false; } } const registeredItemsProviders = []; export function registerItem(registration) { registeredItemsProviders.push(registration); } export function maybeRemoveItem(registration) { const itemIndex = registeredItemsProviders.findIndex(item => item.actionId === registration.actionId && item.location === registration.location); if (itemIndex < 0) { return false; } registeredItemsProviders.splice(itemIndex, 1); return true; } function getRegisteredItems() { return registeredItemsProviders; } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export var ItemLocation; (function (ItemLocation) { ItemLocation["DEVICE_MODE_MENU_SAVE"] = "deviceModeMenu/save"; ItemLocation["MAIN_MENU"] = "mainMenu"; ItemLocation["MAIN_MENU_DEFAULT"] = "mainMenu/default"; ItemLocation["MAIN_MENU_FOOTER"] = "mainMenu/footer"; ItemLocation["MAIN_MENU_HELP_DEFAULT"] = "mainMenuHelp/default"; ItemLocation["NAVIGATOR_MENU_DEFAULT"] = "navigatorMenu/default"; ItemLocation["TIMELINE_MENU_OPEN"] = "timelineMenu/open"; })(ItemLocation || (ItemLocation = {}));