Add autocomplete of page links
continuous-integration/drone/push Build is passing Details

single-block-api
Peter Stuifzand 4 years ago
parent 7956df26b4
commit f2cc16341d

@ -91,10 +91,18 @@ func loadBackrefs(fp *FilePages, p string) (map[string][]Backref, error) {
links := renderLinks(strings.TrimLeft(ref.Link.Line, " *"))
pageText := renderMarkdown2(links)
removeBrackets := func(r rune) rune {
if r == '[' || r == ']' || r == '*' {
return -1
}
return r
}
result[ref.Name] = append(result[ref.Name], Backref{
Name: ref.Name,
Title: title,
Line: template.HTML(pageText),
Name: ref.Name,
Title: title,
LineHTML: template.HTML(pageText),
Line: strings.Map(removeBrackets, ref.Link.Line),
})
}

@ -1093,6 +1093,14 @@
"del": "^4.1.1"
}
},
"cli": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/cli/-/cli-0.4.3.tgz",
"integrity": "sha1-5oGcjV+qlX9k+Y9mqFBiaMHR8X0=",
"requires": {
"glob": ">= 3.1.4"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@ -1141,6 +1149,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -2935,6 +2948,24 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"fuse": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/fuse/-/fuse-0.4.0.tgz",
"integrity": "sha1-LDjq+IirsKm6eWDP4zOdHz9T9uY=",
"requires": {
"colors": ">=0.6.x",
"fuse.js": "^6.0.0",
"jshint": "0.9.x",
"optimist": ">=0.3.5",
"uglify-js": ">=2.2.x",
"underscore": ">=1.4.x"
}
},
"fuse.js": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.0.0.tgz",
"integrity": "sha512-e5Ap6mhF/WQ9bKqsMFTTR5/DS9qbYab4VXHtMdxCanH+VZkdUV2LqcgMO31etSQv53NXsguQF1bdqkrrPAM2HQ=="
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@ -3857,6 +3888,30 @@
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"jshint": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/jshint/-/jshint-0.9.1.tgz",
"integrity": "sha1-/zLsfwn4QAH3SY7q/WPJ5Puy3A4=",
"requires": {
"cli": "0.4.3",
"minimatch": "0.0.x"
},
"dependencies": {
"lru-cache": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-1.0.6.tgz",
"integrity": "sha1-qlD5cEdCKsclQ72hd6nJ0BjZhFI="
},
"minimatch": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.0.5.tgz",
"integrity": "sha1-lrtJC707poNrv6wRGt91MBsVhN4=",
"requires": {
"lru-cache": "~1.0.2"
}
}
}
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@ -4744,6 +4799,22 @@
"is-wsl": "^1.1.0"
}
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
},
"dependencies": {
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
}
}
},
"original": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
@ -6892,7 +6963,6 @@
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
"integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==",
"dev": true,
"requires": {
"commander": "~2.19.0",
"source-map": "~0.6.1"
@ -6901,17 +6971,20 @@
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
"dev": true
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"underscore": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
"integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg=="
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -7538,15 +7611,20 @@
}
},
"wiki-list-editor": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/wiki-list-editor/-/wiki-list-editor-0.6.4.tgz",
"integrity": "sha512-UkCrS62HR6/Slk2jggxQZryoe3YEbBPrceJ76sl6a4YPni/xWCajLGl/qLsUemQMwyoVI7ApAmLE9GvJeLLPyQ==",
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/wiki-list-editor/-/wiki-list-editor-0.6.5.tgz",
"integrity": "sha512-1/XQ7JSJoet/TCq5RiF0JAHFZYveI3fEW8drc6gPnAfwz4e2t7vHBZV5sDhFO+z4fD+uISfsBveW4oJzVNteNA==",
"requires": {
"dragula": "^3.7.2",
"jquery": "^3.5.1",
"lodash": "^4.17.15"
}
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
},
"worker-farm": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",

@ -12,13 +12,15 @@
"bulma": "^0.7.5",
"css-loader": "^3.2.0",
"file-loader": "^6.0.0",
"fuse": "^0.4.0",
"fuse.js": "^6.0.0",
"jquery-contextmenu": "^2.9.2",
"lunr": "^2.3.8",
"mustache": "^4.0.1",
"node-sass": "^4.14.1",
"sass-loader": "^7.3.1",
"style-loader": "^1.0.0",
"wiki-list-editor": "^0.6.4"
"wiki-list-editor": "^0.6.5"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",

