| /* |
| * Copyright (C) 2021 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.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object |
| { |
| constructor(delegate, property, options = {}) |
| { |
| super(); |
| |
| console.assert(property instanceof WI.CSSProperty); |
| |
| this._delegate = delegate || null; |
| this._property = property; |
| this._readOnly = options.readOnly || false; |
| this._hideDocumentation = !!options.hideDocumentation; |
| this._element = document.createElement("div"); |
| |
| if (options.selectable) |
| this._element.tabIndex = -1; |
| |
| this._contentElement = null; |
| this._nameElement = null; |
| this._valueElement = null; |
| this._cssDocumentationButton = null; |
| this._jumpToEffectivePropertyButton = null; |
| |
| this._nameTextField = null; |
| this._valueTextField = null; |
| |
| this._selected = false; |
| this._hasInvalidVariableValue = false; |
| this._cssDocumentationPopover = null; |
| this._activeInlineSwatch = null; |
| |
| this.update(); |
| property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this.updateStatus, this); |
| property.addEventListener(WI.CSSProperty.Event.Changed, this.updateStatus, this); |
| |
| if (!this._readOnly) { |
| property.addEventListener(WI.CSSProperty.Event.ModifiedChanged, this.updateStatus, this); |
| |
| this._element.addEventListener("blur", (event) => { |
| // Keep selection after tabbing out of Web Inspector window and back. |
| if (document.activeElement === this._element) |
| return; |
| |
| if (this._delegate.spreadsheetStylePropertyBlur) |
| this._delegate.spreadsheetStylePropertyBlur(event, this); |
| }); |
| |
| this._element.addEventListener("mouseenter", (event) => { |
| if (this._delegate.spreadsheetStylePropertyMouseEnter) |
| this._delegate.spreadsheetStylePropertyMouseEnter(event, this); |
| }); |
| |
| new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Slash, () => { |
| this._toggle(); |
| this._select(); |
| }, this._element); |
| } |
| } |
| |
| // Public |
| |
| get element() { return this._element; } |
| get property() { return this._property; } |
| get enabled() { return this._property.enabled; } |
| |
| set index(index) |
| { |
| this._element.dataset.propertyIndex = index; |
| } |
| |
| get selected() |
| { |
| return this._selected; |
| } |
| |
| set selected(value) |
| { |
| if (value === this._selected) |
| return; |
| |
| this._selected = value; |
| this.updateStatus(); |
| } |
| |
| startEditingName() |
| { |
| if (!this._nameTextField) |
| return; |
| |
| this._nameTextField.startEditing(); |
| } |
| |
| startEditingValue() |
| { |
| if (!this._valueTextField) |
| return; |
| |
| this._valueTextField.startEditing(); |
| } |
| |
| detached() |
| { |
| if (this._nameTextField?.editing) |
| this._nameTextField.element.blur(); |
| else if (this._valueTextField?.editing) |
| this._valueTextField.element.blur(); |
| |
| if (this._nameTextField) |
| this._nameTextField.detached(); |
| |
| if (this._valueTextField) |
| this._valueTextField.detached(); |
| |
| this._activeInlineSwatch?.dismissPopover(); |
| this._activeInlineSwatch = null; |
| |
| this._cssDocumentationPopover?.dismiss(); |
| this._cssDocumentationPopover = null; |
| } |
| |
| remove(replacement = null) |
| { |
| if (this._delegate && typeof this._delegate.spreadsheetStylePropertyWillRemove === "function") |
| this._delegate.spreadsheetStylePropertyWillRemove(this); |
| |
| this.element.remove(); |
| |
| if (replacement) |
| this._property.text = replacement; |
| else |
| this._property.remove(); |
| |
| this.detached(); |
| } |
| |
| update() |
| { |
| this.element.removeChildren(); |
| |
| if (this._isEditable()) { |
| this._checkboxElement = this.element.appendChild(document.createElement("input")); |
| this._checkboxElement.classList.add("property-toggle"); |
| this._checkboxElement.type = "checkbox"; |
| this._checkboxElement.checked = this._property.enabled; |
| this._checkboxElement.tabIndex = -1; |
| this._checkboxElement.addEventListener("click", (event) => { |
| event.stopPropagation(); |
| this._toggle(); |
| console.assert(this._checkboxElement.checked === this._property.enabled); |
| }); |
| } |
| |
| this._contentElement = this.element.appendChild(document.createElement("span")); |
| this._contentElement.className = "content"; |
| |
| if (!this._property.enabled) |
| this._contentElement.append("/* "); |
| |
| this._nameElement = this._contentElement.appendChild(document.createElement("span")); |
| this._nameElement.classList.add("name"); |
| this._nameElement.textContent = this._property.name; |
| |
| let colonElement = this._contentElement.appendChild(document.createElement("span")); |
| colonElement.classList.add("colon"); |
| colonElement.textContent = ": "; |
| |
| let valueContainer = this._contentElement.appendChild(document.createElement("span")); |
| valueContainer.className = "value-container"; |
| |
| this._valueElement = valueContainer.appendChild(document.createElement("span")); |
| this._valueElement.classList.add("value"); |
| this._renderValue(this._property.rawValue); |
| |
| if (this._isEditable() && this._property.enabled) { |
| this._nameElement.tabIndex = 0; |
| this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this)); |
| this._nameElement.addEventListener("paste", this._handleNamePaste.bind(this)); |
| |
| this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this)); |
| |
| this._valueElement.tabIndex = 0; |
| this._valueElement.addEventListener("beforeinput", this._handleValueBeforeInput.bind(this)); |
| |
| this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this)); |
| } |
| |
| if (this._isEditable()) { |
| this._setupJumpToSymbol(this._nameElement); |
| this._setupJumpToSymbol(this._valueElement); |
| } |
| |
| let semicolonElement = valueContainer.appendChild(document.createElement("span")); |
| semicolonElement.classList.add("semicolon"); |
| semicolonElement.textContent = ";"; |
| |
| if (this._property.enabled) { |
| this._warningElement = this.element.appendChild(document.createElement("span")); |
| this._warningElement.className = "warning"; |
| } else |
| this._contentElement.append(" */"); |
| |
| this._addCSSDocumentationButton(); |
| |
| if (!this._property.implicit && this._property.ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed && !this._property.isShorthand) { |
| let effectiveProperty = this._property.ownerStyle.nodeStyles.effectivePropertyForName(this._property.name); |
| if (effectiveProperty && !effectiveProperty.styleSheetTextRange) |
| effectiveProperty = effectiveProperty.relatedShorthandProperty; |
| |
| let ownerRule = effectiveProperty ? effectiveProperty.ownerStyle.ownerRule : null; |
| |
| let arrowElement = this._contentElement.appendChild(WI.createGoToArrowButton()); |
| arrowElement.addEventListener("click", (event) => { |
| if (!effectiveProperty || !ownerRule || !event.altKey) { |
| if (this._delegate.spreadsheetStylePropertyShowProperty) |
| this._delegate.spreadsheetStylePropertyShowProperty(this, this._property); |
| return; |
| } |
| |
| let sourceCode = ownerRule.sourceCodeLocation.sourceCode; |
| let {startLine, startColumn} = effectiveProperty.styleSheetTextRange; |
| WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(startLine, startColumn), { |
| ignoreNetworkTab: true, |
| ignoreSearchTab: true, |
| }); |
| }); |
| |
| if (effectiveProperty && ownerRule) |
| arrowElement.title = WI.UIString("Option-click to show source"); |
| } |
| |
| this.updateStatus(); |
| } |
| |
| updateStatus() |
| { |
| let duplicatePropertyExistsBelow = (cssProperty) => { |
| let propertyFound = false; |
| |
| for (let property of this._property.ownerStyle.enabledProperties) { |
| if (property === cssProperty) |
| propertyFound = true; |
| else if (property.name === cssProperty.name && propertyFound) |
| return true; |
| } |
| |
| return false; |
| }; |
| |
| let classNames = [WI.SpreadsheetStyleProperty.StyleClassName]; |
| let elementTitle = ""; |
| |
| if (this._property.overridden) { |
| if (!this._jumpToEffectivePropertyButton && this._delegate && this._delegate.spreadsheetStylePropertySelectByProperty && WI.settings.experimentalEnableStylesJumpToEffective.value) { |
| console.assert(this._property.overridingProperty, `Overridden property is missing overridingProperty: ${this._property.formattedText}`); |
| if (this._property.overridingProperty) { |
| this._jumpToEffectivePropertyButton = WI.createGoToArrowButton(); |
| this._jumpToEffectivePropertyButton.classList.add("select-effective-property"); |
| this._jumpToEffectivePropertyButton.dataset.value = this._property.overridingProperty.rawValue; |
| this._element.append(this._jumpToEffectivePropertyButton); |
| |
| this._jumpToEffectivePropertyButton.addEventListener("click", (event) => { |
| console.assert(this._property.overridingProperty); |
| event.stop(); |
| this._delegate.spreadsheetStylePropertySelectByProperty(this._property.overridingProperty); |
| }); |
| } |
| } |
| |
| classNames.push("overridden"); |
| if (duplicatePropertyExistsBelow(this._property)) { |
| classNames.push("has-warning"); |
| elementTitle = WI.UIString("Duplicate property"); |
| } |
| } |
| |
| if (this._property.implicit) |
| classNames.push("implicit"); |
| |
| if (this._property.ownerStyle.inherited && !this._property.inherited) |
| classNames.push("not-inherited"); |
| |
| if (!this._property.valid && this._property.hasOtherVendorNameOrKeyword()) |
| classNames.push("other-vendor"); |
| else if (this._hasInvalidVariableValue || (!this._property.valid && this._property.value !== "")) { |
| classNames.push("has-warning"); |
| |
| if (!WI.cssManager.propertyNameCompletions?.isValidPropertyName(this._property.name)) { |
| classNames.push("invalid-name"); |
| elementTitle = WI.UIString("Unsupported property name"); |
| } else { |
| classNames.push("invalid-value"); |
| elementTitle = WI.UIString("Unsupported property value"); |
| } |
| } |
| |
| if (!this._property.enabled) |
| classNames.push("disabled"); |
| |
| if (this._property.modified && this._property.name && this._property.rawValue) |
| classNames.push("modified"); |
| |
| if (this._selected) |
| classNames.push("selected"); |
| |
| if (this._valueTextField && this._valueTextField.value.includes("\n")) |
| classNames.push("has-newline"); |
| |
| this._element.className = classNames.join(" "); |
| this._element.title = elementTitle; |
| } |
| |
| applyFilter(filterText) |
| { |
| let matchesName = this._nameElement.textContent.includes(filterText); |
| this._nameElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesName); |
| |
| let matchesValue = this._valueElement.textContent.includes(filterText); |
| this._valueElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesValue); |
| |
| let matches = matchesName || matchesValue; |
| this._contentElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName, !matches); |
| return matches; |
| } |
| |
| additionalFunctionValueCompletionsProvider(functionName) |
| { |
| switch (functionName) { |
| case "var": |
| return Array.from(this._property.ownerStyle.nodeStyles.allCSSVariables); |
| |
| case "attr": |
| return this._property.ownerStyle.node.attributes().map((attribute) => attribute.name); |
| } |
| |
| return []; |
| } |
| |
| // SpreadsheetTextField delegate |
| |
| spreadsheetTextFieldWillStartEditing(textField) |
| { |
| let isEditingName = textField === this._nameTextField; |
| textField.value = isEditingName ? this._property.name : this._property.rawValue; |
| } |
| |
| spreadsheetTextFieldInitialCompletionIndex(textField, completions) |
| { |
| if (textField === this._nameTextField && WI.settings.experimentalCSSSortPropertyNameAutocompletionByUsage.value) |
| return completions.minIndex(WI.CSSProperty.sortByPropertyNameUsageCount); |
| return 0; |
| } |
| |
| spreadsheetTextFieldAllowsNewlines(textField) |
| { |
| return textField === this._valueTextField; |
| } |
| |
| spreadsheetTextFieldDidChange(textField) |
| { |
| if (textField === this._valueTextField) |
| this._handleValueChange(); |
| else if (textField === this._nameTextField) |
| this._handleNameChange(); |
| } |
| |
| spreadsheetTextFieldDidCommit(textField, {direction}) |
| { |
| let willRemoveProperty = false; |
| let isEditingName = textField === this._nameTextField; |
| |
| if (!this._property.name || (!this._property.rawValue && !isEditingName && direction === "forward")) |
| willRemoveProperty = true; |
| |
| if (!isEditingName && !willRemoveProperty) |
| this._renderValue(this._property.rawValue); |
| |
| this._cssDocumentationPopover?.dismiss(); |
| this._cssDocumentationPopover = null; |
| |
| if (direction === "forward") { |
| if (isEditingName && !willRemoveProperty) { |
| // Move focus from the name to the value. |
| this._valueTextField.startEditing(); |
| return; |
| } |
| } else { |
| if (!isEditingName) { |
| // Move focus from the value to the name. |
| this._nameTextField.startEditing(); |
| return; |
| } |
| } |
| |
| if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function") { |
| // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists. |
| this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction, willRemoveProperty}); |
| } |
| |
| if (willRemoveProperty) |
| this.remove(); |
| } |
| |
| spreadsheetTextFieldDidBlur(textField, event, changed) |
| { |
| this._addCSSDocumentationButton(); |
| |
| let focusedOutsideThisProperty = event.relatedTarget !== this._nameElement && event.relatedTarget !== this._valueElement; |
| if (focusedOutsideThisProperty && (!this._nameTextField.value.trim() || !this._valueTextField.value.trim())) { |
| this.remove(); |
| return; |
| } |
| |
| if (textField === this._valueTextField) |
| this._renderValue(this._property.rawValue); |
| |
| if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function") |
| this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction: null}); |
| |
| if (changed) { |
| let target = WI.assumingMainTarget(); |
| if (target.hasCommand("DOM.markUndoableState")) |
| target.DOMAgent.markUndoableState(); |
| } |
| } |
| |
| spreadsheetTextFieldDidBackspace(textField) |
| { |
| if (textField === this._nameTextField) |
| this.spreadsheetTextFieldDidCommit(textField, {direction: "backward"}); |
| else if (textField === this._valueTextField) |
| this._nameTextField.startEditing(); |
| } |
| |
| spreadsheetTextFieldDidPressEsc(textField, textBeforeEditing) |
| { |
| let isNewProperty = !textBeforeEditing; |
| if (isNewProperty) |
| this.remove(); |
| else if (this._delegate.spreadsheetStylePropertyDidPressEsc) |
| this._delegate.spreadsheetStylePropertyDidPressEsc(this); |
| } |
| |
| // Popover delegate |
| |
| willDismissPopover() |
| { |
| this._valueElement.classList.remove(WI.Popover.IgnoreAutoDismissClassName); |
| this._cssDocumentationPopover = null; |
| } |
| |
| // Private |
| |
| _toggle() |
| { |
| this._property.commentOut(this.property.enabled); |
| this.update(); |
| } |
| |
| _select() |
| { |
| if (this._delegate && this._delegate.spreadsheetStylePropertySelect) { |
| let index = parseInt(this._element.dataset.propertyIndex); |
| this._delegate.spreadsheetStylePropertySelect(index); |
| } |
| } |
| |
| _isEditable() |
| { |
| return !this._readOnly && this._property.editable; |
| } |
| |
| _renderValue(value) |
| { |
| this._hasInvalidVariableValue = false; |
| this._activeInlineSwatch = null; |
| |
| const maxValueLength = 150; |
| let tokens = WI.tokenizeCSSValue(value); |
| |
| if (this._property.enabled && !this._property.overridden && this._property.valid && !this._property.hasOtherVendorNameOrKeyword()) |
| tokens = this._replaceSpecialTokens(tokens); |
| |
| tokens = tokens.map((token) => { |
| if (token instanceof Element) |
| return token; |
| |
| let className = ""; |
| |
| if (token.type) { |
| if (token.type.includes("string")) |
| className = "token-string"; |
| else if (token.type.includes("link")) |
| className = "token-link"; |
| else if (token.type.includes("comment")) |
| className = "token-comment"; |
| } |
| |
| if (className) { |
| let span = document.createElement("span"); |
| span.classList.add(className); |
| span.textContent = token.value.truncateMiddle(maxValueLength); |
| |
| if (token.type && token.type.includes("link")) |
| span.addEventListener("contextmenu", this._handleLinkContextMenu.bind(this, token)); |
| |
| return span; |
| } |
| |
| return token.value; |
| }); |
| |
| this._valueElement.removeChildren(); |
| this._valueElement.append(...tokens); |
| } |
| |
| _addCSSDocumentationButton() |
| { |
| if (this._hideDocumentation) |
| return; |
| |
| if (this._cssDocumentationButton) { |
| this._cssDocumentationButton.remove(); |
| this._cssDocumentationButton = null; |
| } |
| |
| if (this.property.isVariable) |
| return; |
| |
| if (!WI.cssManager.propertyNameCompletions?.isValidPropertyName(this._property.name)) |
| return; |
| |
| if (!CSSDocumentation.hasOwnProperty(this._property.name) && !CSSDocumentation.hasOwnProperty(this._property.canonicalName)) |
| return; |
| |
| this._cssDocumentationButton = this._contentElement.appendChild(document.createElement("button")); |
| this._cssDocumentationButton.className = "css-documentation-button"; |
| this._cssDocumentationButton.title = WI.UIString("Click to show documentation", "Click to show documentation @ CSS Documentation Button", "Tooltip to show purpose of the CSS documentation button"); |
| this._cssDocumentationButton.addEventListener("mousedown", this._handleCSSDocumentationButtonClicked.bind(this)); |
| } |
| |
| _handleCSSDocumentationButtonClicked(event) |
| { |
| if (event.button !== 0) |
| return; |
| |
| if (this._valueTextField?.editing) { |
| event.stopPropagation(); |
| event.preventDefault(); |
| this._valueTextField.discardCompletion(); |
| } |
| |
| this._presentCSSDocumentation(); |
| } |
| |
| _presentCSSDocumentation() |
| { |
| this._cssDocumentationPopover ??= new WI.CSSDocumentationPopover(this._property, this); |
| this._cssDocumentationPopover.show(this._nameElement); |
| if (this._isEditable()) |
| this._valueElement.classList.add(WI.Popover.IgnoreAutoDismissClassName); |
| } |
| |
| _createInlineSwatch(type, contents, valueObject) |
| { |
| let tokenElement = document.createElement("span"); |
| let innerElement = document.createElement("span"); |
| for (let item of contents) { |
| if (item instanceof Node) |
| innerElement.appendChild(item); |
| else if (typeof item === "object") |
| innerElement.append(item.value); |
| else |
| innerElement.append(item); |
| } |
| |
| let swatch = new WI.InlineSwatch(type, valueObject, {readOnly: !this._isEditable()}); |
| |
| swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, function(event) { |
| let value = event.data.value && event.data.value.toString(); |
| if (!value) |
| return; |
| |
| innerElement.textContent = value; |
| this._handleValueChange(); |
| |
| if (type === WI.InlineSwatch.Type.Variable) |
| this._renderValue(this._property.rawValue); |
| }, this); |
| |
| if (type === WI.InlineSwatch.Type.Variable) { |
| swatch.value = () => { |
| return this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(innerElement.textContent); |
| }; |
| } |
| |
| swatch.addEventListener(WI.InlineSwatch.Event.Activated, function(event) { |
| this._activeInlineSwatch = swatch; |
| this._delegate?.stylePropertyInlineSwatchActivated(); |
| }, this); |
| |
| if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") { |
| swatch.addEventListener(WI.InlineSwatch.Event.Deactivated, function(event) { |
| this._delegate.stylePropertyInlineSwatchDeactivated(); |
| }, this); |
| } |
| |
| tokenElement.append(swatch.element, innerElement); |
| |
| return tokenElement; |
| } |
| |
| _replaceSpecialTokens(tokens) |
| { |
| // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.) |
| // FIXME: <https://webkit.org/b/233055> Web Inspector: Add a swatch for justify-content, justify-items, and justify-self |
| |
| if (this._property.name === "box-shadow") |
| return this._addBoxShadowTokens(tokens); |
| |
| if (WI.AlignmentData.isAlignmentAwarePropertyName(this._property.name)) |
| return this._addAlignmentTokens(tokens, this._property.name); |
| |
| if (this._property.isVariable || WI.CSSKeywordCompletions.isColorAwareProperty(this._property.name)) { |
| tokens = this._addGradientTokens(tokens); |
| tokens = this._addColorTokens(tokens); |
| } |
| |
| if (this._property.isVariable || WI.CSSKeywordCompletions.isTimingFunctionAwareProperty(this._property.name)) { |
| tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier"); |
| tokens = this._addTimingFunctionTokens(tokens, "spring"); |
| } |
| |
| tokens = this._addVariableTokens(tokens); |
| |
| return tokens; |
| } |
| |
| _addGradientTokens(tokens) |
| { |
| let gradientRegex = /^(repeating-)?(linear|radial|conic)-gradient$/i; |
| let newTokens = []; |
| let gradientStartIndex = NaN; |
| let openParenthesis = 0; |
| |
| for (let i = 0; i < tokens.length; i++) { |
| let token = tokens[i]; |
| if (token.type && token.type.includes("atom") && gradientRegex.test(token.value)) { |
| gradientStartIndex = i; |
| openParenthesis = 0; |
| } else if (token.value === "(" && !isNaN(gradientStartIndex)) |
| openParenthesis++; |
| else if (token.value === ")" && !isNaN(gradientStartIndex)) { |
| openParenthesis--; |
| if (openParenthesis > 0) { |
| // Matched a CSS function inside of the gradient. |
| continue; |
| } |
| |
| let rawTokens = tokens.slice(gradientStartIndex, i + 1); |
| |
| let text = this._resolveVariables(rawTokens.map((token) => token.value).join("")); |
| rawTokens = this._addVariableTokens(rawTokens); |
| |
| let gradient = WI.Gradient.fromString(text); |
| if (gradient) |
| newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, rawTokens, gradient)); |
| else |
| newTokens.pushAll(rawTokens); |
| |
| gradientStartIndex = NaN; |
| } else if (isNaN(gradientStartIndex)) |
| newTokens.push(token); |
| } |
| |
| return newTokens; |
| } |
| |
| _addColorTokens(tokens) |
| { |
| let newTokens = []; |
| let openParentheses = 0; |
| |
| let pushPossibleColorToken = (text, ...rawTokens) => { |
| let color = WI.Color.fromString(text); |
| if (color) |
| newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, rawTokens, color)); |
| else |
| newTokens.pushAll(rawTokens); |
| }; |
| |
| let colorFunctionStartIndex = NaN; |
| |
| for (let i = 0; i < tokens.length; i++) { |
| let token = tokens[i]; |
| if (token.type && token.type.includes("hex-color")) { |
| // Hex |
| pushPossibleColorToken(token.value, token); |
| } else if (WI.Color.FunctionNames.has(token.value) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) { |
| // Color Function start |
| colorFunctionStartIndex = i; |
| } else if (isNaN(colorFunctionStartIndex) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) { |
| // Color keyword |
| pushPossibleColorToken(token.value, token); |
| } else if (!isNaN(colorFunctionStartIndex)) { |
| if (token.value === "(") { |
| ++openParentheses; |
| continue; |
| } |
| |
| if (token.value !== ")") |
| continue; |
| |
| if (--openParentheses) |
| continue; |
| |
| let rawTokens = tokens.slice(colorFunctionStartIndex, i + 1); |
| |
| let text = this._resolveVariables(rawTokens.map((token) => token.value).join("")); |
| rawTokens = this._addVariableTokens(rawTokens); |
| |
| pushPossibleColorToken(text, ...rawTokens); |
| colorFunctionStartIndex = NaN; |
| } else |
| newTokens.push(token); |
| } |
| |
| return newTokens; |
| } |
| |
| _addTimingFunctionTokens(tokens, tokenType) |
| { |
| let newTokens = []; |
| let startIndex = NaN; |
| let openParenthesis = 0; |
| |
| for (let i = 0; i < tokens.length; i++) { |
| let token = tokens[i]; |
| if (token.value === tokenType && token.type && token.type.includes("atom")) { |
| startIndex = i; |
| openParenthesis = 0; |
| } else if (token.value === "(" && !isNaN(startIndex)) |
| openParenthesis++; |
| else if (token.value === ")" && !isNaN(startIndex)) { |
| |
| openParenthesis--; |
| if (openParenthesis > 0) |
| continue; |
| |
| let rawTokens = tokens.slice(startIndex, i + 1); |
| |
| let text = this._resolveVariables(rawTokens.map((token) => token.value).join("")); |
| rawTokens = this._addVariableTokens(rawTokens); |
| |
| let valueObject; |
| let inlineSwatchType; |
| if (tokenType === "cubic-bezier") { |
| valueObject = WI.CubicBezier.fromString(text); |
| inlineSwatchType = WI.InlineSwatch.Type.Bezier; |
| } else if (tokenType === "spring") { |
| valueObject = WI.Spring.fromString(text); |
| inlineSwatchType = WI.InlineSwatch.Type.Spring; |
| } |
| |
| if (valueObject) |
| newTokens.push(this._createInlineSwatch(inlineSwatchType, rawTokens, valueObject)); |
| else |
| newTokens.pushAll(rawTokens); |
| |
| startIndex = NaN; |
| } else if (token.value in WI.CubicBezier.keywordValues) |
| newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Bezier, [token], WI.CubicBezier.fromString(token.value))); |
| else if (isNaN(startIndex)) |
| newTokens.push(token); |
| } |
| |
| return newTokens; |
| } |
| |
| _addBoxShadowTokens(tokens) |
| { |
| let newTokens = []; |
| let startIndex = 0; |
| let openParentheses = 0; |
| |
| for (let i = 0; i <= tokens.length; i++) { |
| let token = tokens[i]; |
| if (i === tokens.length || (token.value === "," && !openParentheses)) { |
| let rawTokens = tokens.slice(startIndex, i); |
| |
| let firstNonWhitespaceIndex = Infinity; |
| let lastNonWhitespaceIndex = 0; |
| for (let j = 0; j < rawTokens.length; ++j) { |
| if (/\s/.test(rawTokens[j].value)) |
| continue; |
| |
| firstNonWhitespaceIndex = Math.min(firstNonWhitespaceIndex, j); |
| lastNonWhitespaceIndex = Math.max(lastNonWhitespaceIndex, j); |
| } |
| |
| let nonWhitespaceTokens = rawTokens.slice(firstNonWhitespaceIndex, lastNonWhitespaceIndex + 1); |
| |
| newTokens.pushAll(rawTokens.slice(0, firstNonWhitespaceIndex)); |
| |
| let text = this._resolveVariables(nonWhitespaceTokens.map((rawToken) => rawToken.value).join("")); |
| nonWhitespaceTokens = this._addVariableTokens(nonWhitespaceTokens); |
| |
| let boxShadow = WI.BoxShadow.fromString(text); |
| if (boxShadow) |
| newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.BoxShadow, nonWhitespaceTokens, boxShadow)); |
| else |
| newTokens.pushAll(nonWhitespaceTokens); |
| |
| newTokens.pushAll(rawTokens.slice(lastNonWhitespaceIndex + 1)); |
| |
| if (token) |
| newTokens.push(token); |
| |
| startIndex = i + 1; |
| continue; |
| } |
| |
| if (token.value === "(") { |
| ++openParentheses; |
| continue; |
| } |
| |
| if (token.value === ")") { |
| --openParentheses; |
| continue; |
| } |
| } |
| |
| return newTokens; |
| } |
| |
| _addAlignmentTokens(tokens, propertyName) |
| { |
| // FIXME: <https://webkit.org/b/233281> Web Inspector: Alignment swatch should handle multi-token values better |
| let text = this._resolveVariables(tokens.map((token) => token.value).join("")); |
| let swatch = this._createInlineSwatch(WI.InlineSwatch.Type.Alignment, this._addVariableTokens(tokens), new WI.AlignmentData(propertyName, text)); |
| return [swatch]; |
| } |
| |
| _addVariableTokens(tokens) |
| { |
| let newTokens = []; |
| let startIndex = NaN; |
| let openParenthesis = 0; |
| |
| for (let i = 0; i < tokens.length; i++) { |
| let token = tokens[i]; |
| if (token.value === "var" && token.type && token.type.includes("atom")) { |
| if (isNaN(startIndex)) { |
| startIndex = i; |
| openParenthesis = 0; |
| } |
| } else if (token.value === "(" && !isNaN(startIndex)) |
| ++openParenthesis; |
| else if (token.value === ")" && !isNaN(startIndex)) { |
| --openParenthesis; |
| if (openParenthesis > 0) |
| continue; |
| |
| let rawTokens = tokens.slice(startIndex, i + 1); |
| let variableNameIndex = rawTokens.findIndex((token) => token.value.startsWith("--") && /\bvariable-2\b/.test(token.type)); |
| if (variableNameIndex !== -1) { |
| let contents = rawTokens.slice(0, variableNameIndex + 1); |
| |
| if (WI.settings.experimentalEnableStylesJumpToVariableDeclaration.value && this._property.ownerStyle.type !== WI.CSSStyleDeclaration.Type.Computed && this._delegate && this._delegate.spreadsheetStylePropertySelectByProperty) { |
| let effectiveVariableProperty = this._property.ownerStyle.nodeStyles.effectivePropertyForName(rawTokens[variableNameIndex].value); |
| if (effectiveVariableProperty) { |
| let arrowElement = WI.createGoToArrowButton(); |
| arrowElement.classList.add("select-variable-property"); |
| arrowElement.title = WI.UIString("Go to variable"); |
| arrowElement.addEventListener("click", (event) => { |
| event.stop(); |
| this._delegate.spreadsheetStylePropertySelectByProperty(effectiveVariableProperty); |
| }); |
| contents.push(arrowElement); |
| } |
| } |
| |
| let fallbackStartIndex = rawTokens.findIndex((value, i) => i > variableNameIndex + 1 && /\bm-css\b/.test(value.type)); |
| if (fallbackStartIndex !== -1) { |
| contents.pushAll(rawTokens.slice(variableNameIndex + 1, fallbackStartIndex)); |
| |
| let fallbackTokens = rawTokens.slice(fallbackStartIndex); |
| fallbackTokens = this._addBoxShadowTokens(fallbackTokens); |
| fallbackTokens = this._addGradientTokens(fallbackTokens); |
| fallbackTokens = this._addColorTokens(fallbackTokens); |
| fallbackTokens = this._addTimingFunctionTokens(fallbackTokens, "cubic-bezier"); |
| fallbackTokens = this._addTimingFunctionTokens(fallbackTokens, "spring"); |
| fallbackTokens = this._addVariableTokens(fallbackTokens); |
| contents.pushAll(fallbackTokens); |
| } else |
| contents.pushAll(rawTokens.slice(variableNameIndex + 1)); |
| |
| let text = rawTokens.reduce((accumulator, token) => accumulator + token.value, ""); |
| if (this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(text)) |
| newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, contents)); |
| else |
| newTokens.pushAll(contents); |
| } else { |
| this._hasInvalidVariableValue = true; |
| newTokens.pushAll(rawTokens); |
| } |
| |
| startIndex = NaN; |
| } else if (isNaN(startIndex)) |
| newTokens.push(token); |
| } |
| |
| return newTokens; |
| } |
| |
| _resolveVariables(cssText) |
| { |
| return cssText.replace(/var\(--[^\)]+\)/g, (match) => this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(match) || match); |
| } |
| |
| _handleNameChange() |
| { |
| this._property.name = this._nameTextField.value; |
| } |
| |
| _handleValueChange() |
| { |
| let value = this._valueTextField.value; |
| |
| this._property.rawValue = value.trim(); |
| |
| this._element.classList.toggle("has-newline", value.includes("\n")); |
| } |
| |
| _handleNameBeforeInput(event) |
| { |
| if (event.data !== ":" || event.inputType !== "insertText") |
| return; |
| |
| event.preventDefault(); |
| this._nameTextField.discardCompletion(); |
| this._valueTextField.startEditing(); |
| } |
| |
| _handleNamePaste(event) |
| { |
| let text = event.clipboardData.getData("text/plain"); |
| if (!text || !text.includes(":")) |
| return; |
| |
| event.preventDefault(); |
| |
| this.remove(text); |
| |
| if (this._delegate.spreadsheetStylePropertyAddBlankPropertySoon) { |
| this._delegate.spreadsheetStylePropertyAddBlankPropertySoon(this, { |
| index: parseInt(this._element.dataset.propertyIndex) + 1, |
| }); |
| } |
| } |
| |
| _nameCompletionDataProvider(text, options = {}) |
| { |
| return WI.CSSKeywordCompletions.forPartialPropertyName(text, options); |
| } |
| |
| _handleValueBeforeInput(event) |
| { |
| if (event.data !== ";" || event.inputType !== "insertText") |
| return; |
| |
| let text = this._valueTextField.valueWithoutSuggestion(); |
| let selection = window.getSelection(); |
| if (!selection.rangeCount || selection.getRangeAt(0).endOffset !== text.length) |
| return; |
| |
| let unbalancedCharacters = WI.CSSCompletions.completeUnbalancedValue(text); |
| if (unbalancedCharacters) |
| return; |
| |
| event.preventDefault(); |
| this._valueTextField.stopEditing(); |
| this.spreadsheetTextFieldDidCommit(this._valueTextField, {direction: "forward"}); |
| } |
| |
| _valueCompletionDataProvider(text, options = {}) |
| { |
| options.additionalFunctionValueCompletionsProvider = this.additionalFunctionValueCompletionsProvider.bind(this); |
| return WI.CSSKeywordCompletions.forPartialPropertyValue(text, this._nameElement.textContent.trim(), options); |
| } |
| |
| _setupJumpToSymbol(element) |
| { |
| element.addEventListener("mousedown", (event) => { |
| if (event.button !== 0) |
| return; |
| |
| if (!WI.modifierKeys.metaKey) |
| return; |
| |
| if (element.isContentEditable) |
| return; |
| |
| let sourceCodeLocation = null; |
| if (this._property.ownerStyle.ownerRule) |
| sourceCodeLocation = this._property.ownerStyle.ownerRule.sourceCodeLocation; |
| |
| if (!sourceCodeLocation) |
| return; |
| |
| let range = this._property.styleSheetTextRange; |
| const options = { |
| ignoreNetworkTab: true, |
| ignoreSearchTab: true, |
| initiatorHint: WI.TabBrowser.TabNavigationInitiator.LinkClick, |
| }; |
| let sourceCode = sourceCodeLocation.sourceCode; |
| WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options); |
| }); |
| } |
| |
| _handleLinkContextMenu(token, event) |
| { |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| |
| let resolveURL = (url) => { |
| let ownerStyle = this._property.ownerStyle; |
| if (!ownerStyle) |
| return url; |
| |
| let ownerStyleSheet = ownerStyle.ownerStyleSheet; |
| if (!ownerStyleSheet) { |
| let ownerRule = ownerStyle.ownerRule; |
| if (ownerRule) |
| ownerStyleSheet = ownerRule.ownerStyleSheet; |
| } |
| if (ownerStyleSheet) { |
| if (ownerStyleSheet.url) |
| return absoluteURL(url, ownerStyleSheet.url); |
| |
| let parentFrame = ownerStyleSheet.parentFrame; |
| if (parentFrame) |
| return absoluteURL(url, parentFrame.url); |
| } |
| |
| let node = ownerStyle.node; |
| if (!node) { |
| let nodeStyles = ownerStyle.nodeStyles; |
| if (!nodeStyles) { |
| let ownerRule = ownerStyle.ownerRule; |
| if (ownerRule) |
| nodeStyles = ownerRule.nodeStyles; |
| } |
| if (nodeStyles) |
| node = nodeStyles.node; |
| } |
| if (node) { |
| let ownerDocument = node.ownerDocument; |
| if (ownerDocument) |
| return absoluteURL(url, node.ownerDocument.documentURL); |
| } |
| |
| return url; |
| }; |
| |
| WI.appendContextMenuItemsForURL(contextMenu, resolveURL(token.value)); |
| } |
| }; |
| |
| WI.SpreadsheetStyleProperty.StyleClassName = "property"; |