mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 03:00:19 +08:00
feat: type writer mode
This commit is contained in:
parent
d59702bb13
commit
993bd6cfdb
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
4
src/main/actions/view.js
Normal file
@ -0,0 +1,4 @@
|
||||
export const view = (win, item, type) => {
|
||||
const { checked } = item
|
||||
win.webContents.send('AGANI::view', { type, checked })
|
||||
}
|
@ -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'
|
||||
}]
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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') {
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user