Add ability to add notebooks and refactor code

This commit is contained in:
Joey Payne 2016-02-24 17:38:15 -07:00
commit d30477bd9d
12 changed files with 518 additions and 97 deletions

2
.gitignore vendored
View file

@ -41,4 +41,4 @@ jspm_packages
# Optional REPL history
.node_repl_history
dist
app/dist

View file

@ -15,7 +15,17 @@ import Folder from 'material-ui/lib/svg-icons/file/folder'
import Edit from 'material-ui/lib/svg-icons/editor/mode-edit'
import Delete from 'material-ui/lib/svg-icons/action/delete'
import Divider from 'material-ui/lib/divider'
import { SelectableContainerEnhance } from 'material-ui/lib/hoc/selectable-enhance'
import { SelectableContainerEnhance } from '../enhance/SelectableEnhance'
import uuid from 'node-uuid'
import path from 'path-extra'
import * as utils from 'utils'
import glob from 'glob'
import fs from 'fs'
import mkdirp from 'mkdirp'
import jsfile from 'jsonfile'
const colors = styles.Colors
@ -28,8 +38,12 @@ function eventFire(el, etype){
el.dispatchEvent(evObj);
}
}
String.prototype.trunc = String.prototype.trunc || function(n){
return (this.length > n) ? this.substr(0, n-1)+'...' : this;
};
const {AppBar,
const {
AppBar,
AppCanvas,
FontIcon,
IconButton,
@ -54,12 +68,15 @@ let SelectableList = SelectableContainerEnhance(List)
function wrapState(ComposedComponent) {
const StateWrapper = React.createClass({
getInitialState() {
return {selectedIndex: 0};
return {selectedIndex: -1};
},
setIndex(i, func){
this.setState({
selectedIndex: i,
}, func);
},
handleUpdateSelectedIndex(e, index) {
this.setState({
selectedIndex: index,
});
this.setIndex(index);
},
render() {
return (
@ -118,24 +135,38 @@ export default class LibraryNav extends React.Component {
],
notebooks: [
{'state': 'editing', 'title': '', 'notes': 0},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10},
{'state': 'displaying', 'title': 'FieldNotes', 'notes': 10}
]
}
this.sortNotebooks(true)
this.getNotebooks()
}
getNotebooks = () => {
var dataPath = utils.getAppDataPath()
var notebooks = glob.sync(path.join(dataPath, '*.qvnotebook'))
for(var i=0; i<notebooks.length; i++){
var nbFile = notebooks[i]
var obj = jsfile.readFileSync(path.join(nbFile, 'meta.json'))
var notes = glob.sync(path.join(nbFile, '*.qvnote'))
var nb = {
'title': obj.name,
'uuid': obj.uuid,
'notes': notes.length,
'path': nbFile
}
if(nb.title == ''){
nb.state = 'editing'
}
else{
nb.state = 'displaying'
}
this.state.notebooks.push(nb)
this.sortNotebooks(true)
}
};
compareNotebooks = (a, b) => {
let atitle = a.title.toLowerCase()
let btitle = b.title.toLowerCase()
@ -147,10 +178,10 @@ export default class LibraryNav extends React.Component {
return 0
};
sortNotebooks = (notset) => {
sortNotebooks = (dontSet, func) => {
this.state.notebooks.sort(this.compareNotebooks)
if (!notset){
this.setState({notebooks: this.state.notebooks})
if (!dontSet){
this.setState({notebooks: this.state.notebooks}, func)
}
};
@ -188,16 +219,71 @@ export default class LibraryNav extends React.Component {
}
scrollToRenamedNotebook = (original) => {
var newIndex = 0
for (var j = 0; j < this.state.notebooks.length; j++){
var n = this.state.notebooks[j]
if (n == original){
newIndex = j
this.refs.notebookList.setIndex(j, () => {
var nbitem = $(ReactDOM.findDOMNode(this.refs[n.title+newIndex]))
var cont = $('#notebook-list')
var newPos = nbitem.offset().top - cont.offset().top + cont.scrollTop() - cont.height()/2;
$('#notebook-list').animate({
scrollTop: newPos
})
})
}
}
};
createNewNotebook = (notebook, callback) => {
var notePath = utils.getNotebookPath(notebook)
mkdirp(notePath, (err) => {
if(err){
console.log('There was an error creating the directory '+notePath)
console.log(err)
}
else{
if (callback){
callback()
}
this.createNotebookMeta(notebook)
}
})
};
createNotebookMeta = (notebook) => {
var notePath = utils.getNotebookPath(notebook)
var meta = {
'name': notebook.title,
'uuid': notebook.uuid
}
var metaPath = path.join(notePath, 'meta.json')
jsfile.writeFile(metaPath, meta, (err) => {
if(err){
console.log(err)
}
})
};
newNotebookUnfocus = (i) => {
var nb = this.state.notebooks[i]
var tf = this.refs["textField"+i]
var notebookName = tf.getValue()
if (notebookName){
nb.title = notebookName
nb.state = 'displaying'
this.setState({notebooks: this.state.notebooks})
this.sortNotebooks()
this.sortNotebooks(false, ()=>{
this.scrollToRenamedNotebook(nb)
})
this.createNotebookMeta(nb)
}
else if(nb.title){
nb.state = 'displaying'
@ -206,11 +292,23 @@ export default class LibraryNav extends React.Component {
};
addNotebookTapped = () => {
var nbs = this.state.notebooks
nbs.splice(0, 0, {'state': 'editing',
var nb_uuid = uuid.v4().toUpperCase()
var nb = {
'state': 'editing',
'title': '',
'notes': 0})
this.setState({notebooks: nbs})
'uuid': nb_uuid,
'notes': 0
}
this.createNewNotebook(nb, () => {
var nbs = this.state.notebooks
nbs.splice(0, 0, nb)
this.setState({notebooks: nbs}, () => {
this.refs['textField0'].focus()
})
})
};
newNotebookTyped = (i) => {
@ -218,6 +316,7 @@ export default class LibraryNav extends React.Component {
};
menuItemClicked = (i, ev) => {
this.refs.notebookList.setIndex(-1)
var item = this.state.navItems[i]
var type = 'leftClick'
var nativeEvent = ev.nativeEvent
@ -232,14 +331,9 @@ export default class LibraryNav extends React.Component {
var nbs = this.state.notebooks
nbs[i].state = 'editing'
this.setState({notebooks: nbs}, () => {
console.log(i)
var nbsx = this.state.notebooks
this.props.closeContextMenu()
//this.refs['textField'+i].focus()
//$(ReactDOM.findDOMNode(this.refs['listItem'+i])).click()
//console.log(node.find(':input'))
//node.find(':input')[0].focus()
this.refs['textField'+i].focus()
})
this.props.closeContextMenu()
}
deleteTapped = (i) => {
@ -276,12 +370,63 @@ export default class LibraryNav extends React.Component {
this.props.updateContextMenu(this.contextMenuItems(i))
this.props.openContextMenu(x, y)
}
this.refs.mainList.setIndex(-1)
};
notebookList = () => {
return (<div id="notebook-list">
{this.state.notebooks.map((notebook, i) =>{
var l = null
if (notebook.state == 'editing'){
l = <ListItem
key={notebook.uuid || i}
value={i}
innerDivStyle={{'paddingBottom': 0,
'paddingTop': 0}}
leftIcon={<NoteBook color={colors.grey500}/>}
rightIcon={<Badge badgeContent={notebook.notes}/>}>
<TextField
ref={"textField"+i}
fullWidth={true}
hintText={notebook.title.trunc(18) || "Notebook Name"}
underlineShow={false}
onBlur={this.newNotebookUnfocus.bind(this, i)}
onEnterKeyDown={this.newNotebookUnfocus.bind(this, i)}
/>
</ListItem>
}
else{
l = <ListItem
key={notebook.uuid || i}
value={i}
primaryText={notebook.title.trunc(18)}
tooltip={notebook.title}
ref={notebook.title+i}
className="noselect"
onTouchTap={this.noteBookTapped.bind(this, i)}
leftIcon={<NoteBook onTouchTap={this.notebookIconTapped.bind(this, i)} color={colors.grey500}/>}
rightIcon={<Badge
style={{'padding': 0}}
badgeContent={notebook.notes} />}
/>
}
return l
})}
</div>)
};
notebookIconTapped = (i, ev) => {
};
render(){
return (
<div id={this.props.id} className={this.props.className || ""}>
<SelectableList id="main-nav" subheader="Library">
<SelectableList ref='mainList' className="list" id="main-nav" subheader="Library">
{this.state.navItems.map((item, i) => {
return <ListItem
primaryText={item.name}
@ -295,7 +440,7 @@ export default class LibraryNav extends React.Component {
</SelectableList>
<Divider />
<div>
<List id="nblist" subheader={<div>
<SelectableList id="nblist" className="list" ref='notebookList' subheader={<div>
<div className="inline">NoteBooks</div>
<IconButton
onTouchTap={this.addNotebookTapped}
@ -306,43 +451,8 @@ export default class LibraryNav extends React.Component {
color={colors.grey500}/>
</IconButton>
</div>}>
<div id="notebook-list">
{this.state.notebooks.map((notebook, i) =>{
var l = null
if (notebook.state == 'editing'){
l = <ListItem
key={i}
innerDivStyle={{'paddingBottom': 0,
'paddingTop': 0}}
primaryText={<TextField
ref={"textField"+i}
fullWidth={true}
hintText={notebook.title || "Notebook Name"}
underlineShow={false}
onBlur={this.newNotebookUnfocus.bind(this, i)}
onEnterKeyDown={this.newNotebookUnfocus.bind(this, i)}
/>}
leftIcon={<NoteBook color={colors.grey500}/>}
rightIcon={<Badge badgeContent={notebook.notes} />}
/>
}
else{
l = <ListItem
key={i}
primaryText={notebook.title}
ref={notebook.title+i}
className="noselect"
onTouchTap={this.noteBookTapped.bind(this, i)}
leftIcon={<NoteBook color={colors.grey500}/>}
rightIcon={<Badge badgeContent={notebook.notes} />}
/>
}
return l
})}
</div>
</List>
{this.notebookList()}
</SelectableList>
</div>
</div>
)

122
app/containers/App.jsx Normal file
View file

@ -0,0 +1,122 @@
import React from 'react'
import getMuiTheme from 'material-ui/lib/styles/getMuiTheme'
import LibraryNav from 'LibraryNav'
import Styles from 'material-ui/lib/styles'
import Rethink from 'rethinkdbdash'
import mui from 'material-ui'
import * as ContextMenuActions from '../actions'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
const {
Popover,
Menu,
MenuItem} = mui
const DefaultRawTheme = Styles.LightRawTheme
let r = Rethink({
db: 'technote',
servers: [
{host: '162.243.255.144',
port: 28015}
]})
function createTables(){
r.tableCreate('notes').run()
.then(function(){
r.table('notes').indexCreate('account_id').run()
})
.error(function(){})
r.tableCreate('accounts').run().error(function(){})
}
class App extends React.Component {
constructor(props, context){
super(props, context)
createTables()
this.state = {
entries: [],
}
}
static get childContextTypes(){
return {muiTheme: React.PropTypes.object}
}
getChildContext() {
return {
muiTheme: getMuiTheme(DefaultRawTheme)
}
}
entriesTapped = () => {
r.table('notes').getAll('jyapayne@gmail.com', {index: 'account_id'}).run().then(
function(notes){
}
)
};
handleRequestClose = () => {
this.props.contextMenuActions.closeContextMenu()
};
render() {
const { contextMenu, contextMenuActions } = this.props
return (
<div className="fill-height">
<div style={{position: 'absolute',
width: 1,
height: 1,
top: contextMenu.y,
left: contextMenu.x}} ref='menuPos'></div>
<Popover
open={contextMenu.opened}
anchorEl={this.refs.menuPos}
anchorOrigin={{horizontal: 'middle', vertical: 'bottom'}}
targetOrigin={{horizontal: 'left', vertical: 'top'}}
onRequestClose={this.handleRequestClose}>
<Menu desktop={true}>
{contextMenu.items.map(function (el, i){
return el;
})}
</Menu>
</Popover>
<LibraryNav
id="library-nav"
ref="libraryNav"
entriesTapped={this.entriesTapped}
className="left inline fill-height"
{...contextMenuActions}
/>
</div>
)
}
}
App.propTypes = {
contextMenu: React.PropTypes.object.isRequired,
contextMenuActions: React.PropTypes.object.isRequired
}
function mapStateToProps(state) {
return {
contextMenu: state.contextMenu
}
}
function mapDispatchToProps(dispatch) {
return {
contextMenuActions: bindActionCreators(ContextMenuActions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

View file

@ -0,0 +1,142 @@
import React from 'react';
import getMuiTheme from 'material-ui/lib/styles/getMuiTheme';
import StylePropable from 'material-ui/lib/mixins/style-propable';
import ColorManipulator from 'material-ui/lib/utils/color-manipulator';
export const SelectableContainerEnhance = (Component) => {
const composed = React.createClass({
displayName: `Selectable${Component.displayName}`,
propTypes: {
children: React.PropTypes.node,
selectedItemStyle: React.PropTypes.object,
valueLink: React.PropTypes.shape({
value: React.PropTypes.any,
requestChange: React.PropTypes.func,
}).isRequired,
},
contextTypes: {
muiTheme: React.PropTypes.object,
},
childContextTypes: {
muiTheme: React.PropTypes.object,
},
mixins: [
StylePropable,
],
getInitialState() {
return {
muiTheme: this.context.muiTheme || getMuiTheme(),
};
},
getChildContext() {
return {
muiTheme: this.state.muiTheme,
};
},
componentWillReceiveProps(nextProps, nextContext) {
let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({muiTheme: newMuiTheme});
},
getValueLink: function(props) {
return props.valueLink || {
value: props.value,
requestChange: props.onChange,
};
},
extendChild(child, styles, selectedItemStyle) {
if (child && child.type && child.type.displayName === 'ListItem') {
let selected = this.isChildSelected(child, this.props);
let selectedChildrenStyles = {};
if (selected) {
selectedChildrenStyles = this.mergeStyles(styles, selectedItemStyle);
}
let mergedChildrenStyles = this.mergeStyles(child.props.style || {}, selectedChildrenStyles);
this.keyIndex += 1;
return React.cloneElement(child, {
onTouchTap: (e) => {
this.handleItemTouchTap(e, child);
if (child.props.onTouchTap) {
child.props.onTouchTap(e);
}
},
key: this.keyIndex,
style: mergedChildrenStyles,
nestedItems: child.props.nestedItems.map((child) => this.extendChild(child, styles, selectedItemStyle)),
initiallyOpen: this.isInitiallyOpen(child),
});
} else {
var Type = child.type
return <Type {...child.props} {...child.state}>{React.Children.map(child.props.children, (childx) => this.extendChild(childx, styles, selectedItemStyle))}</Type>;
}
},
isInitiallyOpen(child) {
if (child.props.initiallyOpen) {
return child.props.initiallyOpen;
}
return this.hasSelectedDescendant(false, child);
},
hasSelectedDescendant(previousValue, child) {
if (React.isValidElement(child) && child.props.nestedItems && child.props.nestedItems.length > 0) {
return child.props.nestedItems.reduce(this.hasSelectedDescendant, previousValue);
}
return previousValue || this.isChildSelected(child, this.props);
},
isChildSelected(child, props) {
let itemValue = this.getValueLink(props).value;
let childValue = child.props.value;
return (itemValue === childValue);
},
handleItemTouchTap(e, item) {
let valueLink = this.getValueLink(this.props);
let itemValue = item.props.value;
let menuValue = valueLink.value;
if ( itemValue !== menuValue) {
valueLink.requestChange(e, itemValue);
}
},
render() {
const {children, selectedItemStyle} = this.props;
this.keyIndex = 0;
let styles = {};
if (!selectedItemStyle) {
let textColor = this.state.muiTheme.rawTheme.palette.textColor;
let selectedColor = ColorManipulator.fade(textColor, 0.2);
styles = {
backgroundColor: selectedColor,
};
}
let newChildren = React.Children.map(children, (child) => this.extendChild(child, styles, selectedItemStyle));
return (
<Component {...this.props} {...this.state}>
{newChildren}
</Component>
);
},
});
return composed;
};
export default SelectableContainerEnhance;

View file

@ -0,0 +1,8 @@
import { createStore } from 'redux'
import rootReducer from '../reducers'
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState)
return store
}

10
app/utils.jsx Normal file
View file

@ -0,0 +1,10 @@
import path from 'path-extra'
export function getAppDataPath(){
return path.datadir(APP_NAME)
}
export function getNotebookPath(notebook){
var notePath = getAppDataPath()
return path.join(notePath, notebook.uuid+'.qvnotebook')
}

View file

@ -13,6 +13,7 @@
<div id="main">
</div>
<script>
var APP_NAME = 'TechNote'
window.$ = window.jQuery = require('./app/static/jquery-1.12.0.min.js');
$(document).ready(function(){
document.addEventListener("keydown", function (e) {
@ -36,6 +37,6 @@
});
});
</script>
<script type="text/javascript" src="dist/bundle.js"></script>
<script type="text/javascript" src="app/dist/bundle.js"></script>
</body>
</html>

17
index.jsx Normal file
View file

@ -0,0 +1,17 @@
import React from 'react'
import injectTapEventPlugin from "react-tap-event-plugin"
injectTapEventPlugin()
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './app/containers/App'
import configureStore from './app/store/configureStore'
const store = configureStore()
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('main')
)

View file

@ -57,9 +57,14 @@
"linux-x64": true
},
"dependencies": {
"glob": "^7.0.0",
"jsonfile": "^2.2.3",
"material-ui": "^0.14.4",
"materialize-css": "^0.97.5",
"mkdirp": "^0.5.1",
"moment": "^2.11.2",
"node-uuid": "^1.4.7",
"path-extra": "^3.0.0",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"react-quill": "^0.4.1",

View file

@ -11,6 +11,11 @@ body, html{
.noselect{
-webkit-user-select: none;
}
.list > div:first-child{
-webkit-user-select: none;
}
.inline{
display: inline-block;
vertical-align: top;

View file

@ -1,9 +1,9 @@
var path = require('path');
module.exports = {
entry: './app/main.jsx',
entry: './index.jsx',
output: {
path: __dirname + "/dist",
path: __dirname + "/app/dist",
filename: "bundle.js",
sourceMapFilename: 'bundle.map'
},
@ -23,7 +23,8 @@ module.exports = {
resolve:{
extensions: ['', '.js', '.jsx'],
root: [
path.resolve('./app/modules')
path.resolve('./app/components'),
path.resolve('./app')
]
}
};