1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 10:16:26 +00:00

refactor file structure

This commit is contained in:
Rokt33r
2015-12-25 05:41:10 +09:00
parent 152e4129b2
commit 325ae00eeb
53 changed files with 10 additions and 386 deletions

View File

@@ -0,0 +1,95 @@
import React from 'react'
import ReactDOM from 'react-dom'
import modes from 'boost/vars/modes'
import _ from 'lodash'
var ace = window.ace
module.exports = React.createClass({
propTypes: {
code: React.PropTypes.string,
mode: React.PropTypes.string,
className: React.PropTypes.string,
onChange: React.PropTypes.func,
readOnly: React.PropTypes.bool
},
getDefaultProps: function () {
return {
readOnly: false
}
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.readOnly !== this.props.readOnly) {
this.editor.setReadOnly(!!nextProps.readOnly)
}
},
componentDidMount: function () {
var el = ReactDOM.findDOMNode(this.refs.target)
var editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.setValue(this.props.code)
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/xcode')
editor.clearSelection()
editor.moveCursorTo(0, 0)
editor.commands.addCommand({
name: 'Emacs cursor up',
bindKey: {mac: 'Ctrl-P'},
exec: function (editor) {
editor.navigateUp(1)
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
},
readOnly: true
})
editor.setReadOnly(!!this.props.readOnly)
var session = editor.getSession()
let mode = _.findWhere(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setUseSoftTabs(true)
session.setOption('useWorker', false)
session.setUseWrapMode(true)
session.on('change', function (e) {
if (this.props.onChange != null) {
var value = editor.getValue()
this.props.onChange(e, value)
}
}.bind(this))
},
componentDidUpdate: function (prevProps) {
if (this.editor.getValue() !== this.props.code) {
this.editor.setValue(this.props.code)
this.editor.clearSelection()
}
if (prevProps.mode !== this.props.mode) {
var session = this.editor.getSession()
let mode = _.findWhere(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
}
},
getFirstVisibleRow: function () {
return this.editor.getFirstVisibleRow()
},
getCursorPosition: function () {
return this.editor.getCursorPosition()
},
moveCursorTo: function (row, col) {
this.editor.moveCursorTo(row, col)
},
scrollToLine: function (num) {
this.editor.scrollToLine(num, false, false)
},
render: function () {
return (
<div ref='target' className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}></div>
)
}
})

View File

@@ -0,0 +1,20 @@
import React, { PropTypes } from 'react'
const electron = require('electron')
const shell = electron.shell
export default class ExternalLink extends React.Component {
handleClick (e) {
shell.openExternal(this.props.href)
e.preventDefault()
}
render () {
return (
<a onClick={e => this.handleClick(e)} {...this.props}/>
)
}
}
ExternalLink.propTypes = {
href: PropTypes.string
}

View File

@@ -0,0 +1,52 @@
import React, { PropTypes } from 'react'
const BLUE = '#3460C7'
const LIGHTBLUE = '#2BA5F7'
const ORANGE = '#FF8E00'
const YELLOW = '#E8D252'
const GREEN = '#3FD941'
const DARKGREEN = '#1FAD85'
const RED = '#E10051'
const PURPLE = '#B013A4'
function getColorByIndex (index) {
switch (index % 8) {
case 0:
return RED
case 1:
return ORANGE
case 2:
return YELLOW
case 3:
return GREEN
case 4:
return DARKGREEN
case 5:
return LIGHTBLUE
case 6:
return BLUE
case 7:
return PURPLE
default:
return DARKGREEN
}
}
export default class FolderMark extends React.Component {
render () {
let color = getColorByIndex(this.props.color)
let className = 'FolderMark fa fa-square fa-fw'
if (this.props.className != null) {
className += ' active'
}
return (
<i className={className} style={{color: color}}/>
)
}
}
FolderMark.propTypes = {
color: PropTypes.number,
className: PropTypes.string
}

View File

@@ -0,0 +1,57 @@
var React = require('react')
var { PropTypes } = React
import markdown from 'boost/markdown'
var ReactDOM = require('react-dom')
const electron = require('electron')
const shell = electron.shell
function handleAnchorClick (e) {
shell.openExternal(e.target.href)
e.preventDefault()
}
export default class MarkdownPreview extends React.Component {
componentDidMount () {
this.addListener()
}
componentDidUpdate () {
this.addListener()
}
componentWillUnmount () {
this.removeListener()
}
componentWillUpdate () {
this.removeListener()
}
addListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
for (var i = 0; i < anchors.length; i++) {
anchors[i].addEventListener('click', handleAnchorClick)
}
}
removeListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
for (var i = 0; i < anchors.length; i++) {
anchors[i].removeEventListener('click', handleAnchorClick)
}
}
render () {
return (
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + markdown(this.props.content)}}/>
)
}
}
MarkdownPreview.propTypes = {
className: PropTypes.string,
content: PropTypes.string
}

