From 7a6321ca5fe41e433525bf5759def934b7fa8e5c Mon Sep 17 00:00:00 2001 From: tayyyyyyy Date: Wed, 22 May 2019 01:46:09 -0700 Subject: [PATCH] Autocomplete input for ghost buttons --- code/__HELPERS/lists.dm | 4 +- code/datums/helper_datums/input.dm | 95 +++++++++++++++++----- code/modules/mob/dead/observer/observer.dm | 40 ++++----- html/browser/autocomplete.js | 61 ++++++++++++++ 4 files changed, 155 insertions(+), 45 deletions(-) create mode 100644 html/browser/autocomplete.js diff --git a/code/__HELPERS/lists.dm b/code/__HELPERS/lists.dm index 6b5807deaef..9e01753d492 100644 --- a/code/__HELPERS/lists.dm +++ b/code/__HELPERS/lists.dm @@ -353,12 +353,12 @@ var/middle = L.len / 2 + 1 // Copy is first,second-1 return mergeLists(sortList(L.Copy(0,middle)), sortList(L.Copy(middle))) //second parameter null = to end of list -//Mergsorge: uses sortList() but uses the var's name specifically. This should probably be using mergeAtom() instead +//Mergsorge: uses sortAssoc() but uses the var's name specifically. This should probably be using mergeAtom() instead /proc/sortNames(var/list/L) var/list/Q = new() for(var/atom/x in L) Q[x.name] = x - return sortList(Q) + return sortAssoc(Q) /proc/mergeLists(var/list/L, var/list/R) var/Li=1 diff --git a/code/datums/helper_datums/input.dm b/code/datums/helper_datums/input.dm index 3bcf9509e37..19205a4ab62 100644 --- a/code/datums/helper_datums/input.dm +++ b/code/datums/helper_datums/input.dm @@ -1,71 +1,90 @@ /proc/input_async(mob/user=usr, prompt, list/choices) - var/datum/async_input/A = new(choices, prompt) - A.show(user) + var/datum/async_input/A = new(choices, prompt, , user) + A.show() return A /proc/input_ranked_async(mob/user=usr, prompt="Order by greatest to least preference", list/choices) - var/datum/async_input/ranked/A = new(choices, prompt) - A.show(user) + var/datum/async_input/ranked/A = new(choices, prompt, "ranked_input", user) + A.show() + return A + +/proc/input_autocomplete_async(mob/user=usr, prompt="Enter text: ", list/choices) + var/datum/async_input/autocomplete/A = new(choices, prompt, "ac_input", user) + A.show() return A /datum/async_input var/datum/browser/popup + // If associative list, key will be used for display, but the final result will be the value var/list/choices + var/datum/callback/onCloseCb var/flash = TRUE var/immediate_submit = FALSE var/prompt var/result = null var/style = "text-align: center;" + var/mob/user var/window_id var/height = 200 var/width = 400 -/datum/async_input/New(list/new_choices, new_prompt="Pick an option:", new_window_id="async_input") +/datum/async_input/New(list/new_choices, new_prompt="Pick an option:", new_window_id="async_input", mob/new_user=usr) choices = new_choices prompt = new_prompt window_id = new_window_id + user = new_user + popup = new(user, window_id, , width, height, src) /datum/async_input/proc/close() if(popup) popup.close() + if(result && choices[result]) + result = choices[result] + if(onCloseCb) + onCloseCb.Invoke(result) return result -/datum/async_input/proc/show(mob/user) - var/dat = create_ui(user) - popup = new(user, window_id, , width, height, src) - popup.set_content(dat) +// Callback function should take the result as the last argument +/datum/async_input/proc/on_close(var/datum/callback/cb) + onCloseCb = cb + +/datum/async_input/proc/show() + popup.set_content(create_ui()) if(flash && result == null) window_flash(user.client) popup.open() -/datum/async_input/proc/create_ui(mob/user) +/datum/async_input/proc/create_ui() var/dat = "
" - dat += render_prompt(user) - dat += render_choices(user) + dat += render_prompt() + dat += render_choices() dat += "
" dat += "
" - dat += button("Submit", "submit=1", , result == null && !immediate_submit) + dat += render_buttons() dat += "
" return dat -/datum/async_input/proc/render_prompt(mob/user) +/datum/async_input/proc/render_prompt() return "

[prompt]

