diff --git a/packages/xod-client/src/core/assets/icons/cross.svg b/packages/xod-client/src/core/assets/icons/cross.svg new file mode 100644 index 00000000..f61201a8 --- /dev/null +++ b/packages/xod-client/src/core/assets/icons/cross.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/xod-client/src/core/assets/icons/search.svg b/packages/xod-client/src/core/assets/icons/search.svg new file mode 100644 index 00000000..72379a9e --- /dev/null +++ b/packages/xod-client/src/core/assets/icons/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/xod-client/src/core/selectors.js b/packages/xod-client/src/core/selectors.js index 6c246441..1e1aec77 100644 --- a/packages/xod-client/src/core/selectors.js +++ b/packages/xod-client/src/core/selectors.js @@ -30,11 +30,21 @@ export const hasUnsavedChanges = createSelector( // export const getPatchForHelpbox = createSelector( - [Project.getProject, ProjectBrowser.getSelectedPatchPath], - (project, selectedPatchPath) => R.compose( - R.chain(XP.getPatchByPath(R.__, project)), - Maybe - )(selectedPatchPath) + [ + Project.getProject, + ProjectBrowser.getSelectedPatchPath, + Editor.isSuggesterVisible, + Editor.getSuggesterHighlightedPatchPath, + ], + (project, selectedPatchPath, suggesterVisible, suggesterPatchPath) => { + if (suggesterVisible && suggesterPatchPath) { + return XP.getPatchByPath(suggesterPatchPath, project); + } + return R.compose( + R.chain(XP.getPatchByPath(R.__, project)), + Maybe + )(selectedPatchPath); + } ); export const getPatchForQuickHelp = createSelector( [Project.getProject, Editor.getSelection, Project.getCurrentPatchNodes], diff --git a/packages/xod-client/src/core/styles/components/Helpbox.scss b/packages/xod-client/src/core/styles/components/Helpbox.scss index 209cf43f..60c7784b 100644 --- a/packages/xod-client/src/core/styles/components/Helpbox.scss +++ b/packages/xod-client/src/core/styles/components/Helpbox.scss @@ -1,8 +1,7 @@ .Helpbox { position: fixed; - z-index: 5; + z-index: 15; top: 0; - left: $sidebar-width; background: $sidebar-color-bg; border: 1px solid $chrome-outlines; border-radius: 5px; diff --git a/packages/xod-client/src/core/styles/components/PatchDocs.scss b/packages/xod-client/src/core/styles/components/PatchDocs.scss index b92498b7..c8984497 100644 --- a/packages/xod-client/src/core/styles/components/PatchDocs.scss +++ b/packages/xod-client/src/core/styles/components/PatchDocs.scss @@ -77,7 +77,7 @@ $PinInfo-padding-bottom: 10px; overflow: hidden; box-sizing: border-box; } - &--no-outputs { margin-bottom: -20px; } + &--no-outputs { margin-bottom: -15px; } } .input-pin { border: 1px solid $color-pinlabel; diff --git a/packages/xod-client/src/core/styles/components/Suggester.scss b/packages/xod-client/src/core/styles/components/Suggester.scss index ae0b04a9..ead19b8f 100644 --- a/packages/xod-client/src/core/styles/components/Suggester.scss +++ b/packages/xod-client/src/core/styles/components/Suggester.scss @@ -1,3 +1,16 @@ +@mixin custom-search-clear-button() { + position: relative; + z-index: 10; + width: 18px; + height: 18px; + border: none; + border-radius: 50%; + background: transparent url('../assets/icons/cross.svg') no-repeat center center; + &:hover { + background-color: $coal-bright; + } +} + .Suggester { position: absolute; left: 50%; @@ -5,7 +18,7 @@ z-index: 10; width: 400px; - margin: 0 0 0 -200px; + margin: 0 0 0 -400px; box-sizing: border-box; padding: 4px; @@ -19,9 +32,7 @@ box-shadow: 0 6px 24px 2px rgba(0,0,0,.6); - &.with-helpbar { - margin-left: -490px; - } + &-libs {} .loading-icon { position: absolute; @@ -42,12 +53,12 @@ input { width: 100%; font-size: $font-size-xl; - background-color: $input-color-bg; + background: $input-color-bg url('../assets/icons/search.svg') no-repeat 6px 11px; color: $input-color-text; border: 1px solid $input-color-border; border-radius: 2px; box-sizing: border-box; - padding: 6px 4px; + padding: 6px 4px 6px 24px; &:focus { outline: none; @@ -57,8 +68,18 @@ &:disabled { opacity: 0.2; } + + &::-ms-clear { + @include custom-search-clear-button(); + } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + @include custom-search-clear-button(); + } } + &-container { max-height: 600px; cursor: default; @@ -113,9 +134,6 @@ &.is-highlighted { background: $sidebar-color-bg-selected; } - &:hover, &:focus { - background: $sidebar-color-bg-hover; - } .path { display: block; diff --git a/packages/xod-client/src/editor/components/Suggester.jsx b/packages/xod-client/src/editor/components/Suggester.jsx index 8166993b..5e9cf56f 100644 --- a/packages/xod-client/src/editor/components/Suggester.jsx +++ b/packages/xod-client/src/editor/components/Suggester.jsx @@ -1,3 +1,4 @@ +import R from 'ramda'; import React from 'react'; import PropTypes from 'prop-types'; @@ -9,10 +10,13 @@ import regExpEscape from 'escape-string-regexp'; import { isAmong } from 'xod-func-tools'; import { KEYCODE } from '../../utils/constants'; +import { triggerUpdateHelpboxPositionViaSuggester } from '../../editor/utils'; import SuggesterContainer from './SuggesterContainer'; const getSuggestionValue = ({ item }) => item.path; +const getSuggestionIndex = R.uncurryN(2, suggestion => R.findIndex(R.equals(suggestion))); + class Suggester extends React.Component { constructor(props) { super(props); @@ -20,17 +24,27 @@ class Suggester extends React.Component { this.state = { value: '', suggestions: [], + mouseInteraction: true, }; + // `mouseInteraction` is about can User interact with + // suggestions using mouse or not. It avoid bug when + // User scrolls suggestions using keyboard and dont + // touching mouse at all. + // It will be turned on when User moves mouse + // over SuggesterContainer. + this.input = null; this.renderItem = this.renderItem.bind(this); this.onChange = this.onChange.bind(this); + this.onItemMouseOver = this.onItemMouseOver.bind(this); + this.onContainerMouseMove = this.onContainerMouseMove.bind(this); this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this); this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this); this.onSuggestionSelected = this.onSuggestionSelected.bind(this); this.onSuggestionHighlighted = this.onSuggestionHighlighted.bind(this); - this.storeInputReference = this.storeInputReference.bind(this); + this.storeRef = this.storeRef.bind(this); } componentDidMount() { @@ -38,14 +52,17 @@ class Suggester extends React.Component { // A hack to avoid typing "i" into input when pressing Hotkey setTimeout(() => { this.input.focus(); - this.props.onInitialFocus(); }, 1); } } onChange(e, { newValue, method }) { - if (isAmong(['up', 'down', 'click'], method)) return; - + if (isAmong(['up', 'down'], method)) { + this.setState({ + mouseInteraction: false, + }); + return; + } this.setState({ value: newValue }); } @@ -73,10 +90,29 @@ class Suggester extends React.Component { onSuggestionHighlighted({ suggestion }) { if (suggestion) { + this.autosuggest.updateHighlightedSuggestion( + null, + getSuggestionIndex(suggestion, this.state.suggestions) + ); + this.props.showHelpbox(); this.props.onHighlight(getSuggestionValue(suggestion)); + setTimeout(triggerUpdateHelpboxPositionViaSuggester, 1); } } + onItemMouseOver(suggestion) { + return () => { + if (this.state.mouseInteraction) { + this.onSuggestionHighlighted({ suggestion }); + } + }; + } + onContainerMouseMove() { + this.setState({ + mouseInteraction: true, + }); + } + getSuggestions(value) { const { index } = this.props; const inputValue = value.trim().toLowerCase(); @@ -85,20 +121,27 @@ class Suggester extends React.Component { return index.search(regExpEscape(inputValue)); } - storeInputReference(autosuggest) { + storeRef(autosuggest) { if (autosuggest !== null) { + this.autosuggest = autosuggest; this.input = autosuggest.input; } } - renderItem({ item }, { isHighlighted }) { + renderItem(suggestion, { isHighlighted }) { const cls = classNames('Suggester-item', { 'is-highlighted': isHighlighted, }); const value = regExpEscape(this.state.value); + const { item } = suggestion; return ( -
+
this.onSelect(item.path)} + onMouseOver={this.onItemMouseOver(suggestion)} + > +
( - + {children} )} - ref={this.storeInputReference} + ref={this.storeRef} />
); @@ -158,19 +205,18 @@ class Suggester extends React.Component { } Suggester.defaultProps = { - addClassName: '', + extraClassName: '', onBlur: () => {}, onHighlight: () => {}, - onInitialFocus: () => {}, }; Suggester.propTypes = { - addClassName: PropTypes.string, + extraClassName: PropTypes.string, index: PropTypes.object, onAddNode: PropTypes.func.isRequired, onHighlight: PropTypes.func, onBlur: PropTypes.func, - onInitialFocus: PropTypes.func, + showHelpbox: PropTypes.func, }; export default Suggester; diff --git a/packages/xod-client/src/editor/components/SuggesterContainer.jsx b/packages/xod-client/src/editor/components/SuggesterContainer.jsx index a4a4e1de..26dbd740 100644 --- a/packages/xod-client/src/editor/components/SuggesterContainer.jsx +++ b/packages/xod-client/src/editor/components/SuggesterContainer.jsx @@ -41,13 +41,13 @@ class SuggesterContainer extends React.Component { const containerHeight = contentWrapper.clientHeight; const scrollPos = this.scrollRef.state.scrollPos; - const isOutsideUp = (top - height <= scrollPos); - const isOutsideDown = (top + height >= (scrollPos + containerHeight) - height); + const isOutsideUp = (top < scrollPos); + const isOutsideDown = (top + height > (scrollPos + containerHeight)); if (isOutsideDown) { - return (top + height + height) - containerHeight; + return (top + height) - containerHeight; } else if (isOutsideUp) { - return top - height; + return top; } // If it’s inside container do nothing at all @@ -72,10 +72,12 @@ class SuggesterContainer extends React.Component {
{this.props.children}
@@ -84,9 +86,16 @@ class SuggesterContainer extends React.Component { } } +SuggesterContainer.defaultProps = { + onScroll: () => {}, + onMouseMove: () => {}, +}; + SuggesterContainer.propTypes = { containerProps: PropTypes.object, children: PropTypes.object, + onScroll: PropTypes.func, + onMouseMove: PropTypes.func, }; export default SuggesterContainer; diff --git a/packages/xod-client/src/editor/constants.js b/packages/xod-client/src/editor/constants.js index 90084492..af64e3fe 100644 --- a/packages/xod-client/src/editor/constants.js +++ b/packages/xod-client/src/editor/constants.js @@ -97,5 +97,7 @@ 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 +// 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/Editor.jsx b/packages/xod-client/src/editor/containers/Editor.jsx index f1525faa..c2323e67 100644 --- a/packages/xod-client/src/editor/containers/Editor.jsx +++ b/packages/xod-client/src/editor/containers/Editor.jsx @@ -48,7 +48,6 @@ class Editor extends React.Component { this.hideSuggester = this.hideSuggester.bind(this); this.onWorkareaFocus = this.onWorkareaFocus.bind(this); - this.onNodeSuggesterFocus = this.onNodeSuggesterFocus.bind(this); this.onLibSuggesterFocus = this.onLibSuggesterFocus.bind(this); this.patchSize = this.props.size; @@ -75,9 +74,6 @@ class Editor extends React.Component { onWorkareaFocus() { this.props.actions.setFocusedArea(FOCUS_AREAS.WORKAREA); } - onNodeSuggesterFocus() { - this.props.actions.setFocusedArea(FOCUS_AREAS.NODE_SUGGESTER); - } onLibSuggesterFocus() { this.props.actions.setFocusedArea(FOCUS_AREAS.LIB_SUGGESTER); } @@ -169,18 +165,17 @@ class Editor extends React.Component { const suggester = (this.props.suggesterIsVisible) ? ( ) : null; const libSuggester = (this.props.isLibSuggesterVisible) ? ( ({ movePanel: Actions.movePanel, togglePanelAutohide: Actions.togglePanelAutohide, hideHelpbox: Actions.hideHelpbox, + showHelpbox: Actions.showHelpbox, }, dispatch), }); diff --git a/packages/xod-client/src/editor/containers/Helpbox.jsx b/packages/xod-client/src/editor/containers/Helpbox.jsx index 927099ec..2aaaec09 100644 --- a/packages/xod-client/src/editor/containers/Helpbox.jsx +++ b/packages/xod-client/src/editor/containers/Helpbox.jsx @@ -10,14 +10,17 @@ import cn from 'classnames'; import CustomScroll from 'react-custom-scroll'; import * as Actions from '../actions'; -import { isHelpboxVisible } from '../selectors'; +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 } from '../constants'; -import { triggerUpdateHelpboxPosition } from '../utils'; +import { UPDATE_HELPBOX_POSITION, FOCUS_AREAS } from '../constants'; +import { + triggerUpdateHelpboxPositionViaProjectBrowser, + triggerUpdateHelpboxPositionViaSuggester, +} from '../utils'; class Helpbox extends React.Component { constructor(props) { @@ -27,13 +30,13 @@ class Helpbox extends React.Component { this.state = { isVisible: true, - top: 50, + top: 0, + left: 0, pointerTop: 0, height: 0, }; this.updateRef = this.updateRef.bind(this); - this.updatePosition = this.updatePosition.bind(this); this.getHelpboxOffset = this.getHelpboxOffset.bind(this); this.getPointerOffset = this.getPointerOffset.bind(this); @@ -41,7 +44,7 @@ class Helpbox extends React.Component { } componentDidMount() { window.addEventListener(UPDATE_HELPBOX_POSITION, this.onUpdatePosition); - triggerUpdateHelpboxPosition(); + this.triggerUpdatePosition(); } componentDidUpdate(prevProps, prevState) { if (prevState.isVisible && !this.state.isVisible) { @@ -49,7 +52,7 @@ class Helpbox extends React.Component { return; } if (prevState.height !== this.helpboxRef.clientHeight) { - triggerUpdateHelpboxPosition(); + this.triggerUpdatePosition(); } } componentWillUnmount() { @@ -59,27 +62,40 @@ class Helpbox extends React.Component { this.setState({ isVisible: event.detail.isVisible, }); - this.updatePosition(event.detail.top); - } - getHelpboxOffset() { - return { transform: `translateY(${this.state.top}px)` }; - } - getPointerOffset() { - return { transform: `translateY(${this.state.pointerTop}px)` }; - } - updatePosition(elTop) { + + const top = event.detail.top; + // set `right` property to `left` constant, cause + // `right` is a position of right side of target element, + // so it will be left side of helpbox + const left = event.detail.right; + const windowHeight = window.innerHeight; const elHeight = this.helpboxRef.clientHeight; - const isFitWindow = (elTop + elHeight < windowHeight); - const newTop = isFitWindow ? elTop : windowHeight - elHeight; - const newPointer = isFitWindow ? 0 : elTop - newTop; + const isFitWindow = (top + elHeight < windowHeight); + const newTop = isFitWindow ? top : windowHeight - elHeight; + const newPointer = isFitWindow ? 0 : top - newTop; this.setState({ + left, top: newTop, pointerTop: newPointer, height: elHeight, }); } + 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; } @@ -89,7 +105,8 @@ class Helpbox extends React.Component { const isHidden = ( !isVisible || Maybe.isNothing(maybeSelectedPatch) || - !this.state.isVisible + !this.state.isVisible || + this.state.top === 0 ); const docs = maybeSelectedPatch @@ -108,7 +125,7 @@ class Helpbox extends React.Component { onClick={actions.hideHelpbox} />
- + {docs}
@@ -120,6 +137,7 @@ class Helpbox extends React.Component { Helpbox.propTypes = { maybeSelectedPatch: sanctuaryPropType($Maybe(PatchType)), isVisible: PropTypes.bool, + focusedArea: PropTypes.oneOf(R.values(FOCUS_AREAS)), actions: PropTypes.shape({ // eslint-disable-next-line react/no-unused-prop-types hideHelpbox: PropTypes.func.isRequired, @@ -129,6 +147,7 @@ Helpbox.propTypes = { const mapStateToProps = R.applySpec({ isVisible: isHelpboxVisible, maybeSelectedPatch: getPatchForHelpbox, + focusedArea: getFocusedArea, }); const mapDispatchToProps = dispatch => ({ actions: bindActionCreators({ diff --git a/packages/xod-client/src/editor/reducer.js b/packages/xod-client/src/editor/reducer.js index e2d338c3..b50e6f1d 100644 --- a/packages/xod-client/src/editor/reducer.js +++ b/packages/xod-client/src/editor/reducer.js @@ -569,12 +569,15 @@ const editorReducer = (state = {}, action) => { if (R.path(['suggester', 'visible'], state) === true) return state; return R.compose( + R.over(R.lensProp('isHelpboxVisible'), R.T), + R.assoc('focusedArea', FOCUS_AREAS.NODE_SUGGESTER), R.assocPath(['suggester', 'visible'], true), R.assocPath(['suggester', 'placePosition'], action.payload) )(state); } case EAT.HIDE_SUGGESTER: return R.compose( + R.over(R.lensProp('isHelpboxVisible'), R.F), R.assocPath(['suggester', 'visible'], false), R.assocPath(['suggester', 'highlightedPatchPath'], null), R.assocPath(['suggester', 'placePosition'], null) diff --git a/packages/xod-client/src/editor/utils.js b/packages/xod-client/src/editor/utils.js index fd4eb7b0..6ced6682 100644 --- a/packages/xod-client/src/editor/utils.js +++ b/packages/xod-client/src/editor/utils.js @@ -11,7 +11,11 @@ import { subtractPoints, } from '../project/nodeLayout'; -import { SELECTION_ENTITY_TYPE, PANEL_IDS, UPDATE_HELPBOX_POSITION } from './constants'; +import { + SELECTION_ENTITY_TYPE, + PANEL_IDS, + UPDATE_HELPBOX_POSITION, +} from './constants'; export const getTabByPatchPath = R.curry( (patchPath, tabs) => R.compose( @@ -282,36 +286,53 @@ export const getMaximizedPanelsBySidebarId = R.compose( getPanelsBySidebarId ); -export const triggerUpdateHelpboxPosition = (event) => { +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), + 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) { - const scrollTop = container.scrollTop; - const height = container.offsetHeight; - const viewBottom = scrollTop + height; - - const elHeight = selectedElement.offsetHeight; - const elTop = selectedElement.offsetTop; - const elBottom = elHeight + elTop; - - const elVisible = ( - (elBottom <= viewBottom) && elTop >= scrollTop - ); - - const elAbsTop = selectedElement.getClientRects()[0].top; - window.dispatchEvent( new window.CustomEvent( UPDATE_HELPBOX_POSITION, - { - detail: { - isVisible: elVisible, - top: elAbsTop, - }, - } + { 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/projectBrowser/containers/ProjectBrowser.jsx b/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx index 36e2e46f..b1764229 100644 --- a/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx +++ b/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx @@ -37,7 +37,7 @@ import PatchGroupItemContextMenu from '../components/PatchGroupItemContextMenu'; import { PATCH_GROUP_CONTEXT_MENU_ID } from '../constants'; import { PANEL_IDS, SIDEBAR_IDS } from '../../editor/constants'; -import { triggerUpdateHelpboxPosition } from '../../editor/utils'; +import { triggerUpdateHelpboxPositionViaProjectBrowser } from '../../editor/utils'; const pickPatchPartsForComparsion = R.map(R.pick(['dead', 'path'])); @@ -187,7 +187,7 @@ class ProjectBrowser extends React.Component { onBeginDrag={startDraggingPatch} isSelected={path === selectedPatchPath} onClick={(event) => { - triggerUpdateHelpboxPosition(event); + triggerUpdateHelpboxPositionViaProjectBrowser(event); setSelection(path); }} collectPropsFn={collectPropsFn} @@ -308,7 +308,7 @@ class ProjectBrowser extends React.Component { sidebarId={this.props.sidebarId} autohide={this.props.autohide} additionalButtons={this.renderToolbarButtons()} - onScroll={triggerUpdateHelpboxPosition} + onScroll={triggerUpdateHelpboxPositionViaProjectBrowser} > {this.renderPatches()}