aboutsummaryrefslogtreecommitdiff
path: root/assets/interactive.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/interactive.js')
-rw-r--r--assets/interactive.js293
1 files changed, 293 insertions, 0 deletions
diff --git a/assets/interactive.js b/assets/interactive.js
new file mode 100644
index 0000000..05b7556
--- /dev/null
+++ b/assets/interactive.js
@@ -0,0 +1,293 @@
+$(function () {
+ function APIError(code, message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ APIError.prototype.toString = function () {
+ return "APIError " + this.code + ": " + this.message;
+ };
+
+ function default_api_error_handler(data, code) {
+ throw new APIError(code, data);
+ }
+
+ function add_get_param(url, name, val) {
+ name = encodeURIComponent(name);
+ val = encodeURIComponent(val);
+
+ var separator = url.match(/\?/) === null ? "?" : "&";
+ return url + separator + name + "=" + val;
+ }
+
+ function add_get_params(url, params) {
+ for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k))
+ url = add_get_param(url, k, params[k]);
+ return url;
+ }
+
+ function query_api(method, url, arguments, handler, error_handler) {
+ url = add_get_params(url, arguments);
+ error_handler = error_handler || default_api_error_handler;
+
+ var xhr = new XMLHttpRequest();
+
+ function wrap_handler(handler) {
+ return function () {
+ return handler(xhr.response, xhr.status, xhr);
+ }
+ }
+
+ xhr.addEventListener("load", wrap_handler(handler));
+ xhr.addEventListener("error", wrap_handler(error_handler));
+ xhr.open(method, url);
+ xhr.responseType = "json";
+ xhr.send();
+ }
+
+ function Autocomplete() {
+ var self = this;
+
+ this.root = $("<div>")
+ .addClass("autocomplete-root");
+ this.input = $("<input>")
+ .attr("type", "text")
+ .appendTo(this.root)
+ .on("keyup", function (ev) {
+ if (ev.key === "ArrowDown") self.change_selection(1);
+ if (ev.key === "ArrowUp") self.change_selection(-1);
+
+ self.change_listener();
+ })
+ .on("keydown", function (ev) {
+ if (ev.key === "Tab") {
+ if (self.complete_current())
+ ev.preventDefault();
+ }
+ })
+ .on("blur", function (ev) {
+ var visible = false;
+ if (ev.relatedTarget) {
+ visible = $(ev.relatedTarget).closest(".autocomplete-root").get(0) === self.root.get(0);
+ }
+ self.set_visible(visible);
+ });
+ this.options = $("<ul>").appendTo(this.root)
+ .on("mouseout", function () {
+ self.mark_active_by_el(null);
+ });
+
+ this.abort = function () {};
+ this.get_suggestions = function (_, cb) { cb([]); };
+ }
+
+ Autocomplete.prototype.set_visible = function (visible) {
+ this.root.toggleClass("show-suggestions", !!visible);
+ };
+
+ Autocomplete.prototype.mark_active_by_el = function (el) {
+ this.options.find("li").each(function () {
+ var $cur = $(this);
+ $cur.toggleClass("active", this === el);
+ });
+ };
+
+ Autocomplete.prototype.change_listener = function () {
+ var self = this;
+
+ this.abort();
+
+ var input_text = this.input.val();
+ if (input_text === "") {
+ this.set_visible(false);
+ return;
+ }
+
+ this.get_suggestions(input_text, function (suggestions) {
+ console.log(suggestions);
+ suggestions = Array.prototype.slice.call(suggestions, 0);
+ suggestions.sort();
+ var items = suggestions.map(function (s) {
+ var found;
+ self.options.find("li").each(function () {
+ var el = $(this);
+ if (el.text() === s) {
+ found = el;
+ return false;
+ }
+ });
+
+ if (found)
+ return found;
+
+ return $("<li>").text(s).attr("tabindex", 0)
+ .on("click", function (ev) {
+ ev.preventDefault();
+ self.apply_completion($(this).text());
+ })
+ .on("mouseover", function () {
+ self.mark_active_by_el(this);
+ });
+ });
+ self.options.empty();
+ for (var i = 0; i < items.length; i++)
+ self.options.append(items[i]);
+
+ if (!self.options.find("li.active").length)
+ self.options.find("li").first().addClass("active");
+
+ self.set_visible(self.options.find("li").length > 0);
+ });
+ };
+
+ Autocomplete.prototype.change_selection = function (d) {
+ var lis = this.options.find("li");
+ if (lis.length === 0)
+ return;
+
+ var idx = lis.index(lis.filter(".active"));
+ if (idx < 0)
+ idx = 0;
+ idx += d;
+ idx %= lis.length;
+ lis.removeClass("active");
+ lis.eq(idx).addClass("active");
+ };
+
+ Autocomplete.prototype.complete_current = function () {
+ var cur = this.options.find("li.active");
+ if (cur.length !== 1)
+ return false;
+
+ this.apply_completion(cur.text());
+ this.change_listener();
+ return true;
+ };
+
+ Autocomplete.prototype.apply_completion = function (text) {
+ this.input.val(text);
+ };
+
+ var Tags = (function () {
+ var tags;
+
+ function get(callback) {
+ if (tags !== undefined) {
+ callback(tags);
+ return;
+ }
+
+ query_api("GET", "/api/tags", {}, function (resp) {
+ if (Object.prototype.toString.call(resp) !== "[object Array]") {
+ throw "Unexpeced return value from /api/tags";
+ }
+ tags = resp;
+ callback(tags);
+ });
+ }
+
+ return {
+ get: get,
+ clear_cache: function () { tags = null; }
+ };
+ })();
+
+ function selectedTag(tag) {
+ return $("<div>")
+ .addClass("tag")
+ .text(tag)
+ .append($("<input>")
+ .attr({
+ name: "tag[]",
+ type: "hidden",
+ })
+ .val(tag)
+ )
+ .prepend($("<button>")
+ .attr("type", "button")
+ .addClass("delete")
+ .text("X")
+ .on("click", function () {
+ $(this).closest(".tag").remove();
+ })
+ );
+ }
+
+ $("fieldset.tags").each(function () {
+ var $this = $(this);
+ var label = $this.find("legend").text();
+
+ $this.find("input").map(function () { return $(this).val(); }).get().filter(x => !!x);
+
+ var out = $("<div>")
+ .addClass("tag-input labelled")
+ .append($("<label>").text(label).attr("for", "tag-input-field"));
+ var content = $("<div>").addClass("labelled-input").appendTo(out);
+ var tag_list = $("<div>").append($this.find("input")
+ .filter(function () {
+ return !!$(this).val();
+ })
+ .map(function () {
+ return selectedTag($(this).val());
+ }).get()
+ );
+ content.append(tag_list);
+
+ var ac = new Autocomplete();
+ ac.get_suggestions = function (text, callback) {
+ text = String.prototype.toLowerCase.call(text);
+ Tags.get(function (tags) {
+ console.log(["tags", tags]);
+ tags = Array.prototype.filter.call(tags, function (it) {
+ it = String.prototype.toLowerCase.call(it);
+ return it.indexOf(text) > -1;
+ });
+ console.log(["filtered", tags]);
+ callback(tags);
+ });
+ };
+ ac.apply_completion = function (text) {
+ ac.input.val("");
+ tag_list.append(selectedTag(text));
+ };
+
+ ac.input
+ .attr({
+ name: "tag[]",
+ id: "tag-input-field",
+ placeholder: "Another tag"
+ })
+ .addClass("tag-user-input")
+ .on("keydown", function (ev) {
+ if (ev.key === "Enter") {
+ ev.preventDefault();
+
+ if (ev.ctrlKey) {
+ $(this).closest("form").each(function () {
+ this.submit();
+ });
+ return;
+ }
+
+ var tag = $(this).val().trim();
+ if (tag === "")
+ return;
+
+ tag_list.append(selectedTag(tag));
+ $(this).val("");
+ } else if (ev.key === "Backspace") {
+ if ($(this).val() === "") {
+ tag_list.find(".tag").last().remove();
+ }
+ }
+ });
+ content.append(ac.root);
+
+ $this.after(out).remove();
+ });
+
+ $("button.confirm").on("click", function (ev) {
+ if (!window.confirm($(this).data("question")))
+ ev.preventDefault();
+ })
+}); \ No newline at end of file