blob: 7c5736fc153be15bec105f7719a2b72d1c5f5478 [file] [log] [blame]
// 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.
export default class StylePropertyTreeElement 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._hasBeenEditedIncrementally = false;
this._prompt = null;
this._lastComputedValue = null;
/** @type {(!Elements.StylePropertyTreeElement.Context|undefined)} */
this._contextForTest;
}
/**
* @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 swatch = InlineEditor.ColorSwatch.create();
swatch.setColor(color);
swatch.setFormat(Common.Color.detectColorFormat(swatch.color()));
this._addColorContrastInfo(swatch);
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 swatch = InlineEditor.ColorSwatch.create();
swatch.setColor(color);
swatch.setFormat(Common.Color.detectColorFormat(swatch.color()));
swatch.setText(text, computedValue);
this._addColorContrastInfo(swatch);
return swatch;
}
/**
* @param {!InlineEditor.ColorSwatch} swatch
*/
async _addColorContrastInfo(swatch) {
const swatchPopoverHelper = this._parentPane.swatchPopoverHelper();
const swatchIcon = new Elements.ColorSwatchPopoverIcon(this, swatchPopoverHelper, swatch);
if (this.property.name !== 'color' || !this._parentPane.cssModel() || !this.node()) {
return;
}
const cssModel = this._parentPane.cssModel();
const contrastInfo = new ColorPicker.ContrastInfo(await cssModel.backgroundColorsPromise(this.node().id));
swatchIcon.setContrastInfo(contrastInfo);
}
/**
* @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;
}
/**
* @param {string} propertyValue
* @param {string} propertyName
* @return {!Node}
*/
_processGrid(propertyValue, propertyName) {
const splitResult = TextUtils.TextUtils.splitStringByRegexes(propertyValue, [SDK.CSSMetadata.GridAreaRowRegex]);
if (splitResult.length <= 1) {
return createTextNode(propertyValue);
}
const indent = Common.moduleSetting('textEditorIndent').get();
const container = createDocumentFragment();
for (const result of splitResult) {
const value = result.value.trim();
const content = UI.html`<br /><span class='styles-clipboard-only'>${indent.repeat(2)}</span>${value}`;
container.appendChild(content);
}
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 {boolean} disabled
*/
async _toggleDisabled(disabled) {
const oldStyleRange = this._style.range;
if (!oldStyleRange) {
return;
}
this._parentPane.setUserOperation(true);
const success = await this.property.setDisabled(disabled);
this._parentPane.setUserOperation(false);
if (!success) {
return;
}
this._matchedStyles.resetActiveProperties();
this._updatePane();
this.styleTextAppliedForTest();
}
/**
* @override
* @returns {!Promise}
*/
async 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 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[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));
propertyRenderer.setGridHandler(this._processGrid.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);
const lineBreakValue = this.valueElement.firstElementChild && this.valueElement.firstElementChild.tagName === 'BR';
const separator = lineBreakValue ? ':' : ': ';
this.listItemElement.createChild('span', 'styles-name-value-separator').textContent = separator;
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', event => {
this._toggleDisabled(!this.property.disabled);
event.consume();
}, false);
UI.ARIAUtils.setAccessibleName(
enabledCheckboxElement, `${this.nameElement.textContent} ${this.valueElement.textContent}`);
this.listItemElement.insertBefore(enabledCheckboxElement, this.listItemElement.firstChild);
}
}
/**
* @param {!Event} event
*/
_mouseUp(event) {
const activeTreeElement = this._parentPane[ActiveSymbol];
this._parentPane[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 {!Elements.StylePropertyTreeElement.Context} context
* @param {!Event} event
*/
_handleContextMenuEvent(context, event) {
const contextMenu = new UI.ContextMenu(event);
if (this.property.parsedOk && this.section() && this.parent.root) {
contextMenu.defaultSection().appendCheckboxItem(ls`Toggle property and continue editing`, async () => {
this.editingCancelled(null, context);
const sectionIndex = this._parentPane.focusedSectionIndex();
const propertyIndex = this.treeOutline.rootElement().indexOfChild(this);
await this._toggleDisabled(!this.property.disabled);
event.consume();
this._parentPane.continueEditingElement(sectionIndex, propertyIndex);
}, !this.property.disabled);
}
contextMenu.defaultSection().appendItem(ls`Reveal in Sources panel`, this._navigateToSource.bind(this));
contextMenu.show();
}
/**
* @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) {
if (SDK.cssMetadata().isGridAreaDefiningProperty(this.name)) {
this.valueElement.textContent = restoreGridIndents(this.value);
}
this.valueElement.textContent = restoreURLs(this.valueElement.textContent, this.value);
}
/**
* @param {string} value
*/
function restoreGridIndents(value) {
const splitResult = TextUtils.TextUtils.splitStringByRegexes(value, [SDK.CSSMetadata.GridAreaRowRegex]);
return splitResult.map(result => result.value.trim()).join('\n');
}
/**
* @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,
originalProperty: this.property,
previousContent: selectElement.textContent
};
this._contextForTest = context;
// 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 {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 {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, this);
if (selectElement.parentElement) {
selectElement.parentElement.scrollIntoViewIfNeeded(false);
}
this._prompt = new Elements.StylesSidebarPane.CSSPropertyPrompt(this, isEditingName);
this._prompt.setAutocompletionTimeout(0);
this._prompt.addEventListener(
UI.TextPrompt.Events.TextChanged, this._applyFreeFlowStyleTextEdit.bind(this, context));
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);
proxyElement.addEventListener('contextmenu', this._handleContextMenuEvent.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) && !event.shiftKey) {
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;
}
}
/**
* @param {!Elements.StylePropertyTreeElement.Context} context
* @return {!Promise}
*/
async _applyFreeFlowStyleTextEdit(context) {
if (!this._prompt || !this._parentPane.node()) {
return;
}
const enteredText = this._prompt.text();
if (context.isEditingName && enteredText.includes(':')) {
this._editingCommitted(enteredText, context, 'forward');
return;
}
const valueText = this._prompt.textWithCurrentSuggestion();
if (valueText.includes(';')) {
return;
}
// Prevent destructive side-effects during live-edit. crbug.com/433889
const isPseudo = !!this._parentPane.node().pseudoType();
if (isPseudo) {
if (this.name.toLowerCase() === 'content') {
return;
}
const lowerValueText = valueText.trim().toLowerCase();
if (lowerValueText.startsWith('content:') || lowerValueText === 'display: none') {
return;
}
}
if (context.isEditingName) {
if (valueText.includes(':')) {
await this.applyStyleText(valueText, false);
} else if (this._hasBeenEditedIncrementally) {
await this._applyOriginalStyle(context);
}
} else {
await this.applyStyleText(`${this.nameElement.textContent}: ${valueText}`, false);
}
}
/**
* @return {!Promise}
*/
kickFreeFlowStyleEditForTest() {
const context = this._contextForTest;
return this._applyFreeFlowStyleTextEdit(/** @type {!Elements.StylePropertyTreeElement.Context} */ (context));
}
/**
* @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();
if (this._hasBeenEditedIncrementally) {
this._applyOriginalStyle(context);
} else if (this._newProperty) {
this.treeOutline.removeChild(this);
}
this.updateTitle();
// This should happen last, as it clears the info necessary to restore the property value after [Page]Up/Down changes.
this.editingEnded(context);
}
/**
* @param {!Elements.StylePropertyTreeElement.Context} context
*/
async _applyOriginalStyle(context) {
await this.applyStyleText(this._originalPropertyText, false, context.originalProperty);
}
/**
* @param {string} moveDirection
* @return {?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) {
this._removePrompt();
this.editingEnded(context);
const isEditingName = context.isEditingName;
const nameValueEntered = isEditingName && this.nameElement.textContent.includes(':');
// 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) ||
nameValueEntered);
const section = /** @type {!Elements.StylePropertiesSection} */ (this.section());
if (((userInput !== context.previousContent || isDirtyViaPaste) && !this._newProperty) || shouldCommitNewProperty) {
let propertyText;
if (nameValueEntered) {
propertyText = this.nameElement.textContent;
} else 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 {StylePropertyTreeElement}
*/
function moveToNextCallback(alreadyNew, valueChanged, section) {
if (!moveDirection) {
this._parentPane.resetFocus();
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;
}
}
styleTextAppliedForTest() {
}
/**
* @param {string} styleText
* @param {boolean} majorChange
* @param {?SDK.CSSProperty=} property
* @return {!Promise}
*/
applyStyleText(styleText, majorChange, property) {
return this._applyStyleThrottler.schedule(this._innerApplyStyleText.bind(this, styleText, majorChange, property));
}
/**
* @param {string} styleText
* @param {boolean} majorChange
* @param {?SDK.CSSProperty=} property
* @return {!Promise}
*/
async _innerApplyStyleText(styleText, majorChange, property) {
if (!this.treeOutline) {
return;
}
const oldStyleRange = this._style.range;
if (!oldStyleRange) {
return;
}
const hasBeenEditedIncrementally = this._hasBeenEditedIncrementally;
styleText = styleText.replace(/[\xA0\t]/g, ' ').trim(); // Replace &nbsp; with whitespace.
if (!styleText.length && majorChange && this._newProperty && !hasBeenEditedIncrementally) {
// 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 || hasBeenEditedIncrementally;
let success = await this.property.setText(styleText, majorChange, overwriteProperty);
// Revert to the original text if applying the new text failed
if (hasBeenEditedIncrementally && 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._hasBeenEditedIncrementally = true;
this.property = 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;
}
}
export const ActiveSymbol = Symbol('ActiveSymbol');
/* Legacy exported object */
self.Elements = self.Elements || {};
/* Legacy exported object */
Elements = Elements || {};
/** @constructor */
Elements.StylePropertyTreeElement = StylePropertyTreeElement;
Elements.StylePropertyTreeElement.ActiveSymbol = ActiveSymbol;
/** @typedef {{
* expanded: boolean,
* hasChildren: boolean,
* isEditingName: boolean,
* originalProperty: (!SDK.CSSProperty|undefined),
* originalName: (string|undefined),
* originalValue: (string|undefined),
* previousContent: string
* }}
*/
Elements.StylePropertyTreeElement.Context;