mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 21:54:47 +08:00
This patch starts displaying color swatches in suggest box next to css variables that compute to color value. For this to be possible, the ColorSwatch and BezierSwatch were moved under the ui/ module. The CSSShadowSwatch stayed in the inline_editor/ for now because it has a dependency on ShadowModel. R=einbinder, dgozman Change-Id: Ic29a13073dc8236d5ef9a2fe35adf6bcd927290b Reviewed-on: https://chromium-review.googlesource.com/1026882 Commit-Queue: Andrey Lushnikov <lushnikov@chromium.org> Reviewed-by: Dmitry Gozman <dgozman@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#553468} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: b85cc4539f06a76af94addf2d193adaa4edc9b9d
1014 lines
32 KiB
JavaScript
1014 lines
32 KiB
JavaScript
// Copyright 2018 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
Elements.StylePropertyTreeElement = class extends UI.TreeElement {
|
|
/**
|
|
* @param {!Elements.StylesSidebarPane} stylesPane
|
|
* @param {!SDK.CSSMatchedStyles} matchedStyles
|
|
* @param {!SDK.CSSProperty} property
|
|
* @param {boolean} isShorthand
|
|
* @param {boolean} inherited
|
|
* @param {boolean} overloaded
|
|
* @param {boolean} newProperty
|
|
*/
|
|
constructor(stylesPane, matchedStyles, property, isShorthand, inherited, overloaded, newProperty) {
|
|
// Pass an empty title, the title gets made later in onattach.
|
|
super('', isShorthand);
|
|
this._style = property.ownerStyle;
|
|
this._matchedStyles = matchedStyles;
|
|
this.property = property;
|
|
this._inherited = inherited;
|
|
this._overloaded = overloaded;
|
|
this.selectable = false;
|
|
this._parentPane = stylesPane;
|
|
this.isShorthand = isShorthand;
|
|
this._applyStyleThrottler = new Common.Throttler(0);
|
|
this._newProperty = newProperty;
|
|
if (this._newProperty)
|
|
this.listItemElement.textContent = '';
|
|
this._expandedDueToFilter = false;
|
|
this.valueElement = null;
|
|
this.nameElement = null;
|
|
this._expandElement = null;
|
|
this._originalPropertyText = '';
|
|
this._prompt = null;
|
|
this._propertyHasBeenEditedIncrementally = false;
|
|
this._lastComputedValue = null;
|
|
}
|
|
|
|
/**
|
|
* @return {!SDK.CSSMatchedStyles}
|
|
*/
|
|
matchedStyles() {
|
|
return this._matchedStyles;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_editable() {
|
|
return !!(this._style.styleSheetId && this._style.range);
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
inherited() {
|
|
return this._inherited;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
overloaded() {
|
|
return this._overloaded;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} x
|
|
*/
|
|
setOverloaded(x) {
|
|
if (x === this._overloaded)
|
|
return;
|
|
this._overloaded = x;
|
|
this._updateState();
|
|
}
|
|
|
|
get name() {
|
|
return this.property.name;
|
|
}
|
|
|
|
get value() {
|
|
return this.property.value;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_updateFilter() {
|
|
const regex = this._parentPane.filterRegex();
|
|
const matches = !!regex && (regex.test(this.property.name) || regex.test(this.property.value));
|
|
this.listItemElement.classList.toggle('filter-match', matches);
|
|
|
|
this.onpopulate();
|
|
let hasMatchingChildren = false;
|
|
for (let i = 0; i < this.childCount(); ++i)
|
|
hasMatchingChildren |= this.childAt(i)._updateFilter();
|
|
|
|
if (!regex) {
|
|
if (this._expandedDueToFilter)
|
|
this.collapse();
|
|
this._expandedDueToFilter = false;
|
|
} else if (hasMatchingChildren && !this.expanded) {
|
|
this.expand();
|
|
this._expandedDueToFilter = true;
|
|
} else if (!hasMatchingChildren && this.expanded && this._expandedDueToFilter) {
|
|
this.collapse();
|
|
this._expandedDueToFilter = false;
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @return {!Node}
|
|
*/
|
|
_processColor(text) {
|
|
// We can be called with valid non-color values of |text| (like 'none' from border style)
|
|
const color = Common.Color.parse(text);
|
|
if (!color)
|
|
return createTextNode(text);
|
|
|
|
if (!this._editable()) {
|
|
const swatch = InlineEditor.ColorSwatch.create();
|
|
swatch.setColor(color);
|
|
return swatch;
|
|
}
|
|
|
|
const swatchPopoverHelper = this._parentPane.swatchPopoverHelper();
|
|
const swatch = InlineEditor.ColorSwatch.create();
|
|
swatch.setColor(color);
|
|
swatch.setFormat(Common.Color.detectColorFormat(swatch.color()));
|
|
const swatchIcon = new Elements.ColorSwatchPopoverIcon(this, swatchPopoverHelper, swatch);
|
|
|
|
/**
|
|
* @param {?SDK.CSSModel.ContrastInfo} contrastInfo
|
|
*/
|
|
function computedCallback(contrastInfo) {
|
|
swatchIcon.setContrastInfo(contrastInfo);
|
|
}
|
|
|
|
if (Runtime.experiments.isEnabled('colorContrastRatio') && this.property.name === 'color' &&
|
|
this._parentPane.cssModel() && this.node()) {
|
|
const cssModel = this._parentPane.cssModel();
|
|
cssModel.backgroundColorsPromise(this.node().id).then(computedCallback);
|
|
}
|
|
|
|
return swatch;
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @return {!Node}
|
|
*/
|
|
_processVar(text) {
|
|
const computedValue = this._matchedStyles.computeValue(this._style, text);
|
|
if (!computedValue)
|
|
return createTextNode(text);
|
|
const color = Common.Color.parse(computedValue);
|
|
if (!color) {
|
|
const node = createElement('span');
|
|
node.textContent = text;
|
|
node.title = computedValue;
|
|
return node;
|
|
}
|
|
if (!this._editable()) {
|
|
const swatch = InlineEditor.ColorSwatch.create();
|
|
swatch.setText(text, computedValue);
|
|
swatch.setColor(color);
|
|
return swatch;
|
|
}
|
|
|
|
const swatchPopoverHelper = this._parentPane.swatchPopoverHelper();
|
|
const swatch = InlineEditor.ColorSwatch.create();
|
|
swatch.setColor(color);
|
|
swatch.setFormat(Common.Color.detectColorFormat(swatch.color()));
|
|
swatch.setText(text, computedValue);
|
|
new Elements.ColorSwatchPopoverIcon(this, swatchPopoverHelper, swatch);
|
|
return swatch;
|
|
}
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
renderedPropertyText() {
|
|
return this.nameElement.textContent + ': ' + this.valueElement.textContent;
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @return {!Node}
|
|
*/
|
|
_processBezier(text) {
|
|
if (!this._editable() || !UI.Geometry.CubicBezier.parse(text))
|
|
return createTextNode(text);
|
|
const swatchPopoverHelper = this._parentPane.swatchPopoverHelper();
|
|
const swatch = InlineEditor.BezierSwatch.create();
|
|
swatch.setBezierText(text);
|
|
new Elements.BezierPopoverIcon(this, swatchPopoverHelper, swatch);
|
|
return swatch;
|
|
}
|
|
|
|
/**
|
|
* @param {string} propertyValue
|
|
* @param {string} propertyName
|
|
* @return {!Node}
|
|
*/
|
|
_processShadow(propertyValue, propertyName) {
|
|
if (!this._editable())
|
|
return createTextNode(propertyValue);
|
|
let shadows;
|
|
if (propertyName === 'text-shadow')
|
|
shadows = InlineEditor.CSSShadowModel.parseTextShadow(propertyValue);
|
|
else
|
|
shadows = InlineEditor.CSSShadowModel.parseBoxShadow(propertyValue);
|
|
if (!shadows.length)
|
|
return createTextNode(propertyValue);
|
|
const container = createDocumentFragment();
|
|
const swatchPopoverHelper = this._parentPane.swatchPopoverHelper();
|
|
for (let i = 0; i < shadows.length; i++) {
|
|
if (i !== 0)
|
|
container.appendChild(createTextNode(', ')); // Add back commas and spaces between each shadow.
|
|
// TODO(flandy): editing the property value should use the original value with all spaces.
|
|
const cssShadowSwatch = InlineEditor.CSSShadowSwatch.create();
|
|
cssShadowSwatch.setCSSShadow(shadows[i]);
|
|
new Elements.ShadowSwatchPopoverHelper(this, swatchPopoverHelper, cssShadowSwatch);
|
|
const colorSwatch = cssShadowSwatch.colorSwatch();
|
|
if (colorSwatch)
|
|
new Elements.ColorSwatchPopoverIcon(this, swatchPopoverHelper, colorSwatch);
|
|
container.appendChild(cssShadowSwatch);
|
|
}
|
|
return container;
|
|
}
|
|
|
|
_updateState() {
|
|
if (!this.listItemElement)
|
|
return;
|
|
|
|
if (this._style.isPropertyImplicit(this.name))
|
|
this.listItemElement.classList.add('implicit');
|
|
else
|
|
this.listItemElement.classList.remove('implicit');
|
|
|
|
const hasIgnorableError =
|
|
!this.property.parsedOk && Elements.StylesSidebarPane.ignoreErrorsForProperty(this.property);
|
|
if (hasIgnorableError)
|
|
this.listItemElement.classList.add('has-ignorable-error');
|
|
else
|
|
this.listItemElement.classList.remove('has-ignorable-error');
|
|
|
|
if (this.inherited())
|
|
this.listItemElement.classList.add('inherited');
|
|
else
|
|
this.listItemElement.classList.remove('inherited');
|
|
|
|
if (this.overloaded())
|
|
this.listItemElement.classList.add('overloaded');
|
|
else
|
|
this.listItemElement.classList.remove('overloaded');
|
|
|
|
if (this.property.disabled)
|
|
this.listItemElement.classList.add('disabled');
|
|
else
|
|
this.listItemElement.classList.remove('disabled');
|
|
}
|
|
|
|
/**
|
|
* @return {?SDK.DOMNode}
|
|
*/
|
|
node() {
|
|
return this._parentPane.node();
|
|
}
|
|
|
|
/**
|
|
* @return {!Elements.StylesSidebarPane}
|
|
*/
|
|
parentPane() {
|
|
return this._parentPane;
|
|
}
|
|
|
|
/**
|
|
* @return {?Elements.StylePropertiesSection}
|
|
*/
|
|
section() {
|
|
return this.treeOutline && this.treeOutline.section;
|
|
}
|
|
|
|
_updatePane() {
|
|
const section = this.section();
|
|
if (section)
|
|
section.refreshUpdate(this);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_toggleEnabled(event) {
|
|
const disabled = !event.target.checked;
|
|
const oldStyleRange = this._style.range;
|
|
if (!oldStyleRange)
|
|
return;
|
|
|
|
/**
|
|
* @param {boolean} success
|
|
* @this {Elements.StylePropertyTreeElement}
|
|
*/
|
|
function callback(success) {
|
|
this._parentPane.setUserOperation(false);
|
|
|
|
if (!success)
|
|
return;
|
|
this._matchedStyles.resetActiveProperties();
|
|
this._updatePane();
|
|
this.styleTextAppliedForTest();
|
|
}
|
|
|
|
event.consume();
|
|
this._parentPane.setUserOperation(true);
|
|
this.property.setDisabled(disabled).then(callback.bind(this));
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onpopulate() {
|
|
// Only populate once and if this property is a shorthand.
|
|
if (this.childCount() || !this.isShorthand)
|
|
return;
|
|
|
|
const longhandProperties = this._style.longhandProperties(this.name);
|
|
for (let i = 0; i < longhandProperties.length; ++i) {
|
|
const name = longhandProperties[i].name;
|
|
let inherited = false;
|
|
let overloaded = false;
|
|
|
|
const section = this.section();
|
|
if (section) {
|
|
inherited = section.isPropertyInherited(name);
|
|
overloaded =
|
|
this._matchedStyles.propertyState(longhandProperties[i]) === SDK.CSSMatchedStyles.PropertyState.Overloaded;
|
|
}
|
|
|
|
const item = new Elements.StylePropertyTreeElement(
|
|
this._parentPane, this._matchedStyles, longhandProperties[i], false, inherited, overloaded, false);
|
|
this.appendChild(item);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onattach() {
|
|
this.updateTitle();
|
|
|
|
this.listItemElement.addEventListener('mousedown', event => {
|
|
if (event.which === 1)
|
|
this._parentPane[Elements.StylePropertyTreeElement.ActiveSymbol] = this;
|
|
}, false);
|
|
this.listItemElement.addEventListener('mouseup', this._mouseUp.bind(this));
|
|
this.listItemElement.addEventListener('click', event => {
|
|
if (!event.target.hasSelection() && event.target !== this.listItemElement)
|
|
event.consume(true);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onexpand() {
|
|
this._updateExpandElement();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
oncollapse() {
|
|
this._updateExpandElement();
|
|
}
|
|
|
|
_updateExpandElement() {
|
|
if (!this._expandElement)
|
|
return;
|
|
if (this.expanded)
|
|
this._expandElement.setIconType('smallicon-triangle-down');
|
|
else
|
|
this._expandElement.setIconType('smallicon-triangle-right');
|
|
}
|
|
|
|
updateTitleIfComputedValueChanged() {
|
|
const computedValue = this._matchedStyles.computeValue(this.property.ownerStyle, this.property.value);
|
|
if (computedValue === this._lastComputedValue)
|
|
return;
|
|
this._lastComputedValue = computedValue;
|
|
this._innerUpdateTitle();
|
|
}
|
|
|
|
updateTitle() {
|
|
this._lastComputedValue = this._matchedStyles.computeValue(this.property.ownerStyle, this.property.value);
|
|
this._innerUpdateTitle();
|
|
}
|
|
|
|
_innerUpdateTitle() {
|
|
this._updateState();
|
|
if (this.isExpandable())
|
|
this._expandElement = UI.Icon.create('smallicon-triangle-right', 'expand-icon');
|
|
else
|
|
this._expandElement = null;
|
|
|
|
const propertyRenderer =
|
|
new Elements.StylesSidebarPropertyRenderer(this._style.parentRule, this.node(), this.name, this.value);
|
|
if (this.property.parsedOk) {
|
|
propertyRenderer.setVarHandler(this._processVar.bind(this));
|
|
propertyRenderer.setColorHandler(this._processColor.bind(this));
|
|
propertyRenderer.setBezierHandler(this._processBezier.bind(this));
|
|
propertyRenderer.setShadowHandler(this._processShadow.bind(this));
|
|
}
|
|
|
|
this.listItemElement.removeChildren();
|
|
this.nameElement = propertyRenderer.renderName();
|
|
if (this.property.name.startsWith('--'))
|
|
this.nameElement.title = this._matchedStyles.computeCSSVariable(this._style, this.property.name) || '';
|
|
this.valueElement = propertyRenderer.renderValue();
|
|
if (!this.treeOutline)
|
|
return;
|
|
|
|
const indent = Common.moduleSetting('textEditorIndent').get();
|
|
this.listItemElement.createChild('span', 'styles-clipboard-only')
|
|
.createTextChild(indent + (this.property.disabled ? '/* ' : ''));
|
|
this.listItemElement.appendChild(this.nameElement);
|
|
this.listItemElement.createTextChild(': ');
|
|
if (this._expandElement)
|
|
this.listItemElement.appendChild(this._expandElement);
|
|
this.listItemElement.appendChild(this.valueElement);
|
|
this.listItemElement.createTextChild(';');
|
|
if (this.property.disabled)
|
|
this.listItemElement.createChild('span', 'styles-clipboard-only').createTextChild(' */');
|
|
|
|
if (!this.property.parsedOk) {
|
|
// Avoid having longhands under an invalid shorthand.
|
|
this.listItemElement.classList.add('not-parsed-ok');
|
|
|
|
// Add a separate exclamation mark IMG element with a tooltip.
|
|
this.listItemElement.insertBefore(
|
|
Elements.StylesSidebarPane.createExclamationMark(this.property), this.listItemElement.firstChild);
|
|
}
|
|
if (!this.property.activeInStyle())
|
|
this.listItemElement.classList.add('inactive');
|
|
this._updateFilter();
|
|
|
|
if (this.property.parsedOk && this.section() && this.parent.root) {
|
|
const enabledCheckboxElement = createElement('input');
|
|
enabledCheckboxElement.className = 'enabled-button';
|
|
enabledCheckboxElement.type = 'checkbox';
|
|
enabledCheckboxElement.checked = !this.property.disabled;
|
|
enabledCheckboxElement.addEventListener('mousedown', event => event.consume(), false);
|
|
enabledCheckboxElement.addEventListener('click', this._toggleEnabled.bind(this), false);
|
|
this.listItemElement.insertBefore(enabledCheckboxElement, this.listItemElement.firstChild);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_mouseUp(event) {
|
|
const activeTreeElement = this._parentPane[Elements.StylePropertyTreeElement.ActiveSymbol];
|
|
this._parentPane[Elements.StylePropertyTreeElement.ActiveSymbol] = null;
|
|
if (activeTreeElement !== this)
|
|
return;
|
|
if (this.listItemElement.hasSelection())
|
|
return;
|
|
if (UI.isBeingEdited(/** @type {!Node} */ (event.target)))
|
|
return;
|
|
|
|
event.consume(true);
|
|
|
|
if (event.target === this.listItemElement)
|
|
return;
|
|
|
|
if (UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!MouseEvent} */ (event)) && this.section().navigable) {
|
|
this._navigateToSource(/** @type {!Element} */ (event.target));
|
|
return;
|
|
}
|
|
|
|
this.startEditing(/** @type {!Element} */ (event.target));
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {boolean=} omitFocus
|
|
*/
|
|
_navigateToSource(element, omitFocus) {
|
|
if (!this.section().navigable)
|
|
return;
|
|
const propertyNameClicked = element === this.nameElement;
|
|
const uiLocation = Bindings.cssWorkspaceBinding.propertyUILocation(this.property, propertyNameClicked);
|
|
if (uiLocation)
|
|
Common.Revealer.reveal(uiLocation, omitFocus);
|
|
}
|
|
|
|
/**
|
|
* @param {?Element=} selectElement
|
|
*/
|
|
startEditing(selectElement) {
|
|
// FIXME: we don't allow editing of longhand properties under a shorthand right now.
|
|
if (this.parent.isShorthand)
|
|
return;
|
|
|
|
if (this._expandElement && selectElement === this._expandElement)
|
|
return;
|
|
|
|
const section = this.section();
|
|
if (section && !section.editable)
|
|
return;
|
|
|
|
if (selectElement) {
|
|
selectElement = selectElement.enclosingNodeOrSelfWithClass('webkit-css-property') ||
|
|
selectElement.enclosingNodeOrSelfWithClass('value');
|
|
}
|
|
if (!selectElement)
|
|
selectElement = this.nameElement;
|
|
|
|
if (UI.isBeingEdited(selectElement))
|
|
return;
|
|
|
|
const isEditingName = selectElement === this.nameElement;
|
|
if (!isEditingName)
|
|
this.valueElement.textContent = restoreURLs(this.valueElement.textContent, this.value);
|
|
|
|
/**
|
|
* @param {string} fieldValue
|
|
* @param {string} modelValue
|
|
* @return {string}
|
|
*/
|
|
function restoreURLs(fieldValue, modelValue) {
|
|
const urlRegex = /\b(url\([^)]*\))/g;
|
|
const splitFieldValue = fieldValue.split(urlRegex);
|
|
if (splitFieldValue.length === 1)
|
|
return fieldValue;
|
|
const modelUrlRegex = new RegExp(urlRegex);
|
|
for (let i = 1; i < splitFieldValue.length; i += 2) {
|
|
const match = modelUrlRegex.exec(modelValue);
|
|
if (match)
|
|
splitFieldValue[i] = match[0];
|
|
}
|
|
return splitFieldValue.join('');
|
|
}
|
|
|
|
/** @type {!Elements.StylePropertyTreeElement.Context} */
|
|
const context = {
|
|
expanded: this.expanded,
|
|
hasChildren: this.isExpandable(),
|
|
isEditingName: isEditingName,
|
|
previousContent: selectElement.textContent
|
|
};
|
|
|
|
// Lie about our children to prevent expanding on double click and to collapse shorthands.
|
|
this.setExpandable(false);
|
|
|
|
if (selectElement.parentElement)
|
|
selectElement.parentElement.classList.add('child-editing');
|
|
selectElement.textContent = selectElement.textContent; // remove color swatch and the like
|
|
|
|
/**
|
|
* @param {!Elements.StylePropertyTreeElement.Context} context
|
|
* @param {!Event} event
|
|
* @this {Elements.StylePropertyTreeElement}
|
|
*/
|
|
function pasteHandler(context, event) {
|
|
const data = event.clipboardData.getData('Text');
|
|
if (!data)
|
|
return;
|
|
const colonIdx = data.indexOf(':');
|
|
if (colonIdx < 0)
|
|
return;
|
|
const name = data.substring(0, colonIdx).trim();
|
|
const value = data.substring(colonIdx + 1).trim();
|
|
|
|
event.preventDefault();
|
|
|
|
if (!('originalName' in context)) {
|
|
context.originalName = this.nameElement.textContent;
|
|
context.originalValue = this.valueElement.textContent;
|
|
}
|
|
this.property.name = name;
|
|
this.property.value = value;
|
|
this.nameElement.textContent = name;
|
|
this.valueElement.textContent = value;
|
|
this.nameElement.normalize();
|
|
this.valueElement.normalize();
|
|
|
|
this._editingCommitted(event.target.textContent, context, 'forward');
|
|
}
|
|
|
|
/**
|
|
* @param {!Elements.StylePropertyTreeElement.Context} context
|
|
* @param {!Event} event
|
|
* @this {Elements.StylePropertyTreeElement}
|
|
*/
|
|
function blurListener(context, event) {
|
|
let text = event.target.textContent;
|
|
if (!context.isEditingName)
|
|
text = this.value || text;
|
|
this._editingCommitted(text, context, '');
|
|
}
|
|
|
|
this._originalPropertyText = this.property.propertyText;
|
|
|
|
this._parentPane.setEditingStyle(true);
|
|
if (selectElement.parentElement)
|
|
selectElement.parentElement.scrollIntoViewIfNeeded(false);
|
|
|
|
this._prompt = new Elements.StylesSidebarPane.CSSPropertyPrompt(this, isEditingName);
|
|
this._prompt.setAutocompletionTimeout(0);
|
|
if (section)
|
|
section.startEditing();
|
|
|
|
// Do not live-edit "content" property of pseudo elements. crbug.com/433889
|
|
if (!isEditingName && (!this._parentPane.node().pseudoType() || this.name !== 'content'))
|
|
this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged, this._applyFreeFlowStyleTextEdit.bind(this));
|
|
|
|
const proxyElement = this._prompt.attachAndStartEditing(selectElement, blurListener.bind(this, context));
|
|
this._navigateToSource(selectElement, true);
|
|
|
|
proxyElement.addEventListener('keydown', this._editingNameValueKeyDown.bind(this, context), false);
|
|
proxyElement.addEventListener('keypress', this._editingNameValueKeyPress.bind(this, context), false);
|
|
if (isEditingName)
|
|
proxyElement.addEventListener('paste', pasteHandler.bind(this, context), false);
|
|
|
|
selectElement.getComponentSelection().selectAllChildren(selectElement);
|
|
}
|
|
|
|
/**
|
|
* @param {!Elements.StylePropertyTreeElement.Context} context
|
|
* @param {!Event} event
|
|
*/
|
|
_editingNameValueKeyDown(context, event) {
|
|
if (event.handled)
|
|
return;
|
|
|
|
let result;
|
|
|
|
if (isEnterKey(event)) {
|
|
result = 'forward';
|
|
} else if (event.keyCode === UI.KeyboardShortcut.Keys.Esc.code || event.key === 'Escape') {
|
|
result = 'cancel';
|
|
} else if (
|
|
!context.isEditingName && this._newProperty && event.keyCode === UI.KeyboardShortcut.Keys.Backspace.code) {
|
|
// For a new property, when Backspace is pressed at the beginning of new property value, move back to the property name.
|
|
const selection = event.target.getComponentSelection();
|
|
if (selection.isCollapsed && !selection.focusOffset) {
|
|
event.preventDefault();
|
|
result = 'backward';
|
|
}
|
|
} else if (event.key === 'Tab') {
|
|
result = event.shiftKey ? 'backward' : 'forward';
|
|
event.preventDefault();
|
|
}
|
|
|
|
if (result) {
|
|
switch (result) {
|
|
case 'cancel':
|
|
this.editingCancelled(null, context);
|
|
break;
|
|
case 'forward':
|
|
case 'backward':
|
|
this._editingCommitted(event.target.textContent, context, result);
|
|
break;
|
|
}
|
|
|
|
event.consume();
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Elements.StylePropertyTreeElement.Context} context
|
|
* @param {!Event} event
|
|
*/
|
|
_editingNameValueKeyPress(context, event) {
|
|
/**
|
|
* @param {string} text
|
|
* @param {number} cursorPosition
|
|
* @return {boolean}
|
|
*/
|
|
function shouldCommitValueSemicolon(text, cursorPosition) {
|
|
// FIXME: should this account for semicolons inside comments?
|
|
let openQuote = '';
|
|
for (let i = 0; i < cursorPosition; ++i) {
|
|
const ch = text[i];
|
|
if (ch === '\\' && openQuote !== '')
|
|
++i; // skip next character inside string
|
|
else if (!openQuote && (ch === '"' || ch === '\''))
|
|
openQuote = ch;
|
|
else if (openQuote === ch)
|
|
openQuote = '';
|
|
}
|
|
return !openQuote;
|
|
}
|
|
|
|
const keyChar = String.fromCharCode(event.charCode);
|
|
const isFieldInputTerminated =
|
|
(context.isEditingName ? keyChar === ':' :
|
|
keyChar === ';' &&
|
|
shouldCommitValueSemicolon(event.target.textContent, event.target.selectionLeftOffset()));
|
|
if (isFieldInputTerminated) {
|
|
// Enter or colon (for name)/semicolon outside of string (for value).
|
|
event.consume(true);
|
|
this._editingCommitted(event.target.textContent, context, 'forward');
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
*/
|
|
async _applyFreeFlowStyleTextEdit() {
|
|
const valueText = this._prompt.textWithCurrentSuggestion();
|
|
if (valueText.indexOf(';') === -1)
|
|
await this.applyStyleText(this.nameElement.textContent + ': ' + valueText, false);
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
*/
|
|
kickFreeFlowStyleEditForTest() {
|
|
return this._applyFreeFlowStyleTextEdit();
|
|
}
|
|
|
|
/**
|
|
* @param {!Elements.StylePropertyTreeElement.Context} context
|
|
*/
|
|
editingEnded(context) {
|
|
this.setExpandable(context.hasChildren);
|
|
if (context.expanded)
|
|
this.expand();
|
|
const editedElement = context.isEditingName ? this.nameElement : this.valueElement;
|
|
// The proxyElement has been deleted, no need to remove listener.
|
|
if (editedElement.parentElement)
|
|
editedElement.parentElement.classList.remove('child-editing');
|
|
|
|
this._parentPane.setEditingStyle(false);
|
|
}
|
|
|
|
/**
|
|
* @param {?Element} element
|
|
* @param {!Elements.StylePropertyTreeElement.Context} context
|
|
*/
|
|
editingCancelled(element, context) {
|
|
this._removePrompt();
|
|
this._revertStyleUponEditingCanceled();
|
|
// This should happen last, as it clears the info necessary to restore the property value after [Page]Up/Down changes.
|
|
this.editingEnded(context);
|
|
}
|
|
|
|
_revertStyleUponEditingCanceled() {
|
|
if (this._propertyHasBeenEditedIncrementally) {
|
|
this.applyStyleText(this._originalPropertyText, false);
|
|
this._originalPropertyText = '';
|
|
} else if (this._newProperty) {
|
|
this.treeOutline.removeChild(this);
|
|
} else {
|
|
this.updateTitle();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} moveDirection
|
|
* @return {?Elements.StylePropertyTreeElement}
|
|
*/
|
|
_findSibling(moveDirection) {
|
|
let target = this;
|
|
do
|
|
target = (moveDirection === 'forward' ? target.nextSibling : target.previousSibling);
|
|
while (target && target.inherited());
|
|
|
|
return target;
|
|
}
|
|
|
|
/**
|
|
* @param {string} userInput
|
|
* @param {!Elements.StylePropertyTreeElement.Context} context
|
|
* @param {string} moveDirection
|
|
*/
|
|
async _editingCommitted(userInput, context, moveDirection) {
|
|
const hadFocus = this._parentPane.element.hasFocus();
|
|
this._removePrompt();
|
|
this.editingEnded(context);
|
|
const isEditingName = context.isEditingName;
|
|
|
|
// Determine where to move to before making changes
|
|
let createNewProperty, moveToSelector;
|
|
const isDataPasted = 'originalName' in context;
|
|
const isDirtyViaPaste = isDataPasted &&
|
|
(this.nameElement.textContent !== context.originalName ||
|
|
this.valueElement.textContent !== context.originalValue);
|
|
const isPropertySplitPaste =
|
|
isDataPasted && isEditingName && this.valueElement.textContent !== context.originalValue;
|
|
let moveTo = this;
|
|
const moveToOther = (isEditingName ^ (moveDirection === 'forward'));
|
|
const abandonNewProperty = this._newProperty && !userInput && (moveToOther || isEditingName);
|
|
if (moveDirection === 'forward' && (!isEditingName || isPropertySplitPaste) ||
|
|
moveDirection === 'backward' && isEditingName) {
|
|
moveTo = moveTo._findSibling(moveDirection);
|
|
if (!moveTo) {
|
|
if (moveDirection === 'forward' && (!this._newProperty || userInput))
|
|
createNewProperty = true;
|
|
else if (moveDirection === 'backward')
|
|
moveToSelector = true;
|
|
}
|
|
}
|
|
|
|
// Make the Changes and trigger the moveToNextCallback after updating.
|
|
let moveToIndex = moveTo && this.treeOutline ? this.treeOutline.rootElement().indexOfChild(moveTo) : -1;
|
|
const blankInput = userInput.isWhitespace();
|
|
const shouldCommitNewProperty = this._newProperty &&
|
|
(isPropertySplitPaste || moveToOther || (!moveDirection && !isEditingName) || (isEditingName && blankInput));
|
|
const section = /** @type {!Elements.StylePropertiesSection} */ (this.section());
|
|
if (((userInput !== context.previousContent || isDirtyViaPaste) && !this._newProperty) || shouldCommitNewProperty) {
|
|
if (hadFocus)
|
|
this._parentPane.element.focus();
|
|
let propertyText;
|
|
if (blankInput || (this._newProperty && this.valueElement.textContent.isWhitespace())) {
|
|
propertyText = '';
|
|
} else {
|
|
if (isEditingName)
|
|
propertyText = userInput + ': ' + this.property.value;
|
|
else
|
|
propertyText = this.property.name + ': ' + userInput;
|
|
}
|
|
await this.applyStyleText(propertyText, true);
|
|
moveToNextCallback.call(this, this._newProperty, !blankInput, section);
|
|
} else {
|
|
if (isEditingName)
|
|
this.property.name = userInput;
|
|
else
|
|
this.property.value = userInput;
|
|
if (!isDataPasted && !this._newProperty)
|
|
this.updateTitle();
|
|
moveToNextCallback.call(this, this._newProperty, false, section);
|
|
}
|
|
|
|
/**
|
|
* The Callback to start editing the next/previous property/selector.
|
|
* @param {boolean} alreadyNew
|
|
* @param {boolean} valueChanged
|
|
* @param {!Elements.StylePropertiesSection} section
|
|
* @this {Elements.StylePropertyTreeElement}
|
|
*/
|
|
function moveToNextCallback(alreadyNew, valueChanged, section) {
|
|
if (!moveDirection)
|
|
return;
|
|
|
|
// User just tabbed through without changes.
|
|
if (moveTo && moveTo.parent) {
|
|
moveTo.startEditing(!isEditingName ? moveTo.nameElement : moveTo.valueElement);
|
|
return;
|
|
}
|
|
|
|
// User has made a change then tabbed, wiping all the original treeElements.
|
|
// Recalculate the new treeElement for the same property we were going to edit next.
|
|
if (moveTo && !moveTo.parent) {
|
|
const rootElement = section.propertiesTreeOutline.rootElement();
|
|
if (moveDirection === 'forward' && blankInput && !isEditingName)
|
|
--moveToIndex;
|
|
if (moveToIndex >= rootElement.childCount() && !this._newProperty) {
|
|
createNewProperty = true;
|
|
} else {
|
|
const treeElement = moveToIndex >= 0 ? rootElement.childAt(moveToIndex) : null;
|
|
if (treeElement) {
|
|
let elementToEdit =
|
|
!isEditingName || isPropertySplitPaste ? treeElement.nameElement : treeElement.valueElement;
|
|
if (alreadyNew && blankInput)
|
|
elementToEdit = moveDirection === 'forward' ? treeElement.nameElement : treeElement.valueElement;
|
|
treeElement.startEditing(elementToEdit);
|
|
return;
|
|
} else if (!alreadyNew) {
|
|
moveToSelector = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a new attribute in this section (or move to next editable selector if possible).
|
|
if (createNewProperty) {
|
|
if (alreadyNew && !valueChanged && (isEditingName ^ (moveDirection === 'backward')))
|
|
return;
|
|
|
|
section.addNewBlankProperty().startEditing();
|
|
return;
|
|
}
|
|
|
|
if (abandonNewProperty) {
|
|
moveTo = this._findSibling(moveDirection);
|
|
const sectionToEdit = (moveTo || moveDirection === 'backward') ? section : section.nextEditableSibling();
|
|
if (sectionToEdit) {
|
|
if (sectionToEdit.style().parentRule)
|
|
sectionToEdit.startEditingSelector();
|
|
else
|
|
sectionToEdit.moveEditorFromSelector(moveDirection);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (moveToSelector) {
|
|
if (section.style().parentRule)
|
|
section.startEditingSelector();
|
|
else
|
|
section.moveEditorFromSelector(moveDirection);
|
|
}
|
|
}
|
|
}
|
|
|
|
_removePrompt() {
|
|
// BUG 53242. This cannot go into editingEnded(), as it should always happen first for any editing outcome.
|
|
if (this._prompt) {
|
|
this._prompt.detach();
|
|
this._prompt = null;
|
|
}
|
|
const section = this.section();
|
|
if (section)
|
|
section.stopEditing();
|
|
}
|
|
|
|
styleTextAppliedForTest() {
|
|
}
|
|
|
|
/**
|
|
* @param {string} styleText
|
|
* @param {boolean} majorChange
|
|
* @return {!Promise}
|
|
*/
|
|
applyStyleText(styleText, majorChange) {
|
|
return this._applyStyleThrottler.schedule(this._innerApplyStyleText.bind(this, styleText, majorChange));
|
|
}
|
|
|
|
/**
|
|
* @param {string} styleText
|
|
* @param {boolean} majorChange
|
|
* @return {!Promise}
|
|
*/
|
|
async _innerApplyStyleText(styleText, majorChange) {
|
|
if (!this.treeOutline)
|
|
return;
|
|
|
|
const oldStyleRange = this._style.range;
|
|
if (!oldStyleRange)
|
|
return;
|
|
|
|
styleText = styleText.replace(/\s/g, ' ').trim(); // Replace with whitespace.
|
|
if (!styleText.length && majorChange && this._newProperty && !this._propertyHasBeenEditedIncrementally) {
|
|
// The user deleted everything and never applied a new property value via Up/Down scrolling/live editing, so remove the tree element and update.
|
|
this.parent.removeChild(this);
|
|
return;
|
|
}
|
|
|
|
const currentNode = this._parentPane.node();
|
|
this._parentPane.setUserOperation(true);
|
|
|
|
// Append a ";" if the new text does not end in ";".
|
|
// FIXME: this does not handle trailing comments.
|
|
if (styleText.length && !/;\s*$/.test(styleText))
|
|
styleText += ';';
|
|
const overwriteProperty = !this._newProperty || this._propertyHasBeenEditedIncrementally;
|
|
let success = await this.property.setText(styleText, majorChange, overwriteProperty);
|
|
// Revert to the original text if applying the new text failed
|
|
if (this._propertyHasBeenEditedIncrementally && majorChange && !success) {
|
|
majorChange = false;
|
|
success = await this.property.setText(this._originalPropertyText, majorChange, overwriteProperty);
|
|
}
|
|
this._parentPane.setUserOperation(false);
|
|
|
|
if (!success) {
|
|
if (majorChange) {
|
|
// It did not apply, cancel editing.
|
|
if (this._newProperty)
|
|
this.treeOutline.removeChild(this);
|
|
else
|
|
this.updateTitle();
|
|
}
|
|
this.styleTextAppliedForTest();
|
|
return;
|
|
}
|
|
|
|
this._matchedStyles.resetActiveProperties();
|
|
this._propertyHasBeenEditedIncrementally = true;
|
|
this.property = this._style.propertyAt(this.property.index);
|
|
|
|
if (currentNode === this.node())
|
|
this._updatePane();
|
|
|
|
this.styleTextAppliedForTest();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {boolean}
|
|
*/
|
|
ondblclick() {
|
|
return true; // handled
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!Event} event
|
|
* @return {boolean}
|
|
*/
|
|
isEventWithinDisclosureTriangle(event) {
|
|
return event.target === this._expandElement;
|
|
}
|
|
};
|
|
|
|
/** @typedef {{expanded: boolean, hasChildren: boolean, isEditingName: boolean, previousContent: string}} */
|
|
Elements.StylePropertyTreeElement.Context;
|
|
Elements.StylePropertyTreeElement.ActiveSymbol = Symbol('ActiveSymbol');
|