mirror of
https://github.com/marktext/marktext.git
synced 2025-05-04 03:32:36 +08:00
feat: search value, find next find prev
This commit is contained in:
parent
f30cbfd837
commit
d59702bb13
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,4 +1,20 @@
|
|||||||
### 0.3.1
|
### 0.4.0
|
||||||
|
|
||||||
|
**Feature**
|
||||||
|
|
||||||
|
- Search value in document, Use **FIND PREV** and **FIND NEXT** to selection previous one or next one.
|
||||||
|
|
||||||
|
Add animation of highlight word.
|
||||||
|
|
||||||
|
Auto focus the search input when open search panel.
|
||||||
|
|
||||||
|
close the search panel will auto selection the last highlight word by ESC button.
|
||||||
|
|
||||||
|
- Replace value
|
||||||
|
|
||||||
|
Replace All
|
||||||
|
|
||||||
|
Replace one and auto highlight the next word.
|
||||||
|
|
||||||
**Bug fix**
|
**Bug fix**
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#### TODO LIST
|
#### TODO LIST
|
||||||
|
|
||||||
- [ ] Support Search and Replacement.
|
- [x] Support Search and Replacement.
|
||||||
|
|
||||||
- [ ] add Dark, Light and GitHub theme.
|
- [ ] add Dark, Light and GitHub theme.
|
||||||
|
|
||||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marktext",
|
"name": "marktext",
|
||||||
"version": "0.2.2",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marktext",
|
"name": "marktext",
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"author": "Jocs <luoran1988@126.com>",
|
"author": "Jocs <luoran1988@126.com>",
|
||||||
"description": "A markdown editor",
|
"description": "A markdown editor",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -80,7 +80,9 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
|
|||||||
'AG_TASK_LIST_ITEM',
|
'AG_TASK_LIST_ITEM',
|
||||||
'AG_TASK_LIST_ITEM_CHECKBOX',
|
'AG_TASK_LIST_ITEM_CHECKBOX',
|
||||||
'AG_CHECKBOX_CHECKED',
|
'AG_CHECKBOX_CHECKED',
|
||||||
'AG_TABLE_TOOL_BAR'
|
'AG_TABLE_TOOL_BAR',
|
||||||
|
'AG_SELECTION',
|
||||||
|
'AG_HIGHLIGHT'
|
||||||
])
|
])
|
||||||
|
|
||||||
export const codeMirrorConfig = {
|
export const codeMirrorConfig = {
|
||||||
|
@ -15,6 +15,7 @@ import copyCutCtrl from './copyCutCtrl'
|
|||||||
import paragraphCtrl from './paragraphCtrl'
|
import paragraphCtrl from './paragraphCtrl'
|
||||||
import tabCtrl from './tabCtrl'
|
import tabCtrl from './tabCtrl'
|
||||||
import formatCtrl from './formatCtrl'
|
import formatCtrl from './formatCtrl'
|
||||||
|
import searchCtrl from './searchCtrl'
|
||||||
import importMarkdown from '../utils/importMarkdown'
|
import importMarkdown from '../utils/importMarkdown'
|
||||||
|
|
||||||
const prototypes = [
|
const prototypes = [
|
||||||
@ -31,6 +32,7 @@ const prototypes = [
|
|||||||
tableBlockCtrl,
|
tableBlockCtrl,
|
||||||
paragraphCtrl,
|
paragraphCtrl,
|
||||||
formatCtrl,
|
formatCtrl,
|
||||||
|
searchCtrl,
|
||||||
importMarkdown
|
importMarkdown
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -61,6 +63,11 @@ class ContentState {
|
|||||||
|
|
||||||
init () {
|
init () {
|
||||||
const lastBlock = this.getLastBlock()
|
const lastBlock = this.getLastBlock()
|
||||||
|
this.searchMatches = {
|
||||||
|
value: '',
|
||||||
|
matches: [],
|
||||||
|
index: -1
|
||||||
|
}
|
||||||
this.cursor = {
|
this.cursor = {
|
||||||
start: {
|
start: {
|
||||||
key: lastBlock.key,
|
key: lastBlock.key,
|
||||||
@ -84,10 +91,12 @@ class ContentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render (isRenderCursor = true) {
|
render (isRenderCursor = true) {
|
||||||
const { blocks, cursor } = this
|
const { blocks, cursor, searchMatches: { matches, index } } = this
|
||||||
const activeBlocks = this.getActiveBlocks()
|
const activeBlocks = this.getActiveBlocks()
|
||||||
|
matches.forEach((m, i) => {
|
||||||
this.stateRender.render(blocks, cursor, activeBlocks)
|
m.active = i === index
|
||||||
|
})
|
||||||
|
this.stateRender.render(blocks, cursor, activeBlocks, matches)
|
||||||
if (isRenderCursor) this.setCursor()
|
if (isRenderCursor) this.setCursor()
|
||||||
this.pre2CodeMirror()
|
this.pre2CodeMirror()
|
||||||
console.log('render')
|
console.log('render')
|
||||||
|
@ -25,7 +25,9 @@ const paragraphCtrl = ContentState => {
|
|||||||
.filter(p => PARAGRAPH_TYPES.includes(p.type))
|
.filter(p => PARAGRAPH_TYPES.includes(p.type))
|
||||||
|
|
||||||
start.type = startBlock.type
|
start.type = startBlock.type
|
||||||
|
start.block = startBlock
|
||||||
end.type = endBlock.type
|
end.type = endBlock.type
|
||||||
|
end.block = endBlock
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
|
98
src/editor/contentState/searchCtrl.js
Normal file
98
src/editor/contentState/searchCtrl.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
const defaultSearchOption = {
|
||||||
|
caseSensitive: false,
|
||||||
|
selectHighlight: false,
|
||||||
|
highlightIndex: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchCtrl = ContentState => {
|
||||||
|
ContentState.prototype.replaceOne = function (match, value) {
|
||||||
|
const {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
key
|
||||||
|
} = match
|
||||||
|
const block = this.getBlock(key)
|
||||||
|
const {
|
||||||
|
text
|
||||||
|
} = block
|
||||||
|
|
||||||
|
block.text = text.substring(0, start) + value + text.substring(end)
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentState.prototype.replace = function (replaceValue, opt = { isSingle: true }) {
|
||||||
|
const { isSingle, caseSensitive } = opt
|
||||||
|
const { matches, value, index } = this.searchMatches
|
||||||
|
if (matches.length) {
|
||||||
|
if (isSingle) {
|
||||||
|
this.replaceOne(matches[index], replaceValue)
|
||||||
|
} else {
|
||||||
|
// replace all
|
||||||
|
for (const match of matches) {
|
||||||
|
this.replaceOne(match, replaceValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const highlightIndex = index < matches.length - 1 ? index : index - 1
|
||||||
|
this.search(value, { caseSensitive, highlightIndex: isSingle ? highlightIndex : -1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentState.prototype.search = function (value, opt = {}) {
|
||||||
|
value = value.trim()
|
||||||
|
let matches = []
|
||||||
|
const { caseSensitive, selectHighlight, highlightIndex } = Object.assign(defaultSearchOption, opt)
|
||||||
|
const { blocks } = this
|
||||||
|
const search = blocks => {
|
||||||
|
for (const block of blocks) {
|
||||||
|
let { text, key } = block
|
||||||
|
if (!caseSensitive) {
|
||||||
|
text = text.toLowerCase()
|
||||||
|
value = value.toLowerCase()
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
let i = text.indexOf(value)
|
||||||
|
while (i > -1) {
|
||||||
|
matches.push({
|
||||||
|
key,
|
||||||
|
start: i,
|
||||||
|
end: i + value.length
|
||||||
|
})
|
||||||
|
i = text.indexOf(value, i + value.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (block.children.length) {
|
||||||
|
search(block.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value) search(blocks)
|
||||||
|
let index = -1
|
||||||
|
if (highlightIndex !== -1) {
|
||||||
|
index = highlightIndex
|
||||||
|
} else if (matches.length) {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectHighlight) {
|
||||||
|
const { matches, index } = this.searchMatches
|
||||||
|
const light = matches[index]
|
||||||
|
if (light) {
|
||||||
|
const key = light.key
|
||||||
|
this.cursor = {
|
||||||
|
start: {
|
||||||
|
key,
|
||||||
|
offset: light.start
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
key,
|
||||||
|
offset: light.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.assign(this.searchMatches, { value, matches, index })
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default searchCtrl
|
@ -1,3 +1,15 @@
|
|||||||
|
@keyframes highlight {
|
||||||
|
from {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
} /* ignored */
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h1.ag-active::before,
|
h1.ag-active::before,
|
||||||
h2.ag-active::before,
|
h2.ag-active::before,
|
||||||
h3.ag-active::before,
|
h3.ag-active::before,
|
||||||
@ -13,16 +25,24 @@ h6.ag-active::before {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: -25px;
|
left: -25px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #C0C4CC;
|
||||||
border-radius: 5px;
|
border-radius: 3px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #ddd;
|
color: #C0C4CC;
|
||||||
transform: scale(.7);
|
transform: scale(.7);
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::selection {
|
*::selection, .ag-selection {
|
||||||
background: #efefef;
|
background: #E4E7ED;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-highlight {
|
||||||
|
animation-name: highlight;
|
||||||
|
animation-duration: .25s;
|
||||||
|
display: inline-block;
|
||||||
|
background: rgb(249, 226, 153);
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
@ -242,7 +262,7 @@ pre.ag-active .ag-language-input {
|
|||||||
caret-color: #303133;
|
caret-color: #303133;
|
||||||
}
|
}
|
||||||
.ag-gray {
|
.ag-gray {
|
||||||
color: lavender;
|
color: #C0C4CC;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +282,8 @@ pre.ag-active .ag-language-input {
|
|||||||
color: #000;
|
color: #000;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.ag-hide {
|
|
||||||
|
.ag-hide, .ag-hide .ag-highlight, .ag-hide .ag-selection {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
@ -412,6 +412,31 @@ class Aganippe {
|
|||||||
this.contentState.format(type)
|
this.contentState.format(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search (value, opt) {
|
||||||
|
const { selectHighlight } = opt
|
||||||
|
this.contentState.search(value, opt)
|
||||||
|
this.contentState.render(!!selectHighlight)
|
||||||
|
return this.contentState.searchMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
replace (value, opt) {
|
||||||
|
this.contentState.replace(value, opt)
|
||||||
|
this.contentState.render(false)
|
||||||
|
return this.contentState.searchMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
find (action/* pre or next */) {
|
||||||
|
let { matches, index } = this.contentState.searchMatches
|
||||||
|
const len = matches.length
|
||||||
|
if (!len) return
|
||||||
|
index = action === 'next' ? index + 1 : index - 1
|
||||||
|
if (index < 0) index = len - 1
|
||||||
|
if (index >= len) index = 0
|
||||||
|
this.contentState.searchMatches.index = index
|
||||||
|
this.contentState.render(false)
|
||||||
|
return this.contentState.searchMatches
|
||||||
|
}
|
||||||
|
|
||||||
on (event, listener) {
|
on (event, listener) {
|
||||||
const { eventCenter } = this
|
const { eventCenter } = this
|
||||||
eventCenter.subscribe(event, listener)
|
eventCenter.subscribe(event, listener)
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { LOWERCASE_TAGS, CLASS_OR_ID } from '../config'
|
import { LOWERCASE_TAGS, CLASS_OR_ID } from '../config'
|
||||||
import { conflict, isLengthEven, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils'
|
import { conflict, isLengthEven, union, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils'
|
||||||
import { insertAfter, operateClassName } from '../utils/domManipulate.js'
|
import { insertAfter, operateClassName } from '../utils/domManipulate.js'
|
||||||
import { tokenizer } from './parse'
|
import { tokenizer } from './parse'
|
||||||
import { validEmoji } from '../emojis'
|
import { validEmoji } from '../emojis'
|
||||||
|
|
||||||
|
// for test
|
||||||
|
window.tokenizer = tokenizer
|
||||||
|
|
||||||
const snabbdom = require('snabbdom')
|
const snabbdom = require('snabbdom')
|
||||||
const patch = snabbdom.init([ // Init patch function with chosen modules
|
const patch = snabbdom.init([ // Init patch function with chosen modules
|
||||||
require('snabbdom/modules/class').default, // makes it easy to toggle classes
|
require('snabbdom/modules/class').default, // makes it easy to toggle classes
|
||||||
@ -45,11 +48,15 @@ class StateRender {
|
|||||||
return outerClass || (this.checkConflicted(block, token, cursor) ? CLASS_OR_ID['AG_GRAY'] : CLASS_OR_ID['AG_HIDE'])
|
return outerClass || (this.checkConflicted(block, token, cursor) ? CLASS_OR_ID['AG_GRAY'] : CLASS_OR_ID['AG_HIDE'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHighlightClassName (active) {
|
||||||
|
return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [render]: 2 steps:
|
* [render]: 2 steps:
|
||||||
* render vdom
|
* render vdom
|
||||||
*/
|
*/
|
||||||
render (blocks, cursor, activeBlocks) {
|
render (blocks, cursor, activeBlocks, matches) {
|
||||||
const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
||||||
|
|
||||||
const renderBlock = block => {
|
const renderBlock = block => {
|
||||||
@ -115,8 +122,10 @@ class StateRender {
|
|||||||
|
|
||||||
return h(blockSelector, data, block.children.map(child => renderBlock(child)))
|
return h(blockSelector, data, block.children.map(child => renderBlock(child)))
|
||||||
} else {
|
} else {
|
||||||
|
// highlight search key in block
|
||||||
|
const highlights = matches.filter(m => m.key === block.key)
|
||||||
let children = block.text
|
let children = block.text
|
||||||
? tokenizer(block.text).reduce((acc, token) => {
|
? tokenizer(block.text, highlights).reduce((acc, token) => {
|
||||||
const chunk = this[token.type](h, cursor, block, token)
|
const chunk = this[token.type](h, cursor, block, token)
|
||||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||||
}, [])
|
}, [])
|
||||||
@ -184,111 +193,212 @@ class StateRender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hr (h, cursor, block, token, outerClass) {
|
hr (h, cursor, block, token, outerClass) {
|
||||||
|
const { start, end } = token.range
|
||||||
|
const content = this.highlight(h, block, start, end, token)
|
||||||
return [
|
return [
|
||||||
h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker)
|
h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, content)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
header (h, cursor, block, token, outerClass) {
|
header (h, cursor, block, token, outerClass) {
|
||||||
const className = this.getClassName(outerClass, block, token, cursor)
|
const className = this.getClassName(outerClass, block, token, cursor)
|
||||||
|
const { start, end } = token.range
|
||||||
|
const content = this.highlight(h, block, start, end, token)
|
||||||
return [
|
return [
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker)
|
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, content)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
['code_fense'] (h, cursor, block, token, outerClass) {
|
['code_fense'] (h, cursor, block, token, outerClass) {
|
||||||
|
const { start, end } = token.range
|
||||||
|
const { marker } = token
|
||||||
|
|
||||||
|
const markerContent = this.highlight(h, block, start, start + marker.length, token)
|
||||||
|
const content = this.highlight(h, block, start + marker.length, end, token)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${CLASS_OR_ID['AG_GRAY']}`, token.marker),
|
h(`span.${CLASS_OR_ID['AG_GRAY']}`, markerContent),
|
||||||
h(`span.${CLASS_OR_ID['AG_LANGUAGE']}`, token.content)
|
h(`span.${CLASS_OR_ID['AG_LANGUAGE']}`, content)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
backlash (h, cursor, block, token, outerClass) {
|
backlash (h, cursor, block, token, outerClass) {
|
||||||
const className = this.getClassName(outerClass, block, token, cursor)
|
const className = this.getClassName(outerClass, block, token, cursor)
|
||||||
|
const { start, end } = token.range
|
||||||
|
const content = this.highlight(h, block, start, end, token)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker)
|
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, content)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
['inline_code'] (h, cursor, block, token, outerClass) {
|
['inline_code'] (h, cursor, block, token, outerClass) {
|
||||||
const className = this.getClassName(outerClass, block, token, cursor)
|
const className = this.getClassName(outerClass, block, token, cursor)
|
||||||
|
const { marker } = token
|
||||||
|
const { start, end } = token.range
|
||||||
|
|
||||||
|
const startMarker = this.highlight(h, block, start, start + marker.length, token)
|
||||||
|
const endMarker = this.highlight(h, block, end - marker.length, end, token)
|
||||||
|
const content = this.highlight(h, block, start + marker.length, end - marker.length, token)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker),
|
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, startMarker),
|
||||||
h('code', token.content),
|
h('code', content),
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker)
|
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, endMarker)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
// change text to highlight vdom
|
||||||
|
highlight (h, block, rStart, rEnd, token) {
|
||||||
|
const { text } = block
|
||||||
|
const { highlights } = token
|
||||||
|
let result = []
|
||||||
|
let unions = []
|
||||||
|
let pos = rStart
|
||||||
|
|
||||||
text (h, cursor, block, token) {
|
if (highlights) {
|
||||||
return token.content
|
for (const light of highlights) {
|
||||||
|
const un = union({ start: rStart, end: rEnd }, light)
|
||||||
|
if (un) unions.push(un)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (unions.length) {
|
||||||
|
for (const u of unions) {
|
||||||
|
const { start, end, active } = u
|
||||||
|
const className = this.getHighlightClassName(active)
|
||||||
|
|
||||||
|
if (pos < start) {
|
||||||
|
result.push(text.substring(pos, start))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(h(`span.${className}`, text.substring(start, end)))
|
||||||
|
pos = end
|
||||||
|
}
|
||||||
|
if (pos < rEnd) {
|
||||||
|
result.push(block.text.substring(pos, rEnd))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = [ text.substring(rStart, rEnd) ]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
// render token of text type to vdom.
|
||||||
|
text (h, cursor, block, token) {
|
||||||
|
const { start, end } = token.range
|
||||||
|
return this.highlight(h, block, start, end, token)
|
||||||
|
}
|
||||||
|
// render token of emoji to vdom
|
||||||
emoji (h, cursor, block, token, outerClass) {
|
emoji (h, cursor, block, token, outerClass) {
|
||||||
|
const { start: rStart, end: rEnd } = token.range
|
||||||
const className = this.getClassName(outerClass, block, token, cursor)
|
const className = this.getClassName(outerClass, block, token, cursor)
|
||||||
const validation = validEmoji(token.content)
|
const validation = validEmoji(token.content)
|
||||||
const finalClass = validation ? className : CLASS_OR_ID['AG_WARN']
|
const finalClass = validation ? className : CLASS_OR_ID['AG_WARN']
|
||||||
|
const CONTENT_CLASSNAME = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`
|
||||||
|
let startMarkerCN = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`
|
||||||
|
let endMarkerCN = startMarkerCN
|
||||||
|
let content = token.content
|
||||||
|
let pos = rStart + token.marker.length
|
||||||
|
|
||||||
|
if (token.highlights && token.highlights.length) {
|
||||||
|
content = []
|
||||||
|
for (const light of token.highlights) {
|
||||||
|
let { start, end, active } = light
|
||||||
|
const HIGHLIGHT_CLASSNAME = this.getHighlightClassName(active)
|
||||||
|
if (start === rStart) {
|
||||||
|
startMarkerCN += `.${HIGHLIGHT_CLASSNAME}`
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
if (end === rEnd) {
|
||||||
|
endMarkerCN += `.${HIGHLIGHT_CLASSNAME}`
|
||||||
|
end--
|
||||||
|
}
|
||||||
|
if (pos < start) {
|
||||||
|
content.push(block.text.substring(pos, start))
|
||||||
|
}
|
||||||
|
if (start < end) {
|
||||||
|
content.push(h(`span.${HIGHLIGHT_CLASSNAME}`, block.text.substring(start, end)))
|
||||||
|
}
|
||||||
|
pos = end
|
||||||
|
}
|
||||||
|
if (pos < rEnd - token.marker.length) {
|
||||||
|
content.push(block.text.substring(pos, rEnd - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const emojiVdom = validation
|
const emojiVdom = validation
|
||||||
? h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, { dataset: { emoji: validation.emoji } }, token.content)
|
? h(CONTENT_CLASSNAME, {
|
||||||
: h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, token.content)
|
dataset: {
|
||||||
|
emoji: validation.emoji
|
||||||
|
}
|
||||||
|
}, content)
|
||||||
|
: h(CONTENT_CLASSNAME, content)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`, token.marker),
|
h(startMarkerCN, token.marker),
|
||||||
emojiVdom,
|
emojiVdom,
|
||||||
h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`, token.marker)
|
h(endMarkerCN, token.marker)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// render factory of `del`,`em`,`strong`
|
// render factory of `del`,`em`,`strong`
|
||||||
delEmStrongFac (type, h, cursor, block, token, outerClass) {
|
delEmStrongFac (type, h, cursor, block, token, outerClass) {
|
||||||
const className = this.getClassName(outerClass, block, token, cursor)
|
const className = this.getClassName(outerClass, block, token, cursor)
|
||||||
|
const COMMON_MARKER = `span.${className}.${CLASS_OR_ID['AG_REMOVE']}`
|
||||||
|
const { marker } = token
|
||||||
|
const { start, end } = token.range
|
||||||
|
const backlashStart = end - marker.length - token.backlash.length
|
||||||
|
const content = [
|
||||||
|
...token.children.reduce((acc, to) => {
|
||||||
|
const chunk = this[to.type](h, cursor, block, to, className)
|
||||||
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||||
|
}, []),
|
||||||
|
...this.backlashInToken(token.backlash, className, backlashStart, token)
|
||||||
|
]
|
||||||
|
const startMarker = this.highlight(h, block, start, start + marker.length, token)
|
||||||
|
const endMarker = this.highlight(h, block, end - marker.length, end, token)
|
||||||
|
|
||||||
if (isLengthEven(token.backlash)) {
|
if (isLengthEven(token.backlash)) {
|
||||||
return [
|
return [
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker),
|
h(COMMON_MARKER, startMarker),
|
||||||
h(type, [
|
h(type, content),
|
||||||
...token.children.reduce((acc, to) => {
|
h(COMMON_MARKER, endMarker)
|
||||||
const chunk = this[to.type](h, cursor, block, to, className)
|
|
||||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
||||||
}, []),
|
|
||||||
...this.backlashInToken(token.backlash, className)
|
|
||||||
]),
|
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker)
|
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
token.marker,
|
...startMarker,
|
||||||
...token.children.reduce((acc, to) => {
|
...content,
|
||||||
const chunk = this[to.type](h, cursor, block, to, className)
|
...endMarker
|
||||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
||||||
}, []),
|
|
||||||
...this.backlashInToken(token.backlash, className),
|
|
||||||
token.marker
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO HIGHLIGHT
|
||||||
backlashInToken (backlashes, outerClass) {
|
backlashInToken (backlashes, outerClass, start, token) {
|
||||||
|
const { highlights = [] } = token
|
||||||
const chunks = backlashes.split('')
|
const chunks = backlashes.split('')
|
||||||
const len = chunks.length
|
const len = chunks.length
|
||||||
const result = []
|
const result = []
|
||||||
let i
|
let i
|
||||||
|
|
||||||
for (i = 0; i < len; i++) {
|
for (i = 0; i < len; i++) {
|
||||||
|
const chunk = chunks[i]
|
||||||
|
const light = highlights.filter(light => union({ start: start + i, end: start + i + 1 }, light))
|
||||||
|
let selector = 'span'
|
||||||
|
if (light.length) {
|
||||||
|
const className = this.getHighlightClassName(light[0].active)
|
||||||
|
selector += `.${className}`
|
||||||
|
}
|
||||||
if (isEven(i)) {
|
if (isEven(i)) {
|
||||||
result.push(
|
result.push(
|
||||||
h(`span.${outerClass}`, chunks[i])
|
h(`${selector}.${outerClass}`, chunk)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
result.push(
|
result.push(
|
||||||
h(`span.${CLASS_OR_ID['AG_BACKLASH']}`, chunks[i])
|
h(`${selector}.${CLASS_OR_ID['AG_BACKLASH']}`, chunk)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// result.push(
|
|
||||||
// h(`span.${CLASS_OR_ID['AG_BUG']}`) // the extral a tag for fix bug
|
|
||||||
// )
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
// I dont want operate dom directly, is there any better method? need help!
|
// I dont want operate dom directly, is there any better method? need help!
|
||||||
@ -296,6 +406,23 @@ class StateRender {
|
|||||||
const className = this.getClassName(outerClass, block, token, cursor)
|
const className = this.getClassName(outerClass, block, token, cursor)
|
||||||
const imageClass = CLASS_OR_ID['AG_IMAGE_MARKED_TEXT']
|
const imageClass = CLASS_OR_ID['AG_IMAGE_MARKED_TEXT']
|
||||||
|
|
||||||
|
const {
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
} = token.range
|
||||||
|
const titleContent = this.highlight(h, block, start, start + 2 + token.title.length, token)
|
||||||
|
const srcContent = this.highlight(
|
||||||
|
h, block,
|
||||||
|
start + 2 + token.title.length + token.backlash.first.length,
|
||||||
|
start + 2 + token.title.length + token.backlash.first.length + 2 + token.src.length,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
const firstBracketContent = this.highlight(h, block, start, start + 2, token)
|
||||||
|
const lastBracketContent = this.highlight(h, block, end - 1, end, token)
|
||||||
|
|
||||||
|
const firstBacklashStart = start + 2 + token.title.length
|
||||||
|
const secondBacklashStart = end - 1 - token.backlash.second.length
|
||||||
|
|
||||||
if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
|
if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
|
||||||
let id
|
let id
|
||||||
let isSuccess
|
let isSuccess
|
||||||
@ -347,11 +474,11 @@ class StateRender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const children = [
|
const children = [
|
||||||
`,
|
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
|
||||||
')'
|
...lastBracketContent
|
||||||
]
|
]
|
||||||
|
|
||||||
return isSuccess
|
return isSuccess
|
||||||
@ -362,27 +489,29 @@ class StateRender {
|
|||||||
: [h(selector, children)]
|
: [h(selector, children)]
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
'
|
const chunk = this[to.type](h, cursor, block, to, className)
|
||||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||||
}, []),
|
}, []),
|
||||||
...this.backlashInToken(token.backlash.first, className),
|
...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
|
||||||
'](',
|
...srcContent,
|
||||||
token.src,
|
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
|
||||||
...this.backlashInToken(token.backlash.second, className),
|
...lastBracketContent
|
||||||
')'
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// render auto_link to vdom
|
||||||
['auto_link'] (h, cursor, block, token, outerClass) {
|
['auto_link'] (h, cursor, block, token, outerClass) {
|
||||||
|
const { start, end } = token.range
|
||||||
|
const content = this.highlight(h, block, start, end, token)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h('a', {
|
h('a', {
|
||||||
props: {
|
props: {
|
||||||
href: token.href
|
href: token.href
|
||||||
}
|
}
|
||||||
}, token.href)
|
}, content)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,24 +519,50 @@ class StateRender {
|
|||||||
link (h, cursor, block, token, outerClass) {
|
link (h, cursor, block, token, outerClass) {
|
||||||
const className = this.getClassName(outerClass, block, token, cursor)
|
const className = this.getClassName(outerClass, block, token, cursor)
|
||||||
const linkClassName = className === CLASS_OR_ID['AG_HIDE'] ? className : CLASS_OR_ID['AG_LINK_IN_BRACKET']
|
const linkClassName = className === CLASS_OR_ID['AG_HIDE'] ? className : CLASS_OR_ID['AG_LINK_IN_BRACKET']
|
||||||
|
const { start, end } = token.range
|
||||||
|
const firstMiddleBracket = this.highlight(h, block, start, start + 3, token)
|
||||||
|
|
||||||
|
const firstBracket = this.highlight(h, block, start, start + 1, token)
|
||||||
|
const middleBracket = this.highlight(
|
||||||
|
h, block,
|
||||||
|
start + 1 + token.anchor.length + token.backlash.first.length,
|
||||||
|
start + 1 + token.anchor.length + token.backlash.first.length + 2,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
const hrefContent = this.highlight(
|
||||||
|
h, block,
|
||||||
|
start + 1 + token.anchor.length + token.backlash.first.length + 2,
|
||||||
|
start + 1 + token.anchor.length + token.backlash.first.length + 2 + token.href.length,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
const middleHref = this.highlight(
|
||||||
|
h, block, start + 1 + token.anchor.length + token.backlash.first.length,
|
||||||
|
block, start + 1 + token.anchor.length + token.backlash.first.length + 2 + token.href.length,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
|
||||||
|
const lastBracket = this.highlight(h, block, end - 1, end, token)
|
||||||
|
|
||||||
|
const firstBacklashStart = start + 1 + token.anchor.length
|
||||||
|
const secondBacklashStart = end - 1 - token.backlash.second.length
|
||||||
|
|
||||||
if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
|
if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
|
||||||
if (!token.children.length && !token.backlash.first) { // no-text-link
|
if (!token.children.length && !token.backlash.first) { // no-text-link
|
||||||
return [
|
return [
|
||||||
h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, '[]('),
|
h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, firstMiddleBracket),
|
||||||
h(`a.${CLASS_OR_ID['AG_NOTEXT_LINK']}`, {
|
h(`a.${CLASS_OR_ID['AG_NOTEXT_LINK']}`, {
|
||||||
props: {
|
props: {
|
||||||
href: token.href + encodeURI(token.backlash.second)
|
href: token.href + encodeURI(token.backlash.second)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
token.href,
|
...hrefContent,
|
||||||
...this.backlashInToken(token.backlash.second, className)
|
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token)
|
||||||
]),
|
]),
|
||||||
h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, ')')
|
h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, lastBracket)
|
||||||
]
|
]
|
||||||
} else { // has children
|
} else { // has children
|
||||||
return [
|
return [
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, '['),
|
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, firstBracket),
|
||||||
h('a', {
|
h('a', {
|
||||||
dataset: {
|
dataset: {
|
||||||
href: token.href + encodeURI(token.backlash.second)
|
href: token.href + encodeURI(token.backlash.second)
|
||||||
@ -417,27 +572,27 @@ class StateRender {
|
|||||||
const chunk = this[to.type](h, cursor, block, to, className)
|
const chunk = this[to.type](h, cursor, block, to, className)
|
||||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||||
}, []),
|
}, []),
|
||||||
...this.backlashInToken(token.backlash.first, className)
|
...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token)
|
||||||
]),
|
]),
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, ']('),
|
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, middleBracket),
|
||||||
h(`span.${linkClassName}.${CLASS_OR_ID['AG_REMOVE']}`, [
|
h(`span.${linkClassName}.${CLASS_OR_ID['AG_REMOVE']}`, [
|
||||||
token.href,
|
...hrefContent,
|
||||||
...this.backlashInToken(token.backlash.second, className)
|
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token)
|
||||||
]),
|
]),
|
||||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, ')')
|
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, lastBracket)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
'[',
|
...firstBracket,
|
||||||
...token.children.reduce((acc, to) => {
|
...token.children.reduce((acc, to) => {
|
||||||
const chunk = this[to.type](h, cursor, block, to, className)
|
const chunk = this[to.type](h, cursor, block, to, className)
|
||||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||||
}, []),
|
}, []),
|
||||||
...this.backlashInToken(token.backlash.first, className),
|
...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
|
||||||
`](${token.href}`,
|
...middleHref,
|
||||||
...this.backlashInToken(token.backlash.second, className),
|
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
|
||||||
')'
|
...lastBracket
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { beginRules, inlineRules } from './rules'
|
import { beginRules, inlineRules } from './rules'
|
||||||
import { isLengthEven } from '../utils'
|
import { isLengthEven, union } from '../utils'
|
||||||
|
|
||||||
|
const CAN_NEST_RULES = ['strong', 'em', 'link', 'del', 'image'] // image can not nest but it has children
|
||||||
|
|
||||||
const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => {
|
const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => {
|
||||||
const tokens = []
|
const tokens = []
|
||||||
@ -59,10 +61,11 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => {
|
|||||||
content: '',
|
content: '',
|
||||||
range: {
|
range: {
|
||||||
start: pos,
|
start: pos,
|
||||||
end: pos + backTo[0].length
|
end: pos + backTo[1].length
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
pending += pending + backTo[2]
|
pending += pending + backTo[2]
|
||||||
|
pendingStartPos = pos + backTo[1].length
|
||||||
src = src.substring(backTo[0].length)
|
src = src.substring(backTo[0].length)
|
||||||
pos = pos + backTo[0].length
|
pos = pos + backTo[0].length
|
||||||
continue
|
continue
|
||||||
@ -186,11 +189,34 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => {
|
|||||||
return tokens
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tokenizer = src => {
|
export const tokenizer = (src, highlights = []) => {
|
||||||
return tokenizerFac(src, beginRules, inlineRules, 0)
|
const tokens = tokenizerFac(src, beginRules, inlineRules, 0)
|
||||||
|
const postTokenizer = tokens => {
|
||||||
|
for (const token of tokens) {
|
||||||
|
for (const light of highlights) {
|
||||||
|
const highlight = union(token.range, light)
|
||||||
|
if (highlight) {
|
||||||
|
if (token.highlights && Array.isArray(token.highlights)) {
|
||||||
|
token.highlights.push(highlight)
|
||||||
|
} else {
|
||||||
|
token.highlights = [highlight]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CAN_NEST_RULES.indexOf(token.type) > -1) {
|
||||||
|
postTokenizer(token.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (highlights.length) {
|
||||||
|
postTokenizer(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
// transform `tokens` to text ignore the range of token
|
// transform `tokens` to text ignore the range of token
|
||||||
|
// the opposite of tokenizer
|
||||||
export const generator = tokens => {
|
export const generator = tokens => {
|
||||||
let result = ''
|
let result = ''
|
||||||
const getBash = bash => bash !== undefined ? bash : ''
|
const getBash = bash => bash !== undefined ? bash : ''
|
||||||
|
@ -94,7 +94,7 @@ const importRegister = ContentState => {
|
|||||||
case 'h4':
|
case 'h4':
|
||||||
case 'h5':
|
case 'h5':
|
||||||
case 'h6':
|
case 'h6':
|
||||||
const textValue = child.childNodes[0].value
|
const textValue = child.childNodes.length ? child.childNodes[0].value : ''
|
||||||
const match = /\d/.exec(child.nodeName)
|
const match = /\d/.exec(child.nodeName)
|
||||||
value = match ? '#'.repeat(+match[0]) + textValue : textValue
|
value = match ? '#'.repeat(+match[0]) + textValue : textValue
|
||||||
block = this.createBlock(child.nodeName, value)
|
block = this.createBlock(child.nodeName, value)
|
||||||
|
@ -45,6 +45,25 @@ export const conflict = (arr1, arr2) => {
|
|||||||
return !(arr1[1] < arr2[0] || arr2[1] < arr1[0])
|
return !(arr1[1] < arr2[0] || arr2[1] < arr1[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const union = ({ start: tStart, end: tEnd }, { start: lStart, end: lEnd, active }) => {
|
||||||
|
if (!(tEnd <= lStart || lEnd <= tStart)) {
|
||||||
|
if (lStart < tStart) {
|
||||||
|
return {
|
||||||
|
start: tStart,
|
||||||
|
end: tEnd < lEnd ? tEnd : lEnd,
|
||||||
|
active
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
start: lStart,
|
||||||
|
end: tEnd < lEnd ? tEnd : lEnd,
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/jashkenas/underscore
|
// https://github.com/jashkenas/underscore
|
||||||
export const throttle = (func, wait = 50) => {
|
export const throttle = (func, wait = 50) => {
|
||||||
let context
|
let context
|
||||||
|
@ -32,5 +32,31 @@ export default {
|
|||||||
label: 'Select All',
|
label: 'Select All',
|
||||||
accelerator: 'CmdOrCtrl+A',
|
accelerator: 'CmdOrCtrl+A',
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
|
}, {
|
||||||
|
type: 'separator'
|
||||||
|
}, {
|
||||||
|
label: 'Find',
|
||||||
|
accelerator: 'CmdOrCtrl+F',
|
||||||
|
click: (menuItem, browserWindow) => {
|
||||||
|
actions.edit(browserWindow, 'find')
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Find Next',
|
||||||
|
accelerator: 'Alt+CmdOrCtrl+U',
|
||||||
|
click: (menuItem, browserWindow) => {
|
||||||
|
actions.edit(browserWindow, 'fineNext')
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'FindPrev',
|
||||||
|
accelerator: 'Shift+CmdOrCtrl+U',
|
||||||
|
click: (menuItem, browserWindow) => {
|
||||||
|
actions.edit(browserWindow, 'findPrev')
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Replace',
|
||||||
|
accelerator: 'Alt+CmdOrCtrl+F',
|
||||||
|
click: (menuItem, browserWindow) => {
|
||||||
|
actions.edit(browserWindow, 'replace')
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -6,19 +6,25 @@
|
|||||||
:word-count="wordCount"
|
:word-count="wordCount"
|
||||||
></title-bar>
|
></title-bar>
|
||||||
<editor></editor>
|
<editor></editor>
|
||||||
|
<search></search>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Editor from '@/components/editor'
|
import Editor from '@/components/editor'
|
||||||
import TitleBar from '@/components/titleBar'
|
import TitleBar from '@/components/titleBar'
|
||||||
|
import Search from '@/components/search'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'marktext',
|
name: 'marktext',
|
||||||
components: {
|
components: {
|
||||||
Editor,
|
Editor,
|
||||||
TitleBar
|
TitleBar,
|
||||||
|
Search
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['filename', 'windowActive', 'wordCount'])
|
...mapState(['filename', 'windowActive', 'wordCount'])
|
||||||
@ -33,7 +39,7 @@
|
|||||||
dispatch('GET_FILENAME')
|
dispatch('GET_FILENAME')
|
||||||
dispatch('LISTEN_FOR_FILE_LOAD')
|
dispatch('LISTEN_FOR_FILE_LOAD')
|
||||||
dispatch('LISTEN_FOR_FILE_CHANGE')
|
dispatch('LISTEN_FOR_FILE_CHANGE')
|
||||||
dispatch('LISTEN_FOR_UNDO_REDO')
|
dispatch('LISTEN_FOR_EDIT')
|
||||||
dispatch('LISTEN_FOR_EXPORT')
|
dispatch('LISTEN_FOR_EXPORT')
|
||||||
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
||||||
}
|
}
|
||||||
@ -43,5 +49,6 @@
|
|||||||
<style>
|
<style>
|
||||||
.editor-container {
|
.editor-container {
|
||||||
padding-top: 22px;
|
padding-top: 22px;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="editor-wrapper">
|
||||||
<div ref="editor" class="editor-component"></div>
|
<div ref="editor" class="editor-component"></div>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible.sync="dialogTableVisible"
|
:visible.sync="dialogTableVisible"
|
||||||
@ -79,6 +79,9 @@
|
|||||||
bus.$on('export', this.handleExport)
|
bus.$on('export', this.handleExport)
|
||||||
bus.$on('paragraph', this.handleEditParagraph)
|
bus.$on('paragraph', this.handleEditParagraph)
|
||||||
bus.$on('format', this.handleInlineFormat)
|
bus.$on('format', this.handleInlineFormat)
|
||||||
|
bus.$on('searchValue', this.handleSearch)
|
||||||
|
bus.$on('replaceValue', this.handReplace)
|
||||||
|
bus.$on('find', this.handleFind)
|
||||||
|
|
||||||
this.editor.on('change', (markdown, wordCount) => {
|
this.editor.on('change', (markdown, wordCount) => {
|
||||||
this.$store.dispatch('SAVE_FILE', { markdown, wordCount })
|
this.$store.dispatch('SAVE_FILE', { markdown, wordCount })
|
||||||
@ -92,6 +95,18 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleSearch (value, opt) {
|
||||||
|
const searchMatches = this.editor.search(value, opt)
|
||||||
|
this.$store.dispatch('SEARCH', searchMatches)
|
||||||
|
},
|
||||||
|
handReplace (value, opt) {
|
||||||
|
const searchMatches = this.editor.replace(value, opt)
|
||||||
|
this.$store.dispatch('SEARCH', searchMatches)
|
||||||
|
},
|
||||||
|
handleFind (action) {
|
||||||
|
const searchMatches = this.editor.find(action)
|
||||||
|
this.$store.dispatch('SEARCH', searchMatches)
|
||||||
|
},
|
||||||
async handleExport (type) {
|
async handleExport (type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'styledHtml': {
|
case 'styledHtml': {
|
||||||
@ -155,6 +170,8 @@
|
|||||||
bus.$off('file-loaded', this.handleFileLoaded)
|
bus.$off('file-loaded', this.handleFileLoaded)
|
||||||
bus.$off('export-styled-html', this.handleExport('styledHtml'))
|
bus.$off('export-styled-html', this.handleExport('styledHtml'))
|
||||||
bus.$off('paragraph', this.handleEditParagraph)
|
bus.$off('paragraph', this.handleEditParagraph)
|
||||||
|
bus.$off('searchValue', this.handleSearch)
|
||||||
|
bus.$off('find', this.handleFind)
|
||||||
this.editor = null
|
this.editor = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,8 +180,11 @@
|
|||||||
<style>
|
<style>
|
||||||
@import '../../editor/themes/light.css';
|
@import '../../editor/themes/light.css';
|
||||||
@import '../../editor/index.css';
|
@import '../../editor/index.css';
|
||||||
.editor-component {
|
.editor-wrapper {
|
||||||
height: calc(100vh - 22px);
|
height: calc(100vh - 22px);
|
||||||
|
}
|
||||||
|
.editor-component {
|
||||||
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.v-modal {
|
.v-modal {
|
||||||
|
@ -1,13 +1,254 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-bar">
|
<div class="search-bar"
|
||||||
|
@click.stop="noop"
|
||||||
|
v-show="showSearch"
|
||||||
|
>
|
||||||
|
<section class="search">
|
||||||
|
<el-tooltip class="item"
|
||||||
|
effect="dark"
|
||||||
|
content="Replacement"
|
||||||
|
placement="top"
|
||||||
|
:visible-arrow="false"
|
||||||
|
:open-delay="1000"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
v-if="type !== 'replace'"
|
||||||
|
@click="typeClick"
|
||||||
|
>
|
||||||
|
<svg class="icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-findreplace"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip class="item"
|
||||||
|
effect="dark"
|
||||||
|
content="Case sensitive"
|
||||||
|
placement="top"
|
||||||
|
:visible-arrow="false"
|
||||||
|
:open-delay="1000"
|
||||||
|
>
|
||||||
|
<button class="button" @click="caseClick" :class="{ 'active': caseSensitive }">
|
||||||
|
<svg class="icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-case"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="searchValue"
|
||||||
|
@keyup="search($event)"
|
||||||
|
ref="search"
|
||||||
|
placeholder="Search"
|
||||||
|
>
|
||||||
|
<span class="search-result">{{`${highlightIndex + 1} / ${highlightCount}`}}</span>
|
||||||
|
</div>
|
||||||
|
<button class="button" @click="find('prev')">
|
||||||
|
<svg class="icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-arrow-up"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="button" @click="find('next')">
|
||||||
|
<svg class="icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-arrowdown"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
<section class="replace" v-if="type === 'replace'">
|
||||||
|
<button class="button active" @click="typeClick">
|
||||||
|
<svg class="icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-findreplace"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="input-wrapper replace-input">
|
||||||
|
<input type="text" v-model="replaceValue" placeholder="Replacement">
|
||||||
|
</div>
|
||||||
|
<button class="button" @click="replace(false)">
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<button class="button" @click="replace(true)">
|
||||||
|
Replace
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import bus from '../bus'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showSearch: false,
|
||||||
|
type: 'search',
|
||||||
|
searchValue: '',
|
||||||
|
replaceValue: '',
|
||||||
|
caseSensitive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
searchMatches: function (newValue, oldValue) {
|
||||||
|
if (!newValue || !oldValue) return
|
||||||
|
const { value } = newValue
|
||||||
|
if (value && value !== oldValue.value) {
|
||||||
|
this.searchValue = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['searchMatches']),
|
||||||
|
highlightIndex () {
|
||||||
|
if (this.searchMatches) {
|
||||||
|
return this.searchMatches.index
|
||||||
|
} else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlightCount () {
|
||||||
|
if (this.searchMatches) {
|
||||||
|
return this.searchMatches.matches.length
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
bus.$on('find', () => {
|
||||||
|
this.showSearch = true
|
||||||
|
this.type = 'search'
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.search.focus()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
bus.$on('replace', () => {
|
||||||
|
this.showSearch = true
|
||||||
|
this.type = 'replace'
|
||||||
|
})
|
||||||
|
bus.$on('findNext', () => {
|
||||||
|
this.find('next')
|
||||||
|
})
|
||||||
|
bus.$on('findPrev', () => {
|
||||||
|
this.find('prev')
|
||||||
|
})
|
||||||
|
document.addEventListener('click', this.docClick)
|
||||||
|
document.addEventListener('keyup', this.docKeyup)
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
document.removeEventListener('click', this.docClick)
|
||||||
|
document.removeEventListener('keyup', this.docKeyup)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
docKeyup (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.emitSearch(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
docClick (isSelect) {
|
||||||
|
if (!this.showSearch) return
|
||||||
|
this.emitSearch()
|
||||||
|
},
|
||||||
|
emitSearch (selectHighlight = false) {
|
||||||
|
this.showSearch = false
|
||||||
|
this.searchValue = ''
|
||||||
|
this.replaceValue = ''
|
||||||
|
bus.$emit('searchValue', this.searchValue, { selectHighlight })
|
||||||
|
},
|
||||||
|
caseClick () {
|
||||||
|
this.caseSensitive = !this.caseSensitive
|
||||||
|
},
|
||||||
|
typeClick () {
|
||||||
|
this.type = this.type === 'search' ? 'replace' : 'search'
|
||||||
|
},
|
||||||
|
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') {
|
||||||
|
const { caseSensitive } = this
|
||||||
|
bus.$emit('searchValue', this.searchValue, { caseSensitive })
|
||||||
|
} else {
|
||||||
|
this.find('next')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replace (isSingle = true) {
|
||||||
|
const { caseSensitive, replaceValue } = this
|
||||||
|
bus.$emit('replaceValue', replaceValue, { caseSensitive, isSingle })
|
||||||
|
},
|
||||||
|
noop () {}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.search-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgb(245, 245, 245);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.search, .replace {
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.search-bar .button {
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 30px;
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 3px 5px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.button.active {
|
||||||
|
color: rgb(242, 134, 94);
|
||||||
|
}
|
||||||
|
.search-bar .button > svg {
|
||||||
|
width: 1.6em;
|
||||||
|
height: 1.6em;
|
||||||
|
}
|
||||||
|
.search-bar .button:hover {
|
||||||
|
background: #EBEEF5;
|
||||||
|
}
|
||||||
|
.search-bar .button:active {
|
||||||
|
background: #DCDFE6;
|
||||||
|
}
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.input-wrapper .search-result {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #C0C4CC;
|
||||||
|
}
|
||||||
|
.input-wrapper input {
|
||||||
|
flex: 1;
|
||||||
|
height: 30px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -48,6 +48,7 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.title-bar {
|
.title-bar {
|
||||||
|
background: rgb(252, 252, 252);
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -7,12 +7,13 @@ import store from './store'
|
|||||||
import './assets/symbolIcon'
|
import './assets/symbolIcon'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
import { Dialog, Form, FormItem, InputNumber, Button } from 'element-ui'
|
import { Dialog, Form, FormItem, InputNumber, Button, Tooltip } from 'element-ui'
|
||||||
Vue.use(Dialog)
|
Vue.use(Dialog)
|
||||||
Vue.use(Form)
|
Vue.use(Form)
|
||||||
Vue.use(FormItem)
|
Vue.use(FormItem)
|
||||||
Vue.use(InputNumber)
|
Vue.use(InputNumber)
|
||||||
Vue.use(Button)
|
Vue.use(Button)
|
||||||
|
Vue.use(Tooltip)
|
||||||
|
|
||||||
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
|
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
|
||||||
Vue.http = Vue.prototype.$http = axios
|
Vue.http = Vue.prototype.$http = axios
|
||||||
|
@ -4,6 +4,11 @@ import bus from '../bus'
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
filename: 'Untitled - unsaved',
|
filename: 'Untitled - unsaved',
|
||||||
|
searchMatches: {
|
||||||
|
index: -1,
|
||||||
|
matches: [],
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
pathname: '',
|
pathname: '',
|
||||||
isSaved: true,
|
isSaved: true,
|
||||||
markdown: '',
|
markdown: '',
|
||||||
@ -17,6 +22,9 @@ const state = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
SET_SEARCH (state, value) {
|
||||||
|
state.searchMatches = value
|
||||||
|
},
|
||||||
SET_WIN_STATUS (state, status) {
|
SET_WIN_STATUS (state, status) {
|
||||||
state.windowActive = status
|
state.windowActive = status
|
||||||
},
|
},
|
||||||
@ -39,6 +47,9 @@ const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
SEARCH ({ commit }, value) {
|
||||||
|
commit('SET_SEARCH', value)
|
||||||
|
},
|
||||||
LINTEN_WIN_STATUS ({ commit }) {
|
LINTEN_WIN_STATUS ({ commit }) {
|
||||||
ipcRenderer.on('AGANI::window-active-status', (e, { status }) => {
|
ipcRenderer.on('AGANI::window-active-status', (e, { status }) => {
|
||||||
commit('SET_WIN_STATUS', status)
|
commit('SET_WIN_STATUS', status)
|
||||||
@ -103,6 +114,16 @@ const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
SELECTION_CHANGE ({ commit }, changes) {
|
SELECTION_CHANGE ({ commit }, changes) {
|
||||||
|
const { start, end } = changes
|
||||||
|
if (start.key === end.key && start.block.text) {
|
||||||
|
const value = start.block.text.substring(start.offset, end.offset)
|
||||||
|
commit('SET_SEARCH', {
|
||||||
|
matches: [],
|
||||||
|
index: -1,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
ipcRenderer.send('AGANI::selection-change', changes)
|
ipcRenderer.send('AGANI::selection-change', changes)
|
||||||
},
|
},
|
||||||
SELECTION_FORMATS ({ commit }, formats) {
|
SELECTION_FORMATS ({ commit }, formats) {
|
||||||
@ -114,7 +135,7 @@ const actions = {
|
|||||||
bus.$emit('export', type)
|
bus.$emit('export', type)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
LISTEN_FOR_UNDO_REDO ({ commit }) {
|
LISTEN_FOR_EDIT ({ commit }) {
|
||||||
ipcRenderer.on('AGANI::edit', (e, { type }) => {
|
ipcRenderer.on('AGANI::edit', (e, { type }) => {
|
||||||
bus.$emit(type)
|
bus.$emit(type)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user