View File

@@ -0,0 +1,82 @@
import React, { PropTypes } from 'react'
export default class ModeIcon extends React.Component {
getClassName () {
var mode = this.props.mode
switch (mode) {
// Script
case 'javascript':
return 'devicon-javascript-plain'
case 'jsx':
return 'devicon-react-original'
case 'coffee':
return 'devicon-coffeescript-original'
case 'ruby':
return 'devicon-ruby-plain'
case 'erlang':
return 'devicon-erlang-plain'
case 'php':
return 'devicon-php-plain'
// HTML
case 'html':
return 'devicon-html5-plain'
// Stylesheet
case 'css':
return 'devicon-css3-plain'
case 'less':
return 'devicon-less-plain-wordmark'
case 'sass':
case 'scss':
return 'devicon-sass-original'
// Compile
case 'c':
return 'devicon-c-plain'
case 'cpp':
return 'devicon-cplusplus-plain'
case 'csharp':
return 'devicon-csharp-plain'
case 'objc':
return 'devicon-apple-original'
case 'golang':
return 'devicon-go-plain'
case 'java':
return 'devicon-java-plain'
// Framework
case 'django':
return 'devicon-django-plain'
// Config
case 'dockerfile':
return 'devicon-docker-plain'
case 'gitignore':
return 'devicon-git-plain'
// Shell
case 'sh':
case 'batchfile':
case 'powershell':
return 'fa fa-fw fa-terminal'
case 'text':
case 'markdown':
return 'fa fa-fw fa-file-text-o'
}
return 'fa fa-fw fa-code'
}
render () {
let className = `ModeIcon ${this.getClassName()} ${this.props.className}`
return (
<i className={className}/>
)
}
}
ModeIcon.propTypes = {
className: PropTypes.string,
mode: PropTypes.string
}

View File

@@ -0,0 +1,190 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon'
import modes from 'boost/vars/modes'
import _ from 'lodash'
const IDLE_MODE = 'IDLE_MODE'
const EDIT_MODE = 'EDIT_MODE'
export default class ModeSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
mode: IDLE_MODE,
search: '',
focusIndex: 0
}
}
componentDidMount () {
this.blurHandler = e => {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
this.handleBlur()
}
}
window.addEventListener('click', this.blurHandler)
}
componentWillUnmount () {
window.removeEventListener('click', this.blurHandler)
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) {
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
}
}
handleIdleSelectClick (e) {
this.setState({mode: EDIT_MODE})
}
componentDidUpdate (prevProps, prevState) {
if (prevState.mode !== this.state.mode && this.state.mode === EDIT_MODE) {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
searchElement.focus()
if (this.searchKeyDownListener == null) {
this.searchKeyDownListener = e => this.handleSearchKeyDown
}
searchElement.addEventListener('keydown', this.searchKeyDownListener)
}
}
componentWillUpdate (nextProps, nextState) {
if (nextProps.mode !== this.state.mode && nextState.mode === IDLE_MODE) {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) {
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
}
}
}
handleModeOptionClick (modeName) {
return e => {
this.props.onChange(modeName)
this.setState({
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
}
}
handleSearchKeyDown (e) {
switch (e.keyCode) {
// up
case 38:
e.preventDefault()
if (this.state.focusIndex > 0) this.setState({focusIndex: this.state.focusIndex - 1})
break
// down
case 40:
e.preventDefault()
{
let filteredModes = modes
.filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched
})
if (filteredModes.length === this.state.focusIndex + 1) this.setState({focusIndex: filteredModes.length - 1})
else this.setState({focusIndex: this.state.focusIndex + 1})
}
break
// enter
case 13:
e.preventDefault()
{
let filteredModes = modes
.filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched
})
let targetMode = filteredModes[this.state.focusIndex]
if (targetMode != null) {
this.props.onChange(targetMode.name)
this.handleBlur()
}
}
break
// esc
case 27:
e.preventDefault()
e.stopPropagation()
this.handleBlur()
break
case 9:
this.handleBlur()
}
}
handleSearchChange (e) {
this.setState({
search: e.target.value,
focusIndex: 0
})
}
handleBlur () {
if (this.state.mode === EDIT_MODE) {
this.setState({
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
}
if (this.props.onBlur != null) this.props.onBlur()
}
render () {
let className = this.props.className != null
? `ModeSelect ${this.props.className}`
: this.props.className
if (this.state.mode === IDLE_MODE) {
let mode = _.findWhere(modes, {name: this.props.value})
let modeName = mode != null ? mode.name : 'text'
let modeLabel = mode != null ? mode.label : 'Plain text'
return (
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
<ModeIcon mode={modeName}/>
<span className='modeLabel'>{modeLabel}</span>
</div>
)
}
let filteredOptions = modes
.filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(_.escapeRegExp(search))
let aliasMatched = _.some(mode.alias, alias => alias.match(_.escapeRegExp(search)))
return nameMatched || aliasMatched
})
.map((mode, index) => {
return (
<div key={mode.name} className={index === this.state.focusIndex ? 'option active' : 'option'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
)
})
return (
<div className={className + ' edit'}>
<input onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
<div ref='options' className='modeOptions hide'>
{filteredOptions}
</div>
</div>
)
}
}
ModeSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
onBlur: PropTypes.func
}

