2 * Copyright (C) 2017 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
26 WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
28 constructor(delegate, property, options = {})
32 console.assert(property instanceof WI.CSSProperty);
34 this._delegate = delegate || null;
35 this._property = property;
36 this._readOnly = options.readOnly || false;
37 this._element = document.createElement("div");
39 this._contentElement = null;
40 this._nameElement = null;
41 this._valueElement = null;
43 this._nameTextField = null;
44 this._valueTextField = null;
46 this._selected = false;
47 this._hasInvalidVariableValue = false;
50 property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this.updateStatus, this);
51 property.addEventListener(WI.CSSProperty.Event.Changed, this.updateStatus, this);
53 if (!this._readOnly) {
54 this._element.tabIndex = -1;
56 this._element.addEventListener("blur", (event) => {
57 // Keep selection after tabbing out of Web Inspector window and back.
58 if (document.activeElement === this._element)
61 if (this._delegate.spreadsheetStylePropertyBlur)
62 this._delegate.spreadsheetStylePropertyBlur(event, this);
65 this._element.addEventListener("mouseenter", (event) => {
66 if (this._delegate.spreadsheetStylePropertyMouseEnter)
67 this._delegate.spreadsheetStylePropertyMouseEnter(event, this);
70 this._element.copyHandler = this;
76 get element() { return this._element; }
77 get property() { return this._property; }
78 get enabled() { return this._property.enabled; }
82 this._element.dataset.propertyIndex = index;
87 return this._selected;
92 if (value === this._selected)
95 this._selected = value;
101 if (!this._nameTextField)
104 this._nameTextField.startEditing();
109 if (!this._valueTextField)
112 this._valueTextField.startEditing();
117 if (this._nameTextField)
118 this._nameTextField.detached();
120 if (this._valueTextField)
121 this._valueTextField.detached();
126 if (this._nameTextField && this._nameTextField.editing)
127 this._nameTextField.element.blur();
128 else if (this._valueTextField && this._valueTextField.editing)
129 this._valueTextField.element.blur();
132 remove(replacement = null)
134 this.element.remove();
137 this._property.replaceWithText(replacement);
139 this._property.remove();
143 if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
144 this._delegate.spreadsheetStylePropertyRemoved(this);
149 this.element.removeChildren();
151 if (this._isEditable()) {
152 this._checkboxElement = this.element.appendChild(document.createElement("input"));
153 this._checkboxElement.classList.add("property-toggle");
154 this._checkboxElement.type = "checkbox";
155 this._checkboxElement.checked = this._property.enabled;
156 this._checkboxElement.tabIndex = -1;
157 this._checkboxElement.addEventListener("click", (event) => {
158 event.stopPropagation();
159 let disabled = !this._checkboxElement.checked;
160 this._property.commentOut(disabled);
165 this._contentElement = this.element.appendChild(document.createElement("span"));
166 this._contentElement.className = "content";
168 if (!this._property.enabled)
169 this._contentElement.append("/* ");
171 this._nameElement = this._contentElement.appendChild(document.createElement("span"));
172 this._nameElement.classList.add("name");
173 this._nameElement.textContent = this._property.name;
175 let colonElement = this._contentElement.appendChild(document.createElement("span"));
176 colonElement.classList.add("colon");
177 colonElement.textContent = ": ";
179 this._valueElement = this._contentElement.appendChild(document.createElement("span"));
180 this._valueElement.classList.add("value");
181 this._renderValue(this._property.rawValue);
183 if (this._isEditable() && this._property.enabled) {
184 this._nameElement.tabIndex = 0;
185 this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this));
186 this._nameElement.addEventListener("paste", this._handleNamePaste.bind(this));
188 this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
190 this._valueElement.tabIndex = 0;
191 this._valueElement.addEventListener("beforeinput", this._handleValueBeforeInput.bind(this));
193 this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
196 if (this._isEditable()) {
197 this._setupJumpToSymbol(this._nameElement);
198 this._setupJumpToSymbol(this._valueElement);
201 let semicolonElement = this._contentElement.appendChild(document.createElement("span"));
202 semicolonElement.classList.add("semicolon");
203 semicolonElement.textContent = ";";
205 if (this._property.enabled) {
206 this._warningElement = this.element.appendChild(document.createElement("span"));
207 this._warningElement.className = "warning";
209 this._contentElement.append(" */");
211 if (!this._property.implicit && this._property.ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed) {
212 let effectiveProperty = this._property.ownerStyle.nodeStyles.effectivePropertyForName(this._property.name);
213 if (effectiveProperty && !effectiveProperty.styleSheetTextRange)
214 effectiveProperty = effectiveProperty.relatedShorthandProperty;
216 let ownerRule = effectiveProperty ? effectiveProperty.ownerStyle.ownerRule : null;
218 let arrowElement = this._contentElement.appendChild(WI.createGoToArrowButton());
219 arrowElement.addEventListener("click", (event) => {
220 if (!effectiveProperty || !ownerRule || !event.altKey) {
221 if (this._delegate.spreadsheetStylePropertyShowProperty)
222 this._delegate.spreadsheetStylePropertyShowProperty(this, this._property);
226 let sourceCode = ownerRule.sourceCodeLocation.sourceCode;
227 let {startLine, startColumn} = effectiveProperty.styleSheetTextRange;
228 WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(startLine, startColumn), {
229 ignoreNetworkTab: true,
230 ignoreSearchTab: true,
234 if (effectiveProperty && ownerRule)
235 arrowElement.title = WI.UIString("Option-click to show source");
243 let duplicatePropertyExistsBelow = (cssProperty) => {
244 let propertyFound = false;
246 for (let property of this._property.ownerStyle.enabledProperties) {
247 if (property === cssProperty)
248 propertyFound = true;
249 else if (property.name === cssProperty.name && propertyFound)
256 let classNames = [WI.SpreadsheetStyleProperty.StyleClassName];
257 let elementTitle = "";
259 if (this._property.overridden) {
260 classNames.push("overridden");
261 if (duplicatePropertyExistsBelow(this._property)) {
262 classNames.push("has-warning");
263 elementTitle = WI.UIString("Duplicate property");
267 if (this._property.implicit)
268 classNames.push("implicit");
270 if (this._property.ownerStyle.inherited && !this._property.inherited)
271 classNames.push("not-inherited");
273 if (!this._property.valid && this._property.hasOtherVendorNameOrKeyword())
274 classNames.push("other-vendor");
275 else if (this._hasInvalidVariableValue || (!this._property.valid && this._property.value !== "")) {
276 let propertyNameIsValid = false;
277 if (WI.CSSCompletions.cssNameCompletions)
278 propertyNameIsValid = WI.CSSCompletions.cssNameCompletions.isValidPropertyName(this._property.name);
280 classNames.push("has-warning");
282 if (!propertyNameIsValid) {
283 classNames.push("invalid-name");
284 elementTitle = WI.UIString("Unsupported property name");
286 classNames.push("invalid-value");
287 elementTitle = WI.UIString("Unsupported property value");
291 if (!this._property.enabled)
292 classNames.push("disabled");
294 if (this._property.modified)
295 classNames.push("modified");
298 classNames.push("selected");
300 this._element.className = classNames.join(" ");
301 this._element.title = elementTitle;
304 applyFilter(filterText)
306 let matchesName = this._nameElement.textContent.includes(filterText);
307 this._nameElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesName);
309 let matchesValue = this._valueElement.textContent.includes(filterText);
310 this._valueElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesValue);
312 let matches = matchesName || matchesValue;
313 this._contentElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName, !matches);
317 handleCopyEvent(event)
319 this._delegate.spreadsheetStylePropertyCopy(event, this);
322 // SpreadsheetTextField delegate
324 spreadsheetTextFieldWillStartEditing(textField)
326 let isEditingName = textField === this._nameTextField;
327 textField.value = isEditingName ? this._property.name : this._property.rawValue;
330 spreadsheetTextFieldDidChange(textField)
332 if (textField === this._valueTextField)
333 this._handleValueChange();
334 else if (textField === this._nameTextField)
335 this._handleNameChange();
338 spreadsheetTextFieldDidCommit(textField, {direction})
340 let propertyName = this._nameTextField.value.trim();
341 let propertyValue = this._valueTextField.value.trim();
342 let willRemoveProperty = false;
343 let isEditingName = textField === this._nameTextField;
345 if (!propertyName || (!propertyValue && !isEditingName && direction === "forward"))
346 willRemoveProperty = true;
348 if (!isEditingName && !willRemoveProperty)
349 this._renderValue(propertyValue);
351 if (direction === "forward") {
352 if (isEditingName && !willRemoveProperty) {
353 // Move focus from the name to the value.
354 this._valueTextField.startEditing();
358 if (!isEditingName) {
359 // Move focus from the value to the name.
360 this._nameTextField.startEditing();
365 if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function") {
366 // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists.
367 this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction, willRemoveProperty});
370 if (willRemoveProperty)
374 spreadsheetTextFieldDidBlur(textField, event, changed)
376 let focusedOutsideThisProperty = event.relatedTarget !== this._nameElement && event.relatedTarget !== this._valueElement;
377 if (focusedOutsideThisProperty && (!this._nameTextField.value.trim() || !this._valueTextField.value.trim())) {
382 if (textField === this._valueTextField)
383 this._renderValue(this._valueElement.textContent);
385 if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function")
386 this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction: null});
388 if (changed && window.DOMAgent)
389 DOMAgent.markUndoableState();
392 spreadsheetTextFieldDidBackspace(textField)
394 if (textField === this._nameTextField)
395 this.spreadsheetTextFieldDidCommit(textField, {direction: "backward"});
396 else if (textField === this._valueTextField)
397 this._nameTextField.startEditing();
400 spreadsheetTextFieldDidPressEsc(textField, textBeforeEditing)
402 let isNewProperty = !textBeforeEditing;
405 else if (this._delegate.spreadsheetStylePropertyDidPressEsc)
406 this._delegate.spreadsheetStylePropertyDidPressEsc(this);
413 return !this._readOnly && this._property.editable;
418 this._hasInvalidVariableValue = false;
420 const maxValueLength = 150;
421 let tokens = WI.tokenizeCSSValue(value);
423 if (this._property.enabled) {
424 // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.)
426 // CSS variables may contain color - display color picker for them.
427 if (this._property.variable || WI.CSSKeywordCompletions.isColorAwareProperty(this._property.name)) {
428 tokens = this._addGradientTokens(tokens);
429 tokens = this._addColorTokens(tokens);
431 tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier");
432 tokens = this._addTimingFunctionTokens(tokens, "spring");
433 tokens = this._addVariableTokens(tokens);
436 tokens = tokens.map((token) => {
437 if (token instanceof Element)
443 if (token.type.includes("string"))
444 className = "token-string";
445 else if (token.type.includes("link"))
446 className = "token-link";
447 else if (token.type.includes("comment"))
448 className = "token-comment";
452 let span = document.createElement("span");
453 span.classList.add(className);
454 span.textContent = token.value.truncateMiddle(maxValueLength);
456 if (token.type && token.type.includes("link"))
457 span.addEventListener("contextmenu", this._handleLinkContextMenu.bind(this, token));
465 this._valueElement.removeChildren();
466 this._valueElement.append(...tokens);
469 _createInlineSwatch(type, text, valueObject)
471 let tokenElement = document.createElement("span");
472 let innerElement = document.createElement("span");
473 innerElement.textContent = text;
475 let readOnly = !this._isEditable();
476 let swatch = new WI.InlineSwatch(type, valueObject, readOnly);
478 swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
479 let value = event.data.value && event.data.value.toString();
483 innerElement.textContent = value;
484 this._handleValueChange();
487 if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchActivated === "function") {
488 swatch.addEventListener(WI.InlineSwatch.Event.Activated, () => {
489 this._swatchActive = true;
490 this._delegate.stylePropertyInlineSwatchActivated();
494 if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") {
495 swatch.addEventListener(WI.InlineSwatch.Event.Deactivated, () => {
496 this._swatchActive = false;
497 this._delegate.stylePropertyInlineSwatchDeactivated();
501 tokenElement.append(swatch.element, innerElement);
503 // Prevent the value from editing when clicking on the swatch.
504 swatch.element.addEventListener("click", (event) => {
505 if (this._swatchActive || event.shiftKey)
512 _addGradientTokens(tokens)
514 let gradientRegex = /^(repeating-)?(linear|radial)-gradient$/i;
516 let gradientStartIndex = NaN;
517 let openParenthesis = 0;
519 for (let i = 0; i < tokens.length; i++) {
520 let token = tokens[i];
521 if (token.type && token.type.includes("atom") && gradientRegex.test(token.value)) {
522 gradientStartIndex = i;
524 } else if (token.value === "(" && !isNaN(gradientStartIndex))
526 else if (token.value === ")" && !isNaN(gradientStartIndex)) {
528 if (openParenthesis > 0) {
529 // Matched a CSS function inside of the gradient.
533 let rawTokens = tokens.slice(gradientStartIndex, i + 1);
534 let text = rawTokens.map((token) => token.value).join("");
535 let gradient = WI.Gradient.fromString(text);
537 newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, text, gradient));
539 newTokens.push(...rawTokens);
541 gradientStartIndex = NaN;
542 } else if (isNaN(gradientStartIndex))
543 newTokens.push(token);
549 _addColorTokens(tokens)
553 let pushPossibleColorToken = (text, ...rawTokens) => {
554 let color = WI.Color.fromString(text);
556 newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, text, color));
558 newTokens.push(...rawTokens);
561 let colorFunctionStartIndex = NaN;
563 for (let i = 0; i < tokens.length; i++) {
564 let token = tokens[i];
565 if (token.type && token.type.includes("hex-color")) {
567 pushPossibleColorToken(token.value, token);
568 } else if (WI.Color.FunctionNames.has(token.value) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
569 // Color Function start
570 colorFunctionStartIndex = i;
571 } else if (isNaN(colorFunctionStartIndex) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
573 pushPossibleColorToken(token.value, token);
574 } else if (!isNaN(colorFunctionStartIndex)) {
575 // Color Function end
576 if (token.value !== ")")
579 let rawTokens = tokens.slice(colorFunctionStartIndex, i + 1);
580 let text = rawTokens.map((token) => token.value).join("");
581 pushPossibleColorToken(text, ...rawTokens);
582 colorFunctionStartIndex = NaN;
584 newTokens.push(token);
590 _addTimingFunctionTokens(tokens, tokenType)
593 let startIndex = NaN;
594 let openParenthesis = 0;
596 for (let i = 0; i < tokens.length; i++) {
597 let token = tokens[i];
598 if (token.value === tokenType && token.type && token.type.includes("atom")) {
601 } else if (token.value === "(" && !isNaN(startIndex))
603 else if (token.value === ")" && !isNaN(startIndex)) {
606 if (openParenthesis > 0)
609 let rawTokens = tokens.slice(startIndex, i + 1);
610 let text = rawTokens.map((token) => token.value).join("");
613 let inlineSwatchType;
614 if (tokenType === "cubic-bezier") {
615 valueObject = WI.CubicBezier.fromString(text);
616 inlineSwatchType = WI.InlineSwatch.Type.Bezier;
617 } else if (tokenType === "spring") {
618 valueObject = WI.Spring.fromString(text);
619 inlineSwatchType = WI.InlineSwatch.Type.Spring;
623 newTokens.push(this._createInlineSwatch(inlineSwatchType, text, valueObject));
625 newTokens.push(...rawTokens);
628 } else if (isNaN(startIndex))
629 newTokens.push(token);
635 _addVariableTokens(tokens)
638 let startIndex = NaN;
639 let openParenthesis = 0;
641 for (let i = 0; i < tokens.length; i++) {
642 let token = tokens[i];
643 if (token.value === "var" && token.type && token.type.includes("atom")) {
646 } else if (token.value === "(" && !isNaN(startIndex))
648 else if (token.value === ")" && !isNaN(startIndex)) {
650 if (openParenthesis > 0)
653 let rawTokens = tokens.slice(startIndex, i + 1);
654 let tokenValues = rawTokens.map((token) => token.value);
655 let variableName = tokenValues.find((value, i) => value.startsWith("--") && /\bvariable-2\b/.test(rawTokens[i].type));
657 const dontCreateIfMissing = true;
658 let variableProperty = this._property.ownerStyle.nodeStyles.computedStyle.propertyForName(variableName, dontCreateIfMissing);
659 if (variableProperty) {
660 let valueObject = variableProperty.value.trim();
661 newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, tokenValues.join(""), valueObject));
663 this._hasInvalidVariableValue = true;
664 newTokens.push(...rawTokens);
668 } else if (isNaN(startIndex))
669 newTokens.push(token);
677 this._property.name = this._nameElement.textContent.trim();
682 this._property.rawValue = this._valueElement.textContent.trim();
685 _handleNameBeforeInput(event)
687 if (event.data !== ":" || event.inputType !== "insertText")
690 event.preventDefault();
691 this._nameTextField.discardCompletion();
692 this._valueTextField.startEditing();
695 _handleNamePaste(event)
697 let text = event.clipboardData.getData("text/plain");
698 if (!text || !text.includes(":"))
701 event.preventDefault();
705 if (this._delegate.spreadsheetStylePropertyAddBlankPropertySoon) {
706 this._delegate.spreadsheetStylePropertyAddBlankPropertySoon(this, {
707 index: parseInt(this._element.dataset.propertyIndex) + 1,
712 _nameCompletionDataProvider(prefix)
716 completions: WI.CSSCompletions.cssNameCompletions.startsWith(prefix)
720 _handleValueBeforeInput(event)
722 if (event.data !== ";" || event.inputType !== "insertText")
725 let text = this._valueTextField.valueWithoutSuggestion();
726 let selection = window.getSelection();
727 if (!selection.rangeCount || selection.getRangeAt(0).endOffset !== text.length)
730 let unbalancedCharacters = WI.CSSCompletions.completeUnbalancedValue(text);
731 if (unbalancedCharacters)
734 event.preventDefault();
735 this._valueTextField.stopEditing();
736 this.spreadsheetTextFieldDidCommit(this._valueTextField, {direction: "forward"});
739 _valueCompletionDataProvider(prefix)
741 // For "border: 1px so|", we want to suggest "solid" based on "so" prefix.
742 let match = prefix.match(/[a-z0-9()-]+$/i);
744 return {completions: [], prefix: ""};
747 let propertyName = this._nameElement.textContent.trim();
750 completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix)
754 _setupJumpToSymbol(element)
756 element.addEventListener("mousedown", (event) => {
757 if (event.button !== 0)
760 if (!WI.modifierKeys.metaKey)
763 if (element.isContentEditable)
766 let sourceCodeLocation = null;
767 if (this._property.ownerStyle.ownerRule)
768 sourceCodeLocation = this._property.ownerStyle.ownerRule.sourceCodeLocation;
770 if (!sourceCodeLocation)
773 let range = this._property.styleSheetTextRange;
775 ignoreNetworkTab: true,
776 ignoreSearchTab: true,
778 let sourceCode = sourceCodeLocation.sourceCode;
779 WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options);
783 _handleLinkContextMenu(token, event)
785 let contextMenu = WI.ContextMenu.createFromEvent(event);
787 let resolveURL = (url) => {
788 let ownerStyle = this._property.ownerStyle;
792 let ownerStyleSheet = ownerStyle.ownerStyleSheet;
793 if (!ownerStyleSheet) {
794 let ownerRule = ownerStyle.ownerRule;
796 ownerStyleSheet = ownerRule.ownerStyleSheet;
798 if (ownerStyleSheet) {
799 if (ownerStyleSheet.url)
800 return absoluteURL(url, ownerStyleSheet.url);
802 let parentFrame = ownerStyleSheet.parentFrame;
804 return absoluteURL(url, parentFrame.url);
807 let node = ownerStyle.node;
809 let nodeStyles = ownerStyle.nodeStyles;
811 let ownerRule = ownerStyle.ownerRule;
813 nodeStyles = ownerRule.nodeStyles;
816 node = nodeStyles.node;
819 let ownerDocument = node.ownerDocument;
821 return absoluteURL(url, node.ownerDocument.documentURL);
827 WI.appendContextMenuItemsForURL(contextMenu, resolveURL(token.value));
831 WI.SpreadsheetStyleProperty.StyleClassName = "property";