| /* |
| * Copyright (C) 2017 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.SpreadsheetCSSStyleDeclarationEditor = class SpreadsheetCSSStyleDeclarationEditor extends WI.View |
| { |
| constructor(delegate, style) |
| { |
| super(); |
| |
| this.element.classList.add(WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName); |
| |
| this._delegate = delegate; |
| this.style = style; |
| this._propertyViews = []; |
| |
| this._focused = false; |
| this._inlineSwatchActive = false; |
| |
| this._showsImplicitProperties = false; |
| this._alwaysShowPropertyNames = new Set; |
| this._propertyVisibilityMode = WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.ShowAll; |
| this._hideFilterNonMatchingProperties = false; |
| this._sortPropertiesByName = false; |
| |
| this._propertyPendingStartEditing = null; |
| this._pendingAddBlankPropertyIndexOffset = NaN; |
| this._filterText = null; |
| |
| this._anchorIndex = NaN; |
| this._focusIndex = NaN; |
| } |
| |
| // Public |
| |
| initialLayout() |
| { |
| if (!this._style) |
| return; |
| |
| this.element.addEventListener("focus", () => { this.focused = true; }, true); |
| this.element.addEventListener("blur", (event) => { |
| let focusedElement = event.relatedTarget; |
| if (focusedElement && this.element.contains(focusedElement)) |
| return; |
| |
| this.focused = false; |
| }, true); |
| |
| this.element.addEventListener("keydown", this._handleKeyDown.bind(this)); |
| this.element.addEventListener("cut", this._handleCut.bind(this)); |
| this.element.addEventListener("copy", this._handleCopy.bind(this)); |
| } |
| |
| layout() |
| { |
| // Prevent layout of properties when one of them is being edited. A full layout resets |
| // the focus, text selection, and completion state <http://webkit.org/b/182619>. |
| if (this.editing && !this._propertyPendingStartEditing && isNaN(this._pendingAddBlankPropertyIndexOffset)) |
| return; |
| |
| super.layout(); |
| |
| this.element.removeChildren(); |
| |
| if (this._style) |
| this._style.updatePropertiesModifiedState(); |
| |
| let properties = this.propertiesToRender; |
| |
| // FIXME: Only re-layout properties that have been modified and preserve focus whenever possible. |
| this._propertyViews = []; |
| |
| let propertyViewPendingStartEditing = null; |
| for (let index = 0; index < properties.length; index++) { |
| let property = properties[index]; |
| let propertyView = new WI.SpreadsheetStyleProperty(this, property, {selectable: true}); |
| propertyView.index = index; |
| this.element.append(propertyView.element); |
| this._propertyViews.push(propertyView); |
| |
| if (property === this._propertyPendingStartEditing) |
| propertyViewPendingStartEditing = propertyView; |
| } |
| |
| if (propertyViewPendingStartEditing) { |
| propertyViewPendingStartEditing.startEditingName(); |
| this._propertyPendingStartEditing = null; |
| } |
| |
| if (this._filterText) |
| this.applyFilter(this._filterText); |
| |
| if (!isNaN(this._pendingAddBlankPropertyIndexOffset)) |
| this.addBlankProperty(this._propertyViews.length - 1 - this._pendingAddBlankPropertyIndexOffset); |
| else if (this.hasSelectedProperties()) |
| this.selectProperties(this._anchorIndex, this._focusIndex); |
| |
| this._updateDebugLockStatus(); |
| } |
| |
| detached() |
| { |
| this._inlineSwatchActive = false; |
| this.focused = false; |
| |
| for (let propertyView of this._propertyViews) |
| propertyView.detached(); |
| } |
| |
| hidden() |
| { |
| for (let propertyView of this._propertyViews) |
| propertyView.hidden(); |
| } |
| |
| get style() |
| { |
| return this._style; |
| } |
| |
| set style(style) |
| { |
| if (this._style === style) |
| return; |
| |
| if (this._style) |
| this._style.removeEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this); |
| |
| this._style = style || null; |
| |
| if (this._style) |
| this._style.addEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this); |
| |
| this.needsLayout(); |
| } |
| |
| get editing() |
| { |
| return this._focused || this._inlineSwatchActive; |
| } |
| |
| set focused(value) |
| { |
| this._focused = value; |
| this._updateStyleLock(); |
| } |
| |
| set inlineSwatchActive(value) |
| { |
| this._inlineSwatchActive = value; |
| this._updateStyleLock(); |
| } |
| |
| set showsImplicitProperties(value) |
| { |
| if (value === this._showsImplicitProperties) |
| return; |
| |
| this._showsImplicitProperties = value; |
| |
| this.needsLayout(); |
| } |
| |
| set alwaysShowPropertyNames(propertyNames) |
| { |
| this._alwaysShowPropertyNames = new Set(propertyNames); |
| |
| this.needsLayout(); |
| } |
| |
| set propertyVisibilityMode(propertyVisibilityMode) |
| { |
| if (this._propertyVisibilityMode === propertyVisibilityMode) |
| return; |
| |
| this._propertyVisibilityMode = propertyVisibilityMode; |
| |
| this.needsLayout(); |
| } |
| |
| set hideFilterNonMatchingProperties(value) |
| { |
| if (value === this._hideFilterNonMatchingProperties) |
| return; |
| |
| this._hideFilterNonMatchingProperties = value; |
| |
| this.needsLayout(); |
| } |
| |
| set sortPropertiesByName(value) |
| { |
| if (value === this._sortPropertiesByName) |
| return; |
| |
| this._sortPropertiesByName = value; |
| this.needsLayout(); |
| } |
| |
| get propertiesToRender() |
| { |
| let properties = []; |
| if (!this._style) |
| return properties; |
| |
| if (this._style._styleSheetTextRange) |
| properties = this._style.visibleProperties; |
| else |
| properties = this._style.properties; |
| |
| if (this._sortPropertiesByName) |
| properties.sort((a, b) => a.name.extendedLocaleCompare(b.name)); |
| |
| let hideVariables = this._propertyVisibilityMode === SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.HideVariables; |
| let hideNonVariables = this._propertyVisibilityMode === SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.HideNonVariables; |
| |
| return properties.filter((property) => { |
| if (!property.isVariable && hideNonVariables) |
| return false; |
| |
| if (property.isVariable && hideVariables) |
| return false; |
| |
| return !property.implicit || this._showsImplicitProperties || this._alwaysShowPropertyNames.has(property.canonicalName); |
| }); |
| } |
| |
| get selectionRange() |
| { |
| let startIndex = Math.min(this._anchorIndex, this._focusIndex); |
| let endIndex = Math.max(this._anchorIndex, this._focusIndex); |
| return [startIndex, endIndex]; |
| } |
| |
| startEditingFirstProperty() |
| { |
| let firstEditableProperty = this._editablePropertyAfter(-1); |
| if (firstEditableProperty) |
| firstEditableProperty.startEditingName(); |
| else { |
| const appendAfterLast = -1; |
| this.addBlankProperty(appendAfterLast); |
| } |
| } |
| |
| startEditingLastProperty() |
| { |
| let lastEditableProperty = this._editablePropertyBefore(this._propertyViews.length); |
| if (lastEditableProperty) |
| lastEditableProperty.startEditingValue(); |
| else { |
| const appendAfterLast = -1; |
| this.addBlankProperty(appendAfterLast); |
| } |
| } |
| |
| highlightProperty(property) |
| { |
| let propertiesMatch = (cssProperty) => { |
| if (cssProperty.attached && !cssProperty.overridden) { |
| if (cssProperty.canonicalName === property.canonicalName || hasMatchingLonghandProperty(cssProperty)) |
| return true; |
| } |
| |
| return false; |
| }; |
| |
| let hasMatchingLonghandProperty = (cssProperty) => { |
| let cssProperties = cssProperty.relatedLonghandProperties; |
| |
| if (!cssProperties.length) |
| return false; |
| |
| for (let property of cssProperties) { |
| if (propertiesMatch(property)) |
| return true; |
| } |
| |
| return false; |
| }; |
| |
| for (let i = 0; i < this._propertyViews.length; ++i) { |
| if (propertiesMatch(this._propertyViews[i].property)) { |
| this.selectProperties(i, i); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| addBlankProperty(index) |
| { |
| this._pendingAddBlankPropertyIndexOffset = NaN; |
| |
| if (index === -1) { |
| // Append to the end. |
| index = this._propertyViews.length; |
| } |
| |
| this._propertyPendingStartEditing = this._style.newBlankProperty(index); |
| this.needsLayout(); |
| } |
| |
| hasSelectedProperties() |
| { |
| return !isNaN(this._anchorIndex) && !isNaN(this._focusIndex); |
| } |
| |
| selectProperties(anchorIndex, focusIndex) |
| { |
| console.assert(anchorIndex < this._propertyViews.length, `anchorIndex (${anchorIndex}) is greater than the last property index (${this._propertyViews.length})`); |
| console.assert(focusIndex < this._propertyViews.length, `focusIndex (${focusIndex}) is greater than the last property index (${this._propertyViews.length})`); |
| |
| if (isNaN(anchorIndex) || isNaN(focusIndex)) { |
| console.error(`Nothing to select. anchorIndex (${anchorIndex}) and focusIndex (${focusIndex}) must be numbers.`); |
| this.deselectProperties(); |
| return; |
| } |
| |
| this._anchorIndex = anchorIndex; |
| this._focusIndex = focusIndex; |
| |
| let [startIndex, endIndex] = this.selectionRange; |
| |
| for (let i = 0; i < this._propertyViews.length; ++i) { |
| let propertyView = this._propertyViews[i]; |
| let isSelected = i >= startIndex && i <= endIndex; |
| propertyView.selected = isSelected; |
| } |
| |
| this._suppressBlur = true; |
| let property = this._propertyViews[focusIndex]; |
| property.element.focus(); |
| this._suppressBlur = false; |
| } |
| |
| extendSelectedProperties(focusIndex) |
| { |
| this.selectProperties(this._anchorIndex, focusIndex); |
| } |
| |
| deselectProperties() |
| { |
| for (let propertyView of this._propertyViews) |
| propertyView.selected = false; |
| |
| this._focused = false; |
| this._anchorIndex = NaN; |
| this._focusIndex = NaN; |
| } |
| |
| placeTextCaretInFocusedProperty() |
| { |
| if (isNaN(this._focusIndex)) |
| return; |
| |
| let focusedProperty = this._propertyViews[this._focusIndex]; |
| let selection = window.getSelection(); |
| selection.removeAllRanges(); |
| selection.setBaseAndExtent(focusedProperty.element, 0, focusedProperty.element, 0); |
| } |
| |
| applyFilter(filterText) |
| { |
| this._filterText = filterText; |
| |
| if (!this.didInitialLayout) |
| return; |
| |
| let matches = false; |
| for (let propertyView of this._propertyViews) { |
| if (propertyView.applyFilter(this._filterText)) { |
| matches = true; |
| |
| if (this._hideFilterNonMatchingProperties) |
| this.element.append(propertyView.element); |
| } else if (this._hideFilterNonMatchingProperties) |
| propertyView.element.remove(); |
| } |
| |
| this.dispatchEventToListeners(WI.SpreadsheetCSSStyleDeclarationEditor.Event.FilterApplied, {matches}); |
| } |
| |
| // SpreadsheetStyleProperty delegate |
| |
| spreadsheetStylePropertyBlur(event, property) |
| { |
| if (this._suppressBlur) |
| return; |
| |
| if (this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyBlur) |
| this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyBlur(event, property); |
| } |
| |
| spreadsheetStylePropertyMouseEnter(event, property) |
| { |
| if (this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyMouseEnter) |
| this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyMouseEnter(event, property); |
| } |
| |
| spreadsheetStylePropertyFocusMoved(propertyView, {direction, willRemoveProperty}) |
| { |
| this._updatePropertiesStatus(); |
| |
| if (!direction) |
| return; |
| |
| let movedFromIndex = this._propertyViews.indexOf(propertyView); |
| console.assert(movedFromIndex !== -1, "Property doesn't exist, focusing on a selector as a fallback."); |
| if (movedFromIndex === -1) { |
| if (this._style.selectorEditable) |
| this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingRuleSelector(); |
| |
| return; |
| } |
| |
| if (direction === "forward") { |
| // Move from the value to the next enabled property's name. |
| let propertyView = this._editablePropertyAfter(movedFromIndex); |
| if (propertyView) |
| propertyView.startEditingName(); |
| else { |
| if (willRemoveProperty) { |
| const delta = 1; |
| this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingAdjacentRule(this, delta); |
| } else { |
| const appendAfterLast = -1; |
| this.addBlankProperty(appendAfterLast); |
| } |
| } |
| } else { |
| let propertyView = this._editablePropertyBefore(movedFromIndex); |
| if (propertyView) { |
| // Move from the property's name to the previous enabled property's value. |
| propertyView.startEditingValue(); |
| } else { |
| // Move from the first property's name to the rule's selector. |
| if (this._style.selectorEditable) |
| this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingRuleSelector(); |
| else { |
| const delta = -1; |
| this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingAdjacentRule(this, delta); |
| } |
| } |
| } |
| } |
| |
| spreadsheetStylePropertySelect(index) |
| { |
| this.selectProperties(index, index); |
| } |
| |
| spreadsheetStylePropertySelectByProperty(property) |
| { |
| if (this._delegate && this._delegate.spreadsheetCSSStyleDeclarationEditorSelectProperty) |
| this._delegate.spreadsheetCSSStyleDeclarationEditorSelectProperty(property); |
| } |
| |
| spreadsheetStylePropertyAddBlankPropertySoon(propertyView, {index}) |
| { |
| if (isNaN(index)) |
| index = this._propertyViews.length; |
| this._pendingAddBlankPropertyIndexOffset = this._propertyViews.length - index; |
| } |
| |
| spreadsheetStylePropertyWillRemove(propertyView) |
| { |
| this._propertyViews.remove(propertyView); |
| |
| for (let index = 0; index < this._propertyViews.length; index++) |
| this._propertyViews[index].index = index; |
| |
| let wasFocused = document.activeElement && propertyView.element.contains(document.activeElement); |
| if (wasFocused) |
| this.focused = false; |
| } |
| |
| spreadsheetStylePropertyShowProperty(propertyView, property) |
| { |
| if (this._delegate.spreadsheetCSSStyleDeclarationEditorShowProperty) |
| this._delegate.spreadsheetCSSStyleDeclarationEditorShowProperty(this, property); |
| } |
| |
| spreadsheetStylePropertyDidPressEsc(propertyView) |
| { |
| let index = this._propertyViews.indexOf(propertyView); |
| console.assert(index !== -1, `Can't find StyleProperty to select (${propertyView.property.name})`); |
| if (index !== -1) |
| this.selectProperties(index, index); |
| } |
| |
| stylePropertyInlineSwatchActivated() |
| { |
| this.inlineSwatchActive = true; |
| } |
| |
| stylePropertyInlineSwatchDeactivated() |
| { |
| this.inlineSwatchActive = false; |
| } |
| |
| // Private |
| |
| _handleKeyDown(event) |
| { |
| if (!this.hasSelectedProperties() || !this._propertyViews.length) |
| return; |
| |
| if (event.key === "ArrowUp" || event.key === "ArrowDown") { |
| let delta = event.key === "ArrowUp" ? -1 : 1; |
| let focusIndex = Number.constrain(this._focusIndex + delta, 0, this._propertyViews.length - 1); |
| |
| if (event.shiftKey) |
| this.extendSelectedProperties(focusIndex); |
| else |
| this.selectProperties(focusIndex, focusIndex); |
| |
| event.stop(); |
| } else if (event.key === "Tab" || event.key === "Enter") { |
| if (!this.style.editable) |
| return; |
| |
| let property = this._propertyViews[this._focusIndex]; |
| if (property && property.enabled) { |
| event.stop(); |
| property.startEditingName(); |
| } |
| } else if (event.key === "Backspace" || event.key === "Delete") { |
| if (!this.style.editable) { |
| InspectorFrontendHost.beep(); |
| return; |
| } |
| |
| this._removeSelectedProperties(); |
| event.stop(); |
| } else if ((event.code === "Space" && !event.shiftKey && !event.metaKey && !event.ctrlKey) || (event.key === "/" && event.commandOrControlKey && !event.shiftKey)) { |
| if (!this.style.editable) |
| return; |
| |
| let [startIndex, endIndex] = this.selectionRange; |
| |
| // Toggle the first selected property and set this state to all selected properties. |
| let disabled = this._propertyViews[startIndex].property.enabled; |
| |
| for (let i = endIndex; i >= startIndex; --i) { |
| let propertyView = this._propertyViews[i]; |
| propertyView.property.commentOut(disabled); |
| propertyView.update(); |
| } |
| |
| event.stop(); |
| |
| } else if (event.key === "a" && event.commandOrControlKey) { |
| |
| this.selectProperties(0, this._propertyViews.length - 1); |
| event.stop(); |
| |
| } else if (event.key === "Esc") |
| this.deselectProperties(); |
| } |
| |
| _handleCopy(event) |
| { |
| if (!this.hasSelectedProperties()) |
| return; |
| |
| this._copySelectedProperties(event); |
| } |
| |
| _handleCut(event) |
| { |
| if (!this.style.editable) { |
| InspectorFrontendHost.beep(); |
| return; |
| } |
| |
| if (!this.hasSelectedProperties()) |
| return; |
| |
| this._copySelectedProperties(event); |
| this._removeSelectedProperties(); |
| } |
| |
| _copySelectedProperties(event) |
| { |
| let [startIndex, endIndex] = this.selectionRange; |
| let formattedProperties = this._propertyViews.slice(startIndex, endIndex + 1).map((propertyView) => propertyView.property.formattedText); |
| event.clipboardData.setData("text/plain", formattedProperties.join("\n")); |
| event.stop(); |
| } |
| |
| _removeSelectedProperties() |
| { |
| console.assert(this.style.editable); |
| let [startIndex, endIndex] = this.selectionRange; |
| |
| let propertyIndexToSelect = NaN; |
| if (endIndex + 1 !== this._propertyViews.length) |
| propertyIndexToSelect = startIndex; |
| else if (startIndex > 0) |
| propertyIndexToSelect = startIndex - 1; |
| |
| this.deselectProperties(); |
| |
| for (let i = endIndex; i >= startIndex; i--) |
| this._propertyViews[i].remove(); |
| |
| if (!isNaN(propertyIndexToSelect)) |
| this.selectProperties(propertyIndexToSelect, propertyIndexToSelect); |
| } |
| |
| _editablePropertyAfter(propertyIndex) |
| { |
| for (let index = propertyIndex + 1; index < this._propertyViews.length; index++) { |
| let property = this._propertyViews[index]; |
| if (property.enabled) |
| return property; |
| } |
| |
| return null; |
| } |
| |
| _editablePropertyBefore(propertyIndex) |
| { |
| for (let index = propertyIndex - 1; index >= 0; index--) { |
| let property = this._propertyViews[index]; |
| if (property.enabled) |
| return property; |
| } |
| |
| return null; |
| } |
| |
| _propertiesChanged(event) |
| { |
| if (this.editing && isNaN(this._pendingAddBlankPropertyIndexOffset)) |
| this._updatePropertiesStatus(); |
| else |
| this.needsLayout(); |
| } |
| |
| _updatePropertiesStatus() |
| { |
| for (let propertyView of this._propertyViews) |
| propertyView.updateStatus(); |
| } |
| |
| _updateStyleLock() |
| { |
| if (!this._style) |
| return; |
| |
| this._style.locked = this._focused || this._inlineSwatchActive; |
| this._updateDebugLockStatus(); |
| } |
| |
| _updateDebugLockStatus() |
| { |
| if (!this._style || !WI.settings.debugEnableStyleEditingDebugMode.value) |
| return; |
| |
| this.element.classList.toggle("debug-style-locked", this._style.locked); |
| } |
| }; |
| |
| WI.SpreadsheetCSSStyleDeclarationEditor.Event = { |
| FilterApplied: "spreadsheet-css-style-declaration-editor-filter-applied", |
| }; |
| |
| WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName = "spreadsheet-style-declaration-editor"; |
| |
| WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode = { |
| ShowAll: Symbol("variable-visibility-show-all"), |
| HideVariables: Symbol("variable-visibility-hide-variables"), |
| HideNonVariables: Symbol("variable-visibility-hide-non-variables"), |
| }; |