diff --git a/src/editor/contentState/paragraphCtrl.js b/src/editor/contentState/paragraphCtrl.js
index 8519f71c..c39bd9f6 100644
--- a/src/editor/contentState/paragraphCtrl.js
+++ b/src/editor/contentState/paragraphCtrl.js
@@ -16,6 +16,7 @@ const getCurrentLevel = type => {
const paragraphCtrl = ContentState => {
ContentState.prototype.selectionChange = function () {
const { start, end } = selection.getCursorRange()
+ const cursorCoords = selection.getCursorCoords()
const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)
const startParents = this.getParents(startBlock)
@@ -32,7 +33,8 @@ const paragraphCtrl = ContentState => {
return {
start,
end,
- affiliation
+ affiliation,
+ cursorCoords
}
}
diff --git a/src/editor/index.css b/src/editor/index.css
index dad4791c..7ca1cbcc 100644
--- a/src/editor/index.css
+++ b/src/editor/index.css
@@ -35,6 +35,7 @@ h6.ag-active::before {
*::selection, .ag-selection {
background: #E4E7ED;
+ color: #303133;
}
.ag-highlight {
@@ -262,7 +263,7 @@ pre.ag-active .ag-language-input {
caret-color: #303133;
}
.ag-gray {
- color: #C0C4CC;
+ color: #E4E7ED;
text-decoration: none;
}
diff --git a/src/editor/selection.js b/src/editor/selection.js
index d9205fb9..fe3644e5 100644
--- a/src/editor/selection.js
+++ b/src/editor/selection.js
@@ -815,6 +815,26 @@ class Selection {
}
}
+ getCursorCoords () {
+ const sel = this.doc.getSelection()
+ let range
+ let x = 0
+ let y = 0
+
+ if (sel.rangeCount) {
+ range = sel.getRangeAt(0).cloneRange()
+ if (range.getClientRects) {
+ range.collapse(true)
+ const rects = range.getClientRects()
+ if (rects.length) {
+ ({ x, y } = rects[0])
+ }
+ }
+ }
+
+ return { x, y }
+ }
+
getSelectionEnd () {
const node = this.doc.getSelection().focusNode
const endNode = (node && node.nodeType === 3 ? node.parentNode : node)
diff --git a/src/editor/themes/light.css b/src/editor/themes/light.css
index 1ca961b2..c2153dea 100644
--- a/src/editor/themes/light.css
+++ b/src/editor/themes/light.css
@@ -34,13 +34,13 @@
}
html, body {
- font-size: 16px;
+ font-size: 15px;
background: rgb(252, 252, 252);
}
body {
font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
- color: #303133;
+ color: #606266;
line-height: 1.6;
}
diff --git a/src/editor/utils/index.js b/src/editor/utils/index.js
index 8cb8d133..1f08dfe3 100644
--- a/src/editor/utils/index.js
+++ b/src/editor/utils/index.js
@@ -6,6 +6,13 @@ const getId = () => {
return `${prefix}${Math.random().toString(32).slice(2)}`
}
+const easeInOutQuad = function (t, b, c, d) {
+ t /= d / 2
+ if (t < 1) return c / 2 * t * t + b
+ t--
+ return -c / 2 * (t * (t - 2) - 1) + b
+}
+
/**
* get unique id base on a set.
*/
@@ -184,6 +191,42 @@ export const getImageSrc = src => {
}
}
+export const animatedScrollTo = function (element, to, duration, callback) {
+ let start = element.scrollTop
+ let change = to - start
+ let animationStart = +new Date()
+ let animating = true
+ let lastpos = null
+
+ const animateScroll = function () {
+ if (!animating) {
+ return
+ }
+ requestAnimationFrame(animateScroll)
+ const now = +new Date()
+ const val = Math.floor(easeInOutQuad(now - animationStart, start, change, duration))
+ if (lastpos) {
+ if (lastpos === element.scrollTop) {
+ lastpos = val
+ element.scrollTop = val
+ } else {
+ animating = false
+ }
+ } else {
+ lastpos = val
+ element.scrollTop = val
+ }
+ if (now > animationStart + duration) {
+ element.scrollTop = to
+ animating = false
+ if (callback) {
+ callback()
+ }
+ }
+ }
+ requestAnimationFrame(animateScroll)
+}
+
/**
* [genUpper2LowerKeyHash generate constants map hash, the value is lowercase of the key,
* also translate `_` to `-`]
diff --git a/src/main/actions/view.js b/src/main/actions/view.js
new file mode 100644
index 00000000..5bb230a1
--- /dev/null
+++ b/src/main/actions/view.js
@@ -0,0 +1,4 @@
+export const view = (win, item, type) => {
+ const { checked } = item
+ win.webContents.send('AGANI::view', { type, checked })
+}
diff --git a/src/main/menus/view.js b/src/main/menus/view.js
index 2e1c0fa4..48a4441a 100755
--- a/src/main/menus/view.js
+++ b/src/main/menus/view.js
@@ -1,3 +1,5 @@
+import * as actions from '../actions/view'
+
let viewMenu = {
label: 'View',
submenu: [{
@@ -14,6 +16,31 @@ let viewMenu = {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
}
}
+ }, {
+ type: 'separator'
+ }, {
+ label: 'Source Code Mode',
+ accelerator: 'Alt+CmdOrCtrl+S',
+ type: 'checkbox',
+ click (item, browserWindow) {
+ actions.view(browserWindow, item, 'sourceCode')
+ }
+ }, {
+ label: 'Typewriter Mode',
+ accelerator: 'Alt+CmdOrCtrl+T',
+ type: 'checkbox',
+ click (item, browserWindow) {
+ actions.view(browserWindow, item, 'typewriter')
+ }
+ }, {
+ label: 'Focus Mode',
+ accelerator: 'Alt+CmdOrCtrl+M',
+ type: 'checkbox',
+ click (item, browserWindow) {
+ actions.view(browserWindow, item, 'focus')
+ }
+ }, {
+ type: 'separator'
}]
}
diff --git a/src/renderer/App.vue b/src/renderer/App.vue
index c7ac7f7a..b83ea5e1 100644
--- a/src/renderer/App.vue
+++ b/src/renderer/App.vue
@@ -5,7 +5,11 @@
:active="windowActive"
:word-count="wordCount"
>
-
+
@@ -27,7 +31,7 @@
return {}
},
computed: {
- ...mapState(['filename', 'windowActive', 'wordCount'])
+ ...mapState(['filename', 'windowActive', 'wordCount', 'typewriter', 'focus', 'sourceCode'])
},
created () {
const { dispatch } = this.$store
@@ -40,6 +44,7 @@
dispatch('LISTEN_FOR_FILE_LOAD')
dispatch('LISTEN_FOR_FILE_CHANGE')
dispatch('LISTEN_FOR_EDIT')
+ dispatch('LISTEN_FOR_VIEW')
dispatch('LISTEN_FOR_EXPORT')
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
}
@@ -49,6 +54,5 @@
diff --git a/src/renderer/components/Editor.vue b/src/renderer/components/Editor.vue
index 5ce6bf3c..2665e0e7 100644
--- a/src/renderer/components/Editor.vue
+++ b/src/renderer/components/Editor.vue
@@ -1,6 +1,12 @@
-
-
+
+
import Aganippe from '../../editor'
import bus from '../bus'
+ import { animatedScrollTo } from '../../editor/utils'
+
+ const STANDAR_Y = 320
+ const PARAGRAPH_CMD = [
+ 'ul-bullet', 'ul-task', 'ol-order', 'pre', 'blockquote', 'heading 1', 'heading 2', 'heading 3',
+ 'heading 4', 'heading 5', 'heading 6', 'upgrade heading', 'degrade heading', 'paragraph', 'hr'
+ ]
export default {
+ props: {
+ typewriter: {
+ type: Boolean,
+ required: true
+ },
+ focus: {
+ type: Boolean,
+ required: true
+ },
+ sourceCode: {
+ type: Boolean,
+ required: true
+ }
+ },
data () {
return {
editor: null,
@@ -87,6 +114,13 @@
this.$store.dispatch('SAVE_FILE', { markdown, wordCount })
})
this.editor.on('selectionChange', changes => {
+ const editor = this.editor.container
+ const { y } = changes.cursorCoords
+
+ if (this.typewriter) {
+ animatedScrollTo(editor, editor.scrollTop + y - STANDAR_Y, 100)
+ }
+
this.$store.dispatch('SELECTION_CHANGE', changes)
})
this.editor.on('selectionFormats', formats => {
@@ -104,8 +138,13 @@
this.$store.dispatch('SEARCH', searchMatches)
},
handleFind (action) {
+ const { container } = this.editor
const searchMatches = this.editor.find(action)
this.$store.dispatch('SEARCH', searchMatches)
+ // Scroll to highlight
+ const anchor = document.querySelector('.ag-highlight')
+ const { y } = anchor.getBoundingClientRect()
+ animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, 300)
},
async handleExport (type) {
switch (type) {
@@ -128,31 +167,14 @@
}
},
handleEditParagraph (type) {
- switch (type) {
- case 'table':
- this.tableChecker = { rows: 2, columns: 2 }
- this.dialogTableVisible = true
- this.$nextTick(() => {
- this.$refs.rowInput.focus()
- })
- break
- case 'ul-bullet':
- case 'ul-task':
- case 'ol-order':
- case 'pre':
- case 'blockquote':
- case 'heading 1':
- case 'heading 2':
- case 'heading 3':
- case 'heading 4':
- case 'heading 5':
- case 'heading 6':
- case 'upgrade heading':
- case 'degrade heading':
- case 'paragraph':
- case 'hr':
- this.editor && this.editor.updateParagraph(type)
- break
+ if (type === 'table') {
+ this.tableChecker = { rows: 2, columns: 2 }
+ this.dialogTableVisible = true
+ this.$nextTick(() => {
+ this.$refs.rowInput.focus()
+ })
+ } else if (PARAGRAPH_CMD.indexOf(type) > -1) {
+ this.editor && this.editor.updateParagraph(type)
}
},
handleInlineFormat (type) {
@@ -186,6 +208,11 @@
.editor-component {
height: 100%;
overflow: auto;
+ box-sizing: border-box;
+ }
+ .typewriter .editor-component {
+ padding-top: calc(50vh - 136px);
+ padding-bottom: calc(50vh - 54px);
}
.v-modal {
background: #fff;
diff --git a/src/renderer/components/search.vue b/src/renderer/components/search.vue
index f4ae9069..edc916ce 100644
--- a/src/renderer/components/search.vue
+++ b/src/renderer/components/search.vue
@@ -164,9 +164,6 @@
},
find (action) {
bus.$emit('find', action)
- // const anchor = document.querySelector('.ag-highlight')
- // console.log(this.scroll)
- // this.scroll.animateScroll(anchor)
},
search (event) {
if (event.key !== 'Enter') {
diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js
index d11b2ae8..10c17583 100644
--- a/src/renderer/store/editor.js
+++ b/src/renderer/store/editor.js
@@ -9,6 +9,9 @@ const state = {
matches: [],
value: ''
},
+ typewriter: false, // typewriter mode
+ focus: false, // focus mode
+ sourceCode: false, // source code mode
pathname: '',
isSaved: true,
markdown: '',
@@ -22,6 +25,9 @@ const state = {
}
const mutations = {
+ SET_MODE (state, { type, checked }) {
+ state[type] = checked
+ },
SET_SEARCH (state, value) {
state.searchMatches = value
},
@@ -140,6 +146,11 @@ const actions = {
bus.$emit(type)
})
},
+ LISTEN_FOR_VIEW ({ commit }) {
+ ipcRenderer.on('AGANI::view', (e, data) => {
+ commit('SET_MODE', data)
+ })
+ },
LISTEN_FOR_PARAGRAPH_INLINE_STYLE ({ commit }) {
ipcRenderer.on('AGANI::paragraph', (e, { type }) => {
bus.$emit('paragraph', type)