blob: 1eb022b1579a0765d6d04ddc97e4b6f4dd47970c [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._inlineSwatchActive = false;
this._focused = false;
this._propertyPendingStartEditing = null;
this._pendingAddBlankPropertyIndexOffset = NaN;
this._filterText = null;
}
// Public
initialLayout()
{
if (!this.style.editable)
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);
}
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();
let properties = this._propertiesToRender;
this.element.classList.toggle("no-properties", !properties.length);
// 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);
propertyView.index = index;
this.element.append(propertyView.element);
this._propertyViews.push(propertyView);
if (property === this._propertyPendingStartEditing)
propertyViewPendingStartEditing = propertyView;
}
if (propertyViewPendingStartEditing) {
propertyViewPendingStartEditing.nameTextField.startEditing();
this._propertyPendingStartEditing = null;
}
if (this._filterText)
this.applyFilter(this._filterText);
if (!isNaN(this._pendingAddBlankPropertyIndexOffset))
this.addBlankProperty(this._propertyViews.length - 1 - this._pendingAddBlankPropertyIndexOffset);
}
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();
}
startEditingFirstProperty()
{
let firstEditableProperty = this._editablePropertyAfter(-1);
if (firstEditableProperty)
firstEditableProperty.nameTextField.startEditing();
else {
const appendAfterLast = -1;
this.addBlankProperty(appendAfterLast);
}
}
startEditingLastProperty()
{
let lastEditableProperty = this._editablePropertyBefore(this._propertyViews.length);
if (lastEditableProperty)
lastEditableProperty.valueTextField.startEditing();
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 cssProperty of this.style.properties) {
if (propertiesMatch(cssProperty)) {
let propertyView = cssProperty.__propertyView;
if (propertyView) {
propertyView.highlight();
if (cssProperty.editable)
propertyView.valueTextField.startEditing();
}
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();
}
spreadsheetStylePropertyFocusMoved(propertyView, {direction, willRemoveProperty})
{
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.cssStyleDeclarationTextEditorStartEditingRuleSelector();
return;
}
if (direction === "forward") {
// Move from the value to the next enabled property's name.
let propertyView = this._editablePropertyAfter(movedFromIndex);
if (propertyView)
propertyView.nameTextField.startEditing();
else {
if (willRemoveProperty) {
// Move from the last value in the rule to the next rule's selector.
let reverse = false;
this._delegate.cssStyleDeclarationEditorStartEditingAdjacentRule(reverse);
} 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.valueTextField.startEditing()
} else {
// Move from the first property's name to the rule's selector.
if (this._style.selectorEditable)
this._delegate.cssStyleDeclarationTextEditorStartEditingRuleSelector();
}
}
}
// SpreadsheetStyleProperty delegate
spreadsheetStylePropertyAddBlankPropertySoon(propertyView, {index})
{
if (isNaN(index))
index = this._propertyViews.length;
this._pendingAddBlankPropertyIndexOffset = this._propertyViews.length - index;
}
spreadsheetStylePropertyRemoved(propertyView)
{
this._propertyViews.remove(propertyView);
for (let index = 0; index < this._propertyViews.length; index++)
this._propertyViews[index].index = index;
this._focused = false;
}
stylePropertyInlineSwatchActivated()
{
this.inlineSwatchActive = true;
}
stylePropertyInlineSwatchDeactivated()
{
this.inlineSwatchActive = false;
}
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;
}
this.dispatchEventToListeners(WI.SpreadsheetCSSStyleDeclarationEditor.Event.FilterApplied, {matches});
}
// Private
get _propertiesToRender()
{
if (this._style._styleSheetTextRange)
return this._style.allVisibleProperties;
return this._style.allProperties;
}
_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)) {
for (let propertyView of this._propertyViews)
propertyView.updateStatus();
} else
this.needsLayout();
}
_updateStyleLock()
{
this.style.locked = this._focused || this._inlineSwatchActive;
}
};
WI.SpreadsheetCSSStyleDeclarationEditor.Event = {
FilterApplied: "spreadsheet-css-style-declaration-editor-filter-applied",
};
WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName = "spreadsheet-style-declaration-editor";