feat: type writer mode

This commit is contained in:
Jocs 2018-03-02 01:59:49 +08:00
parent d59702bb13
commit 993bd6cfdb
11 changed files with 173 additions and 37 deletions

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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 `-`]

4
src/main/actions/view.js Normal file
View File

@ -0,0 +1,4 @@
export const view = (win, item, type) => {
const { checked } = item
win.webContents.send('AGANI::view', { type, checked })
}

View File

@ -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'
}]
}

View File

@ -5,7 +5,11 @@
:active="windowActive"
:word-count="wordCount"
></title-bar>
<editor></editor>
<editor
:typewriter="typewriter"
:focus="focus"
:source-code="sourceCode"
></editor>
<search></search>
</div>
</template>
@ -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 @@
<style>
.editor-container {
padding-top: 22px;
flex-direction: column;
}
</style>

View File

@ -1,6 +1,12 @@
<template>
<div class="editor-wrapper">
<div ref="editor" class="editor-component"></div>
<div
class="editor-wrapper"
:class="{ 'typewriter': typewriter, 'focus': focus, 'source-code': sourceCode }"
>
<div
ref="editor"
class="editor-component"
></div>
<el-dialog
:visible.sync="dialogTableVisible"
:show-close="isShowClose"
@ -54,8 +60,29 @@
<script>
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;

View File

@ -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') {

View File

@ -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)