diff --git a/.eslintrc.json b/.eslintrc.json
index addb12a..0986763 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -21,6 +21,7 @@
"no-implied-eval": "error",
"no-self-compare": "error",
"no-unused-expressions": "error",
- "curly": "error"
+ "curly": "error",
+ "no-unused-vars": ["error", {"args": "none"}]
}
}
diff --git a/asset/index.html b/asset/index.html
index 550ebcc..775976a 100644
--- a/asset/index.html
+++ b/asset/index.html
@@ -2,144 +2,46 @@
+
+ render({ headline, byline, icon, href, outlined, primaryAction } = this) {
+ return html`
+
+ `;
+ }
+}
+customElements.define("card-mdc", Card);
diff --git a/src/card/card.scss b/src/card/card.scss
new file mode 100644
index 0000000..2252ebf
--- /dev/null
+++ b/src/card/card.scss
@@ -0,0 +1,2 @@
+@import "../style/shared.scss";
+@import "@material/card/mdc-card.scss";
diff --git a/src/card/exports.js b/src/card/exports.js
new file mode 100644
index 0000000..0324d63
--- /dev/null
+++ b/src/card/exports.js
@@ -0,0 +1,2 @@
+export * from "./card.js";
+//export * from "./card-list.js";
diff --git a/src/card/style.js b/src/card/style.js
new file mode 100644
index 0000000..2f48b78
--- /dev/null
+++ b/src/card/style.js
@@ -0,0 +1,49 @@
+import { css } from "lit-element";
+import _cardStyle from "./card-scss.js";
+// .card-header
+// margin: 1.25rem 0;
+// padding: 16px;
+
+export default [
+ _cardStyle,
+ css`
+ :host {
+ grid-gap: 4px;
+ }
+
+ a {
+ color: var(--mdc-theme-primary);
+ }
+
+ :host h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ }
+ .card-icon {
+ padding-right: 8px;
+ }
+
+ .card-header,
+ .card-main {
+ padding: 12px;
+ }
+
+ /* .mdc-card__actions:hover {
+ display: none;
+ } */
+
+ /* .mdc-card__actions {
+ display: none;
+ } */
+
+ /* .mdc-card:hover + .mdc-card__actions {
+ display: block;
+ } */
+ `
+];
diff --git a/src/demo-app.js b/src/demo-app.js
new file mode 100644
index 0000000..faf5864
--- /dev/null
+++ b/src/demo-app.js
@@ -0,0 +1,90 @@
+import { AppShellMixin } from "./app-shell/exports.js";
+import { LitElement, html } from "lit-element";
+import { get as translator } from "lit-translate";
+
+const routes = [];
+
+import "./card/exports.js";
+import "./button/exports.js";
+import "./input/exports.js";
+
+export class DemoApp extends AppShellMixin({
+ superclass: LitElement,
+ html,
+ translator,
+ routes
+}) {
+ render() {
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+ button-mdc
+ outlined
+ raised
+ secondary
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+}
+customElements.define("demo-app", DemoApp);
diff --git a/src/dialog/exports.js b/src/dialog/exports.js
new file mode 100644
index 0000000..50df41d
--- /dev/null
+++ b/src/dialog/exports.js
@@ -0,0 +1 @@
+export * from "@material/mwc-dialog";
diff --git a/src/dict/npolar/en.js b/src/dict/npolar/en.js
new file mode 100644
index 0000000..b8b998a
--- /dev/null
+++ b/src/dict/npolar/en.js
@@ -0,0 +1,7 @@
+export default {
+ name: "Norwegian Polar Institute",
+ homepage: "https://npolar.no/en",
+ npdc: {
+ name: "Norwegian Polar Data Centre"
+ }
+};
diff --git a/src/dict/npolar/nb.js b/src/dict/npolar/nb.js
new file mode 100644
index 0000000..097b92b
--- /dev/null
+++ b/src/dict/npolar/nb.js
@@ -0,0 +1,7 @@
+export default {
+ name: "Norsk Polarinstitutt",
+ homepage: "https://npolar.no",
+ npdc: {
+ name: "Norwegian Polar Data Centre"
+ }
+};
diff --git a/src/dict/person/en.js b/src/dict/person/en.js
new file mode 100644
index 0000000..48c31ca
--- /dev/null
+++ b/src/dict/person/en.js
@@ -0,0 +1,3 @@
+export default {
+ Contributors: "Contributors"
+};
diff --git a/src/dict/person/nb.js b/src/dict/person/nb.js
new file mode 100644
index 0000000..72f7616
--- /dev/null
+++ b/src/dict/person/nb.js
@@ -0,0 +1,3 @@
+export default {
+ Contributors: "Bidragsytere"
+};
diff --git a/src/fetch-error/fetch-error.js b/src/fetch-error/fetch-error.js
new file mode 100644
index 0000000..76e10bb
--- /dev/null
+++ b/src/fetch-error/fetch-error.js
@@ -0,0 +1,15 @@
+import { SnackError } from "../snack-bar/snack-error.js";
+import { get as t } from "lit-translate";
+export class FetchError extends SnackError {
+ constructor() {
+ super();
+ window.addEventListener(
+ "fetch-error",
+ ({ detail: { status, statusText, method, url, body } }) => {
+ this.labelText = `${t(`${status}`)} ${t(statusText)}`;
+ this.open();
+ }
+ );
+ }
+}
+customElements.define("fetch-error", FetchError);
diff --git a/src/fetch-ok/fetch-ok.js b/src/fetch-ok/fetch-ok.js
new file mode 100644
index 0000000..bb3e864
--- /dev/null
+++ b/src/fetch-ok/fetch-ok.js
@@ -0,0 +1,16 @@
+import { SnackBar } from "../snack-bar/snack-bar.js";
+import { get as tr } from "lit-translate";
+export class FetchOK extends SnackBar {
+ constructor() {
+ super();
+ window.addEventListener(
+ "fetch-ok",
+ ({ detail: { status, statusText, url, method } }) => {
+ console.log("fetch-ok <-", { status, statusText, url, method });
+ this.labelText = `${status} ${tr(statusText)}`;
+ this.open();
+ }
+ );
+ }
+}
+customElements.define("fetch-ok", FetchOK);
diff --git a/src/h.js b/src/h.js
new file mode 100644
index 0000000..b73a2a3
--- /dev/null
+++ b/src/h.js
@@ -0,0 +1,32 @@
+const _h = (text, { html, icon, n }) =>
+ html`
+
+ ${icon
+ ? html`
+
+ `
+ : ""}
+ ${text}
+
+ `;
+
+export const h0 = (text, { html, icon, n = 1 } = {}) =>
+ _h(text, { html, icon, n });
+
+export const h1 = (text, { html, icon, n = 2 } = {}) =>
+ _h(text, { html, icon, n });
+
+export const h2 = (text, { html, icon, n = 3 } = {}) =>
+ _h(text, { html, icon, n });
+
+export const h3 = (text, { html, icon, n = 4 } = {}) =>
+ _h(text, { html, icon, n });
+
+export const h4 = (text, { html, icon, n = 5 } = {}) =>
+ _h(text, { html, icon, n });
+
+export const h5 = (text, { html, icon, n = 6 } = {}) =>
+ _h(text, { html, icon, n });
+
+export const h6 = (text, { html, icon, n = 7 } = {}) =>
+ _h(text, { html, icon, n });
diff --git a/src/host.js b/src/host.js
index 7e23358..6c2114b 100644
--- a/src/host.js
+++ b/src/host.js
@@ -7,3 +7,46 @@ export const emit = (host, name, detail) => {
});
host.dispatchEvent(event);
};
+
+export const addListeners = (host, { to, handlers } = {}) => {
+ console.warn("deprecated, import from host/event.js not host.js");
+ for (const [eventname, on] of handlers) {
+ to.addEventListener(eventname, on);
+ }
+};
+
+export const getCSSProperty = (host, p) =>
+ getComputedStyle(host)
+ .getPropertyValue(p)
+ .trim();
+
+// const toggleCSSProperties = (host, p0, p1) => {
+// const c0 = getComputedStyle(host).getPropertyValue(p0);
+// const c1 = getComputedStyle(host).getPropertyValue(p1);
+// host.style.setProperty(p0, c1);
+// host.style.setProperty(p1, c0);
+// };
+
+export const PRIMARY = "--mdc-theme-primary";
+export const ON_PRIMARY = "--mdc-theme-on-primary";
+export const SECONDARY = "--mdc-theme-secondary";
+export const ON_SECONDARY = "--mdc-theme-on-secondary";
+
+export const getThemeProperties = host => {
+ return {
+ primary: getCSSProperty(host, PRIMARY),
+ onPrimary: getCSSProperty(host, ON_PRIMARY),
+ secondary: getCSSProperty(host, SECONDARY),
+ onSecondary: getCSSProperty(host, ON_SECONDARY)
+ };
+};
+/* eslint-disable consistent-return */
+export const getAttribute = (host, name) => {
+ let element = host;
+ while (element !== null && element.parentElement) {
+ if (element.hasAttribute(name)) {
+ return element.getAttribute(name);
+ }
+ element = element.parentElement;
+ }
+};
diff --git a/src/host/event.js b/src/host/event.js
new file mode 100644
index 0000000..2d5c876
--- /dev/null
+++ b/src/host/event.js
@@ -0,0 +1,19 @@
+export const addListeners = ({ host, attachTo = host, handlers }) => {
+ for (const [eventname, on] of handlers) {
+ attachTo.addEventListener(eventname, on);
+ }
+};
+export const removeListeners = ({ host, attachTo = host, handlers }) => {
+ for (const [eventname, on] of handlers) {
+ attachTo.addEventListener(eventname, on);
+ }
+};
+export const emit = ({ host, name, detail }) => {
+ const event = new CustomEvent(name, {
+ detail,
+ bubbles: true,
+ composed: true,
+ cancelable: true
+ });
+ host.dispatchEvent(event);
+};
diff --git a/src/host/theme.js b/src/host/theme.js
new file mode 100644
index 0000000..815c12b
--- /dev/null
+++ b/src/host/theme.js
@@ -0,0 +1,35 @@
+export const getCSSProperty = (property, { host }) =>
+ getComputedStyle(host)
+ .getPropertyValue(property)
+ .trim();
+
+// const toggleCSSProperties = (host, p0, p1) => {
+// const c0 = getComputedStyle(host).getPropertyValue(p0);
+// const c1 = getComputedStyle(host).getPropertyValue(p1);
+// host.style.setProperty(p0, c1);
+// host.style.setProperty(p1, c0);
+// };
+
+export const PRIMARY = "--mdc-theme-primary";
+export const ON_PRIMARY = "--mdc-theme-on-primary";
+export const SECONDARY = "--mdc-theme-secondary";
+export const ON_SECONDARY = "--mdc-theme-on-secondary";
+
+export const getThemeProperties = host => {
+ return {
+ primary: getCSSProperty(PRIMARY, { host }),
+ onPrimary: getCSSProperty(ON_PRIMARY, { host }),
+ secondary: getCSSProperty(SECONDARY, { host }),
+ onSecondary: getCSSProperty(ON_SECONDARY, { host })
+ };
+};
+/* eslint-disable consistent-return */
+export const getAttributeFromSelfOrAncestor = (name, { host }) => {
+ let element = host;
+ while (element !== null && element.parentElement) {
+ if (element.hasAttribute(name)) {
+ return element.getAttribute(name);
+ }
+ element = element.parentElement;
+ }
+};
diff --git a/src/ic-on/exports.js b/src/ic-on/exports.js
new file mode 100644
index 0000000..912d0ac
--- /dev/null
+++ b/src/ic-on/exports.js
@@ -0,0 +1,2 @@
+export * from "./ic-on.js";
+export * from "@material/mwc-icon";
diff --git a/src/ic-on/ic-on.js b/src/ic-on/ic-on.js
new file mode 100644
index 0000000..8bee9af
--- /dev/null
+++ b/src/ic-on/ic-on.js
@@ -0,0 +1,24 @@
+import { html, LitElement } from "lit-element";
+import styles from "./ic-on-scss.js";
+
+export class IcOn extends LitElement {
+ static get properties() {
+ return {
+ icon: { type: String }
+ };
+ }
+ static get styles() {
+ return styles;
+ }
+ updated(p) {
+ super.updated(p);
+ this.style.setProperty("color", "var(--mdc-theme-primary)");
+ }
+ render() {
+ const { icon } = this;
+ return html`
+
${icon || ""}
+ `;
+ }
+}
+customElements.define("ic-on", IcOn);
diff --git a/src/ic-on/ic-on.scss b/src/ic-on/ic-on.scss
new file mode 100644
index 0000000..f5541ac
--- /dev/null
+++ b/src/ic-on/ic-on.scss
@@ -0,0 +1,10 @@
+@import "../style/shared.scss";
+@import "@material/mwc-icon/src/mwc-icon-host.scss";
+
+:host {
+ color: var(--mdc-theme-primary);
+ font-family: var(--mdc-icon-font, "Material Icons");
+}
+// .mdc-theme--text-icon-on-light {
+// color: var(--mdc-theme-primary);
+// }
diff --git a/src/input/checkbox.js b/src/input/checkbox.js
new file mode 100644
index 0000000..cce3648
--- /dev/null
+++ b/src/input/checkbox.js
@@ -0,0 +1,2 @@
+import "@material/mwc-checkbox/mwc-checkbox.js";
+//export from "checkbox/src/mwc-checkbox-base.js";
diff --git a/src/input/input-search.js b/src/input/input-search.js
index ba9a68b..9b95938 100644
--- a/src/input/input-search.js
+++ b/src/input/input-search.js
@@ -2,7 +2,7 @@ import { Input } from "./input.js";
export class InputSearch extends Input {
constructor() {
super();
- this.icon = "search";
+ this.iconTrailing = "search";
}
}
customElements.define("input-search", InputSearch);
diff --git a/src/input/input-toggle-icon.js b/src/input/input-toggle-icon.js
index 898fc4b..19f2925 100644
--- a/src/input/input-toggle-icon.js
+++ b/src/input/input-toggle-icon.js
@@ -1,7 +1,7 @@
import { Input } from "./input.js";
import "@material/mwc-icon-button-toggle";
import { emit } from "../host.js";
-import { html } from "lit-html";
+//import { html } from "lit-html";
// Outlined input field with (trailing) toggle icon, use like:
//
@@ -13,30 +13,30 @@ export class InputToggleIcon extends Input {
on: { type: Boolean },
autocomplete: { type: String },
onIcon: { type: String },
- offIcon: { type: String }
+ offIcon: { type: String },
};
}
constructor() {
super();
this.on = false;
- this.iconTrailing = true; // needs to be !undefined
+ this.iconTrailing = this.onIcon; // needs to be !undefined
}
- renderIcon() {
- const { onIcon, offIcon, on } = this;
- return html`
-
- `;
- }
+ // renderIcon() {
+ // const { onIcon, offIcon, on } = this;
+ // return html`
+ //
+ // `;
+ // }
connectedCallback() {
super.connectedCallback();
- this.renderRoot.addEventListener("MDCIconButtonToggle:change", e => {
+ this.renderRoot.addEventListener("MDCIconButtonToggle:change", (e) => {
emit(this, "input-toggle-icon", e.detail);
});
}
diff --git a/src/input/input.js b/src/input/input.js
index 83fab36..5e83cf2 100644
--- a/src/input/input.js
+++ b/src/input/input.js
@@ -1,68 +1,94 @@
import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base.js";
import { html } from "lit-html";
import { ifDefined } from "lit-html/directives/if-defined.js";
+import { classMap } from "lit-html/directives/class-map.js";
import { emit } from "../host.js";
import { INPUT, CHANGE } from "../event.js";
import style from "./input-scss.js";
+const { stringify } = JSON;
+
export class Input extends TextFieldBase {
static get styles() {
return style;
}
+}
+
+export class InputText extends TextFieldBase {
+ static get styles() {
+ return style;
+ }
static get properties() {
const reflect = true;
return {
...super.properties,
+ type: { type: String },
value: { type: String },
+ step: { type: String },
autocomplete: { type: String, reflect },
minlength: { type: Number, reflect },
maxlength: { type: Number, reflect },
name: { type: String, reflect },
+ path: { type: String, reflect },
+ size: { type: String, reflect },
readonly: { type: Boolean, reflect },
- autofocus: { type: Boolean, reflect }
+ autofocus: { type: Boolean, reflect },
+ errors: { type: Array }
};
}
+ get valueAsNumber() {
+ return Number(this.value);
+ }
+
constructor() {
super();
this.readonly = false;
this.disabled = false;
this.outlined = true;
this.autofocus = false;
- this.autocomplete = "off"; // @todo check datalist!
+ this.validationMessage = "";
+ this.validateOnInitialRender = true;
+ this.autocomplete = "off";
}
- //
- //
- //
- //
- //
- //
+ get foundation() {
+ return this.mdcFoundation; //MDCTextFieldFoundation
+ }
get input() {
- return this.mdcFoundation;
+ return this.renderRoot.querySelector("input");
}
firstUpdated() {
if (this.hasAttribute("fullwidth")) {
this.removeAttribute("outlined");
}
-
- const input = this.renderRoot.querySelector("input");
- input.addEventListener("input", () => emit(this, INPUT));
- input.addEventListener("change", () => emit(this, CHANGE));
+ this.input.addEventListener("input", () => emit(this, INPUT));
+ this.input.addEventListener("input", e => console.log(this, e));
+ this.input.addEventListener("change", () => emit(this, CHANGE));
super.firstUpdated();
}
updated(p) {
- // Needed or else the floating label does not float when setting value programmatically
if (p.has("value") && "mdcFoundation" in this) {
- this.mdcFoundation.setValue(this.value);
+ //const { value } = this;
+ // [Was] Needed or else the floating label does not float when setting value programmatically
+ //this.mdcFoundation.setValue(value);
+ this.errors = Object.entries(this.validity)
+ .filter(([k, v]) => !["valid", "customError"].includes(k) && v === true)
+ .map(([k]) => k);
+
+ if (this.errors.length > 0) {
+ this.setCustomValidity(stringify(this.errors));
+ } else {
+ this.setCustomValidity("");
+ }
}
super.updated(p);
}
- // Copied from [mwc-textfield-base.ts](https://github.com/material-components/material-components-web-components/blob/master/packages/textfield/src/mwc-textfield-base.ts)
+ // Modified from [mwc-textfield-base.ts](https://github.com/material-components/material-components-web-components/blob/master/packages/textfield/src/mwc-textfield-base.ts)
// to add attributes: "autocomplete", "autofocus", "minlength", "minlength", "name", "readonly"
// Parent's [readme](https://github.com/material-components/material-components-web-components/tree/master/packages/textfield)
renderInput() {
@@ -72,6 +98,7 @@ export class Input extends TextFieldBase {
class="mdc-text-field__input"
type=${this.type}
name=${ifDefined(this.name ? this.name : undefined)}
+ path=${ifDefined(this.path ? this.path : undefined)}
value=${ifDefined(this.value ? this.value : undefined)}
?disabled=${this.disabled}
?readonly=${this.readonly}
@@ -86,6 +113,7 @@ export class Input extends TextFieldBase {
min=${ifDefined(this.min ? Number(this.min) : undefined)}
max=${ifDefined(this.max ? Number(this.max) : undefined)}
step=${ifDefined(this.step ? this.step : undefined)}
+ size=${ifDefined(this.size ? this.size : undefined)}
@input=${this.handleInputChange}
@blur=${this.onInputBlur}
autocomplete=${ifDefined(
@@ -94,5 +122,35 @@ export class Input extends TextFieldBase {
/>
`;
}
+
+ renderHelperText(charCounterTemplate) {
+ const showValidationMessage =
+ this.validationMessage && this.validationMessage.length;
+ const classes = {
+ "mdc-text-field-helper-text--persistent": this.helperPersistent,
+ "mdc-text-field-helper-text--validation-msg": true
+ };
+ const rootClasses = {
+ hidden: !this.shouldRenderHelperText
+ };
+ return html`
+
+
+ ${showValidationMessage ? this.validationMessage : this.helper}
+
+ ${charCounterTemplate}
+
+ `;
+ }
}
customElements.define("input-mdc", Input);
+customElements.define("input-text", class InputText extends Input {});
+customElements.define(
+ "input-number",
+ class InputNumber extends InputText {
+ constructor() {
+ super();
+ this.type = "number";
+ }
+ }
+);
diff --git a/src/input/input.scss b/src/input/input.scss
index 217cc77..1b77522 100644
--- a/src/input/input.scss
+++ b/src/input/input.scss
@@ -1,8 +1,7 @@
-@import "src/style/shared.scss";
-//@import "@material/textfield/mdc-text-field.scss";
+@import "../style/shared.scss";
@import "@material/mwc-textfield/src/mwc-textfield.scss";
// Sets icon color on toggle icon inputs
:host mwc-icon-button-toggle {
- color: var(--mdc-theme-primary); //rgb(22, 70, 104);
+ color: var(--mdc-theme-primary);
}
diff --git a/src/list/base.js b/src/list/base.js
new file mode 100644
index 0000000..7389adf
--- /dev/null
+++ b/src/list/base.js
@@ -0,0 +1,98 @@
+import { MDCList } from "@material/list";
+import { html, LitElement } from "lit-element";
+import style from "./list-scss.js";
+
+export const ul = (list = []) => html`
+ ${
+ list && list.length > 0
+ ? list.map(
+ li => html`${li} `
+ )
+ : ""
+ }
+ `;
+
+export const ol = (list = []) => html`
+ ${
+ list && list.length > 0
+ ? list.map(
+ li => html`${li} `
+ )
+ : ""
+ }
+ `;
+
+const _list = Symbol();
+export class ListBase extends LitElement {
+ static get properties() {
+ const reflect = true;
+ return {
+ list: { type: Array },
+ ordered: { type: Boolean, reflect },
+ "multi-select": { type: Boolean, reflect }
+ };
+ }
+
+ static get styles() {
+ return [style];
+ }
+
+ createList() {
+ const ul = this.renderRoot.querySelector(".mdc-list");
+ const mdclist = new MDCList(ul);
+ mdclist.singleSelection = !this["multi-select"];
+ return mdclist;
+ }
+
+ firstUpdated() {
+ super.firstUpdated();
+ this[_list] = this.createList();
+
+ const slot = this.shadowRoot.querySelector("slot");
+ slot.addEventListener("slotchange", () => {
+ const items = slot.assignedElements();
+
+ items.map(item => {
+ if (
+ ["HTMLLIElement", "HTMLAnchorElement"].includes(item.constructor.name)
+ ) {
+ item.classList.add("mdc-list-item");
+ item.classList.add("mdc-list-item__text");
+ }
+ });
+ });
+ }
+
+ render() {
+ // [Style Customization](https://github.com/material-components/material-components-web/tree/master/packages/mdc-list#style-customization)
+ // CSS Classes
+ // CSS Class Description
+ // mdc-list Mandatory, for the list element.
+ // mdc-list--non-interactive Optional, disables interactivity affordances.
+ // mdc-list--dense Optional, styles the density of the list, making it appear more compact.
+ // mdc-list--avatar-list Optional, configures the leading tiles of each row to display images instead of icons. This will make the graphics of the list items larger.
+ // mdc-list--two-line Optional, modifier to style list with two lines (primary and secondary lines).
+ // mdc-list-item Mandatory, for the list item element.
+ // mdc-list-item__text Mandatory. Wrapper for list item text content (displayed as middle column of the list item).
+ // mdc-list-item__primary-text Optional, primary text for the list item. Should be the child of mdc-list-item__text.
+ // mdc-list-item__secondary-text Optional, secondary text for the list item. Displayed below the primary text. Should be the child of mdc-list-item__text.
+ // mdc-list-item--disabled Optional, styles the row in the disabled state.
+ // mdc-list-item--selected Optional, styles the row in the selected* state.
+ // mdc-list-item--activated Optional, styles the row in the activated* state.
+ // mdc-list-item__graphic Optional, the first tile in the row (in LTR languages, the first column of the list item). Typically an icon or image.
+ // mdc-list-item__meta Optional, the last tile in the row (in LTR languages, the last column of the list item). Typically small text, icon. or image.
+ // mdc-list-group Optional, wrapper around two or more mdc-list elements to be grouped together.
+ // mdc-list-group__subheader Optional, heading text displayed above each list in a group.
+ // mdc-list-divider Optional, for list divider element.
+ // mdc-list-divider--padded Optional, leaves gaps on each side of divider to match padding of list-item__meta.
+ // mdc-list-divider--inset Optional, increases the leading margin of the divider so that it does not intersect the avatar column.
+ // NOTE: The mdc-list-divider class can be used between list items OR between two lists (see respective examples under List Dividers).
+ //
+ // NOTE: In Material Design, the selected and activated states apply in different, mutually-exclusive situations:
+ //
+ // Selected state should be applied on the .mdc-list-item when it is likely to frequently change due to user choice. E.g., selecting one or more photos to share in Google Photos.
+ // Activated state is more permanent than selected state, and will NOT change soon relative to the lifetime of the page. Common examples are navigation components such as the list within a navigation drawer.
+ const { ordered, list } = this;
+ return ordered ? ol(list) : ul(list);
+ }
+}
diff --git a/src/list/compare.js b/src/list/compare.js
new file mode 100644
index 0000000..52e2da1
--- /dev/null
+++ b/src/list/compare.js
@@ -0,0 +1,33 @@
+const lang = "nb";
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator
+const collatorFactory = (
+ locale = lang,
+ {
+ usage = "search",
+ caseFirst = "lower",
+ numeric = true,
+ sensitivity = "base"
+ } = {}
+) =>
+ new Intl.Collator(lang, {
+ ignorePunctuation: true,
+ caseFirst,
+ usage,
+ numeric,
+ sensitivity
+ });
+
+// const searchcollator = collatorFactory(lang, {
+// usage: "search",
+// sensitivity: "base"
+// });
+// var s = "congres";
+// var a = ["Congrès", "congres", "Assemblée", "poisson"];
+// var matches = a.filter(v => searchcollator.compare(v, s) === 0);
+// console.log(matches);
+// console.log(searchcollator.resolvedOptions());
+
+const { compare } = collatorFactory();
+
+export { compare };
diff --git a/src/list/exports.js b/src/list/exports.js
new file mode 100644
index 0000000..e475bd6
--- /dev/null
+++ b/src/list/exports.js
@@ -0,0 +1,8 @@
+import "@material/mwc-menu";
+import "@material/mwc-select";
+import "@material/mwc-list/mwc-list";
+import "@material/mwc-list/mwc-check-list-item.js";
+import "@material/mwc-list/mwc-list-item";
+export * from "./select.js";
+export * from "./list-icon.js";
+export * from "./list-twoline.js";
diff --git a/src/list/li-twoline.js b/src/list/li-twoline.js
new file mode 100644
index 0000000..6d79f04
--- /dev/null
+++ b/src/list/li-twoline.js
@@ -0,0 +1,54 @@
+import "../ic-on/ic-on.js";
+
+const toggleMenuOpen = e => {
+ const menu = e.currentTarget.nextElementSibling.querySelector("#menu");
+ const { open } = menu;
+ menu.open = !open;
+};
+
+export const li2 = ({
+ icon,
+ href,
+ primary,
+ secondary,
+ menu,
+ html
+} = {}) => html`
+
+
+ ${icon}
+
+ ${href
+ ? html`
+
+ ${primary}
+ ${secondary}
+
+ `
+ : html`
+
+ ${primary}
+ ${secondary}
+
+ `}
+ ${menu && menu.length > 0
+ ? html`
+
+
+ more_vert
+
+
+
+
+
+ `
+ : ""}
+
+`;
diff --git a/src/list/li.js b/src/list/li.js
index 96ff5ff..1a11211 100644
--- a/src/list/li.js
+++ b/src/list/li.js
@@ -1,13 +1,25 @@
+import "../ic-on/ic-on.js";
import { html } from "lit-element";
-
-const buttonIcon = icon =>
- html`
-
- `;
-
-export const a = (text, { href, cssclass, icon, tabindex }) => html`
-
- ${icon ? buttonIcon(icon) : ""}
- ${text}
-
+export const li = ({ icon, href, primary }) => html`
+
+
+ ${icon}
+
+ ${href
+ ? html`
+
+ ${primary}
+
+ `
+ : html`
+
+ ${primary}
+
+ `}
+
+
`;
diff --git a/src/list/list-icon.js b/src/list/list-icon.js
new file mode 100644
index 0000000..a750b8a
--- /dev/null
+++ b/src/list/list-icon.js
@@ -0,0 +1,72 @@
+import { html, LitElement } from "lit-element";
+import _ul from "./list-scss.js";
+import { li } from "./li.js";
+
+const renderEntries = (
+ entries = [],
+ { lang, divider = false, graphic = "avatar", twoline = false } = {}
+) =>
+ entries.map(
+ ([primary, { icon, href, divider } = {}]) => html`
+ ${li({ icon, href, primary })}
+ `
+ );
+
+// const isDateTime = (dt, { html, format }) =>
+// html`
+//
format(dt)
+// `;
+
+export class ListIcon extends LitElement {
+ static get properties() {
+ return {
+ entries: { type: Array },
+ lang: { type: String, reflect: true },
+ graphic: { type: String },
+ "on-secondary": { type: Boolean },
+ oneline: { type: Boolean },
+ "non-interactive": { type: Boolean }
+ };
+ }
+ static get styles() {
+ return [_ul];
+ }
+
+ constructor() {
+ super();
+ this.entries = [];
+ this.oneline = true;
+ this.graphic = "avatar";
+ }
+
+ // updated(p) {
+ // const { secondary } = this;
+ // const theme = getThemeProperties(this);
+ //
+ // if (p.has("on-secondary") && secondary) {
+ // console.log({ theme });
+ // const { secondary } = getThemeProperties(this);
+ // const ul = this.renderRoot.querySelector("ul");
+ // ul.style.setProperty("--mdc-theme-primary", "white"); // ic-on color
+ // ul.style.setProperty("color", ON_PRIMARY);
+ // } else {
+ // this.style.removeProperty(PRIMARY);
+ // }
+ // super.updated(p);
+ // }
+ render() {
+ const { entries, lang, graphic, oneline } = this;
+ const interactive = this["non-interactive"] === true ? false : true;
+
+ return html`
+
+ ${renderEntries(entries, { lang, graphic, oneline })}
+
+ `;
+ }
+}
+customElements.define("list-icon", ListIcon);
diff --git a/src/list/list-item.js b/src/list/list-item.js
new file mode 100644
index 0000000..f581e2e
--- /dev/null
+++ b/src/list/list-item.js
@@ -0,0 +1,17 @@
+import { css } from "lit-element";
+import { ListItemBase } from "@material/mwc-list/mwc-list-item-base.js";
+import styles from "./list-item-scss.js";
+
+const jscss = css`
+ :host {
+ font-family: var(--mdc-icon-font, "Inter var");
+ }
+`;
+
+console.log("Deprecated: list-item");
+export class ListItem extends ListItemBase {
+ static get styles() {
+ return [styles, jscss];
+ }
+}
+customElements.define("list-item", ListItem);
diff --git a/src/list/list-item.scss b/src/list/list-item.scss
new file mode 100644
index 0000000..3cbcf5a
--- /dev/null
+++ b/src/list/list-item.scss
@@ -0,0 +1,7 @@
+@import "../style/shared.scss";
+//@import "@material/list/mdc-list-item.scss";
+@import "@material/mwc-list/src/mwc-list-item.scss";
+
+a {
+ color: var(--mdc-theme-primary);
+}
diff --git a/src/list/list-twoline.js b/src/list/list-twoline.js
new file mode 100644
index 0000000..3f453e6
--- /dev/null
+++ b/src/list/list-twoline.js
@@ -0,0 +1,87 @@
+import { html, LitElement } from "lit-element";
+import _ul from "./list-scss.js";
+import { li2 } from "./li-twoline.js";
+
+// import { registerTranslateConfig, get } from "lit-translate";
+//
+// registerTranslateConfig({
+// lookup: (key, config) => (config.strings != null ? config.strings[key] : key),
+// empty: key => key,
+// loader: lang => {
+// switch (lang) {
+// case "da":
+// return {
+// "The page is being loaded...": "Siden indlæses..."
+// };
+// }
+// }
+// });
+
+const renderEntries = (
+ entries = [],
+ { lang, graphic = "avatar", twoline = false } = {}
+) =>
+ entries.map(
+ ([
+ primary,
+ secondary,
+ { icon, href, divider = false, menu = false } = {}
+ ]) => html`
+
+ ${li2({ html, icon, href, primary, secondary, menu })}
+ `
+ );
+
+export class ListTwoline extends LitElement {
+ static get properties() {
+ return {
+ entries: { type: Array },
+ lang: { type: String, reflect: true },
+ graphic: { type: String },
+ "on-secondary": { type: Boolean },
+ twoline: { type: Boolean },
+ "non-interactive": { type: Boolean }
+ };
+ }
+ static get styles() {
+ return [_ul];
+ }
+
+ constructor() {
+ super();
+ this.entries = [];
+ this.twoline = true;
+ this.graphic = "avatar";
+ }
+
+ // updated(p) {
+ // const { secondary } = this;
+ // const theme = getThemeProperties(this);
+ //
+ // if (p.has("on-secondary") && secondary) {
+ // console.log({ theme });
+ // const { secondary } = getThemeProperties(this);
+ // const ul = this.renderRoot.querySelector("ul");
+ // ul.style.setProperty("--mdc-theme-primary", "white"); // ic-on color
+ // ul.style.setProperty("color", ON_PRIMARY);
+ // } else {
+ // this.style.removeProperty(PRIMARY);
+ // }
+ // super.updated(p);
+ // }
+ render() {
+ const { entries, lang, graphic, twoline } = this;
+ const interactive = this["non-interactive"] === true ? false : true;
+
+ return html`
+
+ ${renderEntries(entries, { lang, graphic, twoline })}
+
+ `;
+ }
+}
+customElements.define("list-twoline", ListTwoline);
diff --git a/src/list/list.scss b/src/list/list.scss
index 005198c..181d419 100644
--- a/src/list/list.scss
+++ b/src/list/list.scss
@@ -1,2 +1,7 @@
-@import "src/style/shared.scss";
+@import "../style/shared.scss";
@import "@material/list/mdc-list.scss";
+@import "@material/mwc-list/src/mwc-list.scss";
+
+a {
+ color: var(--mdc-theme-primary);
+}
diff --git a/src/list/ol.js b/src/list/ol.js
new file mode 100644
index 0000000..bcb3d66
--- /dev/null
+++ b/src/list/ol.js
@@ -0,0 +1,93 @@
+import { html } from "lit-html";
+import { ListBase } from "./base.js";
+import { li } from "./li.js";
+
+const lang = "no";
+const { compare } = new Intl.Collator(lang, {
+ ignorePunctuation: true,
+ caseFirst: "lower"
+});
+
+const _renderOL = (
+ list = [],
+ { text, text2, href, li } = {}
+) => html`
+ ${
+ list && list.length > 0
+ ? list.map(item => li(item, { text, href, text2 }))
+ : ""
+ }
+ `;
+
+export class OrderedList extends ListBase {
+ static get properties() {
+ return {
+ ...super.properties,
+ reverse: { type: Boolean },
+ "sort-by": { type: String },
+ text: { type: Function },
+ text2: { type: Function },
+ href: { type: Function },
+ compare: { type: Function }
+ };
+ }
+
+ constructor() {
+ super();
+ this.ordered = true;
+ this.li = li;
+ this.compare = compare;
+ }
+
+ sort(list, { reverse = false } = {}) {
+ const { text, compare } = this;
+
+ if (list && list.length > 1 && text && compare) {
+ if (reverse) {
+ return list.sort((b, a) => compare(text(a), text(b)));
+ }
+ return list.sort((a, b) => compare(text(a), text(b)));
+ } else {
+ return list;
+ }
+ }
+
+ // attributeUpdated(p) {
+ // // if (p.has("list")) {
+ // // const { list, cmp, text } = this;
+ // // if (list && list.length > 1) {
+ // // //this.list = [...list];
+ // // //console.warn(list, text);
+ // // }
+ // // }
+ // // if (p.has("sort")) {
+ // // console.warn(this.sort);
+ // // const { list, sort } = this;
+ // // if (list && list.length > 1 && sort) {
+ // // // this.list = [...list].sort(sort);
+ // // }
+ // // }
+ // super.updated(p);
+ // }
+
+ render() {
+ const { list, href, text, text2, li } = this;
+ return _renderOL(list, { href, text, text2, li });
+ }
+
+ // export const ol = (
+ // list,
+ // {
+ // header = "",
+ // icon = "url",
+ // href = r => `/${r["@id"]}`,
+ // text = r => r.name,
+ // sort = (a, b) => text(a).localeCompare(text(b))
+ // } = {}
+ // ) => html`
+ //
+ // ${(list || []).sort(sort).map(item => _li(item, { href, text }))}
+ //
+ // `;
+}
+customElements.define("ol-mdc", OrderedList);
diff --git a/src/list/select.js b/src/list/select.js
new file mode 100644
index 0000000..eba4a40
--- /dev/null
+++ b/src/list/select.js
@@ -0,0 +1,192 @@
+import { css, html, LitElement } from "lit-element";
+import { classMap } from "lit-html/directives/class-map.js";
+
+import { MDCSelect } from "@material/select";
+import { cssClasses, strings } from "@material/select/";
+const { CHANGE_EVENT } = strings;
+const {
+ ROOT,
+ DISABLED,
+ REQUIRED,
+ OUTLINED,
+ SELECTED,
+ ACTIVATED,
+ SELECTED_ITEM_CLASS
+} = cssClasses;
+import { emit } from "../host.js";
+import { CHANGE } from "../event.js";
+import scss from "./select-scss.js";
+
+// The select uses an MDCMenu component instance to contain the list of options, but uses the data-value attribute instead of value to represent the options' values.
+// NOTE: The data-value attribute must be present on each option.
+
+const listOption = ({
+ value = "",
+ text = value,
+ role = "option",
+ selected,
+ html
+} = {}) =>
+ html`
+
+ ${text}
+
+ `;
+
+const _mdcselect = Symbol();
+export class ListSelect extends LitElement {
+ static get properties() {
+ return {
+ name: { type: String },
+ path: { type: String },
+ value: { type: String },
+ label: { type: String },
+ options: { type: Array },
+ enum: { type: Array },
+ selectedIndex: { type: Number },
+ valid: { type: Boolean },
+ activated: { type: Boolean },
+ selected: { type: Boolean },
+ required: { type: Boolean },
+ disabled: { type: Boolean },
+ outlined: { type: Boolean }
+ };
+ }
+
+ static get styles() {
+ return [scss, css``];
+ }
+
+ constructor() {
+ super();
+ this.outlined = true;
+ this.valid = true;
+ this.required = false;
+ this.disabled = false;
+ this.selected = false;
+ this.selectedIndex = -1;
+ this.activated = true;
+ this.list = [];
+ }
+
+ firstUpdated() {
+ let { options } = this;
+ if (!options && this.enum && this.enum.length > 0) {
+ options = this.enum.map(e => {
+ return { value: e };
+ });
+ this.options = [...options];
+ }
+ let {
+ //name,
+ //path,
+ value,
+ //label,
+ selectedIndex,
+ valid,
+ required,
+ disabled
+ //outlined
+ } = this;
+ if (value !== undefined) {
+ selectedIndex = options.findIndex(o => o.value === value);
+ } else {
+ valid =
+ selectedIndex >= 0 && required === true && value === undefined
+ ? false
+ : true;
+ }
+
+ const host = this.domquery(".mdc-select");
+ const s = new MDCSelect(host);
+
+ // NOTE: To programmatically set a select as required, use the required property in the MDCSelect API.
+ // https://github.com/material-components/material-components-web/tree/master/packages/mdc-select#mdcselect-api
+ s.value = value;
+ s.selectedIndex = selectedIndex;
+ s.required = required;
+ s.disabled = disabled;
+ s.valid = valid;
+ this[_mdcselect] = s;
+ host.addEventListener(CHANGE_EVENT, this);
+ host.addEventListener("change", e => console.log(e));
+ }
+
+ domquery(qs) {
+ return this.renderRoot.querySelector(qs);
+ }
+
+ handleEvent(e) {
+ const { value, index } = e.detail;
+ if (value !== this.value) {
+ const input = this.domquery("input");
+ this.value = value;
+ input.value = value;
+ emit(input, "change", { index, value });
+ emit(input, CHANGE, { index, value });
+ }
+
+ // if (Number(index) === index) {
+ // emit(input, "change", { index, value });
+ // emit(input, CHANGE, { index, value });
+ // }
+ // @todo selectedIndex
+ // console.log(this.selectedIndex);
+ }
+ render() {
+ const {
+ path,
+ name,
+ value,
+ label,
+ options,
+ activated,
+ selected,
+ required,
+ disabled,
+ outlined
+ } = this;
+
+ // https://github.com/material-components/material-components-web/tree/master/packages/mdc-select#accessibility-a11y
+ // https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html
+ return html`
+
`;
+ }
+}
+customElements.define("list-select", ListSelect);
+//
diff --git a/src/list/select.scss b/src/list/select.scss
new file mode 100644
index 0000000..9224244
--- /dev/null
+++ b/src/list/select.scss
@@ -0,0 +1,6 @@
+@import "../style/shared.scss";
+@import "@material/floating-label/mdc-floating-label";
+@import "@material/list/mdc-list";
+@import "@material/menu-surface/mdc-menu-surface";
+@import "@material/menu/mdc-menu";
+@import "@material/select/mdc-select";
diff --git a/src/list/ul.js b/src/list/ul.js
index 18954a4..04e3448 100644
--- a/src/list/ul.js
+++ b/src/list/ul.js
@@ -1,24 +1,3 @@
-import { html } from "lit-element";
-import { ifDefined } from "lit-html/directives/if-defined.js";
-
-export const li = (c, { href, text, tabIndex }) => html`
-
- ${c.title}
-
-`;
-
-export const ul = (data, { href, text } = {}) => {
- let i = 0;
- return html`
-
- ${(data || []).map(o => li(o, { href, text, tabIndex: i++ }))}
-
- `;
-};
-
-export const h1 = (text, { n = 2 } = {}) => `
-
${text}
-`;
+import { ListBase } from "./base.js";
+export class UnorderedList extends ListBase {}
+customElements.define("ul-mdc", UnorderedList);
diff --git a/src/search-any/handlers.js b/src/search-any/handlers.js
new file mode 100644
index 0000000..bcf9a44
--- /dev/null
+++ b/src/search-any/handlers.js
@@ -0,0 +1,9 @@
+import { oninputFactory } from "./oninput.js";
+
+export const handlers = ({ host }) =>
+ new Map([
+ [
+ "input",
+ oninputFactory({ host, selector: host.inputSelector, method: "search" })
+ ]
+ ]);
diff --git a/src/search-any/oninput.js b/src/search-any/oninput.js
new file mode 100644
index 0000000..c32749d
--- /dev/null
+++ b/src/search-any/oninput.js
@@ -0,0 +1,12 @@
+import { set } from "../url/params.js";
+
+export const oninputFactory = ({ host, selector, method }) => () => {
+ // 1. Get query q from input via selector
+ // 2. Reflect q to URL
+ // 3. Perform search via calling method
+ const input = host.renderRoot.querySelector(selector);
+ const { value } = input;
+ const q = value;
+ set("q", q);
+ host[method]({ q });
+};
diff --git a/src/search-any/render-filters.js b/src/search-any/render-filters.js
new file mode 100644
index 0000000..ffaf8c3
--- /dev/null
+++ b/src/search-any/render-filters.js
@@ -0,0 +1,176 @@
+// Normalize terms by removing term prefixes
+// (needed for api v1 when filtering ranges (date/numeric)
+const normalizeTerm = term => (/-/.test(term) ? term.split("-")[1] : term);
+
+// Get URL for a facet filter
+const u = ([term, value, apiuri], { url, action = "add" } = {}) => {
+ // Clone url before mutating searchParams
+ url = new URL(url);
+ const { searchParams } = url;
+ term = normalizeTerm(term);
+ if (apiuri) {
+ // API v1
+ const apiURL = new URL(apiuri);
+ for (let [k, apivalue] of apiURL.searchParams) {
+ k = normalizeTerm(k);
+ if (k === term) {
+ searchParams.set(k, apivalue);
+ }
+ }
+ } else {
+ //API-v2
+ if ("add" === action) {
+ searchParams.set(term, value);
+ } else {
+ searchParams.delete(term);
+ }
+ }
+ return url;
+};
+
+const closeFilter = ({
+ term,
+ value,
+ prefix = `${term}.`,
+ t,
+ label = t(`${prefix}${term}`),
+ url,
+ html
+}) => html`
+
+
+`;
+
+const addTextFilter = ({
+ term,
+ value,
+ t,
+ prefix = `${term}.`,
+ label = t(`${prefix}${value}`),
+ href,
+ url,
+ html
+}) => html`
+
+
+
+`;
+
+const removeTextFilter = ({
+ term,
+ value,
+ prefix = `${term}.`,
+ t,
+ label = t(`${prefix}${value}`),
+ href,
+ url,
+ html
+}) => html`
+
+`;
+
+const removeFilterFromURL = ({ term, url }) => {
+ const urlclone = new URL(url);
+ const { searchParams } = urlclone;
+ searchParams.delete(term);
+ return urlclone;
+};
+// todo ... exclude term, configure special filterUI per term/termType?
+export const renderFilters = ({
+ host,
+ facets = [],
+ filters = [],
+ html,
+ limit,
+ multi = true,
+ translate = true,
+ t,
+ url = new URL(host.ownerDocument.location)
+} = {}) => {
+ const p = new URLSearchParams(url.search);
+
+ // console.warn("renderFilters", {
+ // p,
+ // host,
+ // facets,
+ // filters,
+ // limit,
+ // multi,
+ // url
+ // });
+
+ const facetedTerms = [...facets].map(([term]) => term);
+ const filtersThatAreNotFaceted = [...p]
+ .filter(([term, value]) => {
+ return (
+ !term.startsWith("filter-") && // don't duplicate API v1 style filter-foo=bar
+ !["q", "sort", "limit", ...facetedTerms].includes(term) && // ignore API v1
+ !["page", "start"].includes(term) // ignore API v2
+ );
+ })
+ .map(([term]) => term);
+
+ return html`
+
+ ${facets
+ .filter(
+ multi ? () => true : ([term]) => !filters.has(normalizeTerm(term))
+ )
+ .map(([term, f]) => {
+ const termIsFiltered = filters.has(normalizeTerm(term));
+ return f.length === 0
+ ? ""
+ : html`
+
+ ${termIsFiltered
+ ? closeFilter({ term, value: p.get(term), url, html, t })
+ : html`
+
+
+ `}
+
+
+ ${f.map(([value, count, apiuri]) =>
+ !(p.get(term) || "").split(",").includes(value)
+ ? addTextFilter({
+ term,
+ value,
+ href: u([term, value, apiuri], {
+ url,
+ action: "add"
+ }),
+ t,
+ html
+ })
+ : removeTextFilter({
+ term,
+ value,
+ href: u([term, value, apiuri], {
+ url,
+ action: "remove"
+ }),
+ t,
+ html
+ })
+ )}
+
+ `;
+ })}
+ ${filtersThatAreNotFaceted.map(
+ term =>
+ html`
+ ${closeFilter({ term, url, value: p.get(term), t, html })}
+
+
+
+ `
+ )}
+
+ `;
+};
diff --git a/src/search-any/search-any.js b/src/search-any/search-any.js
new file mode 100644
index 0000000..695c78c
--- /dev/null
+++ b/src/search-any/search-any.js
@@ -0,0 +1,340 @@
+import { LitElement, html, css } from "lit-element";
+
+// Search/query
+import {
+ searchURL,
+ facetEntries as simplifyV1Facets,
+ isAPIv1Param
+} from "@npolar/fetch-api/src/v1/exports.js";
+
+const mapifyV2Facets = (facets = []) =>
+ Object.keys(facets).map(term => [
+ term,
+ facets[term].map(({ value, count }) => [value, count])
+ ]);
+
+// events
+import { addListeners, emit } from "../host/event.js";
+import { handlers } from "./handlers.js";
+import { renderFilters } from "./render-filters.js";
+
+// URLSearchParams
+import {
+ searchParams,
+ remove as removeURLParam
+} from "../url/params.js";
+
+// styling
+import _mdc from "../style/shared-scss.js";
+import _card from "../card/card-scss.js";
+import _style from "./style.js";
+
+//translate
+import { get as t } from "../translate/exports.js";
+
+// side-effects (define custom elements)
+import "../input/exports.js";
+import "../button/exports.js";
+import "../list/list-icon.js";
+
+const { stringify } = JSON;
+
+const renderResults = ({ host, entries = [], html, t } = {}) => html`
+
+ ${entries.map(
+ ({ title, name, ...rest }) =>
+ html`
+ ${stringify(title || name || rest)}
+ `
+ )}
+
+`;
+
+const renderInput = ({ host, params, html, t } = {}) => html`
+
+`;
+// * {
+// --mdc-typography-button-text-transform: uppercase;
+
+// Search Any NPOLAR JSON API (v1 and v2)
+export class SearchAny extends LitElement {
+ static get styles() {
+ return [
+ _mdc,
+ _card,
+ _style,
+ css`
+ a {
+ color: var(--mdc-theme-secondary);
+ }
+ `
+ ];
+ }
+ static get properties() {
+ return {
+ entries: { type: Object },
+ lang: { type: String, reflect: true },
+ offline: { type: Boolean },
+ authorized: { type: Boolean }
+ };
+ }
+
+ constructor({ endpoint, heading } = {}) {
+ super();
+ this.heading = heading || t(`${this.localName}.heading`);
+ this.endpoint = endpoint;
+ this.renderInput = renderInput;
+ this.renderResults = renderResults;
+ this.renderFilters = renderFilters;
+ this.renderBefore = ({ html }) => ``;
+ this.renderAfter = ({ html }) => ``;
+ this.filterLimit = () => 7;
+ this.inputSelector = "input-search";
+ this.filters = new URLSearchParams();
+ this.not = new URLSearchParams();
+ this.handlers = handlers;
+ }
+
+ async search() {
+ let { filters, not } = this;
+ const { offline, endpoint } = this;
+ const params = searchParams();
+
+ const q = params.has("q") ? params.get("q") : "";
+ const fields = params.has("fields") ? params.get("fields") : undefined;
+ const facets = params.has("facets") ? params.get("facets") : undefined;
+ let sort = params.has("sort") ? params.get("sort") : undefined;
+
+ for (let [k, v] of params) {
+ {
+ let m;
+ if ((m = /^no?t?-(.+)$/.exec(k))) {
+ const nokey = m[1];
+ not.set(nokey, v);
+ } else if (!isAPIv1Param(k)) {
+ // API v1 and npdc-common URL compatability, ie. support: filter-area=Svalbard
+ if ((m = /^filter-(.+)$/.exec(k))) {
+ const f = m[1];
+ filters.set(f, v);
+ } else {
+ console.log("@todo auto-filter enable/disable?");
+ filters.set(k, v);
+ }
+ }
+ }
+ }
+
+ if (q !== "") {
+ sort = undefined;
+ removeURLParam("sort");
+ } else {
+ removeURLParam("q");
+ //addURLParam("sort");
+ }
+
+ if (offline === true) {
+ this.searchOffline();
+ } else {
+ const url = this.searchURL({
+ q,
+ fields,
+ endpoint,
+ filters,
+ not,
+ sort,
+ facets,
+ offline
+ });
+ const r = await fetch(url).catch(e => {
+ // @todo handle search-any when search fetch throws and there is no response (no internet)
+ this.searchOffline();
+ });
+
+ if (r && r.ok) {
+ const body = await r.json();
+
+ if (body.feed) {
+ // API v1
+ const {
+ feed: { opensearch, entries, facets }
+ } = body;
+ this.setFeed({
+ opensearch,
+ entries,
+ facets: simplifyV1Facets(facets)
+ });
+ } else if (body.results && body.stats) {
+ // API v2
+ const { totalResults } = body.stats;
+ const { results, facets } = body;
+
+ this.setFeed({
+ opensearch: { totalResults },
+ entries: results,
+ facets: mapifyV2Facets(facets)
+ });
+ }
+ } else {
+ // @todo handle search-any when response status is not 200
+ // this.searchOffline();
+ }
+ }
+ }
+
+ searchURL({ q, endpoint, sort, ...params }) {
+ return searchURL({
+ q,
+ endpoint,
+ sort,
+ limit: 15,
+ ...params,
+ variant: "feed"
+ });
+ }
+
+ // //import { reverseAll } from "@npolar/idb-store/src/reverse.js";
+ // async searchOffline() {
+ // const params = searchParams();
+ // const q = params.has("q") ? params.get("q") : "";
+ // let entries = await reverseAll({
+ // index: "released",
+ // store: this.store
+ // });
+ // if (q && q.length > 0) {
+ // const re = new RegExp(`${q}`, "i");
+ // entries = (entries || []).filter(d => re.test(stringify(d)));
+ // }
+ // this.setFeed({ entries, opensearch: {} });
+ // }
+
+ setFeed({ opensearch, entries, facets }) {
+ this.entries = entries;
+ this.facets = facets || [];
+ this.opensearch = opensearch || {};
+ }
+
+ removeFilter(name) {
+ this.filters.delete(name);
+ removeURLParam(name);
+ this.search();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ emit({
+ host: this,
+ name: "@npolar/title",
+ detail: { title: this.heading }
+ });
+
+ addListeners({
+ host: this,
+ attachTo: this,
+ handlers: this.handlers({ host: this })
+ });
+
+ this.search();
+ }
+
+ // disconnectedCallback() {
+ // removeListeners({
+ // host: this,
+ // attachTo: this,
+ // handlers: handlers({ host: this, window })
+ // });
+ //
+ // super.disconnectedCallback();
+ // }
+
+ render() {
+ const {
+ entries,
+ facets,
+ renderBefore,
+ renderAfter,
+ renderInput,
+ renderResults,
+ renderFilters,
+ filterLimit,
+ opensearch,
+ filters,
+ } = this;
+ const host = this;
+ const params = searchParams();
+ const q = params.get("q");
+ return html`
+
+ ${renderBefore({ host, params, html, t })}
+
+
+ ${[undefined, ""].includes(q)
+ ? html`
+
+
+ ${t(`${this.localName}.welcome`)}
+
+
+ `
+ : ""}
+
+
+ ${renderInput({ host, params, html, t })}
+
+
+
+
+
+
+ ${renderFilters({
+ host,
+ facets,
+ filters,
+ limit: filterLimit,
+ html,
+ t
+ })}
+
+
+
+ ${opensearch && opensearch.totalResults
+ ? html`
+ ${opensearch.totalResults}
+ ${q === ""
+ ? t(`${this.localName}.documents`)
+ : t("opensearch.totalResults")}
+ `
+ : ""}
+
+
+
+ ${renderResults({ host, entries, html, t })}
+
+
+
+ ${renderAfter({ host, params, html, t })}
+
+
+ `;
+ }
+}
+customElements.define("search-any", SearchAny);
+//
+//
+//
+// //
+//
diff --git a/src/search-any/style.js b/src/search-any/style.js
new file mode 100644
index 0000000..1398722
--- /dev/null
+++ b/src/search-any/style.js
@@ -0,0 +1,95 @@
+import { css } from "lit-element";
+
+export default css`
+
+
+ .center {
+ display: grid;
+ align-items: center;
+ justify-items: center;
+ }
+ .search-box {
+ display: grid;
+ grid-template-columns: 0.5fr 18fr 0.5fr;
+ padding: 12px;
+ grid-gap: 0.5em;
+ }
+
+ .search-results {
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-gap: 0.1em;
+ padding: 12px;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ dl,
+ dt,
+ ul,
+ ol,
+ li,
+ p {
+ margin: 0;
+ padding: 0;
+ }
+
+ .h-scroll {
+ overflow-x: scroll;
+ overflow-y: hidden;
+ white-space: nowrap;
+ max-width: 90vw;
+ }
+ .h-scroll .card {
+ display: inline-block;:
+ }
+
+
+
+ .h-scroll {
+ /*height: 80px;
+ margin-bottom: 20px;
+ width: 100%;*/
+ -webkit-overflow-scrolling: touch;
+ }
+ .h-scroll::-webkit-scrollbar {
+ display: none;
+ }
+
+ :host {
+ /* Disables UPPERCASE on everything, intended for facet buttons */
+ --mdc-typography-button-text-transform: none;
+ }
+
+ .primary-fab {
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ }
+ /* Thanks! https://codepen.io/bramus/pen/POEaXg */
+
+ dl {
+ display: grid;
+ grid-template: auto 1fr;
+ }
+
+ dt {
+ grid-column: 1;
+ }
+
+ dd {
+ grid-column: 2;
+ }
+
+ dt,
+ dd {
+ margin: 0;
+ /*padding: 30em 0.5em;/*
+ border-top: 1px solid rgba(0, 0, 0, 0.1); */
+ }
+
+`;
diff --git a/src/search/area.js b/src/search/area.js
new file mode 100644
index 0000000..364e1e8
--- /dev/null
+++ b/src/search/area.js
@@ -0,0 +1,28 @@
+import { html } from "lit-element";
+import { translate as t } from "lit-translate";
+import { classMap } from "lit-html/directives/class-map.js";
+
+const areas = ["Svalbard", "Bouvetøya"];
+export const areaFilterList = selected => html`
+
+
${t("name.Area")}
+
+ ${areas.map(
+ area =>
+ html`
+
+
+ ${area}
+
+ `
+ )}
+
+
+`;
diff --git a/src/select-sort/select-sort.js b/src/select-sort/select-sort.js
new file mode 100644
index 0000000..3348fe2
--- /dev/null
+++ b/src/select-sort/select-sort.js
@@ -0,0 +1,86 @@
+import { LitElement, html, css } from "lit-element";
+import { get as tg } from "lit-translate";
+import { emit } from "../host.js";
+
+export const defaultList = new Map([
+ [undefined, "relevance"],
+ ["-created"],
+ ["-updated"]
+]);
+
+// Example use:
+//
+export class SelectSort extends LitElement {
+ static get properties() {
+ return {
+ sort: { type: String },
+ lang: { type: String },
+ list: { type: Map },
+ translate: { type: Function }
+ };
+ }
+
+ static get styles() {
+ return css`
+ .mdc-select__selected-text {
+ color: red;
+ }
+ `;
+ }
+
+ constructor() {
+ super();
+ this.translate = tg;
+ this.list = defaultList;
+ }
+
+ // firstUpdated() {
+ // this[_slot] = this.renderRoot.querySelector("slot");
+ // window.addEventListener("@npolar/lang", ({ detail: { lang } }) => {
+ // this.lang = lang;
+ // console.warn("FIXME
is not refreshed on language change");
+ // // This cure is ineffective:
+ // // const select = this.renderRoot.querySelector("mwc-select");
+ // // select.layout();
+ // });
+ // }
+
+ // updated(p) {
+ // if (p.has("list")) {
+ // console.log(this.list);
+ // console.warn(this[_slot]);
+ // }
+ // }
+
+ render() {
+ const { list, sort, lang } = this;
+
+ return html`
+
+ ${[...list].map(
+ ([field, name]) => html`
+
+ emit(this, "@npolar/sort", {
+ sort: field,
+ lang: lang,
+ label: tg(`sort.by.${name || field}`)
+ })}"
+ graphic="icon"
+ value="${field}"
+ >
+ ${tg(`sort.by.${name || field}`)}
+
+ `
+ )}
+
+ `;
+ }
+}
+customElements.define("select-sort", SelectSort);
diff --git a/src/select/exports.js b/src/select/exports.js
index aa21c39..b798a0e 100644
--- a/src/select/exports.js
+++ b/src/select/exports.js
@@ -1,2 +1,7 @@
-export * from "./select.js";
-export * from "./select-enum.js";
+export * from "@material/mwc-select";
+export * from "@material/mwc-list";
+export * from "@material/mwc-list/mwc-list-item";
+
+//export * from "./select.js";
+//export * from "./select-enum.js";
+//export * from "@material/list/list-item.js";
diff --git a/src/select/select-enum.js b/src/select/select-enum.js
index 1cd09cf..1a0fc6e 100644
--- a/src/select/select-enum.js
+++ b/src/select/select-enum.js
@@ -1,26 +1,26 @@
-import { render } from "lit-html";
-import { Select } from "./select.js";
-import { options } from "./option-html.js";
-
-// Convenience element that inject an element for each enum member
-// Use:
-//
-//
-// @todo SelectEnum i18n
-// @todo SelectEnum: move render of options to parent
-export class SelectEnum extends Select {
- static get properties() {
- return {
- ...super.properties,
- enum: { type: Array }
- };
- }
-
- firstUpdated() {
- const select = this.renderRoot.querySelector("select");
- render(options(this.value, { enum: this.enum }), select);
- super.firstUpdated();
- }
-}
-customElements.define("select-enum", SelectEnum);
+// import { render } from "lit-html";
+// import { Select } from "./select.js";
+// import { options } from "./option-html.js";
+//
+// // Convenience element that inject an element for each enum member
+// // Use:
+// //
+// //
+// // @todo SelectEnum i18n
+// // @todo SelectEnum: move render of options to parent
+// export class SelectEnum extends Select {
+// static get properties() {
+// return {
+// ...super.properties,
+// enum: { type: Array }
+// };
+// }
+//
+// firstUpdated() {
+// const select = this.renderRoot.querySelector("select");
+// render(options(this.value, { enum: this.enum }), select);
+// super.firstUpdated();
+// }
+// }
+// customElements.define("select-enum", SelectEnum);
diff --git a/src/select/select-options.js b/src/select/select-options.js
new file mode 100644
index 0000000..30e1af6
--- /dev/null
+++ b/src/select/select-options.js
@@ -0,0 +1,24 @@
+import { Select } from "./select.js";
+
+// Convenience element that inject an element for each enum member
+// Use:
+//
+//
+// @todo SelectEnum i18n
+// @todo SelectEnum: move render of options to parent
+export class SelectOptions extends Select {
+ static get properties() {
+ return {
+ ...super.properties,
+ enum: { type: Array }
+ };
+ }
+
+ firstUpdated() {
+ // const select = this.renderRoot.querySelector("select");
+ // render(options(this.value, { enum: this.enum }), select);
+ // super.firstUpdated();
+ }
+}
+customElements.define("select-options", SelectOptions);
diff --git a/src/select/select.js b/src/select/select.js
index 5548733..362d174 100644
--- a/src/select/select.js
+++ b/src/select/select.js
@@ -1,77 +1,9 @@
-import { css, html } from "lit-element";
-import sharedStyle from "../style/shared-scss.js";
-import { ifDefined } from "lit-html/directives/if-defined.js";
-import { emit } from "../host.js";
-import { INPUT, CHANGE } from "../event.js";
-import { Select as WLSelect } from "weightless/select";
-
-// Select element extending https://github.com/andreasbm/weightless/tree/master/src/lib/select
-export class Select extends WLSelect {
- static get properties() {
- const reflect = true;
- return {
- ...super.properties,
- role: { type: String, reflect }
- };
- }
+import { SelectBase } from "@material/mwc-select/mwc-select-base.js";
+import styles from "./select-scss.js";
+export class Select extends SelectBase {
static get styles() {
- return [
- super.styles,
- sharedStyle,
- css`
- :host {
- --input-font-family: "Inter var";
- --input-color: var(--mdc-theme-secondary);
- --input-label-color: var(--mdc-theme-primary);
- --select-arrow-height: 4;
- }
- `
- ];
- }
-
- constructor() {
- super();
- this.outlined = true;
- this.role = "listbox";
- this.label = this.name;
- }
-
- firstUpdated() {
- const select = this.renderRoot.querySelector("select");
- select.addEventListener("input", () => emit(this, INPUT));
- select.addEventListener("change", () => emit(this, CHANGE));
- super.firstUpdated();
- }
-
- // Added https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role
- // @todo File issue for missing aria role
- renderFormElement() {
- return html`
-
- `;
+ return styles;
}
}
-customElements.define("select-mdc", Select);
+customElements.define("select-1", Select);
diff --git a/src/select/select.scss b/src/select/select.scss
new file mode 100644
index 0000000..11dd2af
--- /dev/null
+++ b/src/select/select.scss
@@ -0,0 +1,2 @@
+@import "../style/shared.scss";
+@import "@material/mwc-select/src/mwc-select.scss";
diff --git a/src/sign-in/render.js b/src/sign-in/render.js
new file mode 100644
index 0000000..2393ce5
--- /dev/null
+++ b/src/sign-in/render.js
@@ -0,0 +1,67 @@
+import { html as h } from "lit-element";
+
+//import { linearProgress } from "@npolar/m-html/src/progress/linear-progress.js";
+//import { translate as t } from "./translate.js";
+const translate = (s, lang = "en") => `${lang !== "en" ? "" : ""}${s}`; // { user, lang = getLang(user) } = {}
+const t = translate;
+//import anonymousAvatar from "./account_box_svg.js"; //html/src/icon/svg/,,,
+
+export default host => {
+ const {
+ email,
+ emailChanged,
+ method,
+ password,
+ lang,
+ signIn,
+ onetimeAction,
+ } = host;
+
+ // ${anonymousAvatar}
+
+ return h`
+
+
+
+
+
+
+
+
+
+ ${t(
+ "sign in",
+ lang
+ )}
+
+
+
+
+ ${t("sign in with email", lang)}
+
+
+
+
+
+
+ (host.method = "password")}">
+ security
+ Password
+
+ (host.method = "email")}">
+ mail
+ Email
+
+
+
+
+`;
+};
diff --git a/src/sign-in/sign-in.js b/src/sign-in/sign-in.js
new file mode 100644
index 0000000..4870195
--- /dev/null
+++ b/src/sign-in/sign-in.js
@@ -0,0 +1,155 @@
+// https://www.chromium.org/developers/design-documents/form-styles-that-chromium-understands
+import { LitElement } from "lit-element";
+import { emit } from "../host.js";
+//import { userStore as store } from "../store/user-store.js";
+import { isValid as isValidJWT, payload } from "@npolar/fetch-api/src/jwt.js";
+import {
+ authenticate,
+ forceNpolarIfDomainIsMissing
+} from "@npolar/fetch-api/src/v1/user/authenticate.js";
+import { onetime } from "@npolar/fetch-api/src/v1/user/onetime.js";
+import render from "./render.js";
+import style from "./style.js";
+import listStyle from "@npolar/mdc/src/list/list-scss.js";
+
+const formatDateTime = (
+ dt,
+ locales = "default",
+ options = {
+ dateStyle: "medium",
+ timeStyle: "short",
+ hour12: false
+ }
+) => new Intl.DateTimeFormat(locales, options).format(dt);
+
+const SIGNIN_EVENT = "@npolar/sign-in";
+// const { warn, error } = console;
+// const { stringify } = JSON;
+
+export class SignIn extends LitElement {
+ static get properties() {
+ const reflect = true;
+ return {
+ email: { type: String, reflect },
+ password: { type: String },
+ method: { type: String },
+ user: { type: Object },
+ jwt: { type: String },
+ statusText: { type: String },
+ response: { type: Object }
+ };
+ }
+ static get styles() {
+ return [listStyle, style];
+ }
+
+ constructor() {
+ super();
+ this.email = localStorage.getItem("email") || "";
+ this.password = "";
+ this.method = "password";
+ this.jwt = "";
+ this.statusText = "";
+ this.response = { status: 0 };
+ }
+
+ handleEvent(e) {
+ const [input] = e.composedPath();
+ const { name, value } = input;
+ if (["email", "password"].includes(name)) {
+ this[name] = value;
+ }
+ }
+
+ async firstUpdated(props) {
+ const form = this.renderRoot.querySelector("form");
+ form.addEventListener("input", this);
+ super.firstUpdated(props);
+ // const jwt = await store.get("jwt");
+ // const user = await store.get("user");
+ // if (jwt && jwt.length) {
+ // this.jwt = jwt;
+ // }
+ // if (user && user.email) {
+ // this.user = user;
+ // this.email = user.email;
+ // }
+
+ // Handle sign-in via email (1-time password)
+ const params = new URLSearchParams(location.search);
+ if (params.has("email") && params.has("p1")) {
+ this.email = params.get("email");
+ this.password = params.get("p1");
+ params.delete("p1");
+ params.delete("email");
+ this.signIn();
+ }
+ }
+
+ async onetimeAction() {
+ const { email } = this;
+ const response = await onetime({ email });
+ const o = await response.json();
+ const { status, statusText } = response;
+ this.response = { ...o, status, statusText };
+ this.statusText = 200 === status ? "Check email to sign-in" : "";
+ // @todo https://material.io/develop/web/components/snackbars/ instead of statusText for successes?
+ }
+
+ async signIn() {
+ let { email, password, user, jwt } = this;
+ if (!email && user && user.email) {
+ email = user.email;
+ this.email = email;
+ }
+ if (!user && email && email.length) {
+ user = { email };
+ }
+ if (!isValidJWT(jwt)) {
+ jwt = null;
+ }
+ const response = await authenticate({ email, password, jwt });
+ const obj = await response.json();
+ const { status, statusText } = response;
+ this.response = { ...obj, status, statusText };
+ this.statusText =
+ statusText === "OK" ? "Signed in at " + new Date().toJSON() : statusText;
+ if (200 === response.status) {
+ const jwt = obj.token;
+ if (isValidJWT(jwt)) {
+ const iat = parseInt(new Date() / 1000);
+ user = { iat, ...payload(jwt) };
+ //store.set("user", user);
+ //store.set("jwt", jwt);
+
+ this.jwt = jwt;
+ emit(this, SIGNIN_EVENT, { jwt, user });
+ }
+ }
+ }
+
+ async emailChanged() {
+ this.email = forceNpolarIfDomainIsMissing(this.email);
+ }
+
+ render() {
+ const { jwt, user, statusText } = this;
+ if (statusText.length === 0 && user && user.exp && user.iat) {
+ const expires = new Date(user.exp * 1000);
+ const issued = new Date(user.iat * 1000);
+ if (new Date() > expires) {
+ this.statusText = `Session expired ${formatDateTime(expires)}`;
+ } else if (jwt && jwt.length) {
+ if (isValidJWT(jwt)) {
+ this.statusText = `Already signed in: session expires ${formatDateTime(
+ expires
+ )}`;
+ } else {
+ this.statusText = `Last signed in ${formatDateTime(issued)}`;
+ }
+ }
+ }
+ return render(this);
+ }
+}
+customElements.define("sign-in", SignIn);
diff --git a/src/sign-in/style.js b/src/sign-in/style.js
new file mode 100644
index 0000000..7ed14d5
--- /dev/null
+++ b/src/sign-in/style.js
@@ -0,0 +1,22 @@
+import { css } from "lit-element";
+
+export default css`
+ form,
+ input-mdc,
+ input-password {
+ display: grid;
+ min-width: 300px;
+ max-width: 600px;
+ margin: 10px auto;
+ }
+ .account_box {
+ width: 20%;
+ display: inline-block;
+ }
+ .error {
+ color: rgba(176, 0, 32);
+ }
+ .info {
+ font-weight: normal;
+ }
+`;
diff --git a/src/signed-in/signed-in.js b/src/signed-in/signed-in.js
new file mode 100644
index 0000000..a3240cb
--- /dev/null
+++ b/src/signed-in/signed-in.js
@@ -0,0 +1,17 @@
+import { SnackBar } from "../snack-bar/snack-bar.js";
+import { get as tr } from "lit-translate";
+export class SignedIn extends SnackBar {
+ constructor() {
+ super();
+ window.addEventListener("@npolar/sign-in", ({ detail: { jwt, user } }) => {
+ console.warn("@npolar/sign-in", { jwt, user });
+ this.labelText = `${user.name} ${tr("signed-in")}`;
+ this.open();
+ });
+ window.addEventListener("@npolar/sign-out", ({ detail: { user } }) => {
+ this.labelText = `${user.name} ${tr("signed-out")}`;
+ this.open();
+ });
+ }
+}
+customElements.define("signed-in", SignedIn);
diff --git a/src/snack-bar/snack-bar.js b/src/snack-bar/snack-bar.js
new file mode 100644
index 0000000..035445b
--- /dev/null
+++ b/src/snack-bar/snack-bar.js
@@ -0,0 +1,21 @@
+import { css } from "lit-element";
+import { SnackbarBase } from "@material/mwc-snackbar/mwc-snackbar-base.js";
+import { style } from "@material/mwc-snackbar/mwc-snackbar-css.js";
+
+export class SnackBar extends SnackbarBase {
+ static get styles() {
+ return [
+ style,
+ css`
+ .mdc-snackbar__label {
+ font-family: var(--mdc-theme-font-family);
+ }
+ `
+ ];
+ }
+ constructor() {
+ super();
+ this.leading = true;
+ }
+}
+customElements.define("snack-bar", SnackBar);
diff --git a/src/snack-bar/snack-error.js b/src/snack-bar/snack-error.js
new file mode 100644
index 0000000..fdbc277
--- /dev/null
+++ b/src/snack-bar/snack-error.js
@@ -0,0 +1,19 @@
+import { SnackBar } from "./snack-bar.js";
+import { css } from "lit-element";
+export class SnackError extends SnackBar {
+ static get styles() {
+ return [
+ super.styles,
+ css`
+ .mdc-snackbar__surface {
+ background: var(--mdc-theme-error);
+ }
+ `
+ ];
+ }
+ constructor() {
+ super();
+ this.leading = false;
+ }
+}
+customElements.define("snack-error", SnackError);
diff --git a/src/style/_core.scss b/src/style/_core.scss
new file mode 100644
index 0000000..42a0161
--- /dev/null
+++ b/src/style/_core.scss
@@ -0,0 +1,30 @@
+// See
+// https://material.io/develop/web/docs/theming/
+// https://material.io/design/color/the-color-system.html#
+// https://css-tricks.com/material-theming-making-material-your-own/
+$mdc-theme-primary: #124f78; /* Equals: rgb(18, 79, 120)- darker: rgb(22, 70, 104) */
+$mdc-theme-secondary: #006db3;
+// $mdc-theme-surface: white;
+// $mdc-theme-on-primary: white;
+// $mdc-theme-on-secondary: white;
+$mdc-theme-on-surface: #202525;
+$mdc-theme-error: #b00020;
+
+/* #124f78; #1d5c86 #164668; #263238; */
+/*--mdc-typography--font-family: "Inter var", sans-serif; */
+
+/* v2 #006db 3
+ #559be5
+ #004383
+ #ffffff
+ */
+/*
+ #039be5
+ #63ccff
+ #006db3
+ #263238
+ #4f5b62
+ #000a12
+ #000000
+ #ffffff
+ */
diff --git a/src/style/color.js b/src/style/color.js
new file mode 100644
index 0000000..da682be
--- /dev/null
+++ b/src/style/color.js
@@ -0,0 +1,21 @@
+export const primary = "#124f78";
+export const secondary = "#006db3";
+
+// $mdc-theme-surface: white;
+// $mdc-theme-on-primary: white;
+// $mdc-theme-on-secondary: white;
+// $mdc-theme-on-surface: #202525;
+// $mdc-theme-error: #b00020;
+
+// https://material.io/resources/color/#!/?view.left=0&view.right=0&primary.color=124f78&secondary.color=66BB6A
+// https://codepen.io/una/pen/VJMBbx
+//
+// #124f78
+// #4b7ba7
+// #00274c
+// #66bb6a
+// #98ee99
+// #338a3e
+// #ffffff
+// #000000
+//
diff --git a/src/style/shared.scss b/src/style/shared.scss
index aa84d7e..20ef4ef 100644
--- a/src/style/shared.scss
+++ b/src/style/shared.scss
@@ -1,29 +1,17 @@
-:host {
- --mdc-theme-primary: rgb(18, 79, 120);
- --mdc-theme-on-primary: #fff;
- --mdc-theme-secondary: #202525;
- --mdc-theme-on-secondary: #fff;
- --mdc-theme-error: #b00020;
- padding: 0;
- margin: 0;
-}
+@import "./_core.scss";
+@import "@material/theme/mdc-theme.scss";
+@import "./typography.scss";
:host[hidden] {
display: none;
}
-
-html,
-body {
- margin: 0;
- padding: 0;
-}
-
+:host a,
a {
- // color: var(--mdc-theme-primary);
text-decoration: none;
}
-
-@import "src/style/typography.scss";
-*[slot] {
- font-family: $mdc-typography-font-family;
+.error {
+ --mdc-theme-primary: var(--mdc-theme-error);
+}
+*:not(:defined) {
+ display: none;
}
diff --git a/src/style/typography.scss b/src/style/typography.scss
index 1bf7ceb..babf5e1 100644
--- a/src/style/typography.scss
+++ b/src/style/typography.scss
@@ -1,7 +1,10 @@
-$mdc-typography-font-family: unquote("Inter var, sans-serif") !default;
+// https://github.com/material-components/material-components-web/tree/master/packages/mdc-typography#css-custom-properties
+$mdc-typography-font-family: unquote("Inter var, sans-serif") !important !default;
-// The relative font URLs below work from the generated /css/style/typography.css
-// Use by including
+@import "@material/typography/mdc-typography.scss";
+
+// The relative font URLs below work from the compiled typography.css
+// Use by including
@font-face {
font-family: "Inter var";
font-weight: 100 900;
@@ -19,4 +22,13 @@ $mdc-typography-font-family: unquote("Inter var, sans-serif") !default;
src: url("../../font/inter/Inter-italic.var.woff2") format("woff2");
}
-@import "@material/typography/mdc-typography.scss";
+[slot] {
+ font-family: $mdc-typography-font-family !important;
+}
+
+//https://github.com/material-components/material-components-web-components/issues/1050#issuecomment-606179722
+// @use "@material/typography";
+//
+// .my-headline {
+// @include typography.typography(headline1);
+// }
diff --git a/src/switch/exports.js b/src/switch/exports.js
new file mode 100644
index 0000000..cb853db
--- /dev/null
+++ b/src/switch/exports.js
@@ -0,0 +1,2 @@
+export * from "./switch-on.js";
+export * from "./render.js";
diff --git a/src/switch/render.js b/src/switch/render.js
new file mode 100644
index 0000000..2511578
--- /dev/null
+++ b/src/switch/render.js
@@ -0,0 +1,8 @@
+import "@material/mwc-formfield";
+import "@material/mwc-switch";
+
+export const render = ({ checked, label, id = label, html } = {}) => html`
+
+
+
+`;
diff --git a/src/switch/switch-on.js b/src/switch/switch-on.js
new file mode 100644
index 0000000..5b57b95
--- /dev/null
+++ b/src/switch/switch-on.js
@@ -0,0 +1,58 @@
+import { css, html } from "lit-element";
+import { SwitchBase } from "@material/mwc-switch/mwc-switch-base.js";
+import { style } from "@material/mwc-switch/mwc-switch-css.js";
+import { ripple } from "@material/mwc-ripple/ripple-directive.js";
+
+import "@material/mwc-formfield";
+
+// export const render = ({ checked, label, id = label, html } = {}) => html`
+//
+//
+//
+// `;
+
+export class SwitchOn extends SwitchBase {
+ static get styles() {
+ return [
+ style,
+ css`
+ .mdc-switch {
+ font-family: var(--mdc-theme-font-family);
+ }
+ `
+ ];
+ }
+
+ firstUpdated() {
+ ///super.firstUpdated();
+ this.mdcRoot.addEventListener("change", e => {
+ this.dispatchEvent(new Event("change", e));
+ });
+ }
+
+ __render() {
+ return html`
+
+
+ `;
+ }
+}
+customElements.define("switch-on", SwitchOn);
diff --git a/src/tab-bar/tab-bar.js b/src/tab-bar/tab-bar.js
new file mode 100644
index 0000000..6144033
--- /dev/null
+++ b/src/tab-bar/tab-bar.js
@@ -0,0 +1,2 @@
+export * from "@material/mwc-tab-bar";
+export * from "@material/mwc-tab";
diff --git a/src/tab-bar/tab-bar.scss b/src/tab-bar/tab-bar.scss
new file mode 100644
index 0000000..5953d53
--- /dev/null
+++ b/src/tab-bar/tab-bar.scss
@@ -0,0 +1,15 @@
+@import "../style/shared.scss";
+
+@import "@material/tab-bar/mdc-tab-bar";
+@import "@material/tab-scroller/mdc-tab-scroller";
+@import "@material/tab-indicator/mdc-tab-indicator";
+@import "@material/tab/mdc-tab";
+@import "@material/mwc-tab-bar/src/mwc-tab-bar.scss";
+
+.tab {
+ display: none;
+}
+
+.tab--active {
+ display: block;
+}
diff --git a/src/table-edit/exports.js b/src/table-edit/exports.js
new file mode 100644
index 0000000..7fcabe1
--- /dev/null
+++ b/src/table-edit/exports.js
@@ -0,0 +1 @@
+export * from "./table-edit.js";
diff --git a/src/table-edit/html.js b/src/table-edit/html.js
new file mode 100644
index 0000000..3900f24
--- /dev/null
+++ b/src/table-edit/html.js
@@ -0,0 +1,111 @@
+const mdt = "mdc-data-table";
+const mcb = "mdc-checkbox";
+
+const __cell = `${mdt}__cell`;
+
+// export const cssClasses = {
+// CELL: 'mdc-data-table__cell',
+// CELL_NUMERIC: 'mdc-data-table__cell--numeric',
+// CONTENT: 'mdc-data-table__content',
+// HEADER_ROW: 'mdc-data-table__header-row',
+// HEADER_ROW_CHECKBOX: 'mdc-data-table__header-row-checkbox',
+// ROOT: 'mdc-data-table',
+// ROW: 'mdc-data-table__row',
+// ROW_CHECKBOX: 'mdc-data-table__row-checkbox',
+// ROW_SELECTED: 'mdc-data-table__row--selected',
+// };
+
+export const cell = (
+ content,
+ {
+ numeric = false,
+
+ html
+ } = {}
+) => html`
+
+ ${content}
+
+`;
+
+export const columnheader = (
+ text,
+ {
+ role = "columnheader",
+ scope = "col",
+ numeric = false,
+ checkbox = false,
+ html
+ } = {}
+) => html`
+
+`;
+
+const _checkbox = ({ checked = false, html }) => {
+ const ch = html`
+
+
+ `;
+ return columnheader(ch, { html });
+};
+
+export const checkbox = ({ html, checked }) => {
+ // checked=>mdc-checkbox--selected ?
+ const cb = html`
+
+ ${_checkbox({ checked, html })}
+
+ `;
+ return cb;
+};
+
+export const columnheaderCheckbox = ({ html }) => {
+ const ch = html`
+
+ `;
+ return columnheader(ch, { html });
+};
+
+//
+// ${patches.map(({ op, path, value, from, extra }) => {
+// const { when, prev, initial } = extra;
+// return html`
+//
+//
+// ${path}
+//
+// ${stringify(value)}
+//
+//
+// ${ds(when)}
+//
+//
+// `;
+// })}
+//
diff --git a/src/table-edit/table-edit.js b/src/table-edit/table-edit.js
new file mode 100644
index 0000000..0892bcb
--- /dev/null
+++ b/src/table-edit/table-edit.js
@@ -0,0 +1,218 @@
+import { css, html, LitElement } from "lit-element";
+import { translate as t } from "lit-translate";
+
+import { emit } from "../host.js";
+
+import "../button/exports.js";
+import { scss, events, MDCDataTable } from "../table/exports.js";
+
+import { cell, checkbox, columnheader, columnheaderCheckbox } from "./html.js";
+const ds = (
+ datestring = new Date().toJSON(),
+ locale = "default",
+ { datestyle = "long" } = {}
+) =>
+ new Intl.DateTimeFormat(locale, { dateStyle: "long" }).format(
+ new Date(datestring)
+ );
+
+const ts = (
+ datestring = new Date().toJSON(),
+ locale = "default",
+ { datestyle, timeStyle = "short" } = {}
+) =>
+ new Intl.DateTimeFormat(locale, { datestyle, timeStyle }).format(
+ new Date(datestring)
+ );
+
+const { stringify } = JSON;
+
+const { SELECTED_ALL, UNSELECTED_ALL, ROW_SELECTION_CHANGED } = events;
+
+const _dataTable = Symbol();
+
+export class TableEdit extends LitElement {
+ static get styles() {
+ return [
+ scss,
+ css`
+ header {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ justify-content: flex-end;
+ font-family: "Inter var";
+ background: var(--mdc-theme-secondary);
+ color: var(--mdc-theme-on-secondary);
+ }
+ ,
+ mwc-icon-button {
+ color: var(--mdc-theme-secondary);
+ }
+ `
+ ];
+ }
+ static get properties() {
+ return {
+ header: { type: String },
+ patches: { type: Array },
+ selectedSet: { type: Set },
+ setkey: { type: Function },
+ lang: { type: String },
+ time: { type: Number }
+ };
+ }
+
+ constructor() {
+ super();
+ this.selectedSet = new Set();
+ this.setkey = (p, rowIndex) => rowIndex; //p.path;
+ window.addEventListener("langChanged", e => {
+ const { lang } = e.detail;
+ this.lang = lang;
+ });
+ }
+
+ updated(p) {
+ if (p.has("patches")) {
+ this[_dataTable].foundation_.adapter_.registerRowCheckboxes();
+ }
+ }
+
+ firstUpdated(p) {
+ const host = this.renderRoot.querySelector(".mdc-data-table");
+ this[_dataTable] = new MDCDataTable(host); //@todo destroy
+
+ const { patches, selectedSet, setkey } = this;
+
+ //This select all rows (checked checkboxes)
+ let i = 0;
+ this.selectedSet = new Set(this.patches.map(p => this.setkey(p, i++)));
+
+ host.addEventListener(ROW_SELECTION_CHANGED, ({ detail }) => {
+ const { rowIndex, selected } = detail;
+
+ const key = setkey(patches[rowIndex], rowIndex);
+ if (selected === true) {
+ selectedSet.add(key);
+ } else {
+ selectedSet.delete(key);
+ }
+ this.time = Number(new Date());
+ });
+
+ host.addEventListener(SELECTED_ALL, () => {
+ let r = 0;
+ this.selectedSet = new Set(this.patches.map(p => setkey(p, r++)));
+ });
+
+ host.addEventListener(UNSELECTED_ALL, () => (this.selectedSet = new Set()));
+ }
+
+ async undo(e) {
+ const { index, key } = e.target.dataset;
+ let j = 0;
+ const patches = [...this.patches];
+ const { setkey } = this;
+
+ const removedIndex = patches.findIndex(p => key === setkey(p, j++));
+ console.log(index, removedIndex);
+ //@todo assert index==removedIndex
+ const [removed] = patches.splice(Number(index), 1);
+
+ emit(this, "@npolar/local-edits", {
+ op: "remove",
+ path: removed.path,
+ value: removed
+ });
+ }
+
+ render() {
+ const {
+ header,
+ patches,
+ selectedSet,
+ undo,
+ setkey
+ } = this;
+ let u = 0;
+ return html`
+
+
+
+ ${header} ${JSON.stringify([...selectedSet])}
+
+
+
+
+
+
+
+
+ ${patches.map(
+ ({ name, op, path, value, from, extra = {} } = {}) => {
+ const { when, remote } = extra;
+ const key = setkey({ op, path, value, from, extra }, u);
+ const selected = selectedSet.has(key);
+ return html`
+
+
+
+ ${checkbox({ html, checked: selected })}
+
+
+ ${cell(name["@value"], { html })} ${cell(op, { html })}
+ ${cell(
+ html`
+
+ ${stringify(value)}
+
+ ${stringify(remote)}
+
+ `,
+ { html, id: `u${u}` }
+ )}
+ ${cell(ds(when), { numeric: true, html })}
+ ${cell(ts(when), { numeric: true, html })}
+ ${cell(
+ html`
+
+ `,
+ { numeric: true, html }
+ )}
+
+ `;
+ }
+ )}
+
+
+
+ `;
+ }
+}
+customElements.define("table-edit", TableEdit);
diff --git a/src/table/exports.js b/src/table/exports.js
new file mode 100644
index 0000000..b6bdc59
--- /dev/null
+++ b/src/table/exports.js
@@ -0,0 +1,5 @@
+export * from "./td.js";
+export { MDCDataTable, events } from "@material/data-table";
+import scss from "./table-scss.js";
+export { scss };
+//export { events } from "@material/data-table/constants";
diff --git a/src/table/table.scss b/src/table/table.scss
new file mode 100644
index 0000000..6739d99
--- /dev/null
+++ b/src/table/table.scss
@@ -0,0 +1,3 @@
+@import "../style/shared.scss";
+@import "@material/checkbox/mdc-checkbox";
+@import "@material/data-table/mdc-data-table";
diff --git a/src/table/td.js b/src/table/td.js
new file mode 100644
index 0000000..05d8016
--- /dev/null
+++ b/src/table/td.js
@@ -0,0 +1,10 @@
+//
+//import { classMap } from "lit-html/directives/class-map.js";
+
+export const td = (text, {numeric=false,html,classMap=()=>""}={}) => {
+ return html`
${text} `;
+};
diff --git a/src/text-area/exports.js b/src/text-area/exports.js
new file mode 100644
index 0000000..26b49d0
--- /dev/null
+++ b/src/text-area/exports.js
@@ -0,0 +1,3 @@
+export * from "./text-area.js";
+export * from "./text-area-per-language.js";
+export * from "@material/mwc-textarea";
diff --git a/src/text-area/style.js b/src/text-area/style.js
new file mode 100644
index 0000000..f401ee2
--- /dev/null
+++ b/src/text-area/style.js
@@ -0,0 +1,2 @@
+import { css } from "lit-element";
+export default css``;
diff --git a/src/text-area/text-area-per-language.js b/src/text-area/text-area-per-language.js
new file mode 100644
index 0000000..1ae473d
--- /dev/null
+++ b/src/text-area/text-area-per-language.js
@@ -0,0 +1,33 @@
+import { get as g } from "lit-translate";
+
+export const textAreaPerLanguage = (
+ prop,
+ obj,
+ {
+ base = "/",
+ path = (prop, lang) => `${base}${prop}/${lang}`,
+ fullwidth = true,
+ outlined = true,
+ rows = 4,
+ label = (prop, lang) => g(`${prop}/${lang}`),
+ schema,
+ languages = schema.languages,
+ html
+ }
+) =>
+ html`
+
+ ${languages.map(lang => {
+ return html`
+
+ `;
+ })}
+
+ `;
diff --git a/src/text-area/text-area.js b/src/text-area/text-area.js
new file mode 100644
index 0000000..f01be8d
--- /dev/null
+++ b/src/text-area/text-area.js
@@ -0,0 +1,65 @@
+import { TextAreaBase } from "@material/mwc-textarea/mwc-textarea-base.js";
+import { html } from "lit-html";
+
+import { ifDefined } from "lit-html/directives/if-defined.js";
+
+import scss from "./textarea-scss.js";
+import style from "./style.js";
+
+export class Textarea extends TextAreaBase {
+ static get styles() {
+ return [scss, style];
+ }
+
+ static get properties() {
+ return {
+ name: { type: String },
+ path: { type: String },
+ value: { type: String },
+ label: { type: String },
+ rows: { type: Number },
+ cols: { type: Number },
+ disabled: { type: Boolean },
+ outlined: { type: Boolean }
+ };
+ }
+
+ // render() {
+ // const classes = {
+ // "mdc-text-field--disabled": this.disabled,
+ // "mdc-text-field--no-label": !this.label,
+ // "mdc-text-field--outlined": this.outlined,
+ // "mdc-text-field--fullwidth": this.fullWidth
+ // };
+ // return html`
+ //
+ // ${this.renderCharCounter()}
+ // ${this.renderInput()}
+ // ${this.outlined ? this.renderOutlined() : this.renderLabelText()}
+ //
+ // ${this.renderHelperText()}
+ // `;
+ // }
+ renderInput() {
+ const maxOrUndef = this.maxLength === -1 ? undefined : this.maxLength;
+ return html`
+
+ `;
+ }
+}
+
+customElements.define("text-area", Textarea);
diff --git a/src/text-area/textarea.scss b/src/text-area/textarea.scss
new file mode 100644
index 0000000..35eaf23
--- /dev/null
+++ b/src/text-area/textarea.scss
@@ -0,0 +1,7 @@
+@import "../style/shared.scss";
+@import "@material/mwc-textarea/src/mwc-textarea.scss";
+
+// Sets icon color on toggle icon inputs
+// :host mwc-icon-button-toggle {
+// color: var(--mdc-theme-primary);
+// }
diff --git a/src/translate/exports.js b/src/translate/exports.js
new file mode 100644
index 0000000..611ba56
--- /dev/null
+++ b/src/translate/exports.js
@@ -0,0 +1,8 @@
+import { registerTranslateConfig, get, use, translate } from "lit-translate";
+
+const register = (lang, { loader, ...translateConfig } = {}) => {
+ registerTranslateConfig({ loader, ...translateConfig });
+ return use(lang);
+};
+
+export { use, get, translate, register, registerTranslateConfig };
diff --git a/src/translate/language.js b/src/translate/language.js
new file mode 100644
index 0000000..2c30a53
--- /dev/null
+++ b/src/translate/language.js
@@ -0,0 +1,29 @@
+export const en = "en";
+export const nn = "nn";
+export const no = "no";
+export const nb = "nb";
+const norwegian = [nn, no, nb];
+
+const chop = code => code.substring(0, 2);
+export const isEnglish = code => chop(code) === en;
+export const isNorwegian = code => norwegian.includes(chop(code));
+
+export const browserLanguages = () => {
+ return [...new Set(window.navigator.languages.map(l => chop(l)))];
+};
+
+export const browserLanguage = () => {
+ const [lang] = browserLanguages();
+ return lang;
+};
+
+export const prefer = ({ fallback = en } = {}) => {
+ const b = browserLanguage();
+ if (isEnglish(b)) {
+ return en;
+ } else if (isNorwegian(b)) {
+ return b;
+ } else {
+ return fallback;
+ }
+};
diff --git a/src/translate/loader.js b/src/translate/loader.js
new file mode 100644
index 0000000..6749b28
--- /dev/null
+++ b/src/translate/loader.js
@@ -0,0 +1,14 @@
+import en from "./text/en.js";
+import nn from "./text/nn.js";
+
+// Lazy loading with ES2020 [dynamic import](https://v8.dev/features/dynamic-import):
+// export const loader = async lang => {
+// const m = await import(`/text/${lang}.js`);
+// return m.default;
+// };
+
+export const loader = async lang => {
+ return lang === "nn" ? nn : en;
+};
+
+//export const loader = dicts => async lang => dicts.get(lang);
diff --git a/src/translate/text/en.js b/src/translate/text/en.js
new file mode 100644
index 0000000..4c4ec1d
--- /dev/null
+++ b/src/translate/text/en.js
@@ -0,0 +1,138 @@
+export default {
+ app: {
+ name: "Place names in Norwegian polar areas",
+ heading: "Place names",
+ welcome: "Norway's polar place names"
+ },
+ any: {
+ Edit: "Edit",
+ New: "Add",
+ rel: "connected to"
+ },
+ case: {
+ case: "case",
+ sak: "case",
+ Case: "Case",
+ Cases: "Cases",
+ New: "Add case"
+ },
+ country: {
+ undefined: "Country missing",
+ Country: "Country",
+ AR: "Argentina",
+ AU: "Australia",
+ AQ: "Antarctica",
+ BE: "Belgium",
+ CL: "Chile",
+ DE: "Germany",
+ GB: "United Kingdom of Great Britain and Northern Ireland",
+ IN: "India",
+ JP: "Japan",
+ NO: "Norway",
+ NZ: "New Zealand",
+ RU: "Russian Federation",
+ US: "United States of America",
+ ZA: "South Africa"
+ },
+ https: {
+ npolar: {
+ no: "https://npolar.no/en"
+ }
+ },
+ lang: {
+ Language: "Language",
+ en: "English",
+ nn: "Norwegian (nynorsk)"
+ },
+ name: {
+ new: {
+ input: {
+ label: "Nytt namn",
+ placeholder: "Nytt namn"
+ },
+ suggest: {
+ text:
+ "Alle kan sende inn forslag til stadnamn. Hugs å ha med med informasjon om kva namnet tyder og nøyaktig plassering."
+ }
+ },
+ history: {
+ Headline: "400 years of naming",
+ "Caption-prefix":
+ "Interactive graph of the number of names originating from each"
+ },
+ definition: "definition",
+ origin: "origin",
+ note: "note",
+ Area: "Area",
+ Facts: "Facts",
+ "Replaced-by": "Replaced by",
+ Placename: "place name",
+ Placenames: "place names",
+ "Not-unique": "Place name is already defined",
+ Origin: "Origin",
+ Replaces: "Replaces",
+ "Same-as": "Same as",
+ Definition: "Definition",
+ Definitions: "Definitions",
+ Note: "Note",
+ New: "New place name",
+ Now: "Now"
+ },
+ patch: {
+ op: "operation",
+ value: "value",
+ path: "path",
+ from: "from",
+ add: "add",
+ replace: "replace",
+ move: "move",
+ extra: {
+ when: "changed"
+ }
+ },
+ period: {
+ decade: "decade",
+ year: "year"
+ },
+ ref: {
+ New: "New reference",
+ References: "References"
+ },
+ "sign-in": {
+ "Sign in": "Sign in"
+ },
+ status: {
+ official: "official",
+ historical: "historical",
+ switch: "Official"
+ },
+ search: {
+ Search: "Search",
+ Filter: "Filter",
+ "Filter-close": "Filter",
+ input: {
+ placeholder: "Search"
+ }
+ },
+ sort: {
+ by: {
+ relevance: "Relevance score",
+ updated: "Recently updated",
+ "-updated": "Least recently updated",
+ created: "Least recently created",
+ "-created": "Recent names",
+ name: "Alphabetical",
+ "properties.label,name.@value": "Alphabetical",
+ "-name": "Reverse alphabetical"
+ }
+ },
+ text: {
+ "missing-nn": "Missing Norwegian (nynorsk)",
+ "missing-en": "Missing English"
+ },
+ "used-since": "since",
+ vocab: {
+ area: "area",
+ country: "country"
+ }
+};
diff --git a/src/translate/text/nn.js b/src/translate/text/nn.js
new file mode 100644
index 0000000..a6e4cb7
--- /dev/null
+++ b/src/translate/text/nn.js
@@ -0,0 +1,150 @@
+import { nn as country } from "../../../vocab/country.js";
+import { nn as terrain } from "../../../vocab/terrain.js";
+
+export default {
+ app: {
+ name: "Stadnamn i norske polarområde",
+ heading: "Stadnamn",
+ welcome: {
+ area: {
+ undefined: "Norske polare stadnamn",
+ Svalbard: "Stadnamn på Svalbard"
+ }
+ }
+ },
+ any: {
+ Edit: "Skriv",
+ New: "Legg til",
+ rel: "kobla til"
+ },
+ case: {
+ case: "sak",
+ sak: "sak",
+ Case: "Sak",
+ Cases: "Saker",
+ New: "Ny sak",
+ Committee: "Stadnamnkomitéen",
+ search: { placeholder: "Søk i protokollane til stadnamnkomitéen" }
+ },
+ country,
+ https: {
+ npolar: {
+ no: "https://npolar.no"
+ }
+ },
+ lang: {
+ Language: "Språk",
+ en: "English",
+ nn: "Norsk (nynorsk)"
+ },
+ "local-edits": {
+ Commit: "Lagre",
+ "Uncommited-changes": "Lokale endringar"
+ },
+ name: {
+ new: {
+ input: {
+ label: "Nytt namn",
+ placeholder: "Nytt namn"
+ },
+ suggest: {
+ text:
+ "Alle kan sende inn forslag til stadnamn. Namna skal ha nynorsk målform og bør vere høvelege, stutte og velklingande. Hugs å ha med med informasjon om kva namnet tyder og nøyaktig plassering.",
+ details:
+ "Forslag handsamast av stadnamnkomitéen, som er offisiell forvaltar av stadnamn i dei norske polarområda.",
+ href:
+ "https://www.npolar.no/nyhet/sjekk-lista-over-nye-stadnamn-pa-svalbard/",
+ hreftext: "Meir informasjon",
+ hreflang: "nn",
+ action: "Foreslå stadnamn"
+ }
+ },
+ Facts: "Fakta",
+ history: {
+ Headline: "400 år med namn",
+ "Caption-prefix": "Interaktiv graf over namngjevingar per"
+ },
+ definition: "definisjon",
+ origin: "opphav",
+ note: "merk",
+ Area: "Område",
+ Definition: "Definisjon",
+ Definitions: "Definisjonar",
+ Origin: "Opphav",
+ New: "Nytt stadnamn",
+ Note: "Merk",
+ Now: "No",
+ "Not-unique": "Dette namnet finst frå før",
+ "Replaced-by": "Nytt namn",
+ Name: "Namn",
+ Placename: "Stadnamn",
+ Placenames: "Stadnamn",
+ Proposer: "Foreslått av",
+ Replaces: "Tidlegare namn",
+ "Same-as": "Same namn",
+ Suggestion: "Forslag"
+ },
+ status: {
+ suggestion: "forslag",
+ official: "offisielt",
+ historical: "historisk",
+ standardised: "standardisert",
+ switch: "Offisielle"
+ },
+ patch: {
+ op: "handling",
+ value: "verdi",
+ path: "sti",
+ from: "frå",
+ add: "legg til",
+ replace: "bytt",
+ move: "flytt",
+ extra: {
+ when: "endra"
+ }
+ },
+ period: {
+ decade: "tiår",
+ year: "år"
+ },
+ ref: {
+ New: "Ny refereanse",
+ References: "Referansar",
+ search: { placeholder: "Søk i referansar" }
+ },
+ search: {
+ Search: "Search",
+ Filter: "Filter",
+ "Filter-close": "Filter",
+ input: {
+ placeholder: "Søk"
+ }
+ },
+ sort: {
+ by: {
+ relevance: "Høgast relevans",
+ "-updated": "Sist oppdatert",
+ "-created": "Nyaste stadnamn",
+ created: "Først oppretta",
+ name: "Alfabetisk",
+ "properties.label,name.@value": "Alfabetisk",
+ "-name": "Omvendt alfabetisk"
+ }
+ },
+ "sign-in": {
+ "Sign in": "Sign in"
+ },
+ text: {
+ "missing-nn": "Nynorsk manglar",
+ "missing-en": "Engelsk manglar"
+ },
+ terrain,
+ "used-since": "frå",
+ vocab: {
+ area: "område",
+ country: "nasjon"
+ },
+ opensearch: {
+ totalResults: "treff"
+ }
+};
diff --git a/src/url/params.js b/src/url/params.js
new file mode 100644
index 0000000..e7463b3
--- /dev/null
+++ b/src/url/params.js
@@ -0,0 +1,40 @@
+export const searchParams = ({
+ location = window.location,
+ url = new URL(location)
+} = {}) => {
+ const { searchParams } = new URL(url);
+ return searchParams;
+};
+const _sp = searchParams;
+
+export const get = (
+ name,
+ { location = window.location, searchParams = _sp(location) } = {}
+) => searchParams.get(name);
+
+export const add = (
+ [name, value],
+ { location = window.location, url = new URL(location) } = {}
+) => {
+ const { searchParams } = url;
+ if (value === undefined) {
+ searchParams.delete(name);
+ } else {
+ searchParams.set(name, value);
+ }
+ history.replaceState(null, "", url.href);
+};
+
+export const set = (name, value, { url = new URL(window.location) } = {}) => {
+ const { searchParams } = url;
+ if (value === undefined) {
+ searchParams.delete(name);
+ } else {
+ searchParams.set(name, value);
+ }
+ history.replaceState(null, "", url.href);
+};
+
+export const remove = (name, { url = new URL(window.location) } = {}) => {
+ set(name, undefined, { url });
+};
diff --git a/tpl-export-scss b/tpl-export-scss
new file mode 100644
index 0000000..51055e4
--- /dev/null
+++ b/tpl-export-scss
@@ -0,0 +1,2 @@
+import {css} from "lit-element";
+export default css`<% content %>`;