Dropdown click outside better handlers

This commit is contained in:
James Barnsley
2018-05-09 14:09:20 +12:00
parent 44d5f13372
commit 03d0ae3397
12 changed files with 247 additions and 157 deletions

View File

@ -2987,6 +2987,12 @@ input[type="submit"] {
font-family: "Overpass"; font-family: "Overpass";
letter-spacing: -0.04em; letter-spacing: -0.04em;
font-weight: 600; font-weight: 600;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
font-size: 14px; font-size: 14px;
color: #000000; color: #000000;
display: inline-block; display: inline-block;
@ -5320,6 +5326,12 @@ main header {
right: 40px; } } right: 40px; } }
.dropdown-field { .dropdown-field {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
font-size: 13px; font-size: 13px;
font-weight: 800; font-weight: 800;
font-family: "Overpass"; font-family: "Overpass";
@ -5343,16 +5355,12 @@ main header {
.dropdown-field.no-label .label > .text { .dropdown-field.no-label .label > .text {
display: none; } display: none; }
.dropdown-field .options { .dropdown-field .options {
color: #000000;
position: absolute; position: absolute;
top: 46px; top: 46px;
right: 10px; right: 10px;
z-index: 97; z-index: 97;
background: #FFFFFF;
display: none; display: none;
box-shadow: 2px 3px 6px rgba(0, 0, 0, 0.2); box-shadow: 2px 3px 6px rgba(0, 0, 0, 0.2); }
border-radius: 3px;
min-width: 120px; }
.dropdown-field .options:before { .dropdown-field .options:before {
content: ''; content: '';
display: block; display: block;
@ -5365,7 +5373,26 @@ main header {
border-left: 5px solid #121212; border-left: 5px solid #121212;
border-right: 5px solid #121212; border-right: 5px solid #121212;
border-bottom: 6px solid #FFFFFF; } border-bottom: 6px solid #FFFFFF; }
.dropdown-field .options .option { .dropdown-field .options > .liner {
color: #000000;
background: #FFFFFF;
min-width: 120px;
max-height: 70vh;
overflow-y: auto;
border-radius: 3px; }
.dropdown-field .options > .liner:before {
content: '';
display: block;
position: absolute;
top: -6px;
right: 10px;
background: #FFFFFF;
width: 0;
height: 0;
border-left: 5px solid #121212;
border-right: 5px solid #121212;
border-bottom: 6px solid #FFFFFF; }
.dropdown-field .options > .liner .option {
-webkit-transition: all 0.2s ease-in-out; -webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out;
@ -5373,7 +5400,7 @@ main header {
position: relative; position: relative;
padding: 9px 12px 8px 24px; padding: 9px 12px 8px 24px;
cursor: pointer; } cursor: pointer; }
.dropdown-field .options .option:before { .dropdown-field .options > .liner .option:before {
-webkit-transition: all 0.2s ease-in-out; -webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out;
@ -5391,14 +5418,14 @@ main header {
background: -moz-linear-gradient(to bottom right, white, rgba(0, 0, 0, 0.5)); background: -moz-linear-gradient(to bottom right, white, rgba(0, 0, 0, 0.5));
background: -o-linear-gradient(to bottom right, white, rgba(0, 0, 0, 0.5)); background: -o-linear-gradient(to bottom right, white, rgba(0, 0, 0, 0.5));
background: linear-gradient(to bottom right, white, rgba(0, 0, 0, 0.5)); } background: linear-gradient(to bottom right, white, rgba(0, 0, 0, 0.5)); }
.dropdown-field .options .option .fa { .dropdown-field .options > .liner .option .fa {
position: absolute; position: absolute;
top: 11px; top: 11px;
left: 8px; left: 8px;
font-size: 10px; } font-size: 10px; }
.notouch .dropdown-field .options .option:hover { .notouch .dropdown-field .options > .liner .option:hover {
color: #000000; } color: #000000; }
.notouch .dropdown-field .options .option:hover:before { .notouch .dropdown-field .options > .liner .option:hover:before {
opacity: 0.15; } opacity: 0.15; }
.autocomplete-field { .autocomplete-field {

View File

@ -18874,8 +18874,15 @@ var DropdownField = function (_React$Component) {
function DropdownField(props) { function DropdownField(props) {
_classCallCheck(this, DropdownField); _classCallCheck(this, DropdownField);
// Create a "unique" id. This is human-controlled to avoid requiring
// other libraries for a very simple purpose: clicking outside
var _this = _possibleConstructorReturn(this, (DropdownField.__proto__ || Object.getPrototypeOf(DropdownField)).call(this, props)); var _this = _possibleConstructorReturn(this, (DropdownField.__proto__ || Object.getPrototypeOf(DropdownField)).call(this, props));
_this.uid = _this.props.name.replace(' ', '_').toLowerCase();
if (_this.props.uid) {
_this.uid += "_" + _this.props.uid;
}
_this.state = { _this.state = {
expanded: false expanded: false
}; };
@ -18894,37 +18901,39 @@ var DropdownField = function (_React$Component) {
value: function componentWillUnmount() { value: function componentWillUnmount() {
window.removeEventListener("click", this.handleClick, false); window.removeEventListener("click", this.handleClick, false);
} }
}, {
key: 'setExpanded',
value: function setExpanded() {
var expanded = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : !this.state.expanded;
if (expanded) {
this.setState({ expanded: expanded });
window.addEventListener("click", this.handleClick, false);
} else {
this.setState({ expanded: expanded });
window.removeEventListener("click", this.handleClick, false);
}
}
}, { }, {
key: 'handleClick', key: 'handleClick',
value: function handleClick(e) { value: function handleClick(e) {
// TODO: remove dependency on jQuery and explore the performance of this functionality // TODO: remove dependency on jQuery and explore the performance of this functionality
if ($(e.target).closest('.dropdown-field').data('key') != this.props.name.replace(' ', '_').toLowerCase() && this.state.expanded) { if ($(e.target).closest('.dropdown-field').attr('data-uid') != this.uid && this.state.expanded) {
this.setState({ this.setExpanded(false);
expanded: false
});
} }
} }
}, { }, {
key: 'handleChange', key: 'handleChange',
value: function handleChange(value, is_selected) { value: function handleChange(value, is_selected) {
var current_value = this.props.value;
if (this.isMultiSelect()) { if (this.isMultiSelect()) {
if (value == 'select-all') { if (value == 'select-all') {
var new_value = []; var new_value = [];
for (var i = 0; i < this.props.options.length; i++) { for (var i = 0; i < this.props.options.length; i++) {
new_value.push(this.props.options[i].value); new_value.push(this.props.options[i].value);
} }
return this.props.handleChange(new_value); } else if (is_selected) {
}
} else {
this.setState({
expanded: !this.state.expanded
});
}
var current_value = this.props.value;
if (this.isMultiSelect()) {
if (is_selected) {
var index = current_value.indexOf(value); var index = current_value.indexOf(value);
current_value.splice(index, 1); current_value.splice(index, 1);
var new_value = current_value; var new_value = current_value;
@ -18934,17 +18943,13 @@ var DropdownField = function (_React$Component) {
} }
} else { } else {
var new_value = value; var new_value = value;
// Collapse our menu
this.setExpanded(false);
} }
return this.props.handleChange(new_value); return this.props.handleChange(new_value);
} }
}, {
key: 'handleToggle',
value: function handleToggle() {
this.setState({
expanded: !this.state.expanded
});
}
}, { }, {
key: 'isMultiSelect', key: 'isMultiSelect',
value: function isMultiSelect() { value: function isMultiSelect() {
@ -18998,11 +19003,11 @@ var DropdownField = function (_React$Component) {
return _react2.default.createElement( return _react2.default.createElement(
'div', 'div',
{ className: className, 'data-key': this.props.name.replace(' ', '_').toLowerCase() }, { className: className, 'data-uid': this.uid },
_react2.default.createElement( _react2.default.createElement(
'div', 'div',
{ className: "label" + (this.props.button ? " button " + this.props.button : ""), onClick: function onClick(e) { { className: "label" + (this.props.button ? " button " + this.props.button : ""), onClick: function onClick(e) {
return _this2.handleToggle(); return _this2.setExpanded();
} }, } },
this.props.icon ? _react2.default.createElement( this.props.icon ? _react2.default.createElement(
'span', 'span',
@ -19020,6 +19025,9 @@ var DropdownField = function (_React$Component) {
_react2.default.createElement( _react2.default.createElement(
'div', 'div',
{ className: 'options' }, { className: 'options' },
_react2.default.createElement(
'div',
{ className: 'liner' },
options.map(function (option) { options.map(function (option) {
if (_this2.isMultiSelect()) { if (_this2.isMultiSelect()) {
var is_selected = current_value.indexOf(option.value) > -1; var is_selected = current_value.indexOf(option.value) > -1;
@ -19036,6 +19044,7 @@ var DropdownField = function (_React$Component) {
); );
}) })
) )
)
); );
} }
}]); }]);
@ -49338,6 +49347,7 @@ function reducer() {
return Object.assign({}, ui, { debug_response: action.response }); return Object.assign({}, ui, { debug_response: action.response });
case 'UI_SET': case 'UI_SET':
console.log(action);
return Object.assign({}, ui, action.data); return Object.assign({}, ui, action.data);
case 'TOGGLE_SIDEBAR': case 'TOGGLE_SIDEBAR':
@ -68061,9 +68071,16 @@ var Search = function (_React$Component) {
} }
} }
} }
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this.props.mopidyActions.clearSearchResults();
this.props.spotifyActions.clearSearchResults();
}
}, { }, {
key: 'componentWillReceiveProps', key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(newProps) { value: function componentWillReceiveProps(newProps) {
if (this.props.params && this.props.params.query && this.props.params.query !== '') { if (this.props.params && this.props.params.query && this.props.params.query !== '') {
var old_context = helpers.getFromUri("searchcontext", this.props.params.query); var old_context = helpers.getFromUri("searchcontext", this.props.params.query);
var old_term = helpers.getFromUri("searchterm", this.props.params.query); var old_term = helpers.getFromUri("searchterm", this.props.params.query);
@ -68437,7 +68454,7 @@ var Search = function (_React$Component) {
}), }),
_react2.default.createElement(_DropdownField2.default, { _react2.default.createElement(_DropdownField2.default, {
icon: 'database', icon: 'database',
name: 'Source', name: 'Sources',
value: this.props.search_uri_schemes, value: this.props.search_uri_schemes,
options: provider_options, options: provider_options,
handleChange: function handleChange(value) { handleChange: function handleChange(value) {
@ -68598,12 +68615,12 @@ var SearchForm = function (_React$Component) {
break; break;
default: default:
var available_views = ["artist", "album", "playlist", "track"]; var available_views = ["all:", "artist:", "album:", "playlist:", ":track"];
var view_defined = false; var view_defined = false;
var query = this.state.query; var query = this.state.query;
for (var i = 0; i < available_views.length; i++) { for (var i = 0; i < available_views.length; i++) {
if (query.startsWith(available_views[i] + ':')) { if (query.startsWith(available_views[i])) {
view_defined = true; view_defined = true;
} }
} }
@ -70644,7 +70661,8 @@ var Snapcast = function (_React$Component) {
// the existing group and middleware handles the behavior shift) // the existing group and middleware handles the behavior shift)
groups_dropdown.push({ groups_dropdown.push({
label: 'New group', label: 'New group',
value: group.id value: group.id,
className: 'grey-text'
}); });
return _react2.default.createElement( return _react2.default.createElement(
@ -70661,6 +70679,7 @@ var Snapcast = function (_React$Component) {
no_status_icon: true, no_status_icon: true,
value: group.id, value: group.id,
options: groups_dropdown, options: groups_dropdown,
uid: group.id + "_" + client.id,
handleChange: function handleChange(value) { handleChange: function handleChange(value) {
_this2.props.pusherActions.setSnapcastClientGroup(client.id, value);_this2.props.uiActions.hideContextMenu(); _this2.props.pusherActions.setSnapcastClientGroup(client.id, value);_this2.props.uiActions.hideContextMenu();
} }

File diff suppressed because one or more lines are too long

View File

@ -77,7 +77,7 @@
// Release details // Release details
// These are automatically injected to built HTML // These are automatically injected to built HTML
var build = "1525593973"; var build = "1525811194";
var version = "3.18.0"; var version = "3.18.0";
// Construct the script tag // Construct the script tag

View File

@ -9,6 +9,13 @@ export default class DropdownField extends React.Component{
constructor(props){ constructor(props){
super(props); super(props);
// Create a "unique" id. This is human-controlled to avoid requiring
// other libraries for a very simple purpose: clicking outside
this.uid = this.props.name.replace(' ','_').toLowerCase();
if (this.props.uid){
this.uid += "_"+this.props.uid;
}
this.state = { this.state = {
expanded: false expanded: false
} }
@ -24,34 +31,33 @@ export default class DropdownField extends React.Component{
window.removeEventListener("click", this.handleClick, false); window.removeEventListener("click", this.handleClick, false);
} }
setExpanded(expanded = !this.state.expanded){
if (expanded){
this.setState({expanded: expanded});
window.addEventListener("click", this.handleClick, false);
} else {
this.setState({expanded: expanded});
window.removeEventListener("click", this.handleClick, false);
}
}
handleClick(e){ handleClick(e){
// TODO: remove dependency on jQuery and explore the performance of this functionality // TODO: remove dependency on jQuery and explore the performance of this functionality
if ($(e.target).closest('.dropdown-field').data('key') != this.props.name.replace(' ','_').toLowerCase() && this.state.expanded){ if ($(e.target).closest('.dropdown-field').attr('data-uid') != this.uid && this.state.expanded){
this.setState({ this.setExpanded(false);
expanded: false
});
} }
} }
handleChange(value, is_selected){ handleChange(value, is_selected){
var current_value = this.props.value;
if (this.isMultiSelect()){ if (this.isMultiSelect()){
if (value == 'select-all'){ if (value == 'select-all'){
var new_value = []; var new_value = [];
for (var i = 0; i < this.props.options.length; i++){ for (var i = 0; i < this.props.options.length; i++){
new_value.push(this.props.options[i].value); new_value.push(this.props.options[i].value);
} }
return this.props.handleChange(new_value); } else if (is_selected){
}
} else {
this.setState({
expanded: !this.state.expanded
});
}
var current_value = this.props.value;
if (this.isMultiSelect()){
if (is_selected){
var index = current_value.indexOf(value); var index = current_value.indexOf(value);
current_value.splice(index, 1); current_value.splice(index, 1);
var new_value = current_value; var new_value = current_value;
@ -61,17 +67,14 @@ export default class DropdownField extends React.Component{
} }
} else { } else {
var new_value = value; var new_value = value;
// Collapse our menu
this.setExpanded(false);
} }
return this.props.handleChange(new_value); return this.props.handleChange(new_value);
} }
handleToggle(){
this.setState({
expanded: !this.state.expanded
});
}
isMultiSelect(){ isMultiSelect(){
return this.props.value instanceof Array; return this.props.value instanceof Array;
} }
@ -119,8 +122,8 @@ export default class DropdownField extends React.Component{
} }
return ( return (
<div className={className} data-key={this.props.name.replace(' ','_').toLowerCase()}> <div className={className} data-uid={this.uid}>
<div className={"label"+(this.props.button ? " button "+this.props.button : "")} onClick={e => this.handleToggle()}> <div className={"label"+(this.props.button ? " button "+this.props.button : "")} onClick={e => this.setExpanded()}>
{this.props.icon ? <span><FontAwesome name={this.props.icon} />&nbsp; </span> : null} {this.props.icon ? <span><FontAwesome name={this.props.icon} />&nbsp; </span> : null}
<span className="text"> <span className="text">
{this.props.name} {this.props.name}
@ -128,6 +131,7 @@ export default class DropdownField extends React.Component{
</span> </span>
</div> </div>
<div className="options"> <div className="options">
<div className="liner">
{ {
options.map(option => { options.map(option => {
if (this.isMultiSelect()){ if (this.isMultiSelect()){
@ -145,6 +149,7 @@ export default class DropdownField extends React.Component{
} }
</div> </div>
</div> </div>
</div>
) )
} }
} }

View File

@ -55,12 +55,12 @@ class SearchForm extends React.Component{
break break
default: default:
var available_views = ["artist","album","playlist","track"]; var available_views = ["all:","artist:","album:","playlist:",":track"];
var view_defined = false; var view_defined = false;
var query = this.state.query; var query = this.state.query;
for (var i = 0; i < available_views.length; i++){ for (var i = 0; i < available_views.length; i++){
if (query.startsWith(available_views[i]+':')){ if (query.startsWith(available_views[i])){
view_defined = true; view_defined = true;
} }
} }

View File

@ -73,7 +73,8 @@ class Snapcast extends React.Component{
// the existing group and middleware handles the behavior shift) // the existing group and middleware handles the behavior shift)
groups_dropdown.push({ groups_dropdown.push({
label: 'New group', label: 'New group',
value: group.id value: group.id,
className: 'grey-text'
}); });
return ( return (
@ -87,6 +88,7 @@ class Snapcast extends React.Component{
no_status_icon no_status_icon
value={group.id} value={group.id}
options={groups_dropdown} options={groups_dropdown}
uid={group.id+"_"+client.id}
handleChange={value => {this.props.pusherActions.setSnapcastClientGroup(client.id, value); this.props.uiActions.hideContextMenu()}} handleChange={value => {this.props.pusherActions.setSnapcastClientGroup(client.id, value); this.props.uiActions.hideContextMenu()}}
/> />
<TextField <TextField

View File

@ -14,7 +14,8 @@ export default function reducer(ui = {}, action){
return Object.assign({}, ui, { debug_response: action.response }) return Object.assign({}, ui, { debug_response: action.response })
case 'UI_SET': case 'UI_SET':
return Object.assign({}, ui, action.data) console.log(action);
return Object.assign({}, ui, action.data);
case 'TOGGLE_SIDEBAR': case 'TOGGLE_SIDEBAR':
var new_state = !ui.sidebar_open var new_state = !ui.sidebar_open

View File

@ -50,7 +50,13 @@ class Search extends React.Component{
} }
} }
componentWillUnmount(){
this.props.mopidyActions.clearSearchResults();
this.props.spotifyActions.clearSearchResults();
}
componentWillReceiveProps(newProps){ componentWillReceiveProps(newProps){
if (this.props.params && this.props.params.query && this.props.params.query !== ''){ if (this.props.params && this.props.params.query && this.props.params.query !== ''){
var old_context = helpers.getFromUri("searchcontext",this.props.params.query); var old_context = helpers.getFromUri("searchcontext",this.props.params.query);
var old_term = helpers.getFromUri("searchterm",this.props.params.query); var old_term = helpers.getFromUri("searchterm",this.props.params.query);
@ -342,7 +348,7 @@ class Search extends React.Component{
/> />
<DropdownField <DropdownField
icon="database" icon="database"
name="Source" name="Sources"
value={this.props.search_uri_schemes} value={this.props.search_uri_schemes}
options={provider_options} options={provider_options}
handleChange={value => {this.props.uiActions.set({search_uri_schemes: value}); this.props.uiActions.hideContextMenu()}} handleChange={value => {this.props.uiActions.set({search_uri_schemes: value}); this.props.uiActions.hideContextMenu()}}
@ -383,7 +389,7 @@ const mapStateToProps = (state, ownProps) => {
mopidy_search_results: (state.mopidy.search_results ? state.mopidy.search_results : {}), mopidy_search_results: (state.mopidy.search_results ? state.mopidy.search_results : {}),
spotify_search_results: (state.spotify.search_results ? state.spotify.search_results : {}), spotify_search_results: (state.spotify.search_results ? state.spotify.search_results : {}),
sort: (state.ui.search_results_sort ? state.ui.search_results_sort : 'name'), sort: (state.ui.search_results_sort ? state.ui.search_results_sort : 'name'),
sort_reverse: (state.ui.search_results_sort_reverse ? true : false), sort_reverse: (state.ui.search_results_sort_reverse ? true : false)
} }
} }

View File

@ -1,5 +1,6 @@
.dropdown-field { .dropdown-field {
@include noselect();
font-size: 13px; font-size: 13px;
font-weight: 800; font-weight: 800;
font-family: "Overpass"; font-family: "Overpass";
@ -40,16 +41,34 @@
} }
.options { .options {
color: $black;
position: absolute; position: absolute;
top: 46px; top: 46px;
right: 10px; right: 10px;
z-index: 97; z-index: 97;
background: $white;
display: none; display: none;
box-shadow: 2px 3px 6px rgba(0,0,0,0.2); box-shadow: 2px 3px 6px rgba(0,0,0,0.2);
border-radius: 3px;
&:before {
content: '';
display: block;
position: absolute;
top: -6px;
right: 10px;
background: $white;
width: 0;
height: 0;
border-left: 5px solid $darkest_grey;
border-right: 5px solid $darkest_grey;
border-bottom: 6px solid $white;
}
& > .liner {
color: $black;
background: $white;
min-width: 120px; min-width: 120px;
max-height: 70vh;
overflow-y: auto;
border-radius: 3px;
&:before { &:before {
content: ''; content: '';
@ -104,5 +123,6 @@
} }
} }
} }
}
} }

View File

@ -73,6 +73,7 @@ input[type="button"],
input[type="submit"] { input[type="submit"] {
@include animate(); @include animate();
@include feature_font(); @include feature_font();
@include noselect();
font-size: 14px; font-size: 14px;
color: $black; color: $black;
display: inline-block; display: inline-block;

View File

@ -224,6 +224,15 @@ $bp_shallow: 650px;
font-weight: 600; font-weight: 600;
} }
@mixin noselect(){
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@mixin invert(){ @mixin invert(){
-webkit-filter: invert(1); -webkit-filter: invert(1);
filter: invert(1); filter: invert(1);