You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-29 22:48:10 +02:00
RN: Replaced broken Picker by custom dropdown
This commit is contained in:
129
ReactNativeClient/lib/components/Dropdown.js
Normal file
129
ReactNativeClient/lib/components/Dropdown.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const React = require('react');
|
||||
const { TouchableOpacity, TouchableWithoutFeedback , Dimensions, Text, Modal, View } = require('react-native');
|
||||
const { ItemList } = require('lib/components/ItemList.js');
|
||||
|
||||
class Dropdown extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.headerRef_ = null;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
headerSize: { x: 0, y: 0, width: 0, height: 0 },
|
||||
listVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
|
||||
setTimeout(() => {
|
||||
this.headerRef_.measure((fx, fy, width, height, px, py) => {
|
||||
this.setState({
|
||||
headerSize: { x: px, y: py, width: width, height: height }
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render() {
|
||||
const items = this.props.items;
|
||||
const itemHeight = 60;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
|
||||
// Dimensions doesn't return quite the right dimensions so leave an extra gap to make
|
||||
// sure nothing is off screen.
|
||||
const listMaxHeight = windowHeight;
|
||||
const listHeight = Math.min(items.length * itemHeight, listMaxHeight); //Dimensions.get('window').height - this.state.headerSize.y - this.state.headerSize.height - 50;
|
||||
const maxListTop = windowHeight - listHeight;
|
||||
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
|
||||
|
||||
const wrapperStyle = {
|
||||
width: this.state.headerSize.width,
|
||||
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
|
||||
marginTop: listTop,
|
||||
marginLeft: this.state.headerSize.x,
|
||||
};
|
||||
|
||||
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
});
|
||||
|
||||
const itemWrapperStyle = Object.assign({}, this.props.itemWrapperStyle ? this.props.itemWrapperStyle : {}, {
|
||||
flex:1,
|
||||
justifyContent: 'center',
|
||||
height: itemHeight,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 10,
|
||||
});
|
||||
|
||||
const headerWrapperStyle = Object.assign({}, this.props.headerWrapperStyle ? this.props.headerWrapperStyle : {}, {
|
||||
height: 35,
|
||||
// borderWidth: 1,
|
||||
// borderColor: '#ccc',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const headerStyle = Object.assign({}, this.props.headerStyle ? this.props.headerStyle : {}, {
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const headerArrowStyle = Object.assign({}, this.props.headerStyle ? this.props.headerStyle : {}, {
|
||||
flex: 0,
|
||||
});
|
||||
|
||||
const itemStyle = Object.assign({}, this.props.itemStyle ? this.props.itemStyle : {}, {
|
||||
|
||||
});
|
||||
|
||||
let headerLabel = '...';
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.value === this.props.selectedValue) headerLabel = item.label;
|
||||
}
|
||||
|
||||
const closeList = () => {
|
||||
this.setState({ listVisible: false });
|
||||
}
|
||||
|
||||
const itemRenderer= (item) => {
|
||||
return (
|
||||
<TouchableOpacity style={itemWrapperStyle} key={item.value} onPress={() => { closeList(); if (this.props.onValueChange) this.props.onValueChange(item.value); }}>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={item.value}>{item.label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'column' }}>
|
||||
<TouchableOpacity style={headerWrapperStyle} ref={(ref) => this.headerRef_ = ref} onPress={() => { this.setState({ listVisible: true }) }}>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>{headerLabel}</Text>
|
||||
<Text style={headerArrowStyle}>{'▼'}</Text>
|
||||
</TouchableOpacity>
|
||||
<Modal transparent={true} visible={this.state.listVisible} onRequestClose={() => { closeList(); }} >
|
||||
<TouchableWithoutFeedback onPressOut={() => { closeList() }}>
|
||||
<View style={{flex:1}}>
|
||||
<View style={wrapperStyle}>
|
||||
<ItemList
|
||||
style={itemListStyle}
|
||||
items={this.props.items}
|
||||
itemHeight={itemHeight}
|
||||
itemRenderer={(item) => { return itemRenderer(item) }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Dropdown };
|
||||
103
ReactNativeClient/lib/components/ItemList.js
Normal file
103
ReactNativeClient/lib/components/ItemList.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const React = require('react');
|
||||
const { Text, TouchableHighlight, View, StyleSheet, ScrollView } = require('react-native');
|
||||
|
||||
class ItemList extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.scrollTop_ = 0;
|
||||
}
|
||||
|
||||
itemCount(props = null) {
|
||||
if (props === null) props = this.props;
|
||||
return this.props.items ? this.props.items.length : this.props.itemComponents.length;
|
||||
}
|
||||
|
||||
updateStateItemIndexes(props = null, height = null) {
|
||||
if (props === null) props = this.props;
|
||||
|
||||
if (height === null) {
|
||||
if (!this.state) return;
|
||||
height = this.state.height;
|
||||
}
|
||||
|
||||
const topItemIndex = Math.max(0, Math.floor(this.scrollTop_ / props.itemHeight));
|
||||
const visibleItemCount = Math.ceil(height / props.itemHeight);
|
||||
|
||||
let bottomItemIndex = topItemIndex + visibleItemCount - 1;
|
||||
if (bottomItemIndex >= this.itemCount(props)) bottomItemIndex = this.itemCount(props) - 1;
|
||||
|
||||
this.setState({
|
||||
topItemIndex: topItemIndex,
|
||||
bottomItemIndex: bottomItemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
topItemIndex: 0,
|
||||
bottomItemIndex: 0,
|
||||
height: 0,
|
||||
itemHeight: this.props.itemHeight ? this.props.itemHeight : 0,
|
||||
});
|
||||
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.itemHeight) {
|
||||
this.setState({
|
||||
itemHeight: newProps.itemHeight,
|
||||
});
|
||||
}
|
||||
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
onScroll(event) {
|
||||
this.scrollTop_ = Math.floor(event.nativeEvent.contentOffset.y);
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
onLayout(event) {
|
||||
this.setState({ height: event.nativeEvent.layout.height });
|
||||
this.updateStateItemIndexes(null, event.nativeEvent.layout.height);
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style ? this.props.style : {};
|
||||
const itemHeight = this.state.itemHeight;
|
||||
|
||||
//if (!this.props.itemHeight) throw new Error('itemHeight is required');
|
||||
|
||||
let itemComps = [];
|
||||
|
||||
if (this.props.items) {
|
||||
const items = this.props.items;
|
||||
|
||||
const blankItem = function(key, height) {
|
||||
return <View key={key} style={{height:height}}></View>
|
||||
}
|
||||
|
||||
itemComps = [blankItem('top', this.state.topItemIndex * this.props.itemHeight)];
|
||||
|
||||
for (let i = this.state.topItemIndex; i <= this.state.bottomItemIndex; i++) {
|
||||
const itemComp = this.props.itemRenderer(items[i]);
|
||||
itemComps.push(itemComp);
|
||||
}
|
||||
|
||||
itemComps.push(blankItem('bottom', (items.length - this.state.bottomItemIndex - 1) * this.props.itemHeight));
|
||||
} else {
|
||||
itemComps = this.props.itemComponents;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView scrollEventThrottle={500} onLayout={(event) => { this.onLayout(event); }} style={style} onScroll={ (event) => { this.onScroll(event) }}>
|
||||
{ itemComps }
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ItemList };
|
||||
@@ -1,6 +1,6 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { View, Text, Button, StyleSheet, TouchableOpacity, Picker, Image } = require('react-native');
|
||||
const { Modal, View, Text, Button, StyleSheet, TouchableOpacity, Image } = require('react-native');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { Log } = require('lib/log.js');
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
@@ -11,6 +11,8 @@ const { FileApi } = require('lib/file-api.js');
|
||||
const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { ItemList } = require('lib/components/ItemList.js');
|
||||
const { Dropdown } = require('lib/components/Dropdown.js');
|
||||
|
||||
// Rather than applying a padding to the whole bar, it is applied to each
|
||||
// individual component (button, picker, etc.) so that the touchable areas
|
||||
@@ -40,11 +42,6 @@ class ScreenHeaderComponent extends Component {
|
||||
shadowColor: '#000000',
|
||||
elevation: 5,
|
||||
},
|
||||
folderPicker: {
|
||||
flex:1,
|
||||
color: theme.raisedHighlightedColor,
|
||||
// Note: cannot set backgroundStyle as that would remove the arrow in the component
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderColor: theme.dividerColor,
|
||||
@@ -265,19 +262,29 @@ class ScreenHeaderComponent extends Component {
|
||||
</MenuOption>);
|
||||
|
||||
const createTitleComponent = () => {
|
||||
const themeId = Setting.value('theme');
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const p = this.props.titlePicker;
|
||||
if (p) {
|
||||
let items = [];
|
||||
for (let i = 0; i < p.items.length; i++) {
|
||||
let item = p.items[i];
|
||||
items.push(<Picker.Item label={item.label} value={item.value} key={item.value}/>);
|
||||
}
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Picker style={this.styles().folderPicker} selectedValue={p.selectedValue} onValueChange={(itemValue, itemIndex) => { if (p.onValueChange) p.onValueChange(itemValue, itemIndex); }}>
|
||||
{ items }
|
||||
</Picker>
|
||||
</View>
|
||||
<Dropdown
|
||||
items={p.items}
|
||||
itemHeight={35}
|
||||
selectedValue={p.selectedValue}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: theme.raisedColor,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(itemValue, itemIndex) => { if (p.onValueChange) p.onValueChange(itemValue, itemIndex); }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
let title = 'title' in this.props && this.props.title !== null ? this.props.title : '';
|
||||
@@ -294,7 +301,7 @@ class ScreenHeaderComponent extends Component {
|
||||
{ saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
|
||||
{ titleComp }
|
||||
{ searchButton(this.styles(), () => this.searchButton_press()) }
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
|
||||
<Text style={this.styles().contextMenuTrigger}> ⋮</Text>
|
||||
</MenuTrigger>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { View, Switch, Slider, StyleSheet, Picker, Text, Button } = require('react-native');
|
||||
const { View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { Dropdown } = require('lib/components/Dropdown.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
|
||||
@@ -26,7 +27,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
body: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
settingContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
paddingTop: theme.marginTop,
|
||||
@@ -38,13 +46,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
fontWeight: 'bold',
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
},
|
||||
settingControl: {
|
||||
color: theme.color,
|
||||
flex: 1,
|
||||
},
|
||||
pickerItem: {
|
||||
fontSize: theme.fontSize,
|
||||
}
|
||||
}
|
||||
|
||||
styles.switchSettingText = Object.assign({}, styles.settingText);
|
||||
@@ -63,6 +70,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
settingToComponent(key, value) {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
let output = null;
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
@@ -72,25 +81,36 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
const md = Setting.settingMetadata(key);
|
||||
|
||||
if (md.isEnum) {
|
||||
// The Picker component doesn't work properly with int values, so
|
||||
// convert everything to string (Setting.setValue will convert
|
||||
// back to the correct type.
|
||||
|
||||
value = value.toString();
|
||||
|
||||
let items = [];
|
||||
const settingOptions = md.options();
|
||||
for (let k in settingOptions) {
|
||||
if (!settingOptions.hasOwnProperty(k)) continue;
|
||||
items.push(<Picker.Item label={settingOptions[k]} value={k.toString()} key={k}/>);
|
||||
items.push({ label: settingOptions[k], value: k.toString() });
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<Text key="label" style={this.styles().settingText}>{md.label()}</Text>
|
||||
<Picker key="control" style={this.styles().settingControl} selectedValue={value} onValueChange={(itemValue, itemIndex) => updateSettingValue(key, itemValue)} >
|
||||
{ items }
|
||||
</Picker>
|
||||
<Dropdown
|
||||
key="control"
|
||||
style={this.styles().settingControl}
|
||||
items={items}
|
||||
selectedValue={value}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(itemValue, itemIndex) => { updateSettingValue(key, itemValue); }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (md.type == Setting.TYPE_BOOL) {
|
||||
@@ -128,12 +148,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
settingComps.push(comp);
|
||||
}
|
||||
|
||||
//style={this.styles().body}
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader title={_('Configuration')}/>
|
||||
<View style={this.styles().body}>
|
||||
<ScrollView >
|
||||
{ settingComps }
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { View, Button, Picker } = require('react-native');
|
||||
const { View, Button } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { Log } = require('lib/log.js');
|
||||
|
||||
Reference in New Issue
Block a user