View File

@@ -0,0 +1,24 @@
import React, { PropTypes } from 'react'
import md5 from 'md5'
export default class ProfileImage extends React.Component {
render () {
let className = this.props.className == null ? 'ProfileImage' : 'ProfileImage ' + this.props.className
let email = this.props.email != null ? this.props.email : ''
let src = 'http://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=' + this.props.size
return (
<img
className={className}
width={this.props.size}
height={this.props.size}
src={src}/>
)
}
}
ProfileImage.propTypes = {
email: PropTypes.string,
size: PropTypes.string,
className: PropTypes.string
}

View File

@@ -0,0 +1,18 @@
import React, { PropTypes } from 'react'
import store from '../store'
import { setTagFilter } from '../actions'
export default class TagLink extends React.Component {
handleClick (e) {
store.dispatch(setTagFilter(this.props.tag))
}
render () {
return (
<a onClick={e => this.handleClick(e)}>{this.props.tag}</a>
)
}
}
TagLink.propTypes = {
tag: PropTypes.string
}

View File

@@ -0,0 +1,168 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import linkState from 'boost/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
}
export default class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
input: '',
isInputFocused: false
}
}
componentDidMount () {
this.blurInputBlurHandler = e => {
if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
this.setState({isInputFocused: false})
}
}
window.addEventListener('click', this.blurInputBlurHandler)
}
componentWillUnmount (e) {
window.removeEventListener('click', this.blurInputBlurHandler)
}
// Suggestは必ずInputの下に位置するようにする
componentDidUpdate () {
if (this.shouldShowSuggest()) {
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
if (suggestElement != null) {
suggestElement.style.top = inputRect.top + 20 + 'px'
suggestElement.style.left = inputRect.left + 'px'
}
}
}
shouldShowSuggest () {
return this.state.isInputFocused && isNotEmptyString(this.state.input)
}
addTag (tag, clearInput = true) {
let tags = this.props.tags.slice(0)
let newTag = tag.trim()
if (newTag.length === 0 && clearInput) {
this.setState({input: ''})
return
}
tags.push(newTag)
tags = _.uniq(tags)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(newTag, tags)
}
if (clearInput) this.setState({input: ''})
}
handleKeyDown (e) {
switch (e.keyCode) {
case 8:
{
if (this.state.input.length > 0) break
e.preventDefault()
let tags = this.props.tags.slice(0)
tags.pop()
this.props.onChange(null, tags)
}
break
case 13:
{
e.preventDefault()
this.addTag(this.state.input)
}
}
}
handleThisClick (e) {
ReactDOM.findDOMNode(this.refs.tagInput).focus()
}
handleInputFocus (e) {
this.setState({isInputFocused: true})
}
handleItemRemoveButton (tag) {
return e => {
e.stopPropagation()
let tags = this.props.tags.slice(0)
tags.splice(tags.indexOf(tag), 1)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(null, tags)
}
}
}
handleSuggestClick (tag) {
return e => {
this.addTag(tag)
}
}
render () {
let { tags, suggestTags } = this.props
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => (
<span key={tag} className='tagItem'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
<span className='tagLabel'>{tag}</span>
</span>))
: null
let suggestElements = this.shouldShowSuggest() ? suggestTags
.filter(tag => {
return tag.match(this.state.input)
})
.map(tag => {
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
})
: null
return (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
<div className='tags'>
{tagElements}
<input
type='text'
onKeyDown={e => this.handleKeyDown(e)}
ref='tagInput'
valueLink={this.linkState('input')}
placeholder='Click here to add tags'
className='tagInput'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='suggestTags'>
{suggestElements}
</div>
)
: null
}
</div>
)
}
}
TagSelect.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
suggestTags: PropTypes.array
}
TagSelect.prototype.linkState = linkState