aboutsummaryrefslogtreecommitdiff
path: root/assets
diff options
context:
space:
mode:
Diffstat (limited to 'assets')
-rw-r--r--assets/interactive.js293
-rw-r--r--assets/styles.css232
2 files changed, 525 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
diff --git a/assets/styles.css b/assets/styles.css
new file mode 100644
index 0000000..b9fca4d
--- /dev/null
+++ b/assets/styles.css
@@ -0,0 +1,232 @@
+html {
+ --tag-bgcolor: hsl(200, 87.8%, 35.5%);
+ --tag-fgcolor: white;
+ --fg: #f9f9f9;
+ --bg: #22222f;
+ --bg-2: #333;
+ --bg-2-highlight: #444;
+ --fg-link-normal: #7187d7;
+ --fg-link-active: #ff3f00;
+ --fg-link-visited: #9f71d7;
+ --col-separator: #aaa;
+}
+
+html {
+ background: var(--bg);
+ color: var(--fg);
+ margin: 0;
+ padding: 1rem;
+ font-family: sans-serif;
+}
+
+body {
+ width: 85%;
+ margin: 10px auto;
+ padding: 0;
+}
+
+@media screen and (max-width: 400px) {
+ body {
+ width: auto;
+ margin: 10px 4px;
+ }
+}
+
+a:link {
+ color: var(--fg-link-normal);
+ text-decoration: none;
+}
+a:link:hover {
+ text-decoration: underline;
+}
+a:link:active {
+ color: var(--fg-link-active);
+}
+a:link:visited {
+ color: var(--fg-link-visited);
+}
+
+.tag {
+ background: var(--tag-bgcolor);
+ color: var(--tag-fgcolor);
+ padding: 5px;
+ display: inline-block;
+ border-radius: 3px;
+ position: relative;
+ height: 1em;
+}
+
+.tag a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.tag a:hover {
+ text-decoration: underline;
+}
+
+.tc-1 { font-size: 0.7em; }
+.tc-2 { font-size: 0.8em; }
+.tc-3 { font-size: 0.9em; }
+.tc-4 { font-size: 1em; }
+.tc-5 { font-size: 1.1em; }
+
+.tag-input .delete {
+ background: var(--tag-bgcolor);
+ border: 1px solid var(--tag-fgcolor);
+ margin: 2px 6px 2px 2px;
+ padding: 1px;
+ color: var(--tag-fgcolor);
+ border-radius: 3px;
+ display: inline-block;
+ font-size: 0.8em;
+ font-family: sans-serif;
+ transition: background 0.2s, color 0.2s;
+ cursor: pointer;
+}
+
+.tag-input .delete:hover {
+ background: var(--tag-fgcolor);
+ color: var(--tag-bgcolor);
+}
+
+.tag-input .labelled-input {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 6px;
+ border-radius: 3px;
+ background-color: var(--bg-2);
+}
+
+.tag-input .labelled-input:hover {
+ background-color: var(--bg-2-highlight);
+}
+
+.tag-user-input {
+ flex-grow: 1;
+ min-width: 250px;
+ background: transparent;
+ border: none;
+ color: inherit;
+ font-size: inherit;
+ border-bottom: 1px solid var(--bg);
+}
+
+#note-content {
+ background: transparent;
+ color: inherit;
+ border: 1px solid var(--fg);
+ margin: 0;
+ padding: 5px;
+ width: calc(100% - 12px);
+ min-height: 5em;
+ height: 50vh;
+}
+
+@media screen and (max-width: 200px) {
+ .tag-user-input {
+ min-width: 90vw;
+ }
+}
+
+header {
+ margin: 0 0 10px;
+ padding: 0 0 10px;
+ border-bottom: 1px solid var(--col-separator);
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.mainmenu ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.mainmenu li {
+ display: inline-block;
+}
+
+.mainmenu li:not(:first-child) {
+ margin-left: 10px;
+}
+
+.mainmenu a {
+ color: var(--fg);
+}
+
+.homelink {
+ letter-spacing: 3px;
+}
+
+.s-search {
+ flex-grow: 1;
+}
+
+.search-form {
+ display: flex;
+}
+.search-input {
+ background: transparent;
+ color: inherit;
+ border: 0;
+ padding: 4px 10px;
+ border-bottom: 1px solid var(--col-separator);
+ transition: 0.3s border-bottom-color;
+}
+
+.search-input:hover, .search-input:focus {
+ border-bottom-color: var(--fg);
+}
+
+.search-input {
+ flex-grow: 1;
+}
+
+input:placeholder-shown {
+ font-style: italic;
+}
+
+.note-list {
+ list-style: none;
+ padding: 0;
+}
+
+.note-list > li {
+ background: #444;
+ margin: 10px 0;
+ padding: 3px 8px;
+ border-radius: 3px;
+}
+
+.autocomplete-root {
+ position: relative;
+ display: inline-block;
+}
+
+.autocomplete-root ul {
+ display: block;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ position: absolute;
+ z-index: 1;
+ background: var(--bg-2);
+ min-width: 200px;
+ border: 1px solid var(--bg);
+}
+
+.autocomplete-root li {
+ padding: 4px 10px;
+}
+
+.autocomplete-root li.active {
+ background: var(--fg);
+ color: var(--bg);
+}
+
+.autocomplete-root:not(.show-suggestions) ul {
+ display: none;
+} \ No newline at end of file