blob: 2d975e7a842eb6607864dec3a145fd2eb555cc1d [file] [log] [blame]
/*
* 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.HideUnusedInheritedVariables;
this._hiddenUnusedVariables = new Set;
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 (this._hiddenUnusedVariables.size) {
let showHiddenVariablesButtonElement = this.element.appendChild(document.createElement("button"));
showHiddenVariablesButtonElement.classList.add("hidden-variables-button");
showHiddenVariablesButtonElement.title = WI.UIString("Option-click to show unused CSS variables from all rules", "Option-click to show unused CSS variables from all rules @ Styles Sidebar Panel Tooltip", "Tooltip with instructions on how to show all hidden CSS variables");
const labelSingular = WI.UIString("Show %d unused CSS variable", "Show %d unused CSS variable (singular) @ Styles Sidebar Panel", "Text label for button to reveal one unused CSS variable");
const labelPlural = WI.UIString("Show %d unused CSS variables", "Show %d unused CSS variables (plural) @ Styles Sidebar Panel", "Text label for button to reveal multiple unused CSS variables");
let label = this._hiddenUnusedVariables.size > 1 ? labelPlural : labelSingular;
showHiddenVariablesButtonElement.textContent = label.format(this._hiddenUnusedVariables.size);
showHiddenVariablesButtonElement.addEventListener("click", (event) => {
if (event.altKey) {
this._setAllPropertyVisibilityMode(WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.ShowAll);
return;
}
this.propertyVisibilityMode = WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.ShowAll;
});
}
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);
if (this._pendingPropertyToHighlight) {
this.highlightProperty(this._pendingPropertyToHighlight);
this._pendingPropertyToHighlight = null;
}
this._updateDebugLockStatus();
}
detached()
{
this._inlineSwatchActive = false;
this.focused = false;
for (let propertyView of this._propertyViews)
propertyView.detached();
super.detached();
}
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._style.inherited)
properties = properties.filter((property) => property.inherited);
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;
let hideUnusedInheritedVariables = this._propertyVisibilityMode === SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.HideUnusedInheritedVariables;
this._hiddenUnusedVariables = new Set;
return properties.filter((property) => {
if (!property.isVariable && hideNonVariables)
return false;
if (property.isVariable && hideVariables)
return false;
if (property.isVariable && hideUnusedInheritedVariables && this._style.inherited && !this._style.nodeStyles.usedCSSVariables.has(property.name)) {
this._hiddenUnusedVariables.add(property);
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)
{
if (!property.overridden && this._hiddenUnusedVariables.find(propertiesMatch)) {
this._pendingPropertyToHighlight = property;
this.propertyVisibilityMode = WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.ShowAll;
return true;
}
function propertiesMatch(cssProperty) {
if (cssProperty.attached && !cssProperty.overridden) {
if (cssProperty.canonicalName === property.canonicalName || hasMatchingLonghandProperty(cssProperty))
return true;
}
return false;
}
function 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);
}
_setAllPropertyVisibilityMode(propertyVisibilityMode)
{
this._delegate?.spreadsheetCSSStyleDeclarationEditorSetAllPropertyVisibilityMode?.(this, propertyVisibilityMode);
}
};
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"),
HideUnusedInheritedVariables: Symbol("variable-visibility-hide-unused-inherited-variables"),
};