feat: link autocomplete + insert link modal (markdown)
This commit is contained in:
parent
245104c6ae
commit
76ade8df53
@ -10,6 +10,7 @@
|
|||||||
v-icon.mr-3(color='white') mdi-page-next-outline
|
v-icon.mr-3(color='white') mdi-page-next-outline
|
||||||
.body-1(v-if='mode === `create`') Select New Page Location
|
.body-1(v-if='mode === `create`') Select New Page Location
|
||||||
.body-1(v-else-if='mode === `move`') Move / Rename Page Location
|
.body-1(v-else-if='mode === `move`') Move / Rename Page Location
|
||||||
|
.body-1(v-else-if='mode === `select`') Select Page
|
||||||
v-spacer
|
v-spacer
|
||||||
v-progress-circular(
|
v-progress-circular(
|
||||||
indeterminate
|
indeterminate
|
||||||
|
@ -109,7 +109,7 @@
|
|||||||
.editor-markdown-sidebar
|
.editor-markdown-sidebar
|
||||||
v-tooltip(right, color='teal')
|
v-tooltip(right, color='teal')
|
||||||
template(v-slot:activator='{ on }')
|
template(v-slot:activator='{ on }')
|
||||||
v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, disabled).mx-0
|
v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, @click='insertLink').mx-0
|
||||||
v-icon mdi-link-plus
|
v-icon mdi-link-plus
|
||||||
span {{$t('editor:markup.insertLink')}}
|
span {{$t('editor:markup.insertLink')}}
|
||||||
v-tooltip(right, color='teal')
|
v-tooltip(right, color='teal')
|
||||||
@ -130,7 +130,7 @@
|
|||||||
v-tooltip(right, color='teal')
|
v-tooltip(right, color='teal')
|
||||||
template(v-slot:activator='{ on }')
|
template(v-slot:activator='{ on }')
|
||||||
v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, disabled).mx-0
|
v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, disabled).mx-0
|
||||||
v-icon mdi-library-video
|
v-icon mdi-movie
|
||||||
span {{$t('editor:markup.insertVideoAudio')}}
|
span {{$t('editor:markup.insertVideoAudio')}}
|
||||||
v-tooltip(right, color='teal')
|
v-tooltip(right, color='teal')
|
||||||
template(v-slot:activator='{ on }')
|
template(v-slot:activator='{ on }')
|
||||||
@ -176,14 +176,16 @@
|
|||||||
.caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}
|
.caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}
|
||||||
|
|
||||||
markdown-help(v-if='helpShown')
|
markdown-help(v-if='helpShown')
|
||||||
|
page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale')
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { get, sync } from 'vuex-pathify'
|
import { get, sync } from 'vuex-pathify'
|
||||||
import markdownHelp from './markdown/help.vue'
|
import markdownHelp from './markdown/help.vue'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
/* global siteConfig */
|
/* global siteConfig, siteLangs */
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// IMPORTS
|
// IMPORTS
|
||||||
@ -202,6 +204,7 @@ import 'codemirror/addon/display/fullscreen.js'
|
|||||||
import 'codemirror/addon/display/fullscreen.css'
|
import 'codemirror/addon/display/fullscreen.css'
|
||||||
import 'codemirror/addon/selection/mark-selection.js'
|
import 'codemirror/addon/selection/mark-selection.js'
|
||||||
import 'codemirror/addon/search/searchcursor.js'
|
import 'codemirror/addon/search/searchcursor.js'
|
||||||
|
import 'codemirror/addon/hint/show-hint.js'
|
||||||
|
|
||||||
// Markdown-it
|
// Markdown-it
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
@ -353,7 +356,8 @@ export default {
|
|||||||
cursorPos: { ch: 0, line: 1 },
|
cursorPos: { ch: 0, line: 1 },
|
||||||
previewShown: true,
|
previewShown: true,
|
||||||
previewHTML: '',
|
previewHTML: '',
|
||||||
helpShown: false
|
helpShown: false,
|
||||||
|
insertLinkDialog: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -544,6 +548,72 @@ export default {
|
|||||||
mmElm.innerHTML = `<div id="mermaid-id-${mermaidId}">${mermaid.render(`mermaid-id-${mermaidId}`, mermaidDef)}</div>`
|
mmElm.innerHTML = `<div id="mermaid-id-${mermaidId}">${mermaid.render(`mermaid-id-${mermaidId}`, mermaidDef)}</div>`
|
||||||
elm.parentElement.replaceWith(mmElm)
|
elm.parentElement.replaceWith(mmElm)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
autocomplete (cm, change) {
|
||||||
|
if (cm.getModeAt(cm.getCursor()).name !== 'markdown') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links
|
||||||
|
if (change.text[0] === '(') {
|
||||||
|
const curLine = cm.getLine(change.from.line).substring(0, change.from.ch)
|
||||||
|
if (curLine[curLine.length - 1] === ']') {
|
||||||
|
cm.showHint({
|
||||||
|
hint: async (cm, options) => {
|
||||||
|
const cur = cm.getCursor()
|
||||||
|
const token = cm.getTokenAt(cur)
|
||||||
|
try {
|
||||||
|
const respRaw = await this.$apollo.query({
|
||||||
|
query: gql`
|
||||||
|
query ($query: String!, $locale: String) {
|
||||||
|
pages {
|
||||||
|
search(query:$query, locale:$locale) {
|
||||||
|
results {
|
||||||
|
title
|
||||||
|
path
|
||||||
|
locale
|
||||||
|
}
|
||||||
|
totalHits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
query: token.string,
|
||||||
|
locale: this.locale
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-first'
|
||||||
|
})
|
||||||
|
const resp = _.get(respRaw, 'data.pages.search', {})
|
||||||
|
if (resp && resp.totalHits > 0) {
|
||||||
|
return {
|
||||||
|
list: resp.results.map(r => ({
|
||||||
|
text: (siteLangs.length > 0 ? `/${r.locale}/${r.path}` : `/${r.path}`) + ')',
|
||||||
|
displayText: siteLangs.length > 0 ? `/${r.locale}/${r.path} - ${r.title}` : `/${r.path} - ${r.title}`
|
||||||
|
})),
|
||||||
|
from: CodeMirror.Pos(cur.line, token.start),
|
||||||
|
to: CodeMirror.Pos(cur.line, token.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
return {
|
||||||
|
list: [],
|
||||||
|
from: CodeMirror.Pos(cur.line, token.start),
|
||||||
|
to: CodeMirror.Pos(cur.line, token.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insertLink () {
|
||||||
|
this.insertLinkDialog = true
|
||||||
|
},
|
||||||
|
insertLinkHandler ({ locale, path }) {
|
||||||
|
const lastPart = _.last(path.split('/'))
|
||||||
|
this.insertAtCursor({
|
||||||
|
content: siteLangs.length > 0 ? `[${lastPart}](/${locale}/${path})` : `[${lastPart}](/${path})`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -624,6 +694,8 @@ export default {
|
|||||||
})
|
})
|
||||||
this.cm.setOption('extraKeys', keyBindings)
|
this.cm.setOption('extraKeys', keyBindings)
|
||||||
|
|
||||||
|
this.cm.on('inputRead', this.autocomplete)
|
||||||
|
|
||||||
// Handle cursor movement
|
// Handle cursor movement
|
||||||
|
|
||||||
this.cm.on('cursorActivity', c => {
|
this.cm.on('cursorActivity', c => {
|
||||||
@ -921,6 +993,43 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HINT DROPDOWN
|
||||||
|
|
||||||
|
.CodeMirror-hints {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
|
||||||
|
border: 1px solid mc('grey', '700');
|
||||||
|
|
||||||
|
background: mc('grey', '900');
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: .9rem;
|
||||||
|
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-hint {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
white-space: pre;
|
||||||
|
color: #FFF;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.CodeMirror-hint-active {
|
||||||
|
background: mc('blue', '500');
|
||||||
|
color: #FFF;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -34,9 +34,11 @@ module.exports = {
|
|||||||
if (WIKI.config.db.type === 'postgres') {
|
if (WIKI.config.db.type === 'postgres') {
|
||||||
builderSub.where('title', 'ILIKE', `%${q}%`)
|
builderSub.where('title', 'ILIKE', `%${q}%`)
|
||||||
builderSub.orWhere('description', 'ILIKE', `%${q}%`)
|
builderSub.orWhere('description', 'ILIKE', `%${q}%`)
|
||||||
|
builderSub.orWhere('path', 'ILIKE', `%${q.toLowerCase()}%`)
|
||||||
} else {
|
} else {
|
||||||
builderSub.where('title', 'LIKE', `%${q}%`)
|
builderSub.where('title', 'LIKE', `%${q}%`)
|
||||||
builderSub.orWhere('description', 'LIKE', `%${q}%`)
|
builderSub.orWhere('description', 'LIKE', `%${q}%`)
|
||||||
|
builderSub.orWhere('path', 'LIKE', `%${q.toLowerCase()}%`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -60,12 +60,26 @@ module.exports = {
|
|||||||
async query(q, opts) {
|
async query(q, opts) {
|
||||||
try {
|
try {
|
||||||
let suggestions = []
|
let suggestions = []
|
||||||
const results = await WIKI.models.knex.raw(`
|
let qry = `
|
||||||
SELECT id, path, locale, title, description
|
SELECT id, path, locale, title, description
|
||||||
FROM "pagesVector", to_tsquery(?,?) query
|
FROM "pagesVector", to_tsquery(?,?) query
|
||||||
WHERE query @@ "tokens"
|
WHERE (query @@ "tokens" OR path ILIKE ?)
|
||||||
ORDER BY ts_rank(tokens, query) DESC
|
`
|
||||||
`, [this.config.dictLanguage, tsquery(q)])
|
let qryEnd = `ORDER BY ts_rank(tokens, query) DESC`
|
||||||
|
let qryParams = [this.config.dictLanguage, tsquery(q), `%${q.toLowerCase()}%`]
|
||||||
|
|
||||||
|
if (opts.locale) {
|
||||||
|
qry = `${qry} AND locale = ?`
|
||||||
|
qryParams.push(opts.locale)
|
||||||
|
}
|
||||||
|
if (opts.path) {
|
||||||
|
qry = `${qry} AND path ILIKE ?`
|
||||||
|
qryParams.push(`%${opts.path}`)
|
||||||
|
}
|
||||||
|
const results = await WIKI.models.knex.raw(`
|
||||||
|
${qry}
|
||||||
|
${qryEnd}
|
||||||
|
`, qryParams)
|
||||||
if (results.rows.length < 5) {
|
if (results.rows.length < 5) {
|
||||||
const suggestResults = await WIKI.models.knex.raw(`SELECT word, word <-> ? AS rank FROM "pagesWords" WHERE similarity(word, ?) > 0.2 ORDER BY rank LIMIT 5;`, [q, q])
|
const suggestResults = await WIKI.models.knex.raw(`SELECT word, word <-> ? AS rank FROM "pagesWords" WHERE similarity(word, ?) > 0.2 ORDER BY rank LIMIT 5;`, [q, q])
|
||||||
suggestions = suggestResults.rows.map(r => r.word)
|
suggestions = suggestResults.rows.map(r => r.word)
|
||||||
|
Loading…
Reference in New Issue
Block a user