From 489f55d1728fa2588564c1b5039cea69164dc7a7 Mon Sep 17 00:00:00 2001 From: Kirill Shumilov Date: Thu, 30 Jan 2020 18:43:13 +0300 Subject: [PATCH 01/10] feat(xod-client): create ColorPicker component and storyboard for it --- packages/xod-client/package.json | 1 + .../core/styles/components/ColorPicker.scss | 12 ++ packages/xod-client/src/core/styles/main.scss | 1 + .../components/ColorPicker/ColorSlider.jsx | 147 ++++++++++++++++ .../src/editor/components/ColorPicker/Hue.jsx | 158 ++++++++++++++++++ .../components/ColorPicker/HueSlice.jsx | 49 ++++++ .../components/ColorPicker/Lightness.jsx | 58 +++++++ .../components/ColorPicker/Saturation.jsx | 53 ++++++ .../components/ColorPicker/colorPropType.js | 6 + .../editor/components/ColorPicker/index.jsx | 51 ++++++ .../editor/components/ColorPicker/style.js | 4 + packages/xod-client/stories/ColorPicker.jsx | 18 ++ yarn.lock | 12 ++ 13 files changed, 570 insertions(+) create mode 100644 packages/xod-client/src/core/styles/components/ColorPicker.scss create mode 100644 packages/xod-client/src/editor/components/ColorPicker/ColorSlider.jsx create mode 100644 packages/xod-client/src/editor/components/ColorPicker/Hue.jsx create mode 100644 packages/xod-client/src/editor/components/ColorPicker/HueSlice.jsx create mode 100644 packages/xod-client/src/editor/components/ColorPicker/Lightness.jsx create mode 100644 packages/xod-client/src/editor/components/ColorPicker/Saturation.jsx create mode 100644 packages/xod-client/src/editor/components/ColorPicker/colorPropType.js create mode 100644 packages/xod-client/src/editor/components/ColorPicker/index.jsx create mode 100644 packages/xod-client/src/editor/components/ColorPicker/style.js create mode 100644 packages/xod-client/stories/ColorPicker.jsx diff --git a/packages/xod-client/package.json b/packages/xod-client/package.json index 9898fb56b..5936ffefb 100644 --- a/packages/xod-client/package.json +++ b/packages/xod-client/package.json @@ -22,6 +22,7 @@ "big.js": "^5.2.2", "classnames": "^2.2.5", "codemirror": "^5.31.0", + "color-convert": "^2.0.1", "escape-string-regexp": "^1.0.5", "font-awesome": "^4.6.3", "line-intersection": "^1.0.8", diff --git a/packages/xod-client/src/core/styles/components/ColorPicker.scss b/packages/xod-client/src/core/styles/components/ColorPicker.scss new file mode 100644 index 000000000..871a9b015 --- /dev/null +++ b/packages/xod-client/src/core/styles/components/ColorPicker.scss @@ -0,0 +1,12 @@ +.ColorPicker { + .HueSlider { + path.marker { + stroke-linecap: round; + } + } + + .ColorSlider_value, .ColorSlider_label, + .HueSlider_value, .HueSlider_label { + fill: $sidebar-color-text; + } +} diff --git a/packages/xod-client/src/core/styles/main.scss b/packages/xod-client/src/core/styles/main.scss index e1ce0b209..081ca477f 100644 --- a/packages/xod-client/src/core/styles/main.scss +++ b/packages/xod-client/src/core/styles/main.scss @@ -24,6 +24,7 @@ 'components/Button', 'components/CloseButton', 'components/Comment', + 'components/ColorPicker', 'components/ContextMenu', 'components/CodeMirror', 'components/CppPatchDocs', diff --git a/packages/xod-client/src/editor/components/ColorPicker/ColorSlider.jsx b/packages/xod-client/src/editor/components/ColorPicker/ColorSlider.jsx new file mode 100644 index 000000000..c40a9cc1c --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/ColorSlider.jsx @@ -0,0 +1,147 @@ +import * as R from 'ramda'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import colorPropType from './colorPropType'; + +import { BAR_SIZE, MARKER_RADIUS, TEXT_Y } from './style'; + +class ColorSlider extends React.Component { + constructor(props) { + super(props); + + const { width } = props; + const padding = Math.round(width * 0.18); + const innerSize = width - padding; + + this.padding = padding / 2; + this.innerSize = innerSize; + this.outterSize = innerSize + padding; + + this.state = { + dragging: false, + }; + + // Reference to slider element + this.slider = null; + + this.handleStart = this.handleStart.bind(this); + this.handleMove = this.handleMove.bind(this); + this.handleEnd = this.handleEnd.bind(this); + this.handleClick = this.handleClick.bind(this); + this.handleReset = this.handleReset.bind(this); + } + + componentDidMount() { + document.addEventListener('mousemove', this.handleMove); + document.addEventListener('mouseup', this.handleEnd); + } + componentWillUnmount() { + document.removeEventListener('mousemove', this.handleMove); + document.removeEventListener('mouseup', this.handleEnd); + } + + handleStart() { + this.setState({ dragging: true }); + } + handleMove(event) { + if (this.state.dragging) { + event.preventDefault(); + const value = this.props.color.hsl[this.props.index]; + const newValue = R.compose(p => parseInt(p, 10), R.max(0), R.min(100))( + event.clientX / this.innerSize * 100 + ); + // Side effect: Commit new value + R.unless(R.equals(value), this.props.onChange)(newValue); + } + } + handleEnd() { + this.setState({ dragging: false }); + } + handleClick(event) { + const xClick = event.clientX; + const slider = this.slider.getBoundingClientRect(); + const percentage = (xClick - slider.x) / slider.width; + const newValue = Math.round(percentage * 100); + this.props.onChange(newValue); + } + handleReset() { + this.props.onChange(this.props.default); + } + + render() { + const value = this.props.color.hsl[this.props.index]; + return ( + + + {this.props.gradient} + + { + this.slider = el; + }} + x={0} + y={0} + width={this.innerSize} + height={BAR_SIZE} + onClick={this.handleClick} + fill={`url(#gradient_${this.props.type})`} + /> + + + {(value / 100).toFixed(3)} + + + {this.props.label} + + + + + ); + } +} + +ColorSlider.propTypes = { + color: colorPropType, + x: PropTypes.number, + y: PropTypes.number, + width: PropTypes.number, + index: PropTypes.number, // 0 — Hue, 1 — Saturation, 2 — Lightness + gradient: PropTypes.element, + type: PropTypes.string, + label: PropTypes.string, + default: PropTypes.number, + onChange: PropTypes.func, +}; + +ColorSlider.defaultProps = { + x: 0, + y: 0, + width: 220, + default: 100, +}; + +export default ColorSlider; diff --git a/packages/xod-client/src/editor/components/ColorPicker/Hue.jsx b/packages/xod-client/src/editor/components/ColorPicker/Hue.jsx new file mode 100644 index 000000000..19b18c8b3 --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/Hue.jsx @@ -0,0 +1,158 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import colorPropType from './colorPropType'; +import HueSlice from './HueSlice'; + +import { BAR_SIZE } from './style'; + +class Hue extends React.Component { + constructor(props) { + super(props); + + const { width } = props; + const padding = Math.round(width * 0.25); + const innerSize = width - padding; + this.radius = innerSize / 2; + this.outterSize = width; + this.centerOffset = this.outterSize / 2; + this.previewSize = Math.round(this.radius - BAR_SIZE * 3.5); + + this.state = { + dragging: false, + xMouseDelta: 0, + yMouseDelta: 0, + }; + + this.handleStart = this.handleStart.bind(this); + this.handleMove = this.handleMove.bind(this); + this.handleEnd = this.handleEnd.bind(this); + this.handleReset = this.handleReset.bind(this); + } + + componentDidMount() { + document.addEventListener('mousemove', this.handleMove); + document.addEventListener('mouseup', this.handleEnd); + } + componentWillUnmount() { + document.removeEventListener('mousemove', this.handleMove); + document.removeEventListener('mouseup', this.handleEnd); + } + + handleStart(event, deg = this.props.color.hsl[0]) { + const xMouseShouldBe = Math.sin(deg / 180 * Math.PI) * this.radius; + const yMouseShouldBe = -Math.cos(deg / 180 * Math.PI) * this.radius; + + this.setState({ + dragging: true, + xMouseDelta: event.clientX - xMouseShouldBe, + yMouseDelta: event.clientY - yMouseShouldBe, + }); + } + handleMove(event) { + if (this.state.dragging) { + event.preventDefault(); + const xRelativeToCenter = event.clientX - this.state.xMouseDelta; + const yRelativeToCenter = event.clientY - this.state.yMouseDelta; + const degree = + Math.atan(yRelativeToCenter / xRelativeToCenter) * 180 / Math.PI + + 90 + + (xRelativeToCenter >= 0 ? 0 : 180); + const hue = parseInt(degree, 10); + if (this.props.color.hsl[0] !== hue) { + this.props.onChange(hue); + } + } + } + handleEnd() { + this.setState({ dragging: false }); + } + handleReset() { + this.props.onChange(this.props.default); + } + + render() { + return ( + + + + {Array.from({ length: 360 }, (_, deg) => ( + { + this.props.onChange(deg); + this.handleStart(event, deg); + }} + /> + ))} + + + + + {(this.props.color.hsl[0] / 360).toFixed(3)} + + + Hue + + + + ); + } +} + +Hue.propTypes = { + color: colorPropType, + x: PropTypes.number, + y: PropTypes.number, + width: PropTypes.number, + default: PropTypes.number, + onChange: PropTypes.func, +}; + +Hue.defaultProps = { + x: 0, + y: 0, + width: 220, + default: 0, +}; + +export default Hue; diff --git a/packages/xod-client/src/editor/components/ColorPicker/HueSlice.jsx b/packages/xod-client/src/editor/components/ColorPicker/HueSlice.jsx new file mode 100644 index 000000000..0a8edbbcb --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/HueSlice.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { BAR_SIZE, MARKER_SIZE } from './style'; + +const HueSlice = ({ + degree, + color, + radius, + marker, + onMouseDown, + onClick, + onDoubleClick, +}) => { + const thickness = marker ? 0 : 1; + const startX = Math.sin((degree - thickness) / 180 * Math.PI) * radius; + const startY = -Math.cos((degree - thickness) / 180 * Math.PI) * radius; + const endX = Math.sin((degree + thickness) / 180 * Math.PI) * radius; + const endY = -Math.cos((degree + thickness) / 180 * Math.PI) * radius; + return ( + + ); +}; + +HueSlice.propTypes = { + degree: PropTypes.number, + radius: PropTypes.number, + color: PropTypes.string, + marker: PropTypes.bool, + onClick: PropTypes.func, + onDoubleClick: PropTypes.func, + onMouseDown: PropTypes.func, +}; + +HueSlice.defaultProps = { + thickness: 1, + onClick: () => {}, + onMouseDown: () => {}, +}; + +export default HueSlice; diff --git a/packages/xod-client/src/editor/components/ColorPicker/Lightness.jsx b/packages/xod-client/src/editor/components/ColorPicker/Lightness.jsx new file mode 100644 index 000000000..987caada2 --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/Lightness.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import colorPropType from './colorPropType'; +import ColorSlider from './ColorSlider'; + +const Lightness = ({ color, padding, innerSize, onChange, x, y }) => { + const TYPE = 'LightnessSlider'; + const gradient = ( + + + + + + ); + return ( + + ); +}; + +Lightness.propTypes = { + color: colorPropType, + x: PropTypes.number, + y: PropTypes.number, + padding: PropTypes.number, + innerSize: PropTypes.number, + onChange: PropTypes.func, +}; + +Lightness.defaultProps = { + x: 0, + y: 0, + padding: 60, + innerSize: 160, +}; + +export default Lightness; diff --git a/packages/xod-client/src/editor/components/ColorPicker/Saturation.jsx b/packages/xod-client/src/editor/components/ColorPicker/Saturation.jsx new file mode 100644 index 000000000..2e107c155 --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/Saturation.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import colorPropType from './colorPropType'; +import ColorSlider from './ColorSlider'; + +const Saturation = ({ color, padding, innerSize, onChange, x, y }) => { + const TYPE = 'SaturationSlider'; + const gradient = ( + + + + + ); + return ( + + ); +}; + +Saturation.propTypes = { + color: colorPropType, + x: PropTypes.number, + y: PropTypes.number, + padding: PropTypes.number, + innerSize: PropTypes.number, + onChange: PropTypes.func, +}; + +Saturation.defaultProps = { + x: 0, + y: 0, + padding: 60, + innerSize: 160, +}; + +export default Saturation; diff --git a/packages/xod-client/src/editor/components/ColorPicker/colorPropType.js b/packages/xod-client/src/editor/components/ColorPicker/colorPropType.js new file mode 100644 index 000000000..e28cbcaae --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/colorPropType.js @@ -0,0 +1,6 @@ +import PropTypes from 'prop-types'; + +export default PropTypes.exact({ + hsl: PropTypes.arrayOf(PropTypes.number), + hex: PropTypes.string, +}); diff --git a/packages/xod-client/src/editor/components/ColorPicker/index.jsx b/packages/xod-client/src/editor/components/ColorPicker/index.jsx new file mode 100644 index 000000000..341c83b21 --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/index.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import convert from 'color-convert'; + +import colorPropType from './colorPropType'; + +import Hue from './Hue'; +import Saturation from './Saturation'; +import Lightness from './Lightness'; + +const ColorPicker = ({ color, onColorChange }) => { + const updateColor = newHsl => + onColorChange({ + hsl: newHsl, + hex: `#${convert.hsl.hex(newHsl)}`, + }); + const setHue = hueDegree => + updateColor([hueDegree, color.hsl[1], color.hsl[2]]); + const setSaturation = saturation => + updateColor([color.hsl[0], saturation, color.hsl[2]]); + const setLightness = lightness => + updateColor([color.hsl[0], color.hsl[1], lightness]); + + return ( +
+ + + + + +
+ ); +}; + +ColorPicker.propTypes = { + color: colorPropType, + onColorChange: PropTypes.func, +}; + +export default ColorPicker; diff --git a/packages/xod-client/src/editor/components/ColorPicker/style.js b/packages/xod-client/src/editor/components/ColorPicker/style.js new file mode 100644 index 000000000..16388d632 --- /dev/null +++ b/packages/xod-client/src/editor/components/ColorPicker/style.js @@ -0,0 +1,4 @@ +export const BAR_SIZE = 15; +export const MARKER_SIZE = 30; +export const MARKER_RADIUS = MARKER_SIZE / 2; +export const TEXT_Y = Math.round(BAR_SIZE * 2.2); diff --git a/packages/xod-client/stories/ColorPicker.jsx b/packages/xod-client/stories/ColorPicker.jsx new file mode 100644 index 000000000..5354f6b84 --- /dev/null +++ b/packages/xod-client/stories/ColorPicker.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withState } from 'recompose'; +import convert from 'color-convert'; + +import '../src/core/styles/main.scss'; +import ColorPicker from '../src/editor/components/ColorPicker/index'; + +const hsl = [45, 100, 50]; +const hex = convert.hsl.hex(hsl); + +const ColorPickerContainer = withState('color', 'onColorChange', { hsl, hex })( + ({ color, onColorChange }) => ( + + ) +); + +storiesOf('ColorPicker', module).add('base', () => ); diff --git a/yarn.lock b/yarn.lock index 255c2cdf1..5cb1a0119 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4363,6 +4363,13 @@ color-convert@^1.3.0, color-convert@^1.9.0: dependencies: color-name "^1.1.1" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-convert@~0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" @@ -4373,6 +4380,11 @@ color-name@^1.0.0, color-name@^1.1.1: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + color-string@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" From 92bf37ff0efd92a2c628cff3980e69d751581cb5 Mon Sep 17 00:00:00 2001 From: Kirill Shumilov Date: Thu, 30 Jan 2020 19:03:40 +0300 Subject: [PATCH 02/10] refactor(xod-client): extract PointingPopup from Helpbox into a separate reusable component --- .../core/styles/components/CustomScroll.scss | 1 + .../src/core/styles/components/Helpbox.scss | 54 ------ .../core/styles/components/PointingPopup.scss | 72 +++++++ packages/xod-client/src/core/styles/main.scss | 1 + .../src/editor/components/PointingPopup.jsx | 181 ++++++++++++++++++ .../src/editor/components/SidebarPanel.jsx | 4 + .../src/editor/components/Suggester.jsx | 3 - packages/xod-client/src/editor/constants.js | 6 - .../src/editor/containers/Helpbox.jsx | 147 ++++---------- packages/xod-client/src/editor/utils.js | 58 +----- .../components/nodeParts/NodeLabel.jsx | 2 +- .../containers/ProjectBrowser.jsx | 10 +- 12 files changed, 295 insertions(+), 244 deletions(-) create mode 100644 packages/xod-client/src/core/styles/components/PointingPopup.scss create mode 100644 packages/xod-client/src/editor/components/PointingPopup.jsx diff --git a/packages/xod-client/src/core/styles/components/CustomScroll.scss b/packages/xod-client/src/core/styles/components/CustomScroll.scss index 313faf1c2..764c54c6a 100644 --- a/packages/xod-client/src/core/styles/components/CustomScroll.scss +++ b/packages/xod-client/src/core/styles/components/CustomScroll.scss @@ -15,6 +15,7 @@ } .inner-container { + position: relative; overflow-x: hidden; overflow-y: scroll; diff --git a/packages/xod-client/src/core/styles/components/Helpbox.scss b/packages/xod-client/src/core/styles/components/Helpbox.scss index 507c73ff0..a3af81d89 100644 --- a/packages/xod-client/src/core/styles/components/Helpbox.scss +++ b/packages/xod-client/src/core/styles/components/Helpbox.scss @@ -1,35 +1,7 @@ .Helpbox { - position: fixed; - z-index: 15; - top: 0; - background: $sidebar-color-bg; - border: 1px solid $chrome-outlines; - border-radius: 5px; - box-shadow: 5px 5px 10px -5px rgba(0,0,0,.5); max-height: 80%; min-width: 300px; - margin-top: -13px; // to point accurately on selected item - - display: flex; - flex-direction: column; - - transition: opacity 0.1s; - - &--rightSided { - .pointer { - left: auto !important; - right: -7px; - &:before { - transform: rotate(135deg) !important; - } - } - } - &--hidden { - opacity: 0; - pointer-events: none; - } - &-content { display: flex; flex-grow: 1; @@ -41,30 +13,4 @@ * { user-select: text; } span { cursor: text; } } - - .pointer { - position: absolute; - z-index: 2; - left: -7px; - top: 20px; - - &:before { - content: ''; - display: block; - width: 12px; - height: 12px; - - background: $sidebar-color-bg; - border-left: 1px solid $chrome-outlines; - border-top: 1px solid $chrome-outlines; - transform: rotate(-45deg); - } - } - - .no-selection { - color: $sidebar-color-text; - font-size: $font-size-m; - text-align: center; - padding: 10px; - } } diff --git a/packages/xod-client/src/core/styles/components/PointingPopup.scss b/packages/xod-client/src/core/styles/components/PointingPopup.scss new file mode 100644 index 000000000..e196a923c --- /dev/null +++ b/packages/xod-client/src/core/styles/components/PointingPopup.scss @@ -0,0 +1,72 @@ +.PointingPopup { + position: fixed; + z-index: 15; + top: 0; + background: $sidebar-color-bg; + border: 1px solid $chrome-outlines; + border-radius: 5px; + box-shadow: 5px 5px 10px -5px rgba(0,0,0,.5); + + margin-top: -13px; // to point accurately on selected item + + display: flex; + flex-direction: column; + + transition: opacity 0.1s; + + .outer-container, .content-wrapper { + width: auto !important; + } + + &--rightSided { + .pointer { + left: auto !important; + right: -7px; + &:before { + transform: rotate(135deg) !important; + } + } + } + &--hidden { + opacity: 0; + pointer-events: none; + } + + &-content { + display: flex; + flex-grow: 1; + + .PatchDocs { + padding: 12px; + } + + * { user-select: text; } + span { cursor: text; } + } + + .pointer { + position: absolute; + z-index: 2; + left: -7px; + top: 20px; + + &:before { + content: ''; + display: block; + width: 12px; + height: 12px; + + background: $sidebar-color-bg; + border-left: 1px solid $chrome-outlines; + border-top: 1px solid $chrome-outlines; + transform: rotate(-45deg); + } + } + + .no-selection { + color: $sidebar-color-text; + font-size: $font-size-m; + text-align: center; + padding: 10px; + } +} diff --git a/packages/xod-client/src/core/styles/main.scss b/packages/xod-client/src/core/styles/main.scss index 081ca477f..e72de7cac 100644 --- a/packages/xod-client/src/core/styles/main.scss +++ b/packages/xod-client/src/core/styles/main.scss @@ -47,6 +47,7 @@ 'components/Pin', 'components/PinLabel', 'components/PinValue', + 'components/PointingPopup', 'components/PopupPublishProject', 'components/ProjectBrowser', 'components/Sidebar', diff --git a/packages/xod-client/src/editor/components/PointingPopup.jsx b/packages/xod-client/src/editor/components/PointingPopup.jsx new file mode 100644 index 000000000..0ff7f51f4 --- /dev/null +++ b/packages/xod-client/src/editor/components/PointingPopup.jsx @@ -0,0 +1,181 @@ +import * as R from 'ramda'; +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import CustomScroll from 'react-custom-scroll'; + +import CloseButton from '../../core/components/CloseButton'; + +// Element that PoitingPopup points at might be visible partially +// in the container. `allowedOffset` is an amount of pixels that +// might be hidden outside the viewbox of the container. +const ALLOWED_OFFSET = 5; + +const getRelativeOffsetTop = (containerEl, el, offset = 0) => { + if (el === containerEl) return offset; + if (el.tagName === 'BODY') return 0; + const nextOffset = offset + el.offsetTop; + return getRelativeOffsetTop(containerEl, el.offsetParent, nextOffset); +}; + +const calculatePointingPopupPosition = (container, item) => { + const { offsetHeight, scrollTop } = container; + const containerBottom = scrollTop + offsetHeight; + + const elHeight = item.offsetHeight; + const elTop = getRelativeOffsetTop(container, item); + const elBottom = elHeight + elTop; + + const viewBoxTop = scrollTop - ALLOWED_OFFSET; + const viewBoxBottom = containerBottom + ALLOWED_OFFSET; + + const elVisible = elBottom <= viewBoxBottom && elTop >= viewBoxTop; + const elBox = item.getClientRects()[0]; + + return { + isVisible: elVisible, + top: Math.ceil(elBox.top), + left: Math.ceil(elBox.left), + right: Math.ceil(elBox.right), + }; +}; + +class PointingPopup extends React.Component { + constructor(props) { + super(props); + + this.ref = null; + + this.timer = null; + this.state = { + prevPosition: {}, + isVisible: true, + top: 0, + left: 0, + pointerTop: 0, + width: 0, + height: 0, + rightSided: false, + }; + + this.updateRef = this.updateRef.bind(this); + this.getOffset = this.getOffset.bind(this); + this.getPointerOffset = this.getPointerOffset.bind(this); + + this.onUpdatePosition = this.onUpdatePosition.bind(this); + } + componentDidMount() { + this.onUpdatePosition(); + this.timer = setInterval(() => this.onUpdatePosition(), 5); + } + componentDidUpdate(prevProps, prevState) { + if (prevState.isVisible && !this.state.isVisible) { + this.props.hidePopup(); + return; + } + if ( + this.ref && + (prevState.height !== this.ref.clientHeight || + prevState.width !== this.ref.clientWidth) + ) { + this.onUpdatePosition(); + } + } + componentWillUnmount() { + clearInterval(this.timer); + this.timer = null; + this.props.hidePopup(); + } + onUpdatePosition() { + if (!this.ref || !this.props.isVisible) return; + const item = document.querySelector(this.props.selectorPointingAt); + if (!item) return; + const container = item.closest('.inner-container'); + const position = calculatePointingPopupPosition(container, item); + if (R.equals(this.state.prevPosition, position)) return; + + // If popup refers to an element that is too close to the right side and + // popup could not fit window, it will be switched to the "rightSide" + // mode. That also means the pointer will be translated to the right side + // and popup will be positioned at the left side of referred element E.g. + // + // ProjectBrowser at the left side — PointingPopup is not "rightSided" popup; + // ProjectBrowser at the right side — PointingPopup "rightSided". + // + // Also if the popup is too wide to fit either side, a jut would be + // applied so that it will be completely visible at the expense of + // overlaping the referred element. + const windowWidth = window.innerWidth; + const elWidth = this.ref.clientWidth; + const overflow = Math.max(0, position.right + elWidth - windowWidth); + const underflow = Math.max(0, elWidth - position.left); + const rightSided = overflow > underflow; + const jut = rightSided ? underflow : -overflow; + const left = jut + (rightSided ? position.left - elWidth : position.right); + + const top = position.top; + const windowHeight = window.innerHeight; + const elHeight = this.ref.clientHeight; + const isFitWindow = top + elHeight < windowHeight; + const newTop = isFitWindow ? top : windowHeight - elHeight; + const newPointer = isFitWindow ? 0 : top - newTop; + + this.setState({ + prevPosition: position, + isVisible: position.isVisible, + left, + top: newTop, + pointerTop: newPointer, + height: elHeight, + width: elWidth, + rightSided, + }); + } + getOffset() { + return { + transform: `translate(${this.state.left}px, ${this.state.top}px)`, + }; + } + getPointerOffset() { + return { transform: `translateY(${this.state.pointerTop}px)` }; + } + updateRef(el) { + this.ref = el; + } + render() { + const { isVisible } = this.props; + if (!isVisible) return null; + const isHidden = + !isVisible || !this.state.isVisible || this.state.top === 0; + + const cls = cn('PointingPopup', this.props.className, { + 'PointingPopup--hidden': isHidden, + 'PointingPopup--rightSided': this.state.rightSided, + }); + + return ( +
+
+ +
+ {this.props.children} +
+
+ ); + } +} + +PointingPopup.propTypes = { + className: PropTypes.string, + isVisible: PropTypes.bool.isRequired, + children: PropTypes.node, + selectorPointingAt: PropTypes.string, + hidePopup: PropTypes.func.isRequired, +}; + +PointingPopup.defaultProps = { + className: '', + selectorPointingAt: null, +}; + +export default PointingPopup; diff --git a/packages/xod-client/src/editor/components/SidebarPanel.jsx b/packages/xod-client/src/editor/components/SidebarPanel.jsx index 0ae36823e..b59b07eca 100644 --- a/packages/xod-client/src/editor/components/SidebarPanel.jsx +++ b/packages/xod-client/src/editor/components/SidebarPanel.jsx @@ -85,4 +85,8 @@ SidebarPanel.propTypes = { onScroll: PropTypes.func, }; +SidebarPanel.defaultProps = { + onScroll: () => {}, +}; + export default SidebarPanel; diff --git a/packages/xod-client/src/editor/components/Suggester.jsx b/packages/xod-client/src/editor/components/Suggester.jsx index edba1cef3..a89f97186 100644 --- a/packages/xod-client/src/editor/components/Suggester.jsx +++ b/packages/xod-client/src/editor/components/Suggester.jsx @@ -11,7 +11,6 @@ import { isAmong, noop } from 'xod-func-tools'; import { KEYCODE } from '../../utils/constants'; import { restoreFocusOnApp } from '../../utils/browser'; -import { triggerUpdateHelpboxPositionViaSuggester } from '../../editor/utils'; import SuggesterContainer from './SuggesterContainer'; const getSuggestionValue = ({ item }) => item.path; @@ -114,7 +113,6 @@ class Suggester extends React.Component { ); this.props.showHelpbox(); this.props.onHighlight(getSuggestionValue(suggestion)); - setTimeout(triggerUpdateHelpboxPositionViaSuggester, 1); } } @@ -230,7 +228,6 @@ class Suggester extends React.Component { renderSuggestionsContainer={({ containerProps, children }) => ( {children} diff --git a/packages/xod-client/src/editor/constants.js b/packages/xod-client/src/editor/constants.js index 9a3fb4859..00661cb45 100644 --- a/packages/xod-client/src/editor/constants.js +++ b/packages/xod-client/src/editor/constants.js @@ -81,9 +81,3 @@ export const SIDEBAR_IDS = { }; export const PANEL_CONTEXT_MENU_ID = 'PANEL_CONTEXT_MENU_ID'; - -// Event name for pub/sub to update position of Helpbox -// while User: -// - scrolls ProjectBrowser or selects another Patch by click -// - highlights (arrows/mouse over) or scrolls Node Suggester -export const UPDATE_HELPBOX_POSITION = 'UPDATE_HELPBOX_POSITION'; diff --git a/packages/xod-client/src/editor/containers/Helpbox.jsx b/packages/xod-client/src/editor/containers/Helpbox.jsx index 0c72bf159..0d597838f 100644 --- a/packages/xod-client/src/editor/containers/Helpbox.jsx +++ b/packages/xod-client/src/editor/containers/Helpbox.jsx @@ -6,146 +6,65 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Patch as PatchType } from 'xod-project'; import { $Maybe } from 'xod-func-tools'; -import cn from 'classnames'; -import CustomScroll from 'react-custom-scroll'; import * as Actions from '../actions'; import { isHelpboxVisible, getFocusedArea } from '../selectors'; import { getPatchForHelpbox } from '../../core/selectors'; import PatchDocs from '../components/PatchDocs'; import sanctuaryPropType from '../../utils/sanctuaryPropType'; -import CloseButton from '../../core/components/CloseButton'; -import { UPDATE_HELPBOX_POSITION, FOCUS_AREAS } from '../constants'; -import { - triggerUpdateHelpboxPositionViaProjectBrowser, - triggerUpdateHelpboxPositionViaSuggester, -} from '../utils'; +import PointingPopup from '../components/PointingPopup'; + +import { FOCUS_AREAS } from '../constants'; + +const getSelectorByFocusedArea = focusedArea => { + switch (focusedArea) { + case FOCUS_AREAS.PROJECT_BROWSER: + return '.PatchGroupItem.isSelected'; + case FOCUS_AREAS.NODE_SUGGESTER: + return '.Suggester-item.is-highlighted'; + default: + return null; + } +}; class Helpbox extends React.Component { constructor(props) { super(props); - - this.helpboxRef = null; - this.state = { - isVisible: true, - top: 0, - left: 0, - pointerTop: 0, - width: 0, - height: 0, - rightSided: false, + focusedArea: props.focusedArea, + selector: getSelectorByFocusedArea(props.focusedArea), }; - - this.updateRef = this.updateRef.bind(this); - this.getHelpboxOffset = this.getHelpboxOffset.bind(this); - this.getPointerOffset = this.getPointerOffset.bind(this); - - this.onUpdatePosition = this.onUpdatePosition.bind(this); - } - componentDidMount() { - window.addEventListener(UPDATE_HELPBOX_POSITION, this.onUpdatePosition); - this.triggerUpdatePosition(); } - componentDidUpdate(prevProps, prevState) { - if (prevState.isVisible && !this.state.isVisible) { - this.props.actions.hideHelpbox(); - return; + componentDidUpdate() { + if (this.props.focusedArea !== this.state.focusedArea) { + const { focusedArea } = this.props; + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + focusedArea, + selector: getSelectorByFocusedArea(focusedArea), + }); } - if ( - prevState.height !== this.helpboxRef.clientHeight || - prevState.width !== this.helpboxRef.clientWidth - ) { - this.triggerUpdatePosition(); - } - } - componentWillUnmount() { - window.removeEventListener(UPDATE_HELPBOX_POSITION, this.onUpdatePosition); - } - onUpdatePosition(event) { - // If helpbox refers to an element that is too close to the right side and - // helpbox could not fit window, it will be switched to the "rightSide" - // mode. That also means the pointer will be translated to the right side - // and helpbox will be positioned at the left side of referred element E.g. - // - // ProjectBrowser at the left side — Helpbox is not "rightSided" helpbox; - // ProjectBrowser at the right side — Helpbox "rightSided". - // - // Also if the helpbox is too wide to fit either side, a jut would be - // applied so that it will be completely visible at the expense of - // overlaping the referred element. - const windowWidth = window.innerWidth; - const elWidth = this.helpboxRef.clientWidth; - const overflow = Math.max(0, event.detail.right + elWidth - windowWidth); - const underflow = Math.max(0, elWidth - event.detail.left); - const rightSided = overflow > underflow; - const jut = rightSided ? underflow : -overflow; - const left = - jut + (rightSided ? event.detail.left - elWidth : event.detail.right); - - const top = event.detail.top; - const windowHeight = window.innerHeight; - const elHeight = this.helpboxRef.clientHeight; - const isFitWindow = top + elHeight < windowHeight; - const newTop = isFitWindow ? top : windowHeight - elHeight; - const newPointer = isFitWindow ? 0 : top - newTop; - - this.setState({ - isVisible: event.detail.isVisible, - left, - top: newTop, - pointerTop: newPointer, - height: elHeight, - width: elWidth, - rightSided, - }); - } - getHelpboxOffset() { - return { - transform: `translate(${this.state.left}px, ${this.state.top}px)`, - }; - } - getPointerOffset() { - return { transform: `translateY(${this.state.pointerTop}px)` }; - } - triggerUpdatePosition() { - if (this.props.focusedArea === FOCUS_AREAS.PROJECT_BROWSER) { - triggerUpdateHelpboxPositionViaProjectBrowser(); - } - if (this.props.focusedArea === FOCUS_AREAS.NODE_SUGGESTER) { - triggerUpdateHelpboxPositionViaSuggester(); - } - } - updateRef(el) { - this.helpboxRef = el; } render() { const { maybeSelectedPatch, isVisible, actions } = this.props; if (!isVisible) return null; - const isHidden = - !isVisible || - Maybe.isNothing(maybeSelectedPatch) || - !this.state.isVisible || - this.state.top === 0; const docs = maybeSelectedPatch .map(patch => ) .getOrElse(null); - const cls = cn('Helpbox', { - 'Helpbox--hidden': isHidden, - 'Helpbox--rightSided': this.state.rightSided, - }); + if (!this.state.selector) return null; return ( -
-
- -
- {docs} -
-
+ + {docs} + ); } } diff --git a/packages/xod-client/src/editor/utils.js b/packages/xod-client/src/editor/utils.js index 5b4405b37..dea0b4535 100644 --- a/packages/xod-client/src/editor/utils.js +++ b/packages/xod-client/src/editor/utils.js @@ -11,11 +11,7 @@ import { subtractPoints, GAP_IN_SLOTS, } from '../project/nodeLayout'; -import { - SELECTION_ENTITY_TYPE, - PANEL_IDS, - UPDATE_HELPBOX_POSITION, -} from './constants'; +import { SELECTION_ENTITY_TYPE, PANEL_IDS } from './constants'; export const getTabByPatchPath = R.curry((patchPath, tabs) => R.compose(R.find(R.propEq('patchPath', patchPath)), R.values)(tabs) @@ -311,55 +307,3 @@ export const getMaximizedPanelsBySidebarId = R.compose( filterMaximized, getPanelsBySidebarId ); - -const calculateHelpboxPosition = (container, item) => { - const scrollTop = container.scrollTop; - const height = container.offsetHeight; - const viewBottom = scrollTop + height; - - const elHeight = item.offsetHeight; - const elTop = item.offsetTop; - const elBottom = elHeight + elTop; - const elVisible = elBottom <= viewBottom && elTop >= scrollTop; - const elBox = item.getClientRects()[0]; - - return { - isVisible: elVisible, - top: Math.ceil(elBox.top), - left: Math.ceil(elBox.left), - right: Math.ceil(elBox.right), - }; -}; - -export const triggerUpdateHelpboxPositionViaProjectBrowser = event => { - const container = document - .getElementById('ProjectBrowser') - .getElementsByClassName('inner-container')[0]; - const selectedElement = - event && event.type === 'click' - ? event.target.closest('.PatchGroupItem') - : container.getElementsByClassName('isSelected')[0]; - - if (container && selectedElement) { - window.dispatchEvent( - new window.CustomEvent(UPDATE_HELPBOX_POSITION, { - detail: calculateHelpboxPosition(container, selectedElement), - }) - ); - } -}; - -export const triggerUpdateHelpboxPositionViaSuggester = () => { - const suggester = document.getElementById('Suggester'); - if (!suggester) return; - const container = suggester.getElementsByClassName('inner-container')[0]; - const item = suggester.getElementsByClassName('is-highlighted')[0]; - - if (container && item) { - window.dispatchEvent( - new window.CustomEvent(UPDATE_HELPBOX_POSITION, { - detail: calculateHelpboxPosition(container, item), - }) - ); - } -}; diff --git a/packages/xod-client/src/project/components/nodeParts/NodeLabel.jsx b/packages/xod-client/src/project/components/nodeParts/NodeLabel.jsx index 22531c813..5d887b787 100644 --- a/packages/xod-client/src/project/components/nodeParts/NodeLabel.jsx +++ b/packages/xod-client/src/project/components/nodeParts/NodeLabel.jsx @@ -10,7 +10,7 @@ const NodeLabel = ({ text, ...props }) => ( ); NodeLabel.propTypes = { - text: PropTypes.string.isRequired, + text: PropTypes.string, // could be empty for terminals in Helpbox preview }; export default NodeLabel; diff --git a/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx b/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx index ea1367d17..068ba9a19 100644 --- a/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx +++ b/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx @@ -40,7 +40,6 @@ import { FILTER_CONTEXT_MENU_ID, } from '../constants'; import { PANEL_IDS, SIDEBAR_IDS } from '../../editor/constants'; -import { triggerUpdateHelpboxPositionViaProjectBrowser } from '../../editor/utils'; const pickPatchPartsForComparsion = R.map( R.pick(['isDeprecated', 'isUtility', 'dead', 'path']) @@ -75,9 +74,6 @@ class ProjectBrowser extends React.Component { this.renderLocalPatches = this.renderLocalPatches.bind(this); this.renderLibraryPatches = this.renderLibraryPatches.bind(this); } - componentDidMount() { - triggerUpdateHelpboxPositionViaProjectBrowser(); - } shouldComponentUpdate(nextProps) { return !R.eqBy(pickPropsForComparsion, nextProps, this.props); } @@ -202,10 +198,7 @@ class ProjectBrowser extends React.Component { onDoubleClick={() => switchPatch(path)} onBeginDrag={startDraggingPatch} isSelected={path === selectedPatchPath} - onClick={event => { - triggerUpdateHelpboxPositionViaProjectBrowser(event); - setSelection(path); - }} + onClick={() => setSelection(path)} collectPropsFn={collectPropsFn} hoverButtons={this.renderHoveredButtons(collectPropsFn)} /> @@ -360,7 +353,6 @@ class ProjectBrowser extends React.Component { sidebarId={this.props.sidebarId} autohide={this.props.autohide} additionalButtons={this.renderToolbarButtons()} - onScroll={triggerUpdateHelpboxPositionViaProjectBrowser} > {this.renderPatches()} From f8333c2e78074a71c7a8fe31c6a1f8c5e6af8c3f Mon Sep 17 00:00:00 2001 From: Kirill Shumilov Date: Mon, 3 Feb 2020 17:47:33 +0300 Subject: [PATCH 03/10] feat(xod-client): add ColorPinWidget and ColorPicker Widget for it --- .../core/styles/components/ColorPicker.scss | 4 + .../src/core/styles/components/Inspector.scss | 10 ++ packages/xod-client/src/editor/actionTypes.js | 3 + packages/xod-client/src/editor/actions.js | 9 ++ .../editor/components/ColorPicker/index.jsx | 16 ++- .../editor/components/ColorPicker/style.js | 1 + .../components/inspectorWidgets/Widget.jsx | 6 +- .../components/inspectorWidgets/index.js | 3 +- .../pinWidgets/ColorPinWidget.jsx | 113 ++++++++++++++++++ .../inspectorWidgets/pinWidgets/PinWidget.jsx | 2 +- .../editor/containers/ColorPickerWidget.jsx | 89 ++++++++++++++ packages/xod-client/src/editor/reducer.js | 20 ++++ packages/xod-client/src/editor/selectors.js | 15 +++ packages/xod-client/src/editor/state.js | 6 + 14 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 packages/xod-client/src/editor/components/inspectorWidgets/pinWidgets/ColorPinWidget.jsx create mode 100644 packages/xod-client/src/editor/containers/ColorPickerWidget.jsx diff --git a/packages/xod-client/src/core/styles/components/ColorPicker.scss b/packages/xod-client/src/core/styles/components/ColorPicker.scss index 871a9b015..f427df4e2 100644 --- a/packages/xod-client/src/core/styles/components/ColorPicker.scss +++ b/packages/xod-client/src/core/styles/components/ColorPicker.scss @@ -1,4 +1,8 @@ +.ColorPickerWidget { + width: 220px; +} .ColorPicker { + padding: 10px 0; .HueSlider { path.marker { stroke-linecap: round; diff --git a/packages/xod-client/src/core/styles/components/Inspector.scss b/packages/xod-client/src/core/styles/components/Inspector.scss index 9a32bbaf4..f2744e0c4 100644 --- a/packages/xod-client/src/core/styles/components/Inspector.scss +++ b/packages/xod-client/src/core/styles/components/Inspector.scss @@ -262,6 +262,16 @@ float: none; } } + .ClorPicker_toggleBtn { + cursor: pointer; + position: absolute; + top: 6px; + right: 6px; + width: 16px; + height: 16px; + border-radius: 3px; + border: none; + } } .LabelWidget .inspectorTextInput { diff --git a/packages/xod-client/src/editor/actionTypes.js b/packages/xod-client/src/editor/actionTypes.js index 86304bba4..327ec245a 100644 --- a/packages/xod-client/src/editor/actionTypes.js +++ b/packages/xod-client/src/editor/actionTypes.js @@ -64,3 +64,6 @@ export const SIMULATION_ABORT = 'SIMULATION_ABORT'; export const SIMULATION_ERROR = 'SIMULATION_ERROR'; export const TWEAK_PULSE_SENT = 'TWEAK_PULSE_SENT'; + +export const SHOW_COLORPICKER_WIDGET = 'SHOW_COLORPICKER_WIDGET'; +export const HIDE_COLORPICKER_WIDGET = 'HIDE_COLORPICKER_WIDGET'; diff --git a/packages/xod-client/src/editor/actions.js b/packages/xod-client/src/editor/actions.js index 9f0b2b77b..474c314a1 100644 --- a/packages/xod-client/src/editor/actions.js +++ b/packages/xod-client/src/editor/actions.js @@ -901,3 +901,12 @@ export const sendTweakPulse = tweakNodeId => (dispatch, getState) => { }) ); }; + +export const showColorPickerWidget = elementId => ({ + type: ActionType.SHOW_COLORPICKER_WIDGET, + payload: { elementId }, +}); + +export const hideColorPickerWidget = () => ({ + type: ActionType.HIDE_COLORPICKER_WIDGET, +}); diff --git a/packages/xod-client/src/editor/components/ColorPicker/index.jsx b/packages/xod-client/src/editor/components/ColorPicker/index.jsx index 341c83b21..6ee5f9d06 100644 --- a/packages/xod-client/src/editor/components/ColorPicker/index.jsx +++ b/packages/xod-client/src/editor/components/ColorPicker/index.jsx @@ -1,3 +1,4 @@ +import * as R from 'ramda'; import React from 'react'; import PropTypes from 'prop-types'; import convert from 'color-convert'; @@ -8,9 +9,9 @@ import Hue from './Hue'; import Saturation from './Saturation'; import Lightness from './Lightness'; -const ColorPicker = ({ color, onColorChange }) => { +const ColorPicker = ({ color, onChange }) => { const updateColor = newHsl => - onColorChange({ + onChange({ hsl: newHsl, hex: `#${convert.hsl.hex(newHsl)}`, }); @@ -25,8 +26,8 @@ const ColorPicker = ({ color, onColorChange }) => {
@@ -45,7 +46,12 @@ const ColorPicker = ({ color, onColorChange }) => { ColorPicker.propTypes = { color: colorPropType, - onColorChange: PropTypes.func, + onChange: PropTypes.func, }; export default ColorPicker; + +export const hex2color = hex => ({ + hsl: convert.hex.hsl(hex), + hex, +}); diff --git a/packages/xod-client/src/editor/components/ColorPicker/style.js b/packages/xod-client/src/editor/components/ColorPicker/style.js index 16388d632..08a097865 100644 --- a/packages/xod-client/src/editor/components/ColorPicker/style.js +++ b/packages/xod-client/src/editor/components/ColorPicker/style.js @@ -1,3 +1,4 @@ + export const BAR_SIZE = 15; export const MARKER_SIZE = 30; export const MARKER_RADIUS = MARKER_SIZE / 2; diff --git a/packages/xod-client/src/editor/components/inspectorWidgets/Widget.jsx b/packages/xod-client/src/editor/components/inspectorWidgets/Widget.jsx index 7f8a739af..5c325432e 100644 --- a/packages/xod-client/src/editor/components/inspectorWidgets/Widget.jsx +++ b/packages/xod-client/src/editor/components/inspectorWidgets/Widget.jsx @@ -49,9 +49,7 @@ class Widget extends React.Component { onBlur() { this.commit(); } - onChange(value) { - this.updateValue(value); - } + onKeyDown(event) { const keycode = event.keycode || event.which; if (this.keyDownHandlers[keycode]) { @@ -59,7 +57,7 @@ class Widget extends React.Component { } } - updateValue(value, forceCommit = false) { + onChange(value, forceCommit = false) { const commitCallback = this.props.commitOnChange || forceCommit ? this.commit.bind(this) : noop; diff --git a/packages/xod-client/src/editor/components/inspectorWidgets/index.js b/packages/xod-client/src/editor/components/inspectorWidgets/index.js index a2b020edf..20f9b1f32 100644 --- a/packages/xod-client/src/editor/components/inspectorWidgets/index.js +++ b/packages/xod-client/src/editor/components/inspectorWidgets/index.js @@ -8,6 +8,7 @@ import NumberWidget from './pinWidgets/NumberPinWidget'; import PulseWidget from './pinWidgets/PulsePinWidget'; import StringWidget from './pinWidgets/StringPinWidget'; import GenericPinWidget from './pinWidgets/GenericPinWidget'; +import ColorPinWidget from './pinWidgets/ColorPinWidget'; import DisabledInputWidget from './pinWidgets/DisabledInputWidget'; import DescriptionWidget from './DescriptionWidget'; import LabelWidget from './LabelWidget'; @@ -114,7 +115,7 @@ const WIDGET_MAPPING = { normalizeValue: normalizePort, }, [WIDGET_TYPE.COLOR]: { - component: StringWidget, // TODO: Replace with custom widget + component: ColorPinWidget, // TODO: Replace with custom widget dataType: BINDABLE_CUSTOM_TYPES.COLOR, keyDownHandlers: submitAndSelectOnEnter, normalizeValue: normalizeColor, diff --git a/packages/xod-client/src/editor/components/inspectorWidgets/pinWidgets/ColorPinWidget.jsx b/packages/xod-client/src/editor/components/inspectorWidgets/pinWidgets/ColorPinWidget.jsx new file mode 100644 index 000000000..506054f97 --- /dev/null +++ b/packages/xod-client/src/editor/components/inspectorWidgets/pinWidgets/ColorPinWidget.jsx @@ -0,0 +1,113 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { compose, withState, withHandlers, lifecycle } from 'recompose'; + +import { showColorPickerWidget } from '../../../actions'; +import PinWidget from './PinWidget'; +import { hex2color } from '../../ColorPicker'; +import ColorPickerWidget from '../../../containers/ColorPickerWidget'; + +const ColorPinWidget = compose( + withState('focused', 'setFocus', false), + // We have to handle input's selection in a tricky way, + // because we're changing it's value on focus + withState('selection', 'setSelection', [0, 0]), + withState('inputRef', 'setInputRef', null), + // We have to handle it in case we just added a leading quote + // before the literal + lifecycle({ + componentDidUpdate(prevProps) { + if (prevProps.selection !== this.props.selection && this.props.inputRef) { + this.props.inputRef.setSelectionRange( + this.props.selection[0], + this.props.selection[1] + ); + } + }, + }), + withHandlers({ + onChangeHandler: props => event => { + const value = event.target.value; + props.onChange(value); + }, + onFocus: props => event => { + props.setSelection([ + event.target.selectionStart, + event.target.selectionEnd, + ]); + props.setFocus(true); + }, + onBlur: props => _ => { + props.setFocus(false); + props.setSelection([0, 0]); + props.onBlur(); + }, + }) +)(props => ( + + + +