mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 00:32:32 +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**
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#### TODO LIST
|
||||
|
||||
- [ ] Support Search and Replacement.
|
||||
- [x] Support Search and Replacement.
|
||||
|
||||
- [ ] add Dark, Light and GitHub theme.
|
||||
|
||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marktext",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marktext",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.0",
|
||||
"author": "Jocs <luoran1988@126.com>",
|
||||
"description": "A markdown editor",
|
||||
"license": "MIT",
|
||||
|
@ -80,7 +80,9 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
|
||||
'AG_TASK_LIST_ITEM',
|
||||
'AG_TASK_LIST_ITEM_CHECKBOX',
|
||||
'AG_CHECKBOX_CHECKED',
|
||||
'AG_TABLE_TOOL_BAR'
|
||||
'AG_TABLE_TOOL_BAR',
|
||||
'AG_SELECTION',
|
||||
'AG_HIGHLIGHT'
|
||||
])
|
||||
|
||||
export const codeMirrorConfig = {
|
||||
|
@ -15,6 +15,7 @@ import copyCutCtrl from './copyCutCtrl'
|
||||
import paragraphCtrl from './paragraphCtrl'
|
||||
import tabCtrl from './tabCtrl'
|
||||
import formatCtrl from './formatCtrl'
|
||||
import searchCtrl from './searchCtrl'
|
||||
import importMarkdown from '../utils/importMarkdown'
|
||||
|
||||
const prototypes = [
|
||||
@ -31,6 +32,7 @@ const prototypes = [
|
||||
tableBlockCtrl,
|
||||
paragraphCtrl,
|
||||
formatCtrl,
|
||||
searchCtrl,
|
||||
importMarkdown
|
||||
]
|
||||
|
||||
@ -61,6 +63,11 @@ class ContentState {
|
||||
|
||||
init () {
|
||||
const lastBlock = this.getLastBlock()
|
||||
this.searchMatches = {
|
||||
value: '',
|
||||
matches: [],
|
||||
index: -1
|
||||
}
|
||||
this.cursor = {
|
||||
start: {
|
||||
key: lastBlock.key,
|
||||
@ -84,10 +91,12 @@ class ContentState {
|
||||
}
|
||||
|
||||
render (isRenderCursor = true) {
|
||||
const { blocks, cursor } = this
|
||||
const { blocks, cursor, searchMatches: { matches, index } } = this
|
||||
const activeBlocks = this.getActiveBlocks()
|
||||
|
||||
this.stateRender.render(blocks, cursor, activeBlocks)
|
||||
matches.forEach((m, i) => {
|
||||
m.active = i === index
|
||||
})
|
||||
this.stateRender.render(blocks, cursor, activeBlocks, matches)
|
||||
if (isRenderCursor) this.setCursor()
|
||||
this.pre2CodeMirror()
|
||||
console.log('render')
|
||||
|
@ -25,7 +25,9 @@ const paragraphCtrl = ContentState => {
|
||||
.filter(p => PARAGRAPH_TYPES.includes(p.type))
|
||||
|
||||
start.type = startBlock.type
|
||||
start.block = startBlock
|
||||
end.type = endBlock.type
|
||||
end.block = endBlock
|
||||
|
||||
return {
|
||||
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,
|
||||
h2.ag-active::before,
|
||||
h3.ag-active::before,
|
||||
@ -13,16 +25,24 @@ h6.ag-active::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -25px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #C0C4CC;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: #ddd;
|
||||
color: #C0C4CC;
|
||||
transform: scale(.7);
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
*::selection {
|
||||
background: #efefef;
|
||||
*::selection, .ag-selection {
|
||||
background: #E4E7ED;
|
||||
}
|
||||
|
||||
.ag-highlight {
|
||||
animation-name: highlight;
|
||||
animation-duration: .25s;
|
||||
display: inline-block;
|
||||
background: rgb(249, 226, 153);
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
figure {
|
||||
@ -242,7 +262,7 @@ pre.ag-active .ag-language-input {
|
||||
caret-color: #303133;
|
||||
}
|
||||
.ag-gray {
|
||||
color: lavender;
|
||||
color: #C0C4CC;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -262,7 +282,8 @@ pre.ag-active .ag-language-input {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ag-hide {
|
||||
|
||||
.ag-hide, .ag-hide .ag-highlight, .ag-hide .ag-selection {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
@ -412,6 +412,31 @@ class Aganippe {
|
||||
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) {
|
||||
const { eventCenter } = this
|
||||
eventCenter.subscribe(event, listener)
|
||||
|
@ -1,9 +1,12 @@
|
||||
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 { tokenizer } from './parse'
|
||||
import { validEmoji } from '../emojis'
|
||||
|
||||
// for test
|
||||
window.tokenizer = tokenizer
|
||||
|
||||
const snabbdom = require('snabbdom')
|
||||
const patch = snabbdom.init([ // Init patch function with chosen modules
|
||||
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'])
|
||||
}
|
||||
|
||||
getHighlightClassName (active) {
|
||||
return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
|
||||
}
|
||||
|
||||
/**
|
||||
* [render]: 2 steps:
|
||||
* render vdom
|
||||
*/
|
||||
render (blocks, cursor, activeBlocks) {
|
||||
render (blocks, cursor, activeBlocks, matches) {
|
||||
const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
||||
|
||||
const renderBlock = block => {
|
||||
@ -115,8 +122,10 @@ class StateRender {
|
||||
|
||||
return h(blockSelector, data, block.children.map(child => renderBlock(child)))
|
||||
} else {
|
||||
// highlight search key in block
|
||||
const highlights = matches.filter(m => m.key === block.key)
|
||||
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)
|
||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||
}, [])
|
||||
@ -184,111 +193,212 @@ class StateRender {
|
||||
}
|
||||
|
||||
hr (h, cursor, block, token, outerClass) {
|
||||
const { start, end } = token.range
|
||||
const content = this.highlight(h, block, start, end, token)
|
||||
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) {
|
||||
const className = this.getClassName(outerClass, block, token, cursor)
|
||||
const { start, end } = token.range
|
||||
const content = this.highlight(h, block, start, end, token)
|
||||
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) {
|
||||
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 [
|
||||
h(`span.${CLASS_OR_ID['AG_GRAY']}`, token.marker),
|
||||
h(`span.${CLASS_OR_ID['AG_LANGUAGE']}`, token.content)
|
||||
h(`span.${CLASS_OR_ID['AG_GRAY']}`, markerContent),
|
||||
h(`span.${CLASS_OR_ID['AG_LANGUAGE']}`, content)
|
||||
]
|
||||
}
|
||||
|
||||
backlash (h, cursor, block, token, outerClass) {
|
||||
const className = this.getClassName(outerClass, block, token, cursor)
|
||||
const { start, end } = token.range
|
||||
const content = this.highlight(h, block, start, end, token)
|
||||
|
||||
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) {
|
||||
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 [
|
||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker),
|
||||
h('code', token.content),
|
||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker)
|
||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, startMarker),
|
||||
h('code', content),
|
||||
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) {
|
||||
return token.content
|
||||
if (highlights) {
|
||||
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) {
|
||||
const { start: rStart, end: rEnd } = token.range
|
||||
const className = this.getClassName(outerClass, block, token, cursor)
|
||||
const validation = validEmoji(token.content)
|
||||
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
|
||||
? h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, { dataset: { emoji: validation.emoji } }, token.content)
|
||||
: h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, token.content)
|
||||
? h(CONTENT_CLASSNAME, {
|
||||
dataset: {
|
||||
emoji: validation.emoji
|
||||
}
|
||||
}, content)
|
||||
: h(CONTENT_CLASSNAME, content)
|
||||
|
||||
return [
|
||||
h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`, token.marker),
|
||||
h(startMarkerCN, token.marker),
|
||||
emojiVdom,
|
||||
h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`, token.marker)
|
||||
h(endMarkerCN, token.marker)
|
||||
]
|
||||
}
|
||||
|
||||
// render factory of `del`,`em`,`strong`
|
||||
delEmStrongFac (type, h, cursor, block, token, outerClass) {
|
||||
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)) {
|
||||
return [
|
||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker),
|
||||
h(type, [
|
||||
...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)
|
||||
]),
|
||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker)
|
||||
h(COMMON_MARKER, startMarker),
|
||||
h(type, content),
|
||||
h(COMMON_MARKER, endMarker)
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
token.marker,
|
||||
...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),
|
||||
token.marker
|
||||
...startMarker,
|
||||
...content,
|
||||
...endMarker
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
backlashInToken (backlashes, outerClass) {
|
||||
// TODO HIGHLIGHT
|
||||
backlashInToken (backlashes, outerClass, start, token) {
|
||||
const { highlights = [] } = token
|
||||
const chunks = backlashes.split('')
|
||||
const len = chunks.length
|
||||
const result = []
|
||||
let 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)) {
|
||||
result.push(
|
||||
h(`span.${outerClass}`, chunks[i])
|
||||
h(`${selector}.${outerClass}`, chunk)
|
||||
)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
// 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 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)) {
|
||||
let id
|
||||
let isSuccess
|
||||
@ -347,11 +474,11 @@ class StateRender {
|
||||
}
|
||||
|
||||
const children = [
|
||||
`,
|
||||
')'
|
||||
...titleContent,
|
||||
...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
|
||||
...srcContent,
|
||||
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
|
||||
...lastBracketContent
|
||||
]
|
||||
|
||||
return isSuccess
|
||||
@ -362,27 +489,29 @@ class StateRender {
|
||||
: [h(selector, children)]
|
||||
} else {
|
||||
return [
|
||||
'
|
||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||
}, []),
|
||||
...this.backlashInToken(token.backlash.first, className),
|
||||
'](',
|
||||
token.src,
|
||||
...this.backlashInToken(token.backlash.second, className),
|
||||
')'
|
||||
...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
|
||||
...srcContent,
|
||||
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
|
||||
...lastBracketContent
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// render auto_link to vdom
|
||||
['auto_link'] (h, cursor, block, token, outerClass) {
|
||||
const { start, end } = token.range
|
||||
const content = this.highlight(h, block, start, end, token)
|
||||
|
||||
return [
|
||||
h('a', {
|
||||
props: {
|
||||
href: token.href
|
||||
}
|
||||
}, token.href)
|
||||
}, content)
|
||||
]
|
||||
}
|
||||
|
||||
@ -390,24 +519,50 @@ class StateRender {
|
||||
link (h, cursor, block, token, outerClass) {
|
||||
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 { 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 (!token.children.length && !token.backlash.first) { // no-text-link
|
||||
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']}`, {
|
||||
props: {
|
||||
href: token.href + encodeURI(token.backlash.second)
|
||||
}
|
||||
}, [
|
||||
token.href,
|
||||
...this.backlashInToken(token.backlash.second, className)
|
||||
...hrefContent,
|
||||
...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
|
||||
return [
|
||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, '['),
|
||||
h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, firstBracket),
|
||||
h('a', {
|
||||
dataset: {
|
||||
href: token.href + encodeURI(token.backlash.second)
|
||||
@ -417,27 +572,27 @@ class StateRender {
|
||||
const chunk = this[to.type](h, cursor, block, to, className)
|
||||
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']}`, [
|
||||
token.href,
|
||||
...this.backlashInToken(token.backlash.second, className)
|
||||
...hrefContent,
|
||||
...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 {
|
||||
return [
|
||||
'[',
|
||||
...firstBracket,
|
||||
...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.first, className),
|
||||
`](${token.href}`,
|
||||
...this.backlashInToken(token.backlash.second, className),
|
||||
')'
|
||||
...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
|
||||
...middleHref,
|
||||
...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
|
||||
...lastBracket
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
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 tokens = []
|
||||
@ -59,10 +61,11 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => {
|
||||
content: '',
|
||||
range: {
|
||||
start: pos,
|
||||
end: pos + backTo[0].length
|
||||
end: pos + backTo[1].length
|
||||
}
|
||||
})
|
||||
pending += pending + backTo[2]
|
||||
pendingStartPos = pos + backTo[1].length
|
||||
src = src.substring(backTo[0].length)
|
||||
pos = pos + backTo[0].length
|
||||
continue
|
||||
@ -186,11 +189,34 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => {
|
||||
return tokens
|
||||
}
|
||||
|
||||
export const tokenizer = src => {
|
||||
return tokenizerFac(src, beginRules, inlineRules, 0)
|
||||
export const tokenizer = (src, highlights = []) => {
|
||||
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
|
||||
// the opposite of tokenizer
|
||||
export const generator = tokens => {
|
||||
let result = ''
|
||||
const getBash = bash => bash !== undefined ? bash : ''
|
||||
|
@ -94,7 +94,7 @@ const importRegister = ContentState => {
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
const textValue = child.childNodes[0].value
|
||||
const textValue = child.childNodes.length ? child.childNodes[0].value : ''
|
||||
const match = /\d/.exec(child.nodeName)
|
||||
value = match ? '#'.repeat(+match[0]) + textValue : textValue
|
||||
block = this.createBlock(child.nodeName, value)
|
||||
|
@ -45,6 +45,25 @@ export const conflict = (arr1, arr2) => {
|
||||
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
|
||||
export const throttle = (func, wait = 50) => {
|
||||
let context
|
||||
|
@ -32,5 +32,31 @@ export default {
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
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"
|
||||
></title-bar>
|
||||
<editor></editor>
|
||||
<search></search>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Editor from '@/components/editor'
|
||||
import TitleBar from '@/components/titleBar'
|
||||
import Search from '@/components/search'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'marktext',
|
||||
components: {
|
||||
Editor,
|
||||
TitleBar
|
||||
TitleBar,
|
||||
Search
|
||||
},
|
||||
data () {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['filename', 'windowActive', 'wordCount'])
|
||||
@ -33,7 +39,7 @@
|
||||
dispatch('GET_FILENAME')
|
||||
dispatch('LISTEN_FOR_FILE_LOAD')
|
||||
dispatch('LISTEN_FOR_FILE_CHANGE')
|
||||
dispatch('LISTEN_FOR_UNDO_REDO')
|
||||
dispatch('LISTEN_FOR_EDIT')
|
||||
dispatch('LISTEN_FOR_EXPORT')
|
||||
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
||||
}
|
||||
@ -43,5 +49,6 @@
|
||||
<style>
|
||||
.editor-container {
|
||||
padding-top: 22px;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="editor-wrapper">
|
||||
<div ref="editor" class="editor-component"></div>
|
||||
<el-dialog
|
||||
:visible.sync="dialogTableVisible"
|
||||
@ -79,6 +79,9 @@
|
||||
bus.$on('export', this.handleExport)
|
||||
bus.$on('paragraph', this.handleEditParagraph)
|
||||
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.$store.dispatch('SAVE_FILE', { markdown, wordCount })
|
||||
@ -92,6 +95,18 @@
|
||||
})
|
||||
},
|
||||
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) {
|
||||
switch (type) {
|
||||
case 'styledHtml': {
|
||||
@ -155,6 +170,8 @@
|
||||
bus.$off('file-loaded', this.handleFileLoaded)
|
||||
bus.$off('export-styled-html', this.handleExport('styledHtml'))
|
||||
bus.$off('paragraph', this.handleEditParagraph)
|
||||
bus.$off('searchValue', this.handleSearch)
|
||||
bus.$off('find', this.handleFind)
|
||||
this.editor = null
|
||||
}
|
||||
}
|
||||
@ -163,8 +180,11 @@
|
||||
<style>
|
||||
@import '../../editor/themes/light.css';
|
||||
@import '../../editor/index.css';
|
||||
.editor-component {
|
||||
.editor-wrapper {
|
||||
height: calc(100vh - 22px);
|
||||
}
|
||||
.editor-component {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.v-modal {
|
||||
|
@ -1,13 +1,254 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="title-bar"
|
||||
:class="{'active': active}"
|
||||
:class="{ 'active': active }"
|
||||
>
|
||||
<div class="title">
|
||||
<img src="../assets/icons/markdown.svg" v-if="filename">
|
||||
@ -48,6 +48,7 @@
|
||||
|
||||
<style scoped>
|
||||
.title-bar {
|
||||
background: rgb(252, 252, 252);
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
|
@ -7,12 +7,13 @@ import store from './store'
|
||||
import './assets/symbolIcon'
|
||||
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(Form)
|
||||
Vue.use(FormItem)
|
||||
Vue.use(InputNumber)
|
||||
Vue.use(Button)
|
||||
Vue.use(Tooltip)
|
||||
|
||||
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
|
||||
Vue.http = Vue.prototype.$http = axios
|
||||
|
@ -4,6 +4,11 @@ import bus from '../bus'
|
||||
|
||||
const state = {
|
||||
filename: 'Untitled - unsaved',
|
||||
searchMatches: {
|
||||
index: -1,
|
||||
matches: [],
|
||||
value: ''
|
||||
},
|
||||
pathname: '',
|
||||
isSaved: true,
|
||||
markdown: '',
|
||||
@ -17,6 +22,9 @@ const state = {
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_SEARCH (state, value) {
|
||||
state.searchMatches = value
|
||||
},
|
||||
SET_WIN_STATUS (state, status) {
|
||||
state.windowActive = status
|
||||
},
|
||||
@ -39,6 +47,9 @@ const mutations = {
|
||||
}
|
||||
|
||||
const actions = {
|
||||
SEARCH ({ commit }, value) {
|
||||
commit('SET_SEARCH', value)
|
||||
},
|
||||
LINTEN_WIN_STATUS ({ commit }) {
|
||||
ipcRenderer.on('AGANI::window-active-status', (e, { status }) => {
|
||||
commit('SET_WIN_STATUS', status)
|
||||
@ -103,6 +114,16 @@ const actions = {
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
SELECTION_FORMATS ({ commit }, formats) {
|
||||
@ -114,7 +135,7 @@ const actions = {
|
||||
bus.$emit('export', type)
|
||||
})
|
||||
},
|
||||
LISTEN_FOR_UNDO_REDO ({ commit }) {
|
||||
LISTEN_FOR_EDIT ({ commit }) {
|
||||
ipcRenderer.on('AGANI::edit', (e, { type }) => {
|
||||
bus.$emit(type)
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user