| // 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; |