Start work on tag suggestions
Start working on getting tag suggestions for pasteing and editing pages.
This commit is contained in:
parent
67de40c02b
commit
896f452fa6
|
@ -0,0 +1,105 @@
|
||||||
|
console.log("Hello, world!");
|
||||||
|
|
||||||
|
tag_suggestion_list = {
|
||||||
|
list_element: null,
|
||||||
|
suggestion_elements: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
var lel = document.createElement('div');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stolen from medium.com/@jh3y
|
||||||
|
* returns x, y coordinates for absolute positioning of a span within a given text input
|
||||||
|
* at a given selection point
|
||||||
|
* @param {object} input - the input element to obtain coordinates for
|
||||||
|
* @param {number} selectionPoint - the selection point for the input
|
||||||
|
*/
|
||||||
|
function getCursorXY(input, selectionPoint){
|
||||||
|
const {
|
||||||
|
offsetLeft: inputX,
|
||||||
|
offsetTop: inputY,
|
||||||
|
} = input
|
||||||
|
// create a dummy element that will be a clone of our input
|
||||||
|
const div = document.createElement('div')
|
||||||
|
// get the computed style of the input and clone it onto the dummy element
|
||||||
|
const copyStyle = getComputedStyle(input)
|
||||||
|
for (const prop of copyStyle) {
|
||||||
|
div.style[prop] = copyStyle[prop]
|
||||||
|
}
|
||||||
|
// we need a character that will replace whitespace when filling our dummy element if it's a single line <input/>
|
||||||
|
const swap = '.'
|
||||||
|
const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
|
||||||
|
// set the div content to that of the textarea up until selection
|
||||||
|
const textContent = inputValue.substr(0, selectionPoint)
|
||||||
|
// set the text content of the dummy element div
|
||||||
|
div.textContent = textContent
|
||||||
|
if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
|
||||||
|
// if a single line input then the div needs to be single line and not break out like a text area
|
||||||
|
if (input.tagName === 'INPUT') div.style.width = 'auto'
|
||||||
|
// create a marker element to obtain caret position
|
||||||
|
const span = document.createElement('span')
|
||||||
|
// give the span the textContent of remaining content so that the recreated dummy element is as close as possible
|
||||||
|
span.textContent = inputValue.substr(selectionPoint) || '.'
|
||||||
|
// append the span marker to the div
|
||||||
|
div.appendChild(span)
|
||||||
|
// append the dummy element to the body
|
||||||
|
document.body.appendChild(div)
|
||||||
|
// get the marker position, this is the caret position top and left relative to the input
|
||||||
|
const { offsetLeft: spanX, offsetTop: spanY } = span
|
||||||
|
// lastly, remove that dummy element
|
||||||
|
// NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered
|
||||||
|
document.body.removeChild(div)
|
||||||
|
// return an object with the x and y of the caret. account for input positioning so that you don't need to wrap the input
|
||||||
|
return {
|
||||||
|
x: inputX + spanX,
|
||||||
|
y: inputY + spanY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_suggestions(elem, sugg, event){
|
||||||
|
console.log("sugg:",sugg);
|
||||||
|
//Check that the value hasn't change since we fired
|
||||||
|
//off the request
|
||||||
|
recent = elem.value.split(";").pop().trim();
|
||||||
|
if(recent == sugg[0]){
|
||||||
|
var sugx, sugy = getCursorXY(elem,elem.value.length);
|
||||||
|
console.log("Looking at position to display suggestions:",sugx, sugy);
|
||||||
|
for(var i in sugg){
|
||||||
|
console.log("Displaying suggestion:",sugg[i]);
|
||||||
|
lel.setAttribute('style',`left: $(sugx)px; top: $(sugy)px;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hint_tags(elem, event){
|
||||||
|
//Get the most recent tag
|
||||||
|
recent = elem.value.split(";").pop().trim();
|
||||||
|
if(recent.length > 0){
|
||||||
|
console.log("Most recent tag:",recent);
|
||||||
|
//Ask the server for tags that look like this
|
||||||
|
xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/_api?call=suggest&data=" + recent);
|
||||||
|
xhr.onreadystatechange = function(e){
|
||||||
|
if(xhr.readyState === 4){
|
||||||
|
console.log("Event:",e);
|
||||||
|
suggestions = xhr.response.split(";");
|
||||||
|
console.log("suggestions:",suggestions);
|
||||||
|
display_suggestions(elem,suggestions, event);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
tag_el_list = document.getElementsByName("tags");
|
||||||
|
console.assert(tag_el_list.length == 1);
|
||||||
|
tag_el = tag_el_list[0];
|
||||||
|
tag_el.onkeyup = function(event){
|
||||||
|
console.log("Looking at tag:", event);
|
||||||
|
console.log("And element:",tag_el);
|
||||||
|
hint_tags(tag_el, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("DOMContentLoaded",init,false);
|
|
@ -43,6 +43,7 @@ domain * {
|
||||||
route /_css/milligram.css asset_serve_milligram_css
|
route /_css/milligram.css asset_serve_milligram_css
|
||||||
route /_css/milligram.min.css.map asset_serve_milligram_min_css_map
|
route /_css/milligram.min.css.map asset_serve_milligram_min_css_map
|
||||||
route /_faq asset_serve_faq_html
|
route /_faq asset_serve_faq_html
|
||||||
|
route /_js/suggest_tags.js asset_serve_suggest_tags_js
|
||||||
route /favicon.ico asset_serve_favicon_ico
|
route /favicon.ico asset_serve_favicon_ico
|
||||||
route /_paste post_story
|
route /_paste post_story
|
||||||
route /_edit edit_story
|
route /_edit edit_story
|
||||||
|
@ -54,6 +55,7 @@ domain * {
|
||||||
route /_preview preview
|
route /_preview preview
|
||||||
route /_search search
|
route /_search search
|
||||||
route /_archive archive
|
route /_archive archive
|
||||||
|
route /_api api
|
||||||
# Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/'
|
# Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/'
|
||||||
route ^/[^_].* read_story
|
route ^/[^_].* read_story
|
||||||
|
|
||||||
|
@ -111,5 +113,8 @@ domain * {
|
||||||
params post ^/_claim {
|
params post ^/_claim {
|
||||||
validate user v_subdomain
|
validate user v_subdomain
|
||||||
}
|
}
|
||||||
|
params get /_api {
|
||||||
|
validate call v_any
|
||||||
|
validate data v_any
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
local cache = require("cache")
|
||||||
|
local sql = require("lsqlite3")
|
||||||
|
local db = require("db")
|
||||||
|
local queries = require("queries")
|
||||||
|
local util = require("util")
|
||||||
|
|
||||||
|
local stmnt_tags_get
|
||||||
|
|
||||||
|
local oldconfigure = configure
|
||||||
|
function configure(...)
|
||||||
|
stmnt_tags_get = util.sqlassert(db.conn:prepare(queries.select_suggest_tags))
|
||||||
|
return oldconfigure(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function suggest_tags(req,data)
|
||||||
|
print("Suggesting tags!")
|
||||||
|
stmnt_tags_get:bind_names{
|
||||||
|
match = data .. "%"
|
||||||
|
}
|
||||||
|
local err = util.do_sql(stmnt_tags_get)
|
||||||
|
if err == sql.ROW or err == sql.DONE then
|
||||||
|
local tags = {data}
|
||||||
|
for tag in stmnt_tags_get:rows() do
|
||||||
|
print("Found tag:",tag[1])
|
||||||
|
table.insert(tags,tag[1])
|
||||||
|
end
|
||||||
|
stmnt_tags_get:reset()
|
||||||
|
http_response_header(req,"Content-Type","text/plain")
|
||||||
|
http_response(req,200,table.concat(tags,";"))
|
||||||
|
else
|
||||||
|
log(LOG_ALERT,"Failed to get tag suggestions in an unusual way:" .. err .. ":" .. db.conn:errmsg())
|
||||||
|
--This is bad though
|
||||||
|
local page = pages.error({
|
||||||
|
errcode = 500,
|
||||||
|
errcodemsg = "Server error",
|
||||||
|
explanation = string.format(
|
||||||
|
"Failed to retreive tags from database:%d:%q",
|
||||||
|
err,
|
||||||
|
db.conn:errmsg()
|
||||||
|
),
|
||||||
|
})
|
||||||
|
stmnt_tags_get:reset()
|
||||||
|
http_response(req,500,page)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function api_get(req)
|
||||||
|
http_request_populate_qs(req)
|
||||||
|
local call = assert(http_argument_get_string(req,"call"))
|
||||||
|
local data = assert(http_argument_get_string(req,"data"))
|
||||||
|
local body
|
||||||
|
if call == "suggest" then
|
||||||
|
--[[
|
||||||
|
Prevent a malicious user from injecting '%' into the string
|
||||||
|
we're searching for, potentially causing a DoS with a
|
||||||
|
sufficiently backtrack-ey search/tag combination.
|
||||||
|
]]
|
||||||
|
assert(data:match("^[a-zA-Z0-9,%s-]+$"),"Bad characters in tag")
|
||||||
|
return suggest_tags(req,data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return api_get
|
|
@ -61,7 +61,10 @@ local function edit_get(req)
|
||||||
story = story_id,
|
story = story_id,
|
||||||
err = "",
|
err = "",
|
||||||
tags = tags_txt,
|
tags = tags_txt,
|
||||||
unlisted = unlisted == 1
|
unlisted = unlisted == 1,
|
||||||
|
extra_load = {
|
||||||
|
'<script src="/_js/suggest_tags.js"></script>'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
http_response(req,200,ret)
|
http_response(req,200,ret)
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,6 +19,9 @@ local function paste_get(req)
|
||||||
return assert(pages.paste{
|
return assert(pages.paste{
|
||||||
domain = config.domain,
|
domain = config.domain,
|
||||||
err = "",
|
err = "",
|
||||||
|
extra_load = {
|
||||||
|
'<script src="/_js/suggest_tags.js"></script>'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
http_response(req,200,text)
|
http_response(req,200,text)
|
||||||
|
@ -29,6 +32,9 @@ local function paste_get(req)
|
||||||
user = author,
|
user = author,
|
||||||
err = "",
|
err = "",
|
||||||
text="",
|
text="",
|
||||||
|
extra_load = {
|
||||||
|
'<script src="/_js/suggest_tags.js"></script>'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
elseif host ~= config.domain and author == nil then
|
elseif host ~= config.domain and author == nil then
|
||||||
http_response_header(req,"Location",string.format("https://%s/_paste",config.domain))
|
http_response_header(req,"Location",string.format("https://%s/_paste",config.domain))
|
||||||
|
|
|
@ -34,6 +34,7 @@ local endpoint_names = {
|
||||||
claim = {"get","post"},
|
claim = {"get","post"},
|
||||||
search = {"get"},
|
search = {"get"},
|
||||||
archive = {"get"},
|
archive = {"get"},
|
||||||
|
api = {"get"},
|
||||||
}
|
}
|
||||||
local endpoints = {}
|
local endpoints = {}
|
||||||
for name, methods in pairs(endpoint_names) do
|
for name, methods in pairs(endpoint_names) do
|
||||||
|
@ -151,4 +152,13 @@ function archive(req)
|
||||||
endpoints.archive_get(req)
|
endpoints.archive_get(req)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function api(req)
|
||||||
|
local method = http_method_text(req)
|
||||||
|
if method == "GET" then
|
||||||
|
endpoints.api_get(req)
|
||||||
|
elseif method == "POST" then
|
||||||
|
endpoints.api_post(req)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
print("Done with init.lua")
|
print("Done with init.lua")
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
|
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
|
||||||
</div>
|
</div>
|
||||||
<input type="submit">
|
<input type="submit">
|
||||||
|
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
<{cat src/pages/parts/footer.etlua}>
|
<{cat src/pages/parts/footer.etlua}>
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<link href="/_css/milligram.css" rel="stylesheet">
|
<link href="/_css/milligram.css" rel="stylesheet">
|
||||||
<link href="/_css/style.css" rel="stylesheet">
|
<link href="/_css/style.css" rel="stylesheet">
|
||||||
|
<% if extra_load then %>
|
||||||
|
<% for _,load in ipairs(extra_load) do %>
|
||||||
|
<%- load %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</head>
|
</head>
|
||||||
<body class="container">
|
<body class="container">
|
||||||
<main class="wrapper">
|
<main class="wrapper">
|
||||||
|
|
|
@ -26,6 +26,7 @@ int download(struct http_request *);
|
||||||
int preview(struct http_request *);
|
int preview(struct http_request *);
|
||||||
int search(struct http_request *);
|
int search(struct http_request *);
|
||||||
int archive(struct http_request *);
|
int archive(struct http_request *);
|
||||||
|
int api(struct http_request *);
|
||||||
int style(struct http_request *);
|
int style(struct http_request *);
|
||||||
int miligram(struct http_request *);
|
int miligram(struct http_request *);
|
||||||
int do_lua(struct http_request *req, const char *name);
|
int do_lua(struct http_request *req, const char *name);
|
||||||
|
@ -179,6 +180,12 @@ archive(struct http_request *req){
|
||||||
return do_lua(req,"archive");
|
return do_lua(req,"archive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
api(struct http_request *req){
|
||||||
|
printf("Api call!\n");
|
||||||
|
return do_lua(req,"api");
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
home(struct http_request *req){
|
home(struct http_request *req){
|
||||||
return do_lua(req,"home");
|
return do_lua(req,"home");
|
||||||
|
|
Loading…
Reference in New Issue