diff --git a/chip-list.js b/chip-list.js
new file mode 100644
index 0000000..abb7e5e
--- /dev/null
+++ b/chip-list.js
@@ -0,0 +1,181 @@
+import { IgniteElement } from "../ignite-html/ignite-element.js";
+import { IgniteTemplate, list, div, input, button, h4, span } from "../ignite-html/ignite-template.js";
+import { Chip } from "./chip.js";
+import { Popper } from "./popper.js";
+
+class ChipList extends IgniteElement {
+ constructor() {
+ super();
+ }
+
+ get styles() {
+ return `
+ mt-chip-list {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 12px;
+ border: solid 0.13rem transparent;
+ border-radius: 0.3rem;
+ padding: 0.2rem;
+ }
+
+ mt-chip-list:hover {
+ cursor: pointer;
+ }
+
+ mt-chip-list.editing {
+ border: solid 0.13rem #ced4da;
+ border-radius: 0.3rem;
+ }
+
+ mt-chip-list > .input-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+
+ mt-chip-list > .input-container > .input {
+ min-width: 1em;
+ outline: none;
+ }
+
+ mt-chip-list .search-result:hover {
+ background-color: #e0e0e0;
+ border-radius: 0.3em;
+ }
+ `;
+ }
+
+ get properties() {
+ return {
+ items: null,
+ editing: false,
+ input: null,
+ searchBox: null,
+ searching: false,
+ searchResults: [
+ { id: 502, corporation: "Starbucks", content: "Starbucks #1, Spokane WA" },
+ { id: 503, corporation: "Starbucks", content: "Starbucks #2, Spokane WA" }
+ ],
+ searchEmpty: "No results found.",
+ searchFooter: null,
+ blurTimeout: null,
+ documentListener: null
+ };
+ }
+
+ render() {
+ return this.template
+ .style("position", "relative")
+ .class(this.editing, value => { return value ? "editing" : null })
+ .child(
+ new list(this.items, (item) => {
+ return new Chip()
+ .id(item.id)
+ .property("onDelete", () => { this.items = this.items.filter(needle => needle != item) })
+ .child(item.content);
+ }),
+ new div()
+ .class("input-container")
+ .child(
+ new div()
+ .class("input")
+ .attribute("contenteditable", "true")
+ .hide(this.editing, value => { return !value; })
+ .ref(this.input)
+ .onEnter((e) => {
+ e.preventDefault();
+ //Add a new item to the chip list.
+ this.items.push({ content: this.input.textContent.trim() });
+ this.input.innerHTML = "";
+ })
+ .onBackspace((e) => {
+ //If the backspace key is pressed and there is no content, try to remove the last item from the list.
+ if (this.input.textContent.length == 0 || (this.input.textContent.length == 1 && this.input.textContent[0] == " ")) {
+ e.preventDefault();
+ this.items.pop();
+ }
+ })
+ .on("keydown", () => {
+ //If we are not searching and a key was pressed, open the search box.
+ if (!this.searching) {
+ this.searching = true;
+ }
+ })
+ .onBlur((e) => {
+ //Force the input to not lose focus if we are editing.
+ if (this.editing) {
+ e.target.focus();
+ }
+ })
+ ),
+ new Popper()
+ .property("show", this.searching)
+ .child(
+ new div()
+ .class("shadow d-flex flex-column justify-content-center p-2")
+ .style("background-color", "#fff")
+ .child(
+ new span(this.searchEmpty).class("mt-2").hide(this.searchResults, value => { return value != null && value.length > 0; }),
+ new list(this.searchResults, item => {
+ return new div(item.content).class("search-result p-2").onClick(() => this.searchResultClick(item));
+ }),
+ `
`,
+ //new button().class("btn btn-primary text-white").child("Add missing location")
+ this.searchFooter
+ )
+ ),
+ )
+ .onClick((e) => {
+ e.stopPropagation(); //Stop this from bubbling to the document click.
+ this.focus(); //Focus our element if we are not already.
+ })
+ }
+
+ ready() {
+ //Add a listener to the document click to blur our element.
+ this.documentListener = () => this.blur();
+ window.document.addEventListener("click", this.documentListener);
+
+ }
+
+ cleanup() {
+ window.document.removeEventListener("click", this.documentListener);
+ }
+
+ focus() {
+ if (!this.editing) {
+ this.editing = true;
+ this.input.focus();
+ this.input.textContent = " ";
+ }
+ }
+
+ blur() {
+ if (this.editing) {
+ this.editing = false;
+ this.searching = false;
+ this.input.blur();
+ this.input.innerHTML = "";
+ }
+ }
+
+ searchResultClick(item) {
+ console.log("Search item was clicked:", item);
+ this.items.push({ content: item.content });
+ }
+}
+
+class ChipListTemplate extends IgniteTemplate {
+ constructor(...children) {
+ super("mt-chip-list", children);
+ }
+}
+
+customElements.define("mt-chip-list", ChipList);
+
+export {
+ ChipListTemplate as ChipList
+}
\ No newline at end of file
diff --git a/chip.js b/chip.js
new file mode 100644
index 0000000..c541fdb
--- /dev/null
+++ b/chip.js
@@ -0,0 +1,49 @@
+import { IgniteElement } from "../ignite-html/ignite-element.js";
+import { IgniteTemplate, slot, button } from "../ignite-html/ignite-template.js";
+
+class Chip extends IgniteElement {
+ constructor() {
+ super();
+ }
+
+ get properties() {
+ return {
+ onDelete: () => { }
+ }
+ }
+
+ get styles() {
+ return `
+ mt-chip {
+ border-radius: 1em;
+ background-color: #e0e0e0;
+ padding-top: 0.3em;
+ padding-bottom: 0.3em;
+ padding-left: 0.6em;
+ padding-right: 0.6em;
+ }
+ `;
+ }
+
+ render() {
+ return this.template.child(
+ new slot(this),
+ new button()
+ .class("btn ml-1 p-0")
+ .child(``)
+ .onClick(() => this.onDelete())
+ );
+ }
+}
+
+class ChipTemplate extends IgniteTemplate {
+ constructor(...children) {
+ super("mt-chip", children);
+ }
+}
+
+customElements.define("mt-chip", Chip);
+
+export {
+ ChipTemplate as Chip
+}
\ No newline at end of file
diff --git a/editable-label.js b/editable-label.js
index e9ded80..33fef1f 100644
--- a/editable-label.js
+++ b/editable-label.js
@@ -31,6 +31,11 @@ class EditableLabel extends IgniteElement {
padding: 0.2rem;
}
+ mt-editable-label>div:empty:before {
+ content: attr(data-placeholder);
+ opacity: 0.5;
+ }
+
mt-editable-label>button {
margin-left: 0.5em;
background-color: #2196F3;
@@ -57,7 +62,8 @@ class EditableLabel extends IgniteElement {
editOnClick: true,
multiLine: false,
saveButton: true,
- input: null
+ input: null,
+ placeholder: null
};
}
@@ -68,6 +74,7 @@ class EditableLabel extends IgniteElement {
.innerHTML(this.value)
.class(this.editing, (value) => { return value ? "focus" : null; })
.attribute("contenteditable", this.editing)
+ .data("placeholder", this.placeholder)
.ref(this.input)
.onClick(() => this.onClick())
.onBlur(() => this.onBlur())
@@ -84,7 +91,6 @@ class EditableLabel extends IgniteElement {
e.preventDefault();
e.stopPropagation();
}
-
}
onClick() {
@@ -101,7 +107,6 @@ class EditableLabel extends IgniteElement {
if (this.input.innerHTML !== this.value) {
this.value = this.input.innerHTML;
}
-
}
}
}
diff --git a/popper.js b/popper.js
new file mode 100644
index 0000000..7a58642
--- /dev/null
+++ b/popper.js
@@ -0,0 +1,95 @@
+import { IgniteElement } from "../ignite-html/ignite-element.js";
+import { IgniteTemplate, slot } from "../ignite-html/ignite-template.js";
+import { IgniteProperty } from "../ignite-html/ignite-html.js";
+
+class Popper extends IgniteElement {
+ constructor() {
+ super();
+ }
+
+ get properties() {
+ return {
+ position: "bottom",
+ show: new IgniteProperty(false, () => {
+ if (this.show) {
+ this.firstUpdate();
+ } else {
+ if (this.updateTimeout) {
+ clearTimeout(this.updateTimeout);
+ }
+
+ this.updateTimeout = null;
+ }
+ }),
+ updateTimeout: null,
+ offset: "0.5em"
+ }
+ }
+
+ render() {
+ return this.template.child(
+ new slot(this)
+ .style("position", "absolute")
+ .style("top", this.position, true, value => { return value == "bottom" ? "100%" : null; })
+ .style("bottom", this.position, true, value => { return value == "top" ? "100%" : null; })
+ .style("margin-top", this.position, true, value => { return this.position == "bottom" ? this.offset : null })
+ .style("margin-bottom", this.position, true, value => { return this.position == "top" ? this.offset : null })
+ .style("left", "0")
+ .style("width", "100%")
+ .style("z-index", "99999")
+ .hide(this.show, value => { return !value; })
+ );
+ }
+
+ ready() {
+ this.firstUpdate();
+ }
+
+ firstUpdate() {
+ if (this.show && this.updateTimeout == null) {
+ this.updateTimeout = setTimeout(() => this.update(), 200);
+ }
+ }
+
+ update() {
+ if (this.show) {
+ this.updateTimeout = setTimeout(() => this.update(), 200);
+ }
+
+ var thisBounds = this.firstChild.getBoundingClientRect();
+ var parentBounds = this.offsetParent.getBoundingClientRect();
+
+ var thisOffset = 0;
+ if (this.firstChild.offsetTop < 0) {
+ thisOffset = Math.abs(this.firstChild.offsetTop + thisBounds.height);
+ } else {
+ thisOffset = this.firstChild.offsetTop - parentBounds.height;
+ }
+
+ if (thisBounds.y < 0 && this.position != "bottom") {
+ this.position = "bottom";
+ } else if (thisBounds.y + thisBounds.height >= window.innerHeight && this.position != "top") {
+ this.position = "top";
+ } else if (parentBounds.height + parentBounds.y + thisBounds.height + thisOffset <= window.innerHeight && this.position != "bottom") {
+ this.position = "bottom";
+ }
+ }
+
+ cleanup() {
+ if (this.updateTimeout) {
+ clearTimeout(this.updateTimeout);
+ }
+ }
+}
+
+customElements.define("mt-popper", Popper);
+
+class PopperTemplate extends IgniteTemplate {
+ constructor(...children) {
+ super("mt-popper", children);
+ }
+}
+
+export {
+ PopperTemplate as Popper
+}
\ No newline at end of file