@ -0,0 +1,145 @@
// We'll copy the properties below into the mirror div.
// Note that some browsers, such as Firefox, do not concatenate properties
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
// so we have to list every single property explicitly.
var properties = [
'direction', // RTL support
'boxSizing',
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
'height',
'overflowX',
'overflowY', // copy the scrollbar for IE
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
'borderStyle',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
'fontStyle',
'fontVariant',
'fontWeight',
'fontStretch',
'fontSize',
'fontSizeAdjust',
'lineHeight',
'fontFamily',
'textAlign',
'textTransform',
'textIndent',
'textDecoration', // might not make a difference, but better be safe
'letterSpacing',
'wordSpacing',
'tabSize',
'MozTabSize'
];
var isBrowser = (typeof window !== 'undefined');
var isFirefox = (isBrowser && window.mozInnerScreenX != null);
function getCaretCoordinates(element, position, options) {
if (!isBrowser) {
throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
}
var debug = options && options.debug || false;
if (debug) {
var el = document.querySelector('#input-textarea-caret-position-mirror-div');
if (el) el.parentNode.removeChild(el);
}
// The mirror div will replicate the textarea's style
var div = document.createElement('div');
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild(div);
var style = div.style;
var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
var isInput = element.nodeName === 'INPUT';
// Default textarea styles
style.whiteSpace = 'pre-wrap';
if (!isInput)
style.wordWrap = 'break-word'; // only for textarea-s
// Position off-screen
style.position = 'absolute'; // required to return coordinates properly
if (!debug)
style.visibility = 'hidden'; // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach(function (prop) {
if (isInput && prop === 'lineHeight') {
// Special case for <input>s because text is rendered centered and line height may be != height
if (computed.boxSizing === "border-box") {
var height = parseInt(computed.height);
var outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
var targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) {
style.lineHeight = height - outerHeight + "px";
} else if (height === targetHeight) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = 0;
}
} else {
style.lineHeight = computed.height;
}
} else {
style[prop] = computed[prop];
}
});
if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > parseInt(computed.height))
style.overflowY = 'scroll';
} else {
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (isInput)
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
var span = document.createElement('span');
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
div.appendChild(span);
var coordinates = {
top: span.offsetTop + parseInt(computed['borderTopWidth']),
left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
height: parseInt(computed['lineHeight'])
};
if (debug) {
span.style.backgroundColor = '#aaa';
} else {
document.body.removeChild(div);
}
return coordinates;
}
export default getCaretCoordinates;

@ -0,0 +1,20 @@
import Fuse from 'fuse.js'
import $ from 'jquery'
function createTitleSearch() {
return new Promise(function (resolve, reject) {
$.get('/links.json', function (documents) {
const options = {
keys: ['title'],
}
let fuse = new Fuse(documents, options)
resolve({
documents,
search: fuse,
})
})
})
}
export default createTitleSearch

