mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +00:00
Merge branch 'master' into smart_arrows
This commit is contained in:
90
browser/main/modals/PreferencesModal/SnippetEditor.js
Normal file
90
browser/main/modals/PreferencesModal/SnippetEditor.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import CodeMirror from 'codemirror'
|
||||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import styles from './SnippetTab.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
|
||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
const buildCMRulers = (rulers, enableRulers) =>
|
||||
enableRulers ? rulers.map(ruler => ({ column: ruler })) : []
|
||||
|
||||
class SnippetEditor extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.onRef(this)
|
||||
const { rulers, enableRulers } = this.props
|
||||
this.cm = CodeMirror(this.refs.root, {
|
||||
rulers: buildCMRulers(rulers, enableRulers),
|
||||
lineNumbers: this.props.displayLineNumbers,
|
||||
lineWrapping: true,
|
||||
theme: this.props.theme,
|
||||
indentUnit: this.props.indentSize,
|
||||
tabSize: this.props.indentSize,
|
||||
indentWithTabs: this.props.indentType !== 'space',
|
||||
keyMap: this.props.keyMap,
|
||||
scrollPastEnd: this.props.scrollPastEnd,
|
||||
dragDrop: false,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseBrackets: true,
|
||||
mode: 'null'
|
||||
})
|
||||
this.cm.setSize('100%', '100%')
|
||||
let changeDelay = null
|
||||
|
||||
this.cm.on('change', () => {
|
||||
this.snippet.content = this.cm.getValue()
|
||||
|
||||
clearTimeout(changeDelay)
|
||||
changeDelay = setTimeout(() => {
|
||||
this.saveSnippet()
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.onRef(undefined)
|
||||
}
|
||||
|
||||
onSnippetChanged (newSnippet) {
|
||||
this.snippet = newSnippet
|
||||
this.cm.setValue(this.snippet.content)
|
||||
}
|
||||
|
||||
onSnippetNameOrPrefixChanged (newSnippet) {
|
||||
this.snippet.name = newSnippet.name
|
||||
this.snippet.prefix = newSnippet.prefix.toString().replace(/\s/g, '').split(',')
|
||||
this.saveSnippet()
|
||||
}
|
||||
|
||||
saveSnippet () {
|
||||
dataApi.updateSnippet(this.snippet).catch((err) => { throw err })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { fontSize } = this.props
|
||||
let fontFamily = this.props.fontFamily
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
return (
|
||||
<div styleName='SnippetEditor' ref='root' tabIndex='-1' style={{
|
||||
fontFamily: fontFamily.join(', '),
|
||||
fontSize: fontSize
|
||||
}} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SnippetEditor.defaultProps = {
|
||||
readOnly: false,
|
||||
theme: 'xcode',
|
||||
keyMap: 'sublime',
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetEditor, styles)
|
||||
87
browser/main/modals/PreferencesModal/SnippetList.js
Normal file
87
browser/main/modals/PreferencesModal/SnippetList.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import styles from './SnippetTab.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
class SnippetList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
snippets: []
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.reloadSnippetList()
|
||||
eventEmitter.on('snippetList:reload', this.reloadSnippetList.bind(this))
|
||||
}
|
||||
|
||||
reloadSnippetList () {
|
||||
dataApi.fetchSnippet().then(snippets => this.setState({snippets}))
|
||||
}
|
||||
|
||||
handleSnippetContextMenu (snippet) {
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: i18n.__('Delete snippet'),
|
||||
click: () => {
|
||||
this.deleteSnippet(snippet)
|
||||
}
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
deleteSnippet (snippet) {
|
||||
dataApi.deleteSnippet(snippet).then(() => {
|
||||
this.reloadSnippetList()
|
||||
this.props.onSnippetDeleted(snippet)
|
||||
}).catch(err => { throw err })
|
||||
}
|
||||
|
||||
handleSnippetClick (snippet) {
|
||||
this.props.onSnippetClick(snippet)
|
||||
}
|
||||
|
||||
createSnippet () {
|
||||
dataApi.createSnippet().then(() => {
|
||||
this.reloadSnippetList()
|
||||
// scroll to end of list when added new snippet
|
||||
const snippetList = document.getElementById('snippets')
|
||||
snippetList.scrollTop = snippetList.scrollHeight
|
||||
}).catch(err => { throw err })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { snippets } = this.state
|
||||
return (
|
||||
<div styleName='snippet-list'>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-control'>
|
||||
<button styleName='group-control-button' onClick={() => this.createSnippet()}>
|
||||
<i className='fa fa-plus' /> {i18n.__('New Snippet')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul id='snippets' styleName='snippets'>
|
||||
{
|
||||
snippets.map((snippet) => (
|
||||
<li
|
||||
styleName='snippet-item'
|
||||
key={snippet.id}
|
||||
onContextMenu={() => this.handleSnippetContextMenu(snippet)}
|
||||
onClick={() => this.handleSnippetClick(snippet)}>
|
||||
{snippet.name}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetList, styles)
|
||||
116
browser/main/modals/PreferencesModal/SnippetTab.js
Normal file
116
browser/main/modals/PreferencesModal/SnippetTab.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetTab.styl'
|
||||
import SnippetEditor from './SnippetEditor'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import SnippetList from './SnippetList'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
currentSnippet: null
|
||||
}
|
||||
this.changeDelay = null
|
||||
}
|
||||
|
||||
handleSnippetNameOrPrefixChange () {
|
||||
clearTimeout(this.changeDelay)
|
||||
this.changeDelay = setTimeout(() => {
|
||||
// notify the snippet editor that the name or prefix of snippet has been changed
|
||||
this.snippetEditor.onSnippetNameOrPrefixChanged(this.state.currentSnippet)
|
||||
eventEmitter.emit('snippetList:reload')
|
||||
}, 500)
|
||||
}
|
||||
|
||||
handleSnippetClick (snippet) {
|
||||
const { currentSnippet } = this.state
|
||||
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
|
||||
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
|
||||
// notify the snippet editor to load the content of the new snippet
|
||||
this.snippetEditor.onSnippetChanged(changedSnippet)
|
||||
this.setState({currentSnippet: changedSnippet})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onSnippetNameOrPrefixChanged (e, type) {
|
||||
const newSnippet = Object.assign({}, this.state.currentSnippet)
|
||||
if (type === 'name') {
|
||||
newSnippet.name = e.target.value
|
||||
} else {
|
||||
newSnippet.prefix = e.target.value
|
||||
}
|
||||
this.setState({ currentSnippet: newSnippet })
|
||||
this.handleSnippetNameOrPrefixChange()
|
||||
}
|
||||
|
||||
handleDeleteSnippet (snippet) {
|
||||
// prevent old snippet still display when deleted
|
||||
if (snippet.id === this.state.currentSnippet.id) {
|
||||
this.setState({currentSnippet: null})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, storageKey } = this.props
|
||||
const { currentSnippet } = this.state
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>{i18n.__('Snippets')}</div>
|
||||
<SnippetList
|
||||
onSnippetClick={this.handleSnippetClick.bind(this)}
|
||||
onSnippetDeleted={this.handleDeleteSnippet.bind(this)} />
|
||||
<div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={currentSnippet ? currentSnippet.name : ''}
|
||||
onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'name') }}
|
||||
type='text' />
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Snippet prefix')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={currentSnippet ? currentSnippet.prefix : ''}
|
||||
onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'prefix') }}
|
||||
type='text' />
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='snippet-editor-section'>
|
||||
<SnippetEditor
|
||||
storageKey={storageKey}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
onRef={ref => { this.snippetEditor = ref }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SnippetTab.PropTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetTab, styles)
|
||||
180
browser/main/modals/PreferencesModal/SnippetTab.styl
Normal file
180
browser/main/modals/PreferencesModal/SnippetTab.styl
Normal file
@@ -0,0 +1,180 @@
|
||||
@import('./Tab')
|
||||
@import('./ConfigTab')
|
||||
|
||||
.root
|
||||
padding 15px
|
||||
white-space pre
|
||||
line-height 1.4
|
||||
color alpha($ui-text-color, 90%)
|
||||
width 100%
|
||||
font-size 14px
|
||||
|
||||
.group
|
||||
margin-bottom 45px
|
||||
|
||||
.group-header
|
||||
@extend .header
|
||||
color $ui-text-color
|
||||
|
||||
.group-header2
|
||||
font-size 20px
|
||||
color $ui-text-color
|
||||
margin-bottom 15px
|
||||
margin-top 30px
|
||||
|
||||
.group-section
|
||||
margin-bottom 20px
|
||||
display flex
|
||||
line-height 30px
|
||||
|
||||
.group-section-label
|
||||
width 150px
|
||||
text-align left
|
||||
margin-right 10px
|
||||
font-size 14px
|
||||
|
||||
.group-section-control
|
||||
flex 1
|
||||
margin-left 5px
|
||||
|
||||
.group-section-control select
|
||||
outline none
|
||||
border 1px solid $ui-borderColor
|
||||
font-size 16px
|
||||
height 30px
|
||||
width 250px
|
||||
margin-bottom 5px
|
||||
background-color transparent
|
||||
|
||||
.group-section-control-input
|
||||
height 30px
|
||||
vertical-align middle
|
||||
width 400px
|
||||
font-size $tab--button-font-size
|
||||
border solid 1px $border-color
|
||||
border-radius 2px
|
||||
padding 0 5px
|
||||
outline none
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor
|
||||
|
||||
.group-control-button
|
||||
height 30px
|
||||
border none
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
colorPrimaryButton()
|
||||
vertical-align middle
|
||||
padding 0 20px
|
||||
|
||||
.group-checkBoxSection
|
||||
margin-bottom 15px
|
||||
display flex
|
||||
line-height 30px
|
||||
padding-left 15px
|
||||
|
||||
.group-control
|
||||
padding-top 10px
|
||||
box-sizing border-box
|
||||
height 40px
|
||||
text-align right
|
||||
:global
|
||||
.alert
|
||||
display inline-block
|
||||
position absolute
|
||||
top 60px
|
||||
right 15px
|
||||
font-size 14px
|
||||
.success
|
||||
color #1EC38B
|
||||
.error
|
||||
color red
|
||||
.warning
|
||||
color #FFA500
|
||||
|
||||
.snippet-list
|
||||
width 30%
|
||||
height calc(100% - 200px)
|
||||
position absolute
|
||||
|
||||
.snippets
|
||||
height calc(100% - 8px)
|
||||
overflow scroll
|
||||
background: #f5f5f5
|
||||
|
||||
.snippet-item
|
||||
height 50px
|
||||
font-size 15px
|
||||
line-height 50px
|
||||
padding 0 5%
|
||||
cursor pointer
|
||||
position relative
|
||||
|
||||
&::after
|
||||
width 90%
|
||||
height 1px
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
position absolute
|
||||
top 100%
|
||||
left 5%
|
||||
content ''
|
||||
|
||||
&:hover
|
||||
background darken(#f5f5f5, 5)
|
||||
|
||||
.snippet-detail
|
||||
width 70%
|
||||
height calc(100% - 200px)
|
||||
position absolute
|
||||
left 33%
|
||||
|
||||
.SnippetEditor
|
||||
position absolute
|
||||
width 100%
|
||||
height 90%
|
||||
|
||||
body[data-theme="default"], body[data-theme="white"]
|
||||
.snippets
|
||||
background $ui-backgroundColor
|
||||
.snippet-item
|
||||
color black
|
||||
&::after
|
||||
background $ui-borderColor
|
||||
&:hover
|
||||
background darken($ui-backgroundColor, 5)
|
||||
|
||||
body[data-theme="dark"]
|
||||
.snippets
|
||||
background $ui-dark-backgroundColor
|
||||
.snippet-item
|
||||
color white
|
||||
&::after
|
||||
background $ui-dark-borderColor
|
||||
&:hover
|
||||
background darken($ui-dark-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color white
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.snippets
|
||||
background $ui-solarized-dark-backgroundColor
|
||||
.snippet-item
|
||||
color white
|
||||
&::after
|
||||
background $ui-solarized-dark-borderColor
|
||||
&:hover
|
||||
background darken($ui-solarized-dark-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color white
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.snippets
|
||||
background $ui-monokai-backgroundColor
|
||||
.snippet-item
|
||||
color White
|
||||
&::after
|
||||
background $ui-monokai-borderColor
|
||||
&:hover
|
||||
background darken($ui-monokai-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color white
|
||||
@@ -28,6 +28,8 @@ class UiTab extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
|
||||
CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css')
|
||||
this.customCSSCM.getCodeMirror().setSize('400px', '400px')
|
||||
this.handleSettingDone = () => {
|
||||
this.setState({UiAlert: {
|
||||
type: 'success',
|
||||
@@ -99,7 +101,9 @@ class UiTab extends React.Component {
|
||||
smartQuotes: this.refs.previewSmartQuotes.checked,
|
||||
breaks: this.refs.previewBreaks.checked,
|
||||
smartArrows: this.refs.previewSmartArrows.checked,
|
||||
sanitize: this.refs.previewSanitize.value
|
||||
sanitize: this.refs.previewSanitize.value,
|
||||
allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
|
||||
customCSS: this.customCSSCM.getCodeMirror().getValue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +164,7 @@ class UiTab extends React.Component {
|
||||
const { config, codemirrorTheme } = this.state
|
||||
const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};'
|
||||
const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none'
|
||||
const customCSS = config.preview.customCSS
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='group'>
|
||||
@@ -580,6 +585,20 @@ class UiTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Custom CSS')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={config.preview.allowCustomCSS}
|
||||
ref='previewAllowCustomCSS'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Allow custom CSS for preview')}
|
||||
<ReactCodeMirror onChange={e => this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} options={{ lineNumbers: true, mode: 'css', theme: codemirrorTheme }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-control'>
|
||||
<button styleName='group-control-rightButton'
|
||||
|
||||
@@ -6,6 +6,7 @@ import UiTab from './UiTab'
|
||||
import InfoTab from './InfoTab'
|
||||
import Crowdfunding from './Crowdfunding'
|
||||
import StoragesTab from './StoragesTab'
|
||||
import SnippetTab from './SnippetTab'
|
||||
import Blog from './Blog'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
@@ -86,6 +87,14 @@ class Preferences extends React.Component {
|
||||
haveToSave={alert => this.setState({BlogAlert: alert})}
|
||||
/>
|
||||
)
|
||||
case 'SNIPPET':
|
||||
return (
|
||||
<SnippetTab
|
||||
dispatch={dispatch}
|
||||
config={config}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
case 'STORAGES':
|
||||
default:
|
||||
return (
|
||||
@@ -123,7 +132,8 @@ class Preferences extends React.Component {
|
||||
{target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert},
|
||||
{target: 'INFO', label: i18n.__('About')},
|
||||
{target: 'CROWDFUNDING', label: i18n.__('Crowdfunding')},
|
||||
{target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert}
|
||||
{target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert},
|
||||
{target: 'SNIPPET', label: i18n.__('Snippets')}
|
||||
]
|
||||
|
||||
const navButtons = tabs.map((tab) => {
|
||||
|
||||
Reference in New Issue
Block a user