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.min.css.map asset_serve_milligram_min_css_map
|
||||
route /_faq asset_serve_faq_html
|
||||
route /_js/suggest_tags.js asset_serve_suggest_tags_js
|
||||
route /favicon.ico asset_serve_favicon_ico
|
||||
route /_paste post_story
|
||||
route /_edit edit_story
|
||||
|
@ -54,6 +55,7 @@ domain * {
|
|||
route /_preview preview
|
||||
route /_search search
|
||||
route /_archive archive
|
||||
route /_api api
|
||||
# Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/'
|
||||
route ^/[^_].* read_story
|
||||
|
||||
|
@ -111,5 +113,8 @@ domain * {
|
|||
params post ^/_claim {
|
||||
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,
|
||||
err = "",
|
||||
tags = tags_txt,
|
||||
unlisted = unlisted == 1
|
||||
unlisted = unlisted == 1,
|
||||
extra_load = {
|
||||
'<script src="/_js/suggest_tags.js"></script>'
|
||||
}
|
||||
}
|
||||
http_response(req,200,ret)
|
||||
end
|
||||
|
|
|
@ -19,6 +19,9 @@ local function paste_get(req)
|
|||
return assert(pages.paste{
|
||||
domain = config.domain,
|
||||
err = "",
|
||||
extra_load = {
|
||||
'<script src="/_js/suggest_tags.js"></script>'
|
||||
}
|
||||
})
|
||||
end)
|
||||
http_response(req,200,text)
|
||||
|
@ -29,6 +32,9 @@ local function paste_get(req)
|
|||
user = author,
|
||||
err = "",
|
||||
text="",
|
||||
extra_load = {
|
||||
'<script src="/_js/suggest_tags.js"></script>'
|
||||
}
|
||||
})
|
||||
elseif host ~= config.domain and author == nil then
|
||||
http_response_header(req,"Location",string.format("https://%s/_paste",config.domain))
|
||||
|
|
|
@ -34,6 +34,7 @@ local endpoint_names = {
|
|||
claim = {"get","post"},
|
||||
search = {"get"},
|
||||
archive = {"get"},
|
||||
api = {"get"},
|
||||
}
|
||||
local endpoints = {}
|
||||
for name, methods in pairs(endpoint_names) do
|
||||
|
@ -151,4 +152,13 @@ function archive(req)
|
|||
endpoints.archive_get(req)
|
||||
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")
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
|
||||
</div>
|
||||
<input type="submit">
|
||||
|
||||
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
|
||||
</fieldset>
|
||||
</form>
|
||||
<{cat src/pages/parts/footer.etlua}>
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
<% end %>
|
||||
<link href="/_css/milligram.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>
|
||||
<body class="container">
|
||||
<main class="wrapper">
|
||||
|
|
|
@ -26,6 +26,7 @@ int download(struct http_request *);
|
|||
int preview(struct http_request *);
|
||||
int search(struct http_request *);
|
||||
int archive(struct http_request *);
|
||||
int api(struct http_request *);
|
||||
int style(struct http_request *);
|
||||
int miligram(struct http_request *);
|
||||
int do_lua(struct http_request *req, const char *name);
|
||||
|
@ -179,6 +180,12 @@ archive(struct http_request *req){
|
|||
return do_lua(req,"archive");
|
||||
}
|
||||
|
||||
int
|
||||
api(struct http_request *req){
|
||||
printf("Api call!\n");
|
||||
return do_lua(req,"api");
|
||||
}
|
||||
|
||||
int
|
||||
home(struct http_request *req){
|
||||
return do_lua(req,"home");
|
||||
|
|
Loading…
Reference in New Issue