" -/datum/async_input/proc/render_choices(mob/user) +/datum/async_input/proc/render_choices() var/dat = " " for(var/choice in choices) dat += button(choice, "choice=[choice]", choice == result) dat += " " return dat -/datum/async_input/proc/button(label, topic, on=FALSE, disabled=FALSE) +/datum/async_input/proc/render_buttons() + return button("Submit", "submit=1", , result == null && !immediate_submit) + +/datum/async_input/proc/button(label, topic, on=FALSE, disabled=FALSE, id="") var/class = "" if(on) class = "linkOn" if(disabled) class = "linkOff" topic = "" - return "[label]" + return "[label]" /datum/async_input/Topic(href, href_list) if(href_list["submit"] || href_list["close"]) @@ -74,14 +93,14 @@ if(href_list["choice"]) result = href_list["choice"] - show(usr) + show() return /datum/async_input/ranked height = 400 immediate_submit = TRUE -/datum/async_input/ranked/render_choices(mob/user) +/datum/async_input/ranked/render_choices() var/dat = "
" dat += "" for(var/i = 1, i <= choices.len, i++) @@ -103,13 +122,47 @@ if(href_list["upvote"]) var/index = text2num(href_list["upvote"]) choices.Swap(index, index - 1) - show(usr) + show() return if(href_list["downvote"]) var/index = text2num(href_list["downvote"]) choices.Swap(index, index + 1) - show(usr) + show() + return + + ..() + +/datum/async_input/autocomplete + immediate_submit = TRUE + height = 150 + +/datum/async_input/autocomplete/New() + ..() + popup.add_script("autocomplete.js", 'html/browser/autocomplete.js') + +/datum/async_input/autocomplete/render_prompt() + return "" + +/datum/async_input/autocomplete/render_choices() + var/dat = "" + dat += "" + for(var/choice in choices) + dat += "" + return dat + +/datum/async_input/autocomplete/render_buttons() + var/dat = button("Submit", "", , result == null && !immediate_submit, "submit-button") + dat += button("Cancel", "close=1") + return dat + +/datum/async_input/autocomplete/Topic(href, href_list) + if(href_list["submit"]) + // Entering an invalid choice is the same as canceling + if(href_list["submit"] in choices) + result = href_list["submit"] + close() return ..() diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 9be4f92a0b8..fb6d327c8c0 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -397,9 +397,10 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp to_chat(usr, "Not when you're not dead!") return - var/area/A = input("Area to jump to", "BOOYEA") as null|anything in ghostteleportlocs - var/area/thearea = ghostteleportlocs[A] + var/datum/async_input/A = input_autocomplete_async(usr, "Area to jump to: ", ghostteleportlocs) + A.on_close(CALLBACK(src, .proc/teleport)) +/mob/dead/observer/proc/teleport(area/thearea) if(!thearea) return @@ -420,9 +421,8 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp set desc = "Follow and orbit a mob." var/list/mobs = getpois(skip_mindless=1) - var/input = input("Please, select a mob!", "Haunt", null, null) as null|anything in mobs - var/mob/target = mobs[input] - ManualFollow(target) + var/datum/async_input/A = input_autocomplete_async(usr, "Please, select a mob: ", mobs) + A.on_close(CALLBACK(src, .proc/ManualFollow)) // This is the ghost's follow verb with an argument /mob/dead/observer/proc/ManualFollow(var/atom/movable/target) @@ -494,25 +494,21 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp set name = "Jump to Mob" set desc = "Teleport to a mob" - if(isobserver(usr)) //Make sure they're an observer! - var/list/dest = list() //List of possible destinations (mobs) - var/target = null //Chosen target. + if(isobserver(usr)) //Make sure they're an observer! + var/list/dest = getpois(mobs_only=1) //Fill list, prompt user with list + var/datum/async_input/A = input_autocomplete_async(usr, "Enter a mob name: ", dest) + A.on_close(CALLBACK(src, .proc/jump_to_mob)) - dest += getpois(mobs_only=1) //Fill list, prompt user with list - target = input("Please, select a mob!", "Jump to Mob", null, null) as null|anything in dest - - if(!target) //Make sure we actually have a target - return - else - var/mob/M = dest[target] //Destination mob - var/mob/A = src //Source mob - var/turf/T = get_turf(M) //Turf of the destination mob - - if(T && isturf(T)) //Make sure the turf exists, then move the source to that destination. - A.forceMove(T) - else - to_chat(A, "This mob is not located in the game world.") +/mob/dead/observer/proc/jump_to_mob(mob/M) + if(!M) + return + var/mob/A = src //Source mob + var/turf/T = get_turf(M) //Turf of the destination mob + if(T && isturf(T)) //Make sure the turf exists, then move the source to that destination. + A.forceMove(T) + return + to_chat(A, "This mob is not located in the game world.") /* Now a spell. See spells.dm /mob/dead/observer/verb/boo() diff --git a/html/browser/autocomplete.js b/html/browser/autocomplete.js new file mode 100644 index 00000000000..28d3be539c4 --- /dev/null +++ b/html/browser/autocomplete.js @@ -0,0 +1,61 @@ +var $ = document.querySelector.bind(document); +var input; +var submitButton; +var optionsMap = {}; + +function updateTopic() { + if (!input || !submitButton) { + return; + } + + var hrefList = submitButton.getAttribute('href').split(';'); + // Topic must come last in the submit button for this to work + hrefList = hrefList.slice(0, hrefList.length - 1); + hrefList.push(optionsMap[input.value] ? 'submit=' + optionsMap[input.value] : ''); + submitButton.setAttribute('href', hrefList.join(';')); +} + +function clean(name) { + // \improper shows up as 2 characters outside of ascii range + if (name.charCodeAt(0) > 127) { + return name.slice(2); + } + return name; +} + +function setElements() { + input = $('#input'); + submitButton = $('#submit-button'); + var choices = $('#choices'); + + if (!input || !submitButton || !choices) { + return; + } + + + for (var i = 0; i < choices.options.length; i++) { + var name = choices.options[i].value; + var cleaned = clean(name); + optionsMap[cleaned] = name; + choices.options[i].value = cleaned; + } + + input.addEventListener('keyup', function(event) { + if (event.key !== 'Enter') { + return; + } + + if (Object.keys(optionsMap).indexOf(input.value) === -1) { + // Byond doesn't let you to use enter to select + // so we need to prevent unintended submissions + return + } + + submitButton.click(); + event.preventDefault(); + }); + + input.focus(); +} + +window.onload = setElements;