@ -3,8 +3,10 @@ import axios from 'axios';
import qs from 'querystring'
import $ from 'jquery';
import search from './search';
import createPageSearch from './fuse';
import Mustache from 'mustache';
import 'jquery-contextmenu';
import getCaretCoordinates from './caret-position'
import './styles.scss';
import '../node_modules/jquery-contextmenu/dist/jquery.contextMenu.css';
@ -75,6 +77,110 @@ if (holder) {
).save()
})
$(document).on('keydown', '#link-complete', function (event) {
if (!$('#link-complete:visible')) {
return true;
}
const $popup = $('#link-complete')
if (event.key === 'Escape') {
$popup.fadeOut()
return false;
}
if (event.key === 'Enter') {
const linkName = $popup.find('li.selected').text()
$popup.trigger('popup:selected', [linkName])
$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')
}
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')
}
return false
}
return true
})
createPageSearch().then(function ({search}) {
editor.on('start-editing', function (input) {
const $lc = $('#link-complete')
$lc.on('popup:selected', function (event, linkName) {
let value = input.value
let end = input.selectionEnd
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()
return true
})
$(input).on('keydown', function (event) {
const isVisible = $('#link-complete:visible').length > 0;
const $lc = $('#link-complete')
if (event.key === 'Escape' && isVisible) {
$lc.fadeOut()
return true
}
if (event.key === 'ArrowDown' && isVisible) {
$lc.focus()
$lc.find('li:first-child').addClass('selected')
return false
}
return true
})
$(input).on('keyup', function () {
let value = input.value
let end = input.selectionEnd
let start = value.lastIndexOf("[[", end)
let linkEnd = value.lastIndexOf("]]", end - 1)
if (start < linkEnd) {
return
}
let query = value.substring(start, end);
let results = search.search(query)
let pos = getCaretCoordinates(input, value.selectionEnd, {})
let off = $(input).offset()
pos.top += off.top + pos.height
pos.left += off.left
const $lc = $('#link-complete')
$lc.offset(pos)
var template = document.getElementById('link-template').innerHTML;
var rendered = Mustache.render(template, {results: results}, {}, ['[[', ']]']);
$lc.html(rendered).fadeIn()
})
})
editor.on('stop-editing', function (input) {
$('#link-complete').off()
})
})
$.contextMenu({
selector: '.marker',
items: {

@ -30,9 +30,10 @@ var (
)
type Backref struct {
Name string
Title string
Line template.HTML
Name string
Title string
LineHTML template.HTML
Line string
}
// Page
@ -618,6 +619,27 @@ func main() {
mp = NewFilePages("data")
http.Handle("/auth/", &authHandler{})
http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) {
type Document struct {
Title string `json:"title"`
}
var results []Document
pages, err := mp.(*FilePages).AllPages()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
for _, page := range pages {
results = append(results, Document{page.Title})
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&results)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
})
http.HandleFunc("/documents.json", func(w http.ResponseWriter, r *http.Request) {
type Document struct {
Title string `json:"title"`
@ -632,26 +654,36 @@ func main() {
return
}
for _, page := range pages {
content := page.Content
content := strings.Builder{}
var listItems []struct {
Indented int
Text string
}
err = json.NewDecoder(strings.NewReader(content)).Decode(&listItems)
err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
if err == nil {
pageText := ""
for _, item := range listItems {
pageText += strings.Repeat(" ", item.Indented) + "* " + item.Text + "\n"
content.WriteString(item.Text)
content.WriteByte(' ')
}
} else {
content.WriteString(page.Content)
content.WriteByte(' ')
}
content = pageText
for page, refs := range page.Refs {
content.WriteString(page)
content.WriteByte(' ')
for _, ref := range refs {
content.WriteString(ref.Line)
content.WriteByte(' ')
}
}
results = append(results, Document{
Title: page.Title,
Body: content,
Body: content.String(),
URL: page.Name,
})
}

@ -17,7 +17,7 @@
<li><a href="/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.Line }}</li>
<li>{{ $ref.LineHTML }}</li>
{{ end }}
</ul>
</li>

@ -25,6 +25,24 @@
#autocomplete li > a {
white-space: nowrap;
}
#link-complete {
z-index: 1;
width: 217px;
overflow-x: hidden;
overflow-y: auto;
height: 300px;
position: absolute;
background: white;
border: 1px solid #ccc;
outline: none;
}
#link-complete li {
padding: 4px 16px;
}
#link-complete li.selected {
background: lightblue;
}
.monospace {
font-family: "Fira Code Retina", monospace;
@ -142,6 +160,9 @@
border-bottom-color: #ccc;
}
.hide {
display: none;
}
.selected {
background: lightblue;
@ -249,12 +270,13 @@
&mdash; created by <a href="https://peterstuifzand.nl/">Peter Stuifzand</a>
</div>
<div id="save-indicator" class="hidden"></div>
<div id="save-indicator" class="hide"></div>
</div>
<div id="link-complete" class="hide" tabindex="0"></div>
{{ block "footer_scripts" . }}
{{ end }}
<script src="/public/index.bundle.js"></script>
<div id="result-template" class="hidden">
<div id="result-template" class="hide">
<ul>
[[#results]]
<li><a href="/[[ref]]">[[title]]</a></li>
@ -262,5 +284,12 @@
<ll><a href="/edit/[[page]]">Create a page</a></ll>
</ul>
</div>
<div id="link-template" class="hide">
<ul>
[[#results]]
<li>[[item.title]]</li>
[[/results]]
</ul>
</div>
</body>
</html>

@ -11,7 +11,7 @@
<li><a href="/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.Line }}</li>
<li>{{ $ref.LineHTML }}</li>
{{ end }}
</ul>
</li>

Loading…
Cancel
Save