/* * Copyright (C) 2015 Apple 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: * 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. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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. */ WI.BreakpointPopover = class BreakpointPopover extends WI.Popover { constructor(delegate, breakpoint) { console.assert(!breakpoint || breakpoint instanceof WI.Breakpoint, breakpoint); super(delegate); this._breakpoint = breakpoint || null; console.assert(this._breakpoint?.constructor.ReferencePage || this.constructor.ReferencePage, "Should have a link to a reference page."); this._contentElement = null; this._conditionCodeMirror = null; this._ignoreCountInputElement = null; this._actionsContainerElement = null; this._actionViews = []; this._optionsRowElement = null; this._autoContinueCheckboxElement = null; this._targetElement = null; this.windowResizeHandler = this._presentOverTargetElement.bind(this); } // Static static show(breakpoint, targetElement) { const delegate = null; let popover; if (breakpoint instanceof WI.EventBreakpoint) popover = new WI.EventBreakpointPopover(delegate, breakpoint); else if (breakpoint instanceof WI.URLBreakpoint) popover = new WI.URLBreakpointPopover(delegate, breakpoint); else if (breakpoint instanceof WI.SymbolicBreakpoint) popover = new WI.SymbolicBreakpointPopover(delegate, breakpoint); else popover = new WI.BreakpointPopover(delegate, breakpoint); popover.show(targetElement); } static appendContextMenuItems(contextMenu, breakpoint, targetElement) { if (breakpoint.editable && targetElement) { contextMenu.appendItem(WI.UIString("Edit Breakpoint\u2026"), () => { WI.BreakpointPopover.show(breakpoint, targetElement); }); } if (!breakpoint.disabled) { contextMenu.appendItem(WI.UIString("Disable Breakpoint"), () => { breakpoint.disabled = !breakpoint.disabled; }); if (breakpoint.editable && breakpoint.autoContinue) { contextMenu.appendItem(WI.UIString("Cancel Automatic Continue"), () => { breakpoint.autoContinue = !breakpoint.autoContinue; }); } } else { contextMenu.appendItem(WI.UIString("Enable Breakpoint"), () => { breakpoint.disabled = !breakpoint.disabled; }); } if (breakpoint.editable && !breakpoint.autoContinue && !breakpoint.disabled && breakpoint.actions.length) { contextMenu.appendItem(WI.UIString("Set to Automatically Continue"), () => { breakpoint.autoContinue = !breakpoint.autoContinue; }); } if (breakpoint.removable) { contextMenu.appendItem(WI.UIString("Delete Breakpoint"), () => { breakpoint.remove(); }); } else { contextMenu.appendItem(WI.UIString("Reset Breakpoint", "Reset Breakpoint @ Breakpoint Context Menu", "Context menu action for resetting the breakpoint to its initial configuration."), () => { breakpoint.reset(); }); } if (breakpoint instanceof WI.JavaScriptBreakpoint && breakpoint.sourceCodeLocation.hasMappedLocation()) { contextMenu.appendSeparator(); contextMenu.appendItem(WI.UIString("Reveal in Original Resource"), () => { const options = { ignoreNetworkTab: true, ignoreSearchTab: true, }; WI.showOriginalOrFormattedSourceCodeLocation(breakpoint.sourceCodeLocation, options); }); } } // Public get breakpoint() { return this._breakpoint; } show(targetElement) { this._targetElement = targetElement; this._contentElement = document.createElement("div"); this._contentElement.classList.add("edit-breakpoint-popover-content"); if (this._breakpoint) { let toggleLabelElement = this._contentElement.appendChild(document.createElement("label")); toggleLabelElement.className = "toggle"; let toggleCheckboxElement = toggleLabelElement.appendChild(document.createElement("input")); toggleCheckboxElement.type = "checkbox"; toggleCheckboxElement.checked = !this._breakpoint.disabled; toggleCheckboxElement.addEventListener("change", this._handleEnabledCheckboxChange.bind(this)); toggleLabelElement.appendChild(document.createTextNode(this._breakpoint.displayName)); } this._tableElement = this._contentElement.appendChild(document.createElement("table")); if (!this._breakpoint) this.populateContent(); if (this._breakpoint?.editable || this.constructor.supportsEditing) { let conditionLabelElement = document.createElement("label"); conditionLabelElement.textContent = WI.UIString("Condition"); let conditionEditorElement = document.createElement("div"); conditionEditorElement.classList.add("editor", WI.SyntaxHighlightedStyleClassName); this._conditionCodeMirror = WI.CodeMirrorEditor.create(conditionEditorElement, { extraKeys: {Tab: false}, lineWrapping: false, mode: "text/javascript", matchBrackets: true, placeholder: WI.UIString("Conditional expression"), scrollbarStyle: null, value: this._breakpoint?.condition || "", }); let conditionCodeMirrorInputField = this._conditionCodeMirror.getInputField(); conditionCodeMirrorInputField.id = "edit-breakpoint-popover-content-condition"; conditionLabelElement.setAttribute("for", conditionCodeMirrorInputField.id); this.addRow("condition", conditionLabelElement, conditionEditorElement); this._conditionCodeMirror.addKeyMap({ "Enter": () => { this.dismiss(); }, "Esc": () => { this.dismiss(); }, }); this._conditionCodeMirror.on("beforeChange", this._handleConditionCodeMirrorBeforeChange.bind(this)); if (this._breakpoint) this._conditionCodeMirror.on("change", this._handleConditionCodeMirrorChange.bind(this)); let completionController = new WI.CodeMirrorCompletionController(this.codeMirrorCompletionControllerMode, this._conditionCodeMirror, this); completionController.addExtendedCompletionProvider("javascript", WI.javaScriptRuntimeCompletionProvider); let ignoreCountLabelElement = document.createElement("label"); ignoreCountLabelElement.textContent = WI.UIString("Ignore"); let ignoreCountContentFragment = document.createDocumentFragment(); this._ignoreCountInputElement = ignoreCountContentFragment.appendChild(document.createElement("input")); this._ignoreCountInputElement.id = "edit-breakpoint-popover-ignore-count"; this._ignoreCountInputElement.type = "number"; this._ignoreCountInputElement.min = 0; this._ignoreCountInputElement.value = this._breakpoint?.ignoreCount || 0; this._ignoreCountInputElement.addEventListener("change", this._handleIgnoreCountInputChange.bind(this)); this._ignoreCountText = ignoreCountContentFragment.appendChild(document.createElement("label")); this._updateIgnoreCountText(); ignoreCountLabelElement.setAttribute("for", this._ignoreCountInputElement.id); this._ignoreCountText.setAttribute("for", this._ignoreCountInputElement.id); this.addRow("ignore-count", ignoreCountLabelElement, ignoreCountContentFragment); let actionsLabelElement = document.createElement("label"); actionsLabelElement.textContent = WI.UIString("Action"); this._actionsContainerElement = document.createElement("div"); if (!this._breakpoint?.actions.length) this._createAddActionButton(); else { this._contentElement.classList.add("wide"); for (let i = 0; i < this._breakpoint.actions.length; ++i) { let breakpointActionView = new WI.BreakpointActionView(this._breakpoint.actions[i], this, {omitFocus: true}); this._insertBreakpointActionView(breakpointActionView); } } this.addRow("actions", actionsLabelElement, this._actionsContainerElement); let optionsLabelElement = document.createElement("label"); optionsLabelElement.textContent = WI.UIString("Options"); let optionsDocumentFragment = document.createDocumentFragment(); this._autoContinueCheckboxElement = optionsDocumentFragment.appendChild(document.createElement("input")); this._autoContinueCheckboxElement.id = "edit-breakpoint-popover-auto-continue"; this._autoContinueCheckboxElement.type = "checkbox"; this._autoContinueCheckboxElement.checked = this._breakpoint?.autoContinue || false; this._autoContinueCheckboxElement.addEventListener("change", this._handleAutoContinueCheckboxChange.bind(this)); let optionsCheckboxLabel = optionsDocumentFragment.appendChild(document.createElement("label")); optionsCheckboxLabel.textContent = WI.UIString("Automatically continue after evaluating"); optionsLabelElement.setAttribute("for", this._autoContinueCheckboxElement.id); optionsCheckboxLabel.setAttribute("for", this._autoContinueCheckboxElement.id); this._optionsRowElement = this.addRow("options", optionsLabelElement, optionsDocumentFragment); if (!this._breakpoint?.actions.length) this._optionsRowElement.classList.add("hidden"); // CodeMirror needs to refresh after the popover is shown as otherwise it doesn't appear. setTimeout(() => { this._conditionCodeMirror.refresh(); if (this._breakpoint) this._conditionCodeMirror.focus(); this.update(); }); } let referencePage = this._breakpoint?.constructor.ReferencePage || this.constructor.ReferencePage; if (this._breakpoint) referencePage = referencePage.Configuration; this._contentElement.appendChild(referencePage.createLinkElement()); this.content = this._contentElement; this._presentOverTargetElement(); } dismiss() { this._breakpoint ??= this.createBreakpoint({ condition: this._conditionCodeMirror ? this._conditionCodeMirror.getValue().trim() : "", actions: this._actionViews.map((breakpointActionView) => breakpointActionView.action), ignoreCount: this._ignoreCountInputElement ? this._parseIgnoreCountNumber() : 0, autoContinue: this._autoContinueCheckboxElement ? this._autoContinueCheckboxElement.checked : false, }); // Remove Evaluate and Probe actions that have no data. let emptyActions = this._breakpoint?.actions.filter(function(action) { if (action.type === WI.BreakpointAction.Type.Sound) return false; return !action.data?.trim(); }) || []; for (let action of emptyActions) this._breakpoint.removeAction(action); super.dismiss(); } // CodeMirrorCompletionController delegate completionControllerShouldAllowEscapeCompletion() { return false; } // BreakpointActionView delegate breakpointActionViewCodeMirrorCompletionControllerMode(breakpointActionView, codeMirror) { return this.codeMirrorCompletionControllerMode; } breakpointActionViewAppendActionView(breakpointActionView, newBreakpointAction) { this._breakpoint?.addAction(newBreakpointAction, {precedingAction: breakpointActionView.action}); let newBreakpointActionView = new WI.BreakpointActionView(newBreakpointAction, this); let index = this._actionViews.indexOf(breakpointActionView) + 1; this._insertBreakpointActionView(newBreakpointActionView, index); this._optionsRowElement.classList.remove("hidden"); this.update(); } breakpointActionViewRemoveActionView(breakpointActionView) { this._breakpoint?.removeAction(breakpointActionView.action); breakpointActionView.element.remove(); this._actionViews.remove(breakpointActionView); if (!this._actionViews.length) { this._createAddActionButton(); this._optionsRowElement.classList.add("hidden"); this._autoContinueCheckboxElement.checked = false; } this.update(); } breakpointActionViewResized(breakpointActionView) { this.update(); } // Protected get codeMirrorCompletionControllerMode() { // Overridden by subclasses if needed. if (this._breakpoint === WI.debuggerManager.allExceptionsBreakpoint || this._breakpoint === WI.debuggerManager.uncaughtExceptionsBreakpoint) return WI.CodeMirrorCompletionController.Mode.ExceptionBreakpoint; return WI.CodeMirrorCompletionController.Mode.Basic; } populateContent() { throw WI.NotImplementedError.subclassMustOverride(); } addRow(className, label, content) { let rowElement = this._tableElement.appendChild(document.createElement("tr")); rowElement.className = className; let headerElement = rowElement.appendChild(document.createElement("th")); headerElement.append(label); let dataElement = rowElement.appendChild(document.createElement("td")); dataElement.append(content); return rowElement; } createBreakpoint(options = {}) { throw WI.NotImplementedError.subclassMustOverride(); } // Private _presentOverTargetElement() { if (!this._targetElement) return; let targetFrame = WI.Rect.rectFromClientRect(this._targetElement.getBoundingClientRect()); this.present(targetFrame.pad(2), [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); } _parseIgnoreCountNumber() { let ignoreCount = 0; if (this._ignoreCountInputElement.value) { ignoreCount = parseInt(this._ignoreCountInputElement.value, 10); if (isNaN(ignoreCount) || ignoreCount < 0) ignoreCount = 0; } return ignoreCount; } _updateIgnoreCountText() { if (this._parseIgnoreCountNumber() === 1) this._ignoreCountText.textContent = WI.UIString("time before stopping"); else this._ignoreCountText.textContent = WI.UIString("times before stopping"); } _createAddActionButton() { this._contentElement.classList.remove("wide"); this._actionsContainerElement.removeChildren(); let addActionButton = this._actionsContainerElement.appendChild(document.createElement("button")); addActionButton.textContent = WI.UIString("Add Action"); addActionButton.addEventListener("click", this._handleAddActionButtonClick.bind(this)); } _insertBreakpointActionView(breakpointActionView, index = this._actionViews.length) { if (index >= this._actionViews.length) { this._actionsContainerElement.appendChild(breakpointActionView.element); this._actionViews.push(breakpointActionView); } else { this._actionsContainerElement.insertBefore(breakpointActionView.element, this._actionViews[index].element); this._actionViews.splice(index, 0, breakpointActionView) } } _handleEnabledCheckboxChange(event) { this._breakpoint.disabled = !event.target.checked; } _handleConditionCodeMirrorBeforeChange(codeMirror, change) { if (change.update) { let newText = change.text.join("").replace(/\n/g, ""); change.update(change.from, change.to, [newText]); } return true; } _handleConditionCodeMirrorChange(codeMirror, change) { this._breakpoint.condition = this._conditionCodeMirror.getValue().trim(); } _handleIgnoreCountInputChange(event) { let ignoreCount = this._parseIgnoreCountNumber(); this._ignoreCountInputElement.value = ignoreCount; if (this._breakpoint) this._breakpoint.ignoreCount = ignoreCount; this._updateIgnoreCountText(); } _handleAddActionButtonClick(event) { this._contentElement.classList.add("wide"); this._actionsContainerElement.removeChildren(); let action = new WI.BreakpointAction(WI.BreakpointAction.Type.Evaluate); this._breakpoint?.addAction(action); let breakpointActionView = new WI.BreakpointActionView(action, this); this._insertBreakpointActionView(breakpointActionView); this._optionsRowElement.classList.remove("hidden"); setTimeout(() => { this.update(); }); } _handleAutoContinueCheckboxChange(event) { if (this._breakpoint) this._breakpoint.autoContinue = this._autoContinueCheckboxElement.checked; } };