|  | // 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. | 
|  |  | 
|  | /** @type {number} */ | 
|  | const maxRange = 20; | 
|  | /** @type {string} */ | 
|  | const defaultUnit = 'px'; | 
|  | /** @type {number} */ | 
|  | const sliderThumbRadius = 6; | 
|  | /** @type {number} */ | 
|  | const canvasSize = 88; | 
|  |  | 
|  | /** | 
|  | * @unrestricted | 
|  | */ | 
|  | export class CSSShadowEditor extends UI.VBox { | 
|  | constructor() { | 
|  | super(true); | 
|  | this.registerRequiredCSS('inline_editor/cssShadowEditor.css'); | 
|  | 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 = Common.UIString('Type'); | 
|  | this._outsetButton = this._typeField.createChild('button', 'shadow-editor-button-left'); | 
|  | this._outsetButton.textContent = Common.UIString('Outset'); | 
|  | this._outsetButton.addEventListener('click', this._onButtonClick.bind(this), false); | 
|  | this._insetButton = this._typeField.createChild('button', 'shadow-editor-button-right'); | 
|  | this._insetButton.textContent = Common.UIString('Inset'); | 
|  | this._insetButton.addEventListener('click', this._onButtonClick.bind(this), false); | 
|  |  | 
|  | const xField = this.contentElement.createChild('div', 'shadow-editor-field'); | 
|  | this._xInput = this._createTextInput(xField, Common.UIString('X offset')); | 
|  | const yField = this.contentElement.createChild('div', 'shadow-editor-field'); | 
|  | this._yInput = this._createTextInput(yField, Common.UIString('Y offset')); | 
|  | this._xySlider = 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.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, Common.UIString('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, Common.UIString('Spread')); | 
|  | this._spreadSlider = this._createSlider(this._spreadField); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Element} field | 
|  | * @param {string} propertyName | 
|  | * @return {!Element} | 
|  | */ | 
|  | _createTextInput(field, propertyName) { | 
|  | const label = field.createChild('label', 'shadow-editor-label'); | 
|  | label.textContent = propertyName; | 
|  | label.setAttribute('for', propertyName); | 
|  | const textInput = UI.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 {!Element} | 
|  | */ | 
|  | _createSlider(field) { | 
|  | const slider = UI.createSlider(0, maxRange, -1); | 
|  | slider.addEventListener('input', this._onSliderInput.bind(this), false); | 
|  | field.appendChild(slider); | 
|  | return slider; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | */ | 
|  | wasShown() { | 
|  | this._updateUI(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!InlineEditor.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; | 
|  | this._spreadSlider.value = this._model.spreadRadius().amount; | 
|  | 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'); | 
|  | 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 modifiedValue = UI.createReplacementString(event.currentTarget.value, event, customNumberHandler); | 
|  | if (!modifiedValue) { | 
|  | return; | 
|  | } | 
|  | const length = InlineEditor.CSSLength.parse(modifiedValue); | 
|  | if (!length) { | 
|  | return; | 
|  | } | 
|  | if (event.currentTarget === this._blurInput && length.amount < 0) { | 
|  | length.amount = 0; | 
|  | } | 
|  | event.currentTarget.value = length.asCSSText(); | 
|  | event.currentTarget.selectionStart = 0; | 
|  | event.currentTarget.selectionEnd = event.currentTarget.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) { | 
|  | this._changedElement = event.currentTarget; | 
|  | this._changedElement.classList.remove('invalid'); | 
|  | const length = InlineEditor.CSSLength.parse(event.currentTarget.value); | 
|  | if (!length || event.currentTarget === this._blurInput && length.amount < 0) { | 
|  | return; | 
|  | } | 
|  | if (event.currentTarget === this._xInput) { | 
|  | this._model.setOffsetX(length); | 
|  | this._updateCanvas(false); | 
|  | } else if (event.currentTarget === this._yInput) { | 
|  | this._model.setOffsetY(length); | 
|  | this._updateCanvas(false); | 
|  | } else if (event.currentTarget === this._blurInput) { | 
|  | this._model.setBlurRadius(length); | 
|  | this._blurSlider.value = length.amount; | 
|  | } else if (event.currentTarget === this._spreadInput) { | 
|  | this._model.setSpreadRadius(length); | 
|  | this._spreadSlider.value = length.amount; | 
|  | } | 
|  | this.dispatchEventToListeners(Events.ShadowChanged, this._model); | 
|  | } | 
|  |  | 
|  | _onTextBlur() { | 
|  | if (!this._changedElement) { | 
|  | return; | 
|  | } | 
|  | let length = !this._changedElement.value.trim() ? InlineEditor.CSSLength.zero() : | 
|  | InlineEditor.CSSLength.parse(this._changedElement.value); | 
|  | if (!length) { | 
|  | length = InlineEditor.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 = InlineEditor.CSSLength.zero(); | 
|  | } | 
|  | this._model.setBlurRadius(length); | 
|  | this._blurInput.value = length.asCSSText(); | 
|  | this._blurSlider.value = length.amount; | 
|  | } else if (this._changedElement === this._spreadInput) { | 
|  | this._model.setSpreadRadius(length); | 
|  | this._spreadInput.value = length.asCSSText(); | 
|  | this._spreadSlider.value = length.amount; | 
|  | } | 
|  | this._changedElement = null; | 
|  | this.dispatchEventToListeners(Events.ShadowChanged, this._model); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Event} event | 
|  | */ | 
|  | _onSliderInput(event) { | 
|  | if (event.currentTarget === this._blurSlider) { | 
|  | this._model.setBlurRadius( | 
|  | new InlineEditor.CSSLength(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 InlineEditor.CSSLength(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 InlineEditor.CSSLength(newX, this._model.offsetX().unit || defaultUnit)); | 
|  | this._model.setOffsetY(new InlineEditor.CSSLength(newY, this._model.offsetY().unit || defaultUnit)); | 
|  | } else { | 
|  | if (!event.altKey) { | 
|  | this._model.setOffsetX(new InlineEditor.CSSLength(newX, this._model.offsetX().unit || defaultUnit)); | 
|  | } | 
|  | if (!UI.KeyboardShortcut.eventHasCtrlOrMeta(event)) { | 
|  | this._model.setOffsetY(new InlineEditor.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) { | 
|  | let shiftX = 0; | 
|  | let shiftY = 0; | 
|  | if (event.key === 'ArrowRight') { | 
|  | shiftX = 1; | 
|  | } else if (event.key === 'ArrowLeft') { | 
|  | shiftX = -1; | 
|  | } else if (event.key === 'ArrowUp') { | 
|  | shiftY = -1; | 
|  | } else if (event.key === 'ArrowDown') { | 
|  | shiftY = 1; | 
|  | } | 
|  |  | 
|  | if (!shiftX && !shiftY) { | 
|  | return; | 
|  | } | 
|  | event.consume(true); | 
|  |  | 
|  | if (shiftX) { | 
|  | const offsetX = this._model.offsetX(); | 
|  | const newAmount = Number.constrain(offsetX.amount + shiftX, -maxRange, maxRange); | 
|  | if (newAmount === offsetX.amount) { | 
|  | return; | 
|  | } | 
|  | this._model.setOffsetX(new InlineEditor.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 = Number.constrain(offsetY.amount + shiftY, -maxRange, maxRange); | 
|  | if (newAmount === offsetY.amount) { | 
|  | return; | 
|  | } | 
|  | this._model.setOffsetY(new InlineEditor.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') | 
|  | }; | 
|  |  | 
|  | /* Legacy exported object */ | 
|  | self.InlineEditor = self.InlineEditor || {}; | 
|  |  | 
|  | /* Legacy exported object */ | 
|  | InlineEditor = InlineEditor || {}; | 
|  |  | 
|  | /** @constructor */ | 
|  | InlineEditor.CSSShadowEditor = CSSShadowEditor; | 
|  |  | 
|  | InlineEditor.CSSShadowEditor.Events = Events; |