wiki/editor/src/index.js
2020-06-11 22:20:03 +02:00

392 lines
13 KiB
JavaScript

import listEditor from 'wiki-list-editor';
import MarkdownIt from 'markdown-it';
import MarkdownItWikilinks from './wikilinks';
import MarkdownItMark from 'markdown-it-mark';
import axios from 'axios';
import qs from 'querystring'
import $ from 'jquery';
import search from './search';
import createPageSearch from './fuse';
import util from './util';
import Mustache from 'mustache';
import 'jquery-contextmenu';
import getCaretCoordinates from './caret-position'
import moment from 'moment'
import PrismJS from 'prismjs'
import 'prismjs/components/prism-php'
import 'prismjs/components/prism-markup-templating'
import './styles.scss';
import '../node_modules/jquery-contextmenu/dist/jquery.contextMenu.css';
moment.locale('nl')
function isMultiline(input) {
return input.value.startsWith("```", 0)
}
function addSaver(editor, saveUrl, page, beforeSave) {
return {
save() {
return editor.save().then(outputData => {
beforeSave()
let data = {
'json': 1,
'p': page,
'summary': "",
'content': JSON.stringify(outputData),
};
return axios.post(saveUrl, qs.encode(data))
})
}
}
}
function Indicator(element, timeout) {
let timeoutId;
return {
done() {
timeoutId = setTimeout(() => {
element.classList.add('hidden')
}, timeout * 1000);
},
setText(text) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
element.innerText = text;
element.classList.remove('hidden')
},
}
}
function addIndicator(editor, indicator) {
return {
save() {
editor.save().then(() => {
indicator.setText('saved!');
indicator.done();
})
}
}
}
let holder = document.getElementById('editor');
function showSearchResults(searchTool, query, input, value, resultType) {
showSearchResultsExtended('#link-complete', 'link-template', searchTool, query, input, value, resultType, {belowCursor: true})
}
function showSearchResultsExtended(element, template, searchTool, query, input, value, resultType, options) {
const $lc = $(element)
let results = searchTool(query)
if (query.length === 0 || !results.length) {
$lc.fadeOut()
return
}
let opt = options || {};
$lc.data('result-type', resultType)
if (opt.belowCursor) {
let pos = getCaretCoordinates(input, value.selectionEnd, {})
let off = $(input).offset()
pos.top += off.top + pos.height
pos.left += off.left
$lc.offset(pos)
}
var templateText = document.getElementById(template).innerHTML;
var rendered = Mustache.render(templateText, {page: value, results: results}, {}, ['[[', ']]']);
$lc.html(rendered).fadeIn()
}
$(document).on('keydown', '.keyboard-list', function (event) {
console.log('keydown .keyboard-list')
if (!$(':visible', this).length) {
return true
}
const $popup = $(this)
if (event.key === 'Escape') {
$popup.fadeOut()
return false
}
if (event.key === 'Enter') {
const element = $popup.find('li.selected')
const linkName = element.text()
$popup.trigger('popup:selected', [linkName, $popup.data('result-type'), element])
$popup.fadeOut()
return false
}
if (event.key === 'ArrowUp') {
const selected = $popup.find('li.selected')
const prev = selected.prev('li')
if (prev.length) {
prev.addClass('selected')
selected.removeClass('selected')
prev[0].scrollIntoView({block: 'center', inline: 'nearest'})
} else {
// move back from dropdown to input
$popup.trigger('popup:leave')
selected.removeClass('selected')
}
return false
}
if (event.key === 'ArrowDown') {
const selected = $popup.find('li.selected')
const next = selected.next('li');
if (next.length) {
next.addClass('selected')
selected.removeClass('selected')
next[0].scrollIntoView({block: 'center', inline: 'nearest'})
}
return false
}
return true
})
$(document).on('keydown', '#search-input', function (event) {
console.log('keydown #search-input')
let $ac = $('#autocomplete');
let isVisible = $(':visible', $ac)
if (event.key === 'ArrowDown' && isVisible) {
$ac.focus()
$ac.find('li:first-child').addClass('selected')
return false
}
if (event.key === 'Escape') {
$(searchInput).val('');
$ac.fadeOut();
return false;
}
return true
})
$(document).on('popup:selected', '#autocomplete', function (event, linkName, resultType, element) {
if (resultType === 'search-result') {
element.find('a')[0].click()
return false;
}
})
if (holder) {
console.log(holder.dataset)
const MD = new MarkdownIt()
MD.use(MarkdownItWikilinks({
baseURL: holder.dataset.baseUrl,
uriSuffix: '',
relativeBaseURL: '/edit/',
htmlAttributes: {
class: 'wiki-link'
}
})).use(MarkdownItMark)
const options = {
transform(text, callback) {
let converted = (text.startsWith("```", 0)) ? MD.render(text) : MD.renderInline(text)
return callback(converted)
}
}
let editor = listEditor(holder, JSON.parse(holder.dataset.input), options);
editor.on('change', function () {
let element = document.getElementById('editor');
let indicator = Indicator(document.getElementById('save-indicator'), 2);
let saveUrl = element.dataset.saveurl;
let page = element.dataset.page;
indicator.setText('has changes...');
addIndicator(
addSaver(editor, saveUrl, page, () => indicator.setText('saving...')),
indicator
).save()
})
createPageSearch().then(function ({titleSearch, commandSearch, commands}) {
editor.on('start-editing', function (input) {
const $lc = $('#link-complete');
$(input).parents('.list-item').addClass('active');
$lc.on('popup:selected', function (event, linkName, resultType, element) {
let value = input.value
let end = input.selectionEnd
if (resultType === 'link') {
let start = value.lastIndexOf("[[", end)
end += 2
let startAndLink = value.substring(0, start) + "[[" + linkName + "]]"
input.value = startAndLink + value.substring(end)
input.selectionStart = startAndLink.length
input.selectionEnd = startAndLink.length
input.focus()
} else if (resultType === 'command') {
let start = value.lastIndexOf("/", end)
let commandResult = ""
let replace = ""
let prefix = false
let adjustment = 0
let now = moment()
if (linkName === "Current Time") {
commandResult = now.format('HH:mm')
} else if (linkName === "Today") {
commandResult = "[[" + now.format('D MMMM YYYY') + "]]"
} else if (linkName === "Tomorrow") {
commandResult = "[[" + now.add(1, 'day').format('D MMMM YYYY') + "]]"
} else if (linkName === "Yesterday") {
commandResult = "[[" + now.add(-1, 'day').format('D MMMM YYYY') + "]]"
} else if (linkName === "TODO") {
commandResult = "#[[TODO]] "
replace = "#[[DONE]] "
prefix = true
} else if (linkName === "DONE") {
commandResult = "#[[DONE]] "
replace = "#[[TODO]] "
prefix = true
} else if (linkName === "Page Reference") {
commandResult = "[[]]"
adjustment = -2
} else if (linkName === "Code Block") {
commandResult = "```\n\n```"
adjustment = -5
}
let startAndLink = prefix
? commandResult + value.substring(0, start).replace(replace, "")
: value.substring(0, start) + commandResult
input.value = startAndLink + value.substring(end)
input.selectionStart = startAndLink.length + adjustment
input.selectionEnd = startAndLink.length + adjustment
input.focus()
}
return true
})
$lc.on('popup:leave', function (event) {
input.focus()
})
$(input).on('keydown', function (event) {
const isVisible = $('#link-complete:visible').length > 0;
if (event.key === 'Escape' && isVisible) {
$lc.fadeOut()
return false
}
if (event.key === 'ArrowDown' && isVisible) {
$lc.focus()
$lc.find('li:first-child').addClass('selected')
return false
}
let mirror = {
'[': ']',
'=': '=',
}
if (!isMultiline(input) && mirror.hasOwnProperty(event.key)) {
let input = this
let val = input.value
let prefix = val.substring(0, input.selectionStart)
let selection = val.substring(input.selectionStart, input.selectionEnd)
let suffix = val.substring(input.selectionEnd)
input.value = prefix + event.key + selection + mirror[event.key] + suffix
input.selectionStart = prefix.length + event.key.length
input.selectionEnd = input.selectionStart + selection.length
$(input).trigger('input')
return false;
}
return true
})
$(input).on('keyup', function () {
let value = input.value
let end = input.selectionEnd
let [start, insideLink] = util.cursorInsideLink(value, end)
console.log(value, end, start, insideLink)
let insideSearch = false
if (!insideLink) {
start = value.lastIndexOf("/", end)
insideSearch = start >= 0
}
if (insideSearch) {
let query = value.substring(start + 1, end);
showSearchResults(query => commandSearch.search(query), query, input, value, 'command');
return true
} else if (insideLink) {
let query = value.substring(start + 2, end);
showSearchResults(query => titleSearch.search(query), query, input, value, 'link');
return true
} else {
$('#link-complete').fadeOut();
}
})
})
editor.on('stop-editing', function (input) {
$(input).parents('.list-item').removeClass('active');
$('#link-complete').off()
PrismJS.highlightAll()
})
})
$.contextMenu({
selector: '.marker',
items: {
copy: {
name: 'Copy',
callback: function (key, opt) {
editor.copy(this).then(result => {
console.log(result)
})
}
}
}
});
}
let timeout = null;
let searchInput = document.getElementById('search-input');
search(searchInput).then(searcher => {
$(document).on('keyup', '#search-input', function (event) {
clearTimeout(timeout);
timeout = setTimeout(function () {
let query = $(searchInput).val()
if (query === '') {
let autocomplete = document.getElementById('autocomplete');
$(autocomplete).hide()
return false
}
showSearchResultsExtended('#autocomplete', 'result-template', query => searcher.search(query), query, searchInput, query, 'search-result')
// $('#autocomplete').show()
// let result = searcher.search(query)
// const newpage = query.replace(/\s+/g, '_')
// var template = document.getElementById('result-template').innerHTML;
// var rendered = Mustache.render(template, {page: newpage, results: result}, {}, ['[[', ']]']);
// let autocomplete = document.getElementById('autocomplete');
// autocomplete.innerHTML = rendered;
}, 200)
return true
})
})