mirror of
https://github.com/marktext/marktext.git
synced 2025-05-04 04:09:26 +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 => {
|
const paragraphCtrl = ContentState => {
|
||||||
ContentState.prototype.selectionChange = function () {
|
ContentState.prototype.selectionChange = function () {
|
||||||
const { start, end } = selection.getCursorRange()
|
const { start, end } = selection.getCursorRange()
|
||||||
|
const cursorCoords = selection.getCursorCoords()
|
||||||
const startBlock = this.getBlock(start.key)
|
const startBlock = this.getBlock(start.key)
|
||||||
const endBlock = this.getBlock(end.key)
|
const endBlock = this.getBlock(end.key)
|
||||||
const startParents = this.getParents(startBlock)
|
const startParents = this.getParents(startBlock)
|
||||||
@ -32,7 +33,8 @@ const paragraphCtrl = ContentState => {
|
|||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
affiliation
|
affiliation,
|
||||||
|
cursorCoords
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ h6.ag-active::before {
|
|||||||
|
|
||||||
*::selection, .ag-selection {
|
*::selection, .ag-selection {
|
||||||
background: #E4E7ED;
|
background: #E4E7ED;
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-highlight {
|
.ag-highlight {
|
||||||
@ -262,7 +263,7 @@ pre.ag-active .ag-language-input {
|
|||||||
caret-color: #303133;
|
caret-color: #303133;
|
||||||
}
|
}
|
||||||
.ag-gray {
|
.ag-gray {
|
||||||
color: #C0C4CC;
|
color: #E4E7ED;
|
||||||
text-decoration: none;
|
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 () {
|
getSelectionEnd () {
|
||||||
const node = this.doc.getSelection().focusNode
|
const node = this.doc.getSelection().focusNode
|
||||||
const endNode = (node && node.nodeType === 3 ? node.parentNode : node)
|
const endNode = (node && node.nodeType === 3 ? node.parentNode : node)
|
||||||
|
@ -34,13 +34,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
background: rgb(252, 252, 252);
|
background: rgb(252, 252, 252);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
color: #303133;
|
color: #606266;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,13 @@ const getId = () => {
|
|||||||
return `${prefix}${Math.random().toString(32).slice(2)}`
|
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.
|
* 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,
|
* [genUpper2LowerKeyHash generate constants map hash, the value is lowercase of the key,
|
||||||
* also translate `_` to `-`]
|
* 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 = {
|
let viewMenu = {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [{
|
submenu: [{
|
||||||
@ -14,6 +16,31 @@ let viewMenu = {
|
|||||||
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
|
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"
|
:active="windowActive"
|
||||||
:word-count="wordCount"
|
:word-count="wordCount"
|
||||||
></title-bar>
|
></title-bar>
|
||||||
<editor></editor>
|
<editor
|
||||||
|
:typewriter="typewriter"
|
||||||
|
:focus="focus"
|
||||||
|
:source-code="sourceCode"
|
||||||
|
></editor>
|
||||||
<search></search>
|
<search></search>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -27,7 +31,7 @@
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['filename', 'windowActive', 'wordCount'])
|
...mapState(['filename', 'windowActive', 'wordCount', 'typewriter', 'focus', 'sourceCode'])
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
const { dispatch } = this.$store
|
const { dispatch } = this.$store
|
||||||
@ -40,6 +44,7 @@
|
|||||||
dispatch('LISTEN_FOR_FILE_LOAD')
|
dispatch('LISTEN_FOR_FILE_LOAD')
|
||||||
dispatch('LISTEN_FOR_FILE_CHANGE')
|
dispatch('LISTEN_FOR_FILE_CHANGE')
|
||||||
dispatch('LISTEN_FOR_EDIT')
|
dispatch('LISTEN_FOR_EDIT')
|
||||||
|
dispatch('LISTEN_FOR_VIEW')
|
||||||
dispatch('LISTEN_FOR_EXPORT')
|
dispatch('LISTEN_FOR_EXPORT')
|
||||||
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
||||||
}
|
}
|
||||||
@ -49,6 +54,5 @@
|
|||||||
<style>
|
<style>
|
||||||
.editor-container {
|
.editor-container {
|
||||||
padding-top: 22px;
|
padding-top: 22px;
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor-wrapper">
|
<div
|
||||||
<div ref="editor" class="editor-component"></div>
|
class="editor-wrapper"
|
||||||
|
:class="{ 'typewriter': typewriter, 'focus': focus, 'source-code': sourceCode }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="editor"
|
||||||
|
class="editor-component"
|
||||||
|
></div>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible.sync="dialogTableVisible"
|
:visible.sync="dialogTableVisible"
|
||||||
:show-close="isShowClose"
|
:show-close="isShowClose"
|
||||||
@ -54,8 +60,29 @@
|
|||||||
<script>
|
<script>
|
||||||
import Aganippe from '../../editor'
|
import Aganippe from '../../editor'
|
||||||
import bus from '../bus'
|
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 {
|
export default {
|
||||||
|
props: {
|
||||||
|
typewriter: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
focus: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sourceCode: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
editor: null,
|
editor: null,
|
||||||
@ -87,6 +114,13 @@
|
|||||||
this.$store.dispatch('SAVE_FILE', { markdown, wordCount })
|
this.$store.dispatch('SAVE_FILE', { markdown, wordCount })
|
||||||
})
|
})
|
||||||
this.editor.on('selectionChange', changes => {
|
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.$store.dispatch('SELECTION_CHANGE', changes)
|
||||||
})
|
})
|
||||||
this.editor.on('selectionFormats', formats => {
|
this.editor.on('selectionFormats', formats => {
|
||||||
@ -104,8 +138,13 @@
|
|||||||
this.$store.dispatch('SEARCH', searchMatches)
|
this.$store.dispatch('SEARCH', searchMatches)
|
||||||
},
|
},
|
||||||
handleFind (action) {
|
handleFind (action) {
|
||||||
|
const { container } = this.editor
|
||||||
const searchMatches = this.editor.find(action)
|
const searchMatches = this.editor.find(action)
|
||||||
this.$store.dispatch('SEARCH', searchMatches)
|
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) {
|
async handleExport (type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -128,31 +167,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleEditParagraph (type) {
|
handleEditParagraph (type) {
|
||||||
switch (type) {
|
if (type === 'table') {
|
||||||
case 'table':
|
this.tableChecker = { rows: 2, columns: 2 }
|
||||||
this.tableChecker = { rows: 2, columns: 2 }
|
this.dialogTableVisible = true
|
||||||
this.dialogTableVisible = true
|
this.$nextTick(() => {
|
||||||
this.$nextTick(() => {
|
this.$refs.rowInput.focus()
|
||||||
this.$refs.rowInput.focus()
|
})
|
||||||
})
|
} else if (PARAGRAPH_CMD.indexOf(type) > -1) {
|
||||||
break
|
this.editor && this.editor.updateParagraph(type)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleInlineFormat (type) {
|
handleInlineFormat (type) {
|
||||||
@ -186,6 +208,11 @@
|
|||||||
.editor-component {
|
.editor-component {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.typewriter .editor-component {
|
||||||
|
padding-top: calc(50vh - 136px);
|
||||||
|
padding-bottom: calc(50vh - 54px);
|
||||||
}
|
}
|
||||||
.v-modal {
|
.v-modal {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
@ -164,9 +164,6 @@
|
|||||||
},
|
},
|
||||||
find (action) {
|
find (action) {
|
||||||
bus.$emit('find', action)
|
bus.$emit('find', action)
|
||||||
// const anchor = document.querySelector('.ag-highlight')
|
|
||||||
// console.log(this.scroll)
|
|
||||||
// this.scroll.animateScroll(anchor)
|
|
||||||
},
|
},
|
||||||
search (event) {
|
search (event) {
|
||||||
if (event.key !== 'Enter') {
|
if (event.key !== 'Enter') {
|
||||||
|
@ -9,6 +9,9 @@ const state = {
|
|||||||
matches: [],
|
matches: [],
|
||||||
value: ''
|
value: ''
|
||||||
},
|
},
|
||||||
|
typewriter: false, // typewriter mode
|
||||||
|
focus: false, // focus mode
|
||||||
|
sourceCode: false, // source code mode
|
||||||
pathname: '',
|
pathname: '',
|
||||||
isSaved: true,
|
isSaved: true,
|
||||||
markdown: '',
|
markdown: '',
|
||||||
@ -22,6 +25,9 @@ const state = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
SET_MODE (state, { type, checked }) {
|
||||||
|
state[type] = checked
|
||||||
|
},
|
||||||
SET_SEARCH (state, value) {
|
SET_SEARCH (state, value) {
|
||||||
state.searchMatches = value
|
state.searchMatches = value
|
||||||
},
|
},
|
||||||
@ -140,6 +146,11 @@ const actions = {
|
|||||||
bus.$emit(type)
|
bus.$emit(type)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
LISTEN_FOR_VIEW ({ commit }) {
|
||||||
|
ipcRenderer.on('AGANI::view', (e, data) => {
|
||||||
|
commit('SET_MODE', data)
|
||||||
|
})
|
||||||
|
},
|
||||||
LISTEN_FOR_PARAGRAPH_INLINE_STYLE ({ commit }) {
|
LISTEN_FOR_PARAGRAPH_INLINE_STYLE ({ commit }) {
|
||||||
ipcRenderer.on('AGANI::paragraph', (e, { type }) => {
|
ipcRenderer.on('AGANI::paragraph', (e, { type }) => {
|
||||||
bus.$emit('paragraph', type)
|
bus.$emit('paragraph', type)
|
||||||
|
Loading…
Reference in New Issue
Block a user