UNPKG

chrome-devtools-frontend

Version:
494 lines (452 loc) • 17.4 kB
// Copyright 2016 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. import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as UI from '../ui/ui.js'; import {CSSLength, CSSShadowModel} from './CSSShadowModel.js'; // eslint-disable-line no-unused-vars export const UIStrings = { /** *@description Text that refers to some types */ type: 'Type', /** *@description Outset button text content in CSSShadow Editor of the inline editor in the Styles tab */ outset: 'Outset', /** *@description Inset button text content in CSSShadow Editor of the inline editor in the Styles tab */ inset: 'Inset', /** *@description Text in CSSShadow Editor of the inline editor in the Styles tab */ xOffset: 'X offset', /** *@description Text in CSSShadow Editor of the inline editor in the Styles tab */ yOffset: 'Y offset', /** *@description Text in CSSShadow Editor of the inline editor in the Styles tab */ blur: 'Blur', /** *@description Text in CSSShadow Editor of the inline editor in the Styles tab */ spread: 'Spread', }; const str_ = i18n.i18n.registerUIStrings('inline_editor/CSSShadowEditor.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** @type {number} */ const maxRange = 20; /** @type {string} */ const defaultUnit = 'px'; /** @type {number} */ const sliderThumbRadius = 6; /** @type {number} */ const canvasSize = 88; export class CSSShadowEditor extends UI.Widget.VBox { constructor() { super(true); this.registerRequiredCSS('inline_editor/cssShadowEditor.css', {enableLegacyPatching: true}); this.contentElement.tabIndex = 0; this.setDefaultFocusedElement(this.contentElement); this._typeField = this.contentElement.createChild('div', 'shadow-editor-field shadow-editor-flex-field'); this._typeField.createChild('label', 'shadow-editor-label').textContent = i18nString(UIStrings.type); this._outsetButton = this._typeField.createChild('button', 'shadow-editor-button-left'); this._outsetButton.textContent = i18nString(UIStrings.outset); this._outsetButton.addEventListener('click', this._onButtonClick.bind(this), false); this._insetButton = this._typeField.createChild('button', 'shadow-editor-button-right'); this._insetButton.textContent = i18nString(UIStrings.inset); this._insetButton.addEventListener('click', this._onButtonClick.bind(this), false); const xField = this.contentElement.createChild('div', 'shadow-editor-field'); this._xInput = this._createTextInput(xField, i18nString(UIStrings.xOffset)); const yField = this.contentElement.createChild('div', 'shadow-editor-field'); this._yInput = this._createTextInput(yField, i18nString(UIStrings.yOffset)); /** @type {!HTMLCanvasElement} */ this._xySlider = /** @type {!HTMLCanvasElement} */ (xField.createChild('canvas', 'shadow-editor-2D-slider')); this._xySlider.width = canvasSize; this._xySlider.height = canvasSize; this._xySlider.tabIndex = -1; this._halfCanvasSize = canvasSize / 2; this._innerCanvasSize = this._halfCanvasSize - sliderThumbRadius; UI.UIUtils.installDragHandle( this._xySlider, this._dragStart.bind(this), this._dragMove.bind(this), null, 'default'); this._xySlider.addEventListener('keydown', this._onCanvasArrowKey.bind(this), false); this._xySlider.addEventListener('blur', this._onCanvasBlur.bind(this), false); const blurField = this.contentElement.createChild('div', 'shadow-editor-field shadow-editor-flex-field shadow-editor-blur-field'); this._blurInput = this._createTextInput(blurField, i18nString(UIStrings.blur)); this._blurSlider = this._createSlider(blurField); this._spreadField = this.contentElement.createChild('div', 'shadow-editor-field shadow-editor-flex-field'); this._spreadInput = this._createTextInput(this._spreadField, i18nString(UIStrings.spread)); this._spreadSlider = this._createSlider(this._spreadField); /** @type {!CSSShadowModel} */ this._model; /** @type {!UI.Geometry.Point} */ this._canvasOrigin; } /** * @param {!Element} field * @param {string} propertyName * @return {!HTMLInputElement} */ _createTextInput(field, propertyName) { const label = field.createChild('label', 'shadow-editor-label'); label.textContent = propertyName; label.setAttribute('for', propertyName); const textInput = UI.UIUtils.createInput('shadow-editor-text-input', 'text'); field.appendChild(textInput); textInput.id = propertyName; textInput.addEventListener('keydown', this._handleValueModification.bind(this), false); textInput.addEventListener('mousewheel', this._handleValueModification.bind(this), false); textInput.addEventListener('input', this._onTextInput.bind(this), false); textInput.addEventListener('blur', this._onTextBlur.bind(this), false); return textInput; } /** * @param {!Element} field * @return {!HTMLInputElement} */ _createSlider(field) { const slider = UI.UIUtils.createSlider(0, maxRange, -1); slider.addEventListener('input', this._onSliderInput.bind(this), false); field.appendChild(slider); return /** @type {!HTMLInputElement} */ (slider); } /** * @override */ wasShown() { this._updateUI(); } /** * @param {!CSSShadowModel} model */ setModel(model) { this._model = model; this._typeField.classList.toggle('hidden', !model.isBoxShadow()); this._spreadField.classList.toggle('hidden', !model.isBoxShadow()); this._updateUI(); } _updateUI() { this._updateButtons(); this._xInput.value = this._model.offsetX().asCSSText(); this._yInput.value = this._model.offsetY().asCSSText(); this._blurInput.value = this._model.blurRadius().asCSSText(); this._spreadInput.value = this._model.spreadRadius().asCSSText(); this._blurSlider.value = this._model.blurRadius().amount.toString(); this._spreadSlider.value = this._model.spreadRadius().amount.toString(); this._updateCanvas(false); } _updateButtons() { this._insetButton.classList.toggle('enabled', this._model.inset()); this._outsetButton.classList.toggle('enabled', !this._model.inset()); } /** * @param {boolean} drawFocus */ _updateCanvas(drawFocus) { const context = this._xySlider.getContext('2d'); if (!context) { throw new Error('Unable to obtain canvas context'); } context.clearRect(0, 0, this._xySlider.width, this._xySlider.height); // Draw dashed axes. context.save(); context.setLineDash([1, 1]); context.strokeStyle = 'rgba(210, 210, 210, 0.8)'; context.beginPath(); context.moveTo(this._halfCanvasSize, 0); context.lineTo(this._halfCanvasSize, canvasSize); context.moveTo(0, this._halfCanvasSize); context.lineTo(canvasSize, this._halfCanvasSize); context.stroke(); context.restore(); const thumbPoint = this._sliderThumbPosition(); // Draw 2D slider line. context.save(); context.translate(this._halfCanvasSize, this._halfCanvasSize); context.lineWidth = 2; context.strokeStyle = 'rgba(130, 130, 130, 0.75)'; context.beginPath(); context.moveTo(0, 0); context.lineTo(thumbPoint.x, thumbPoint.y); context.stroke(); // Draw 2D slider thumb. if (drawFocus) { context.beginPath(); context.fillStyle = 'rgba(66, 133, 244, 0.4)'; context.arc(thumbPoint.x, thumbPoint.y, sliderThumbRadius + 2, 0, 2 * Math.PI); context.fill(); } context.beginPath(); context.fillStyle = '#4285F4'; context.arc(thumbPoint.x, thumbPoint.y, sliderThumbRadius, 0, 2 * Math.PI); context.fill(); context.restore(); } /** * @param {!Event} event */ _onButtonClick(event) { const insetClicked = (event.currentTarget === this._insetButton); if (insetClicked && this._model.inset() || !insetClicked && !this._model.inset()) { return; } this._model.setInset(insetClicked); this._updateButtons(); this.dispatchEventToListeners(Events.ShadowChanged, this._model); } /** * @param {!Event} event */ _handleValueModification(event) { const target = /** @type {!HTMLInputElement} */ (event.currentTarget); const modifiedValue = UI.UIUtils.createReplacementString(target.value, event, customNumberHandler); if (!modifiedValue) { return; } const length = CSSLength.parse(modifiedValue); if (!length) { return; } if (event.currentTarget === this._blurInput && length.amount < 0) { length.amount = 0; } target.value = length.asCSSText(); target.selectionStart = 0; target.selectionEnd = target.value.length; this._onTextInput(event); event.consume(true); /** * @param {string} prefix * @param {number} number * @param {string} suffix * @return {string} */ function customNumberHandler(prefix, number, suffix) { if (!suffix.length) { suffix = defaultUnit; } return prefix + number + suffix; } } /** * @param {!Event} event */ _onTextInput(event) { const currentTarget = /** @type {!HTMLInputElement} */ (event.currentTarget); this._changedElement = currentTarget; this._changedElement.classList.remove('invalid'); const length = CSSLength.parse(currentTarget.value); if (!length || currentTarget === this._blurInput && length.amount < 0) { return; } if (currentTarget === this._xInput) { this._model.setOffsetX(length); this._updateCanvas(false); } else if (currentTarget === this._yInput) { this._model.setOffsetY(length); this._updateCanvas(false); } else if (currentTarget === this._blurInput) { this._model.setBlurRadius(length); this._blurSlider.value = length.amount.toString(); } else if (currentTarget === this._spreadInput) { this._model.setSpreadRadius(length); this._spreadSlider.value = length.amount.toString(); } this.dispatchEventToListeners(Events.ShadowChanged, this._model); } _onTextBlur() { if (!this._changedElement) { return; } let length = !this._changedElement.value.trim() ? CSSLength.zero() : CSSLength.parse(this._changedElement.value); if (!length) { length = CSSLength.parse(this._changedElement.value + defaultUnit); } if (!length) { this._changedElement.classList.add('invalid'); this._changedElement = null; return; } if (this._changedElement === this._xInput) { this._model.setOffsetX(length); this._xInput.value = length.asCSSText(); this._updateCanvas(false); } else if (this._changedElement === this._yInput) { this._model.setOffsetY(length); this._yInput.value = length.asCSSText(); this._updateCanvas(false); } else if (this._changedElement === this._blurInput) { if (length.amount < 0) { length = CSSLength.zero(); } this._model.setBlurRadius(length); this._blurInput.value = length.asCSSText(); this._blurSlider.value = length.amount.toString(); } else if (this._changedElement === this._spreadInput) { this._model.setSpreadRadius(length); this._spreadInput.value = length.asCSSText(); this._spreadSlider.value = length.amount.toString(); } this._changedElement = null; this.dispatchEventToListeners(Events.ShadowChanged, this._model); } /** * @param {!Event} event */ _onSliderInput(event) { if (event.currentTarget === this._blurSlider) { this._model.setBlurRadius( new CSSLength(Number(this._blurSlider.value), this._model.blurRadius().unit || defaultUnit)); this._blurInput.value = this._model.blurRadius().asCSSText(); this._blurInput.classList.remove('invalid'); } else if (event.currentTarget === this._spreadSlider) { this._model.setSpreadRadius( new CSSLength(Number(this._spreadSlider.value), this._model.spreadRadius().unit || defaultUnit)); this._spreadInput.value = this._model.spreadRadius().asCSSText(); this._spreadInput.classList.remove('invalid'); } this.dispatchEventToListeners(Events.ShadowChanged, this._model); } /** * @param {!MouseEvent} event * @return {boolean} */ _dragStart(event) { this._xySlider.focus(); this._updateCanvas(true); this._canvasOrigin = new UI.Geometry.Point( this._xySlider.totalOffsetLeft() + this._halfCanvasSize, this._xySlider.totalOffsetTop() + this._halfCanvasSize); const clickedPoint = new UI.Geometry.Point(event.x - this._canvasOrigin.x, event.y - this._canvasOrigin.y); const thumbPoint = this._sliderThumbPosition(); if (clickedPoint.distanceTo(thumbPoint) >= sliderThumbRadius) { this._dragMove(event); } return true; } /** * @param {!MouseEvent} event */ _dragMove(event) { let point = new UI.Geometry.Point(event.x - this._canvasOrigin.x, event.y - this._canvasOrigin.y); if (event.shiftKey) { point = this._snapToClosestDirection(point); } const constrainedPoint = this._constrainPoint(point, this._innerCanvasSize); const newX = Math.round((constrainedPoint.x / this._innerCanvasSize) * maxRange); const newY = Math.round((constrainedPoint.y / this._innerCanvasSize) * maxRange); if (event.shiftKey) { this._model.setOffsetX(new CSSLength(newX, this._model.offsetX().unit || defaultUnit)); this._model.setOffsetY(new CSSLength(newY, this._model.offsetY().unit || defaultUnit)); } else { if (!event.altKey) { this._model.setOffsetX(new CSSLength(newX, this._model.offsetX().unit || defaultUnit)); } if (!UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlOrMeta(event)) { this._model.setOffsetY(new CSSLength(newY, this._model.offsetY().unit || defaultUnit)); } } this._xInput.value = this._model.offsetX().asCSSText(); this._yInput.value = this._model.offsetY().asCSSText(); this._xInput.classList.remove('invalid'); this._yInput.classList.remove('invalid'); this._updateCanvas(true); this.dispatchEventToListeners(Events.ShadowChanged, this._model); } _onCanvasBlur() { this._updateCanvas(false); } /** * @param {!Event} event */ _onCanvasArrowKey(event) { const keyboardEvent = /** @type {!KeyboardEvent} */ (event); let shiftX = 0; let shiftY = 0; if (keyboardEvent.key === 'ArrowRight') { shiftX = 1; } else if (keyboardEvent.key === 'ArrowLeft') { shiftX = -1; } else if (keyboardEvent.key === 'ArrowUp') { shiftY = -1; } else if (keyboardEvent.key === 'ArrowDown') { shiftY = 1; } if (!shiftX && !shiftY) { return; } event.consume(true); if (shiftX) { const offsetX = this._model.offsetX(); const newAmount = Platform.NumberUtilities.clamp(offsetX.amount + shiftX, -maxRange, maxRange); if (newAmount === offsetX.amount) { return; } this._model.setOffsetX(new CSSLength(newAmount, offsetX.unit || defaultUnit)); this._xInput.value = this._model.offsetX().asCSSText(); this._xInput.classList.remove('invalid'); } if (shiftY) { const offsetY = this._model.offsetY(); const newAmount = Platform.NumberUtilities.clamp(offsetY.amount + shiftY, -maxRange, maxRange); if (newAmount === offsetY.amount) { return; } this._model.setOffsetY(new CSSLength(newAmount, offsetY.unit || defaultUnit)); this._yInput.value = this._model.offsetY().asCSSText(); this._yInput.classList.remove('invalid'); } this._updateCanvas(true); this.dispatchEventToListeners(Events.ShadowChanged, this._model); } /** * @param {!UI.Geometry.Point} point * @param {number} max * @return {!UI.Geometry.Point} */ _constrainPoint(point, max) { if (Math.abs(point.x) <= max && Math.abs(point.y) <= max) { return new UI.Geometry.Point(point.x, point.y); } return point.scale(max / Math.max(Math.abs(point.x), Math.abs(point.y))); } /** * @param {!UI.Geometry.Point} point * @return {!UI.Geometry.Point} */ _snapToClosestDirection(point) { let minDistance = Number.MAX_VALUE; let closestPoint = point; const directions = [ new UI.Geometry.Point(0, -1), // North new UI.Geometry.Point(1, -1), // Northeast new UI.Geometry.Point(1, 0), // East new UI.Geometry.Point(1, 1) // Southeast ]; for (const direction of directions) { const projection = point.projectOn(direction); const distance = point.distanceTo(projection); if (distance < minDistance) { minDistance = distance; closestPoint = projection; } } return closestPoint; } /** * @return {!UI.Geometry.Point} */ _sliderThumbPosition() { const x = (this._model.offsetX().amount / maxRange) * this._innerCanvasSize; const y = (this._model.offsetY().amount / maxRange) * this._innerCanvasSize; return this._constrainPoint(new UI.Geometry.Point(x, y), this._innerCanvasSize); } } /** @enum {symbol} */ export const Events = { ShadowChanged: Symbol('ShadowChanged') };