| /* | |
| * Copyright (C) 2013 Google 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: | |
| * | |
| * * Redistributions of source code must retain the above copyright | |
| * notice, this list of conditions and the following disclaimer. | |
| * * 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. | |
| * * Neither the name of Google Inc. nor the names of its | |
| * contributors may be used to endorse or promote products derived from | |
| * this software without specific prior written permission. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT | |
| * OWNER OR 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. | |
| */ | |
| Sources.CSSPlugin = class extends Sources.UISourceCodeFrame.Plugin { | |
| /** | |
| * @param {!SourceFrame.SourcesTextEditor} textEditor | |
| */ | |
| constructor(textEditor) { | |
| super(); | |
| this._textEditor = textEditor; | |
| this._swatchPopoverHelper = new InlineEditor.SwatchPopoverHelper(); | |
| this._muteSwatchProcessing = false; | |
| this._hadSwatchChange = false; | |
| /** @type {?InlineEditor.BezierEditor} */ | |
| this._bezierEditor = null; | |
| /** @type {?TextUtils.TextRange} */ | |
| this._editedSwatchTextRange = null; | |
| /** @type {?ColorPicker.Spectrum} */ | |
| this._spectrum = null; | |
| /** @type {?Element} */ | |
| this._currentSwatch = null; | |
| this._textEditor.configureAutocomplete( | |
| {suggestionsCallback: this._cssSuggestions.bind(this), isWordChar: this._isWordChar.bind(this)}); | |
| this._textEditor.addEventListener( | |
| SourceFrame.SourcesTextEditor.Events.ScrollChanged, this._textEditorScrolled, this); | |
| this._textEditor.addEventListener(UI.TextEditor.Events.TextChanged, this._onTextChanged, this); | |
| this._updateSwatches(0, this._textEditor.linesCount - 1); | |
| this._shortcuts = {}; | |
| this._registerShortcuts(); | |
| this._boundHandleKeyDown = this._handleKeyDown.bind(this); | |
| this._textEditor.element.addEventListener('keydown', this._boundHandleKeyDown, false); | |
| } | |
| /** | |
| * @override | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @return {boolean} | |
| */ | |
| static accepts(uiSourceCode) { | |
| return uiSourceCode.contentType().isStyleSheet(); | |
| } | |
| _registerShortcuts() { | |
| const shortcutKeys = UI.ShortcutsScreen.SourcesPanelShortcuts; | |
| for (const descriptor of shortcutKeys.IncreaseCSSUnitByOne) { | |
| this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, 1); | |
| } | |
| for (const descriptor of shortcutKeys.DecreaseCSSUnitByOne) { | |
| this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, -1); | |
| } | |
| for (const descriptor of shortcutKeys.IncreaseCSSUnitByTen) { | |
| this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, 10); | |
| } | |
| for (const descriptor of shortcutKeys.DecreaseCSSUnitByTen) { | |
| this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, -10); | |
| } | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _handleKeyDown(event) { | |
| const shortcutKey = UI.KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event)); | |
| const handler = this._shortcuts[shortcutKey]; | |
| if (handler && handler()) { | |
| event.consume(true); | |
| } | |
| } | |
| _textEditorScrolled() { | |
| if (this._swatchPopoverHelper.isShowing()) { | |
| this._swatchPopoverHelper.hide(true); | |
| } | |
| } | |
| /** | |
| * @param {string} unit | |
| * @param {number} change | |
| * @return {?string} | |
| */ | |
| _modifyUnit(unit, change) { | |
| const unitValue = parseInt(unit, 10); | |
| if (isNaN(unitValue)) { | |
| return null; | |
| } | |
| const tail = unit.substring((unitValue).toString().length); | |
| return String.sprintf('%d%s', unitValue + change, tail); | |
| } | |
| /** | |
| * @param {number} change | |
| * @return {boolean} | |
| */ | |
| _handleUnitModification(change) { | |
| const selection = this._textEditor.selection().normalize(); | |
| let token = this._textEditor.tokenAtTextPosition(selection.startLine, selection.startColumn); | |
| if (!token) { | |
| if (selection.startColumn > 0) { | |
| token = this._textEditor.tokenAtTextPosition(selection.startLine, selection.startColumn - 1); | |
| } | |
| if (!token) { | |
| return false; | |
| } | |
| } | |
| if (token.type !== 'css-number') { | |
| return false; | |
| } | |
| const cssUnitRange = | |
| new TextUtils.TextRange(selection.startLine, token.startColumn, selection.startLine, token.endColumn); | |
| const cssUnitText = this._textEditor.text(cssUnitRange); | |
| const newUnitText = this._modifyUnit(cssUnitText, change); | |
| if (!newUnitText) { | |
| return false; | |
| } | |
| this._textEditor.editRange(cssUnitRange, newUnitText); | |
| selection.startColumn = token.startColumn; | |
| selection.endColumn = selection.startColumn + newUnitText.length; | |
| this._textEditor.setSelection(selection); | |
| return true; | |
| } | |
| /** | |
| * @param {number} startLine | |
| * @param {number} endLine | |
| */ | |
| _updateSwatches(startLine, endLine) { | |
| const swatches = []; | |
| const swatchPositions = []; | |
| const regexes = | |
| [SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex, UI.Geometry.CubicBezier.Regex, Common.Color.Regex]; | |
| const handlers = new Map(); | |
| handlers.set(Common.Color.Regex, this._createColorSwatch.bind(this)); | |
| handlers.set(UI.Geometry.CubicBezier.Regex, this._createBezierSwatch.bind(this)); | |
| for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) { | |
| const line = this._textEditor.line(lineNumber).substring(0, Sources.CSSPlugin.maxSwatchProcessingLength); | |
| const results = TextUtils.TextUtils.splitStringByRegexes(line, regexes); | |
| for (let i = 0; i < results.length; i++) { | |
| const result = results[i]; | |
| if (result.regexIndex === -1 || !handlers.has(regexes[result.regexIndex])) { | |
| continue; | |
| } | |
| const delimiters = /[\s:;,(){}]/; | |
| const positionBefore = result.position - 1; | |
| const positionAfter = result.position + result.value.length; | |
| if (positionBefore >= 0 && !delimiters.test(line.charAt(positionBefore)) || | |
| positionAfter < line.length && !delimiters.test(line.charAt(positionAfter))) { | |
| continue; | |
| } | |
| const swatch = handlers.get(regexes[result.regexIndex])(result.value); | |
| if (!swatch) { | |
| continue; | |
| } | |
| swatches.push(swatch); | |
| swatchPositions.push(TextUtils.TextRange.createFromLocation(lineNumber, result.position)); | |
| } | |
| } | |
| this._textEditor.operation(putSwatchesInline.bind(this)); | |
| /** | |
| * @this {Sources.CSSPlugin} | |
| */ | |
| function putSwatchesInline() { | |
| const clearRange = new TextUtils.TextRange(startLine, 0, endLine, this._textEditor.line(endLine).length); | |
| this._textEditor.bookmarks(clearRange, Sources.CSSPlugin.SwatchBookmark).forEach(marker => marker.clear()); | |
| for (let i = 0; i < swatches.length; i++) { | |
| const swatch = swatches[i]; | |
| const swatchPosition = swatchPositions[i]; | |
| const bookmark = this._textEditor.addBookmark( | |
| swatchPosition.startLine, swatchPosition.startColumn, swatch, Sources.CSSPlugin.SwatchBookmark); | |
| swatch[Sources.CSSPlugin.SwatchBookmark] = bookmark; | |
| } | |
| } | |
| } | |
| /** | |
| * @param {string} text | |
| * @return {?InlineEditor.ColorSwatch} | |
| */ | |
| _createColorSwatch(text) { | |
| const color = Common.Color.parse(text); | |
| if (!color) { | |
| return null; | |
| } | |
| const swatch = InlineEditor.ColorSwatch.create(); | |
| swatch.setColor(color); | |
| swatch.iconElement().title = Common.UIString('Open color picker.'); | |
| swatch.iconElement().addEventListener('click', this._swatchIconClicked.bind(this, swatch), false); | |
| swatch.hideText(true); | |
| return swatch; | |
| } | |
| /** | |
| * @param {string} text | |
| * @return {?InlineEditor.BezierSwatch} | |
| */ | |
| _createBezierSwatch(text) { | |
| if (!UI.Geometry.CubicBezier.parse(text)) { | |
| return null; | |
| } | |
| const swatch = InlineEditor.BezierSwatch.create(); | |
| swatch.setBezierText(text); | |
| swatch.iconElement().title = Common.UIString('Open cubic bezier editor.'); | |
| swatch.iconElement().addEventListener('click', this._swatchIconClicked.bind(this, swatch), false); | |
| swatch.hideText(true); | |
| return swatch; | |
| } | |
| /** | |
| * @param {!Element} swatch | |
| * @param {!Event} event | |
| */ | |
| _swatchIconClicked(swatch, event) { | |
| event.consume(true); | |
| this._hadSwatchChange = false; | |
| this._muteSwatchProcessing = true; | |
| const swatchPosition = swatch[Sources.CSSPlugin.SwatchBookmark].position(); | |
| this._textEditor.setSelection(swatchPosition); | |
| this._editedSwatchTextRange = swatchPosition.clone(); | |
| this._editedSwatchTextRange.endColumn += swatch.textContent.length; | |
| this._currentSwatch = swatch; | |
| if (swatch instanceof InlineEditor.ColorSwatch) { | |
| this._showSpectrum(swatch); | |
| } else if (swatch instanceof InlineEditor.BezierSwatch) { | |
| this._showBezierEditor(swatch); | |
| } | |
| } | |
| /** | |
| * @param {!InlineEditor.ColorSwatch} swatch | |
| */ | |
| _showSpectrum(swatch) { | |
| if (!this._spectrum) { | |
| this._spectrum = new ColorPicker.Spectrum(); | |
| this._spectrum.addEventListener(ColorPicker.Spectrum.Events.SizeChanged, this._spectrumResized, this); | |
| this._spectrum.addEventListener(ColorPicker.Spectrum.Events.ColorChanged, this._spectrumChanged, this); | |
| } | |
| this._spectrum.setColor(swatch.color(), swatch.format()); | |
| this._swatchPopoverHelper.show(this._spectrum, swatch.iconElement(), this._swatchPopoverHidden.bind(this)); | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _spectrumResized(event) { | |
| this._swatchPopoverHelper.reposition(); | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _spectrumChanged(event) { | |
| const colorString = /** @type {string} */ (event.data); | |
| const color = Common.Color.parse(colorString); | |
| if (!color) { | |
| return; | |
| } | |
| this._currentSwatch.setColor(color); | |
| this._changeSwatchText(colorString); | |
| } | |
| /** | |
| * @param {!InlineEditor.BezierSwatch} swatch | |
| */ | |
| _showBezierEditor(swatch) { | |
| if (!this._bezierEditor) { | |
| this._bezierEditor = new InlineEditor.BezierEditor(); | |
| this._bezierEditor.addEventListener(InlineEditor.BezierEditor.Events.BezierChanged, this._bezierChanged, this); | |
| } | |
| let cubicBezier = UI.Geometry.CubicBezier.parse(swatch.bezierText()); | |
| if (!cubicBezier) { | |
| cubicBezier = | |
| /** @type {!UI.Geometry.CubicBezier} */ (UI.Geometry.CubicBezier.parse('linear')); | |
| } | |
| this._bezierEditor.setBezier(cubicBezier); | |
| this._swatchPopoverHelper.show(this._bezierEditor, swatch.iconElement(), this._swatchPopoverHidden.bind(this)); | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _bezierChanged(event) { | |
| const bezierString = /** @type {string} */ (event.data); | |
| this._currentSwatch.setBezierText(bezierString); | |
| this._changeSwatchText(bezierString); | |
| } | |
| /** | |
| * @param {string} text | |
| */ | |
| _changeSwatchText(text) { | |
| this._hadSwatchChange = true; | |
| this._textEditor.editRange( | |
| /** @type {!TextUtils.TextRange} */ (this._editedSwatchTextRange), text, '*swatch-text-changed'); | |
| this._editedSwatchTextRange.endColumn = this._editedSwatchTextRange.startColumn + text.length; | |
| } | |
| /** | |
| * @param {boolean} commitEdit | |
| */ | |
| _swatchPopoverHidden(commitEdit) { | |
| this._muteSwatchProcessing = false; | |
| if (!commitEdit && this._hadSwatchChange) { | |
| this._textEditor.undo(); | |
| } | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _onTextChanged(event) { | |
| if (!this._muteSwatchProcessing) { | |
| this._updateSwatches(event.data.newRange.startLine, event.data.newRange.endLine); | |
| } | |
| } | |
| /** | |
| * @param {string} char | |
| * @return {boolean} | |
| */ | |
| _isWordChar(char) { | |
| return TextUtils.TextUtils.isWordChar(char) || char === '.' || char === '-' || char === '$'; | |
| } | |
| /** | |
| * @param {!TextUtils.TextRange} prefixRange | |
| * @param {!TextUtils.TextRange} substituteRange | |
| * @return {?Promise.<!UI.SuggestBox.Suggestions>} | |
| */ | |
| _cssSuggestions(prefixRange, substituteRange) { | |
| const prefix = this._textEditor.text(prefixRange); | |
| if (prefix.startsWith('$')) { | |
| return null; | |
| } | |
| const propertyToken = this._backtrackPropertyToken(prefixRange.startLine, prefixRange.startColumn - 1); | |
| if (!propertyToken) { | |
| return null; | |
| } | |
| const line = this._textEditor.line(prefixRange.startLine); | |
| const tokenContent = line.substring(propertyToken.startColumn, propertyToken.endColumn); | |
| const propertyValues = SDK.cssMetadata().propertyValues(tokenContent); | |
| return Promise.resolve(propertyValues.filter(value => value.startsWith(prefix)).map(value => ({text: value}))); | |
| } | |
| /** | |
| * @param {number} lineNumber | |
| * @param {number} columnNumber | |
| * @return {?{startColumn: number, endColumn: number, type: string}} | |
| */ | |
| _backtrackPropertyToken(lineNumber, columnNumber) { | |
| const backtrackDepth = 10; | |
| let tokenPosition = columnNumber; | |
| const line = this._textEditor.line(lineNumber); | |
| let seenColon = false; | |
| for (let i = 0; i < backtrackDepth && tokenPosition >= 0; ++i) { | |
| const token = this._textEditor.tokenAtTextPosition(lineNumber, tokenPosition); | |
| if (!token) { | |
| return null; | |
| } | |
| if (token.type === 'css-property') { | |
| return seenColon ? token : null; | |
| } | |
| if (token.type && !(token.type.indexOf('whitespace') !== -1 || token.type.startsWith('css-comment'))) { | |
| return null; | |
| } | |
| if (!token.type && line.substring(token.startColumn, token.endColumn) === ':') { | |
| if (!seenColon) { | |
| seenColon = true; | |
| } else { | |
| return null; | |
| } | |
| } | |
| tokenPosition = token.startColumn - 1; | |
| } | |
| return null; | |
| } | |
| /** | |
| * @override | |
| */ | |
| dispose() { | |
| if (this._swatchPopoverHelper.isShowing()) { | |
| this._swatchPopoverHelper.hide(true); | |
| } | |
| this._textEditor.removeEventListener( | |
| SourceFrame.SourcesTextEditor.Events.ScrollChanged, this._textEditorScrolled, this); | |
| this._textEditor.removeEventListener(UI.TextEditor.Events.TextChanged, this._onTextChanged, this); | |
| this._textEditor.bookmarks(this._textEditor.fullRange(), Sources.CSSPlugin.SwatchBookmark) | |
| .forEach(marker => marker.clear()); | |
| this._textEditor.element.removeEventListener('keydown', this._boundHandleKeyDown, false); | |
| } | |
| }; | |
| /** @type {number} */ | |
| Sources.CSSPlugin.maxSwatchProcessingLength = 300; | |
| /** @type {symbol} */ | |
| Sources.CSSPlugin.SwatchBookmark = Symbol('swatch'); |