Skip to content

Instantly share code, notes, and snippets.

@sandor
Last active September 20, 2017 18:31
Show Gist options
  • Select an option

  • Save sandor/99b163d057abcbf1e66c2b991e2ea3aa to your computer and use it in GitHub Desktop.

Select an option

Save sandor/99b163d057abcbf1e66c2b991e2ea3aa to your computer and use it in GitHub Desktop.
(function () {
'use strict';
// @copyright
// © 2016-2017 Jarosław Foksa
//
// Pointer events polyfills
//
// Make "click", "dblclick" and "contextmenu" look more like pointer events
// (https://github.com/w3c/pointerevents/issues/100#issuecomment-23118584)
{
if (MouseEvent.prototype.hasOwnProperty("pointerType") === false) {
Object.defineProperty(MouseEvent.prototype, "pointerType", {
get() {
return this.sourceCapabilities.firesTouchEvents ? "touch" : "mouse";
}
});
}
}
// Make setPointerCapture also capture the cursor image
{
let setPointerCapture = Element.prototype.setPointerCapture;
Element.prototype.setPointerCapture = function(pointerId) {
setPointerCapture.call(this, pointerId);
let cursor = getComputedStyle(this).cursor;
let styleElement = document.createElement("style");
styleElement.textContent = `body, * { cursor: ${cursor} !important; user-select: none !important; }`;
document.head.append(styleElement);
let finish = () => {
window.removeEventListener("pointerup", finish, true);
this.removeEventListener("lostpointercapture", finish);
styleElement.remove();
};
window.addEventListener("pointerup", finish, true);
this.addEventListener("lostpointercapture", finish);
};
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Web Animations API polyfills
//
{
let Animation = document.createElement("div").animate({}).constructor;
Object.defineProperty(Animation.prototype, "finished", {
get() {
return new Promise((resolve) => {
this.playState === "finished" ? resolve() : this.addEventListener("finish", () => resolve(), {once: true});
});
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Node polyfills (http://dom.spec.whatwg.org, https://github.com/whatwg/dom/issues/161)
//
if (!Node.prototype.append) {
Node.prototype.append = function(child) {
this.appendChild(child);
};
}
if (!Node.prototype.prepend) {
Node.prototype.prepend = function(child) {
this.insertBefore(child, this.firstElementChild);
};
}
if (!Node.prototype.before) {
Node.prototype.before = function(element) {
this.parentElement.insertBefore(element, this);
};
}
if (!Node.prototype.after) {
Node.prototype.after = function(element) {
this.parentElement.insertBefore(element, this.nextElementSibling);
};
}
if (!Node.prototype.replace) {
Node.prototype.replace = function(element) {
this.parentNode.replaceChild(element, this);
};
}
if (!Node.prototype.closest) {
Node.prototype.closest = function(selector) {
return this.parentNode ? this.parentNode.closest(selector) : null;
};
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// DOMPoint polyfill (http://dev.w3.org/fxtf/geometry/#DOMPoint)
//
{
try {
new SVGPoint();
}
catch (error) {
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
let SVGPoint = new Proxy(window.SVGPoint, {
construct(target, args) {
let point = svg.createSVGPoint();
let [x, y] = args;
if (x) { point.x = x; }
if (y) { point.y = y; }
return point;
}
});
window.SVGPoint = SVGPoint;
}
if (!SVGPoint.fromPoint) {
SVGPoint.fromPoint = function(otherPoint) {
return otherPoint ? new SVGPoint(otherPoint.x, otherPoint.y) : new SVGPoint();
};
}
if (!SVGPoint.prototype.toString) {
SVGPoint.prototype.toString = function() {
return "point(" + this.x + ", " + this.y + ")";
};
}
if (!window.DOMPoint) {
window.DOMPoint = SVGPoint;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// DOMRect polyfill (http://dev.w3.org/fxtf/geometry/#DOMRect)
//
{
// Make SVGRect behave like DOMRect
try {
new SVGRect();
}
catch (error) {
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
let SVGRect = new Proxy(window.SVGRect, {
construct(target, args) {
let rect = svg.createSVGRect();
let [x, y, width, height] = args;
if (x) { rect.x = x; }
if (y) { rect.y = y; }
if (width) { rect.width = width; }
if (height) { rect.height = height; }
return rect;
}
});
window.SVGRect = SVGRect;
}
if (!SVGRect.fromRect) {
SVGRect.fromRect = function(otherRect) {
return otherRect ? new SVGRect(otherRect.x, otherRect.y, otherRect.width, otherRect.height) : new SVGRect();
};
}
if (SVGRect.prototype.hasOwnProperty("top") === false) {
Object.defineProperty(SVGRect.prototype, "top", {
enumerable: true,
get() {
return Math.min(this.y, this.y + this.height);
}
});
}
if (SVGRect.prototype.hasOwnProperty("right") === false) {
Object.defineProperty(SVGRect.prototype, "right", {
enumerable: true,
get() {
return Math.max(this.x, this.x + this.width);
}
});
}
if (SVGRect.prototype.hasOwnProperty("bottom") === false) {
Object.defineProperty(SVGRect.prototype, "bottom", {
enumerable: true,
get() {
return Math.max(this.y, this.y + this.height);
}
});
}
if (SVGRect.prototype.hasOwnProperty("left") === false) {
Object.defineProperty(SVGRect.prototype, "left", {
enumerable: true,
get() {
return Math.min(this.x, this.x + this.width);
}
});
}
// Make ClientRect behave like DOMRect
if (window.ClientRect) {
if (window.ClientRect.prototype.hasOwnProperty("x") === false) {
Object.defineProperty(window.ClientRect.prototype, "x", {
get() {
return this.left;
},
set(value) {
this.left = value;
}
});
}
if (window.ClientRect.prototype.hasOwnProperty("y") === false) {
Object.defineProperty(window.ClientRect.prototype, "y", {
get() {
return this.top;
},
set(value) {
this.top = value;
}
});
}
}
// Expose DOMRect
if (!window.DOMRect) {
window.DOMRect = SVGRect;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// DOMMatrix polyfill (http://dev.w3.org/fxtf/geometry/#DOMMatrix)
//
{
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
let SVGMatrix = new Proxy(window.SVGMatrix, {
construct(target, args) {
let matrix = svg.createSVGMatrix();
if (args[0] !== undefined) {
if (Array.isArray(args[0]) && args[0].length === 6) {
let [a, b, c, d, e, f] = args[0];
matrix.a = a;
matrix.b = b;
matrix.c = c;
matrix.d = d;
matrix.e = e;
matrix.f = f;
}
else {
throw new TypeError("Invalid argument passed to SVGMatrix constructor.");
}
}
return matrix;
}
});
window.SVGMatrix = SVGMatrix;
SVGMatrix.fromMatrix = (matrix) => {
let {a, b, c, d, e, f} = matrix;
return new SVGMatrix([a, b, c, d, e, f]);
};
SVGMatrix.prototype.transformPoint = function(point) {
let transformedPoint = new SVGPoint();
transformedPoint.x = this.a * point.x + this.c * point.y + this.e;
transformedPoint.y = this.b * point.x + this.d * point.y + this.f;
return transformedPoint;
};
SVGMatrix.prototype.determinant = function() {
let det = (this.a * this.d) - (this.b * this.c);
return det;
};
SVGMatrix.prototype.isIdentity = function() {
if (this.a === 1 && this.b === 0 && this.c === 0 && this.d === 1 && this.e === 0 && this.f === 0) {
return true;
}
else {
return false;
}
};
SVGMatrix.prototype.multiplySelf = function(matrix) {
let a = (this.a * matrix.a) + (this.c * matrix.b);
let b = (this.b * matrix.a) + (this.d * matrix.b);
let c = (this.a * matrix.c) + (this.c * matrix.d);
let d = (this.b * matrix.c) + (this.d * matrix.d);
let e = (this.a * matrix.e) + (this.c * matrix.f) + this.e;
let f = (this.b * matrix.e) + (this.d * matrix.f) + this.f;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
return this;
};
SVGMatrix.prototype.preMultiplySelf = function(matrix) {
let a = (matrix.a * this.a) + (matrix.c * this.b);
let b = (matrix.b * this.a) + (matrix.d * this.b);
let c = (matrix.a * this.c) + (matrix.c * this.d);
let d = (matrix.b * this.c) + (matrix.d * this.d);
let e = (matrix.a * this.e) + (matrix.c * this.f) + matrix.e;
let f = (matrix.b * this.e) + (matrix.d * this.f) + matrix.f;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
return this;
};
SVGMatrix.prototype.translateSelf = function(tx, ty) {
let translateTransform = {a: 1, b: 0, c: 0, d: 1, e: tx, f: ty};
this.multiplySelf(translateTransform);
return this;
};
SVGMatrix.prototype._translateOriginSelf = function(x, y) {
this.e = this.e + (x * this.a) + (y * this.c);
this.f = this.f + (x * this.b) + (y * this.d);
return this;
};
SVGMatrix.prototype._translateOrigin = function(x, y) {
let transform = new SVGMatrix();
transform.a = this.a;
transform.b = this.b;
transform.c = this.c;
transform.d = this.d;
transform.e = this.e + (x * this.a) + (y * this.c);
transform.f = this.f + (x * this.b) + (y * this.d);
return transform;
};
SVGMatrix.prototype.scaleSelf = function(scale, originX, originY) {
if (originX === undefined) { originX = 0; }
if (originY === undefined) { originY = 0; }
let scaleTransform = {a: scale, b: 0, c: 0, d: scale, e: 0, f: 0};
this._translateOriginSelf(originX, originY);
this.multiplySelf(scaleTransform);
this._translateOriginSelf(-originX, -originY);
return this;
};
SVGMatrix.prototype.scaleNonUniformSelf = function(scaleX, scaleY, originX, originY) {
if (scaleY === undefined) { scaleY = 1; }
if (originX === undefined) { originX = 0; }
if (originY === undefined) { originY = 0; }
let scaleTransform = {a: scaleX, b: 0, c: 0, d: scaleY, e: 0, f: 0};
this._translateOriginSelf(originX, originY);
this.multiplySelf(scaleTransform);
this._translateOriginSelf(-originX, -originY);
return this;
};
SVGMatrix.prototype.rotate = function(angle, originX, originY) {
if (originX === undefined) { originX = 0; }
if (originY === undefined) { originY = 0; }
let angleRad = (Math.PI * angle) / 180;
let cosAngle = Math.cos(angleRad);
let sinAngle = Math.sin(angleRad);
let rotTransform = {a: cosAngle, b: sinAngle, c: -sinAngle, d: cosAngle, e: 0, f: 0};
let rotateTransform = this._translateOrigin(originX, originY);
rotateTransform.multiplySelf(rotTransform);
rotateTransform._translateOriginSelf(-originX, -originY);
return rotateTransform;
};
SVGMatrix.prototype.rotateSelf = function(angle, originX, originY) {
if (originX === undefined) { originX = 0; }
if (originY === undefined) { originY = 0; }
let angleRad = (Math.PI * angle) / 180;
let cosAngle = Math.cos(angleRad);
let sinAngle = Math.sin(angleRad);
let rotTransform = {a: cosAngle, b: sinAngle, c: -sinAngle, d: cosAngle, e: 0, f: 0};
this._translateOriginSelf(originX, originY);
this.multiplySelf(rotTransform);
this._translateOriginSelf(-originX, -originY);
return this;
};
SVGMatrix.prototype.skewXSelf = function(angle) {
let angleRad = (Math.PI * angle) / 180;
let skewTransform = {a: 1, b: 0, c: Math.tan(angleRad), d: 1, e: 0, f: 0};
this.multiplySelf(skewTransform);
return this;
};
SVGMatrix.prototype.skewYSelf = function(angle) {
let angleRad = (Math.PI * angle) / 180;
let skewTransform = {a: 1, b: Math.tan(angleRad), c: 0, d: 1, e: 0, f: 0};
this.multiplySelf(skewTransform);
return this;
};
SVGMatrix.prototype.invertSelf = function(sy) {
let det = this.determinant();
if (det !== 0) {
let a = this.d / det;
let b = -this.b / det;
let c = -this.c / det;
let d = this.a / det;
let e = (this.c * this.f - this.d * this.e) / det;
let f = (this.b * this.e - this.a * this.f) / det;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
}
return this;
};
SVGMatrix.prototype.toString = function() {
return "matrix(" + this.a + ", " + this.b + ", " + this.c + ", " + this.d + ", " + this.e + ", " + this.f + ")";
};
if (SVGMatrix.prototype.hasOwnProperty("is2D") === false) {
Object.defineProperty(SVGMatrix.prototype, "is2D", {
enumerable: true,
get() { return true; }
});
}
if (!window.DOMMatrix) {
window.DOMMatrix = SVGMatrix;
}
}
// @copyright
// © 2016-2017 Jarosław Foksa
let templateElement = document.createElement("template");
// @info
// Template string tag used to parse HTML strings.
// @type
// () => HTMLElement || DocumentFragment
let html = (strings, ...expressions) => {
let parts = [];
for (let i = 0; i < strings.length; i += 1) {
parts.push(strings[i]);
if (expressions[i] !== undefined) parts.push(expressions[i]);
}
let innerHTML = parts.join("");
templateElement.innerHTML = innerHTML;
let fragment = document.importNode(templateElement.content, true);
if (fragment.children.length === 1) {
return fragment.firstElementChild;
}
else {
return fragment;
}
};
// @info
// Template string tag used to parse SVG strings.
// @type
// () => SVGElement || DocumentFragment
let svg = (strings, ...expressions) => {
let parts = [];
for (let i = 0; i < strings.length; i += 1) {
parts.push(strings[i]);
if (expressions[i] !== undefined) parts.push(expressions[i]);
}
let innerHTML = `<svg id="x-stub" xmlns="http://www.w3.org/2000/svg">${parts.join("")}</svg>`;
templateElement.innerHTML = innerHTML;
let fragment = document.importNode(templateElement.content, true);
let stub = fragment.querySelector("svg#x-stub");
if (stub.children.length === 1) {
return stub.firstElementChild;
}
else {
for (let child of [...stub.childNodes]) {
fragment.appendChild(child);
}
stub.remove();
return fragment;
}
};
// @info
// Same as document.createElement(), but you can also create SVG elements.
// @type
// (string) => Element?
let createElement = (name, is = null) => {
let parts = name.split(":");
let element = null;
if (parts.length === 1) {
let [localName] = parts;
if (is === null) {
element = document.createElement(localName);
}
else {
element = document.createElement(localName, is);
}
}
else if (parts.length === 2) {
let [namespace, localName] = parts;
if (namespace === "svg") {
element = document.createElementNS("http://www.w3.org/2000/svg", localName);
}
}
return element;
};
// @info
// Same as standard element.closest() method but can also walk shadow DOM.
// @type
// (Element, string, boolean) => Element?
let closest = (element, selector, walkShadowDOM = true) => {
let matched = element.closest(selector);
if (walkShadowDOM && !matched && element.getRootNode().host) {
return closest(element.getRootNode().host, selector);
}
else {
return matched;
}
};
// @info
// Generate element ID that is unique in the given document fragment.
// @type
// (DocumentFragment, string) => string
let generateUniqueID = (fragment, prefix = "") => {
let counter = 1;
while (true) {
let id = prefix + counter;
if (fragment.querySelector("#" + CSS.escape(id)) === null) {
return id;
}
else {
counter += 1;
}
}
};
// @copyright
// © 2016-2017 Jarosław Foksa
// @info
// Sleep for given period of time (in miliseconds).
// @type
// (number) => Promise
let sleep = (time) => {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(), time);
});
};
// @info
// Get timestamp in Unix format, e.g. 1348271383119 [http://en.wikipedia.org/wiki/Unix_time]
// @type
// () => number
let getTimeStamp = () => {
return Date.now();
};
// @info
// Returns a function, that, when invoked, will only be triggered at most once during a given window of time.
// @src
// [https://github.com/documentcloud/underscore/blob/master/underscore.js#L627]
// @license
// MIT License [https://github.com/documentcloud/underscore/blob/master/LICENSE]
// @type
// (Function, number, Object) => Function
let throttle = (func, wait = 500, context) => {
let args = null;
let timeout = null;
let result = null;
let previous = 0;
let later = () => {
previous = new Date();
timeout = null;
result = func.apply(context, args);
};
let wrapper = (..._args) => {
let now = new Date();
let remaining = wait - (now - previous);
args = _args;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
}
else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
return wrapper;
};
// @info
// Returns a function, that, as long as it continues to be invoked, will not be triggered. The function will be
// called after it stops being called for N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// Check [http://drupalmotion.com/article/debounce-and-throttle-visual-explanation] for a nice explanation of how
// this is different from throttle.
// @src
// [https://github.com/documentcloud/underscore/blob/master/underscore.js#L656]
// @license
// MIT License [https://github.com/documentcloud/underscore/blob/master/LICENSE]
// @type
// (Function, number, Object, boolean) => Function
let debounce = (func, wait, context, immediate = false) => {
let timeout = null;
let result = null;
let wrapper = (...args) => {
let later = () => {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
}
};
let callNow = (immediate && !timeout);
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
}
return result;
};
return wrapper;
};
// @copyright
// © 2016-2017 Jarosław Foksa
let {max} = Math;
let easing = "cubic-bezier(0.4, 0, 0.2, 1)";
let shadowTemplate = html`
<template>
<style>:host{display:block;width:100%;box-sizing:border-box;--arrow-width: 24px;--arrow-height: 24px;--arrow-color: currentColor;--arrow-align: flex-end;--arrow-d: path("M 29.046 31.419 L 50 52.329 L 70.954 31.419 L 78.481 40.038 L 50 68.52 L 21.246 40.31 L 29.046 31.419 Z");--arrow-transform: rotate(0deg);--focused-arrow-background: transparent;--focused-arrow-outline: none;--trigger-effect: none;--ripple-background: currentColor;--ripple-opacity: 0.05}:host([expanded]){--arrow-transform: rotate(-180deg)}:host([animating]){overflow:hidden}#main,#ripples{width:100%;height:100%}#main{position:relative}#ripples{left:0;border-radius:inherit}#arrow-container,#ripples,#ripples .ripple{position:absolute;top:0;pointer-events:none}#ripples .ripple{left:0;background:var(--ripple-background);opacity:var(--ripple-opacity);border-radius:999px;transform:none;transition:all 800ms cubic-bezier(.4,0,.2,1);will-change:opacity,transform;width:200px;height:200px}#arrow-container{width:100%;height:100%;display:flex;align-items:center;justify-content:var(--arrow-align)}#arrow{margin:0 14px 0 0;display:flex;width:var(--arrow-width);height:var(--arrow-height);min-width:var(--arrow-width);color:var(--arrow-color);d:var(--arrow-d);transform:var(--arrow-transform);transition:transform .25s cubic-bezier(.4,0,.2,1)}#arrow:focus{background:var(--focused-arrow-background);outline:var(--focused-arrow-outline)}#arrow path{fill:currentColor;d:inherit}</style>
<main id="main">
<div id="ripples"></div>
<div id="arrow-container">
<svg id="arrow" viewBox="0 0 100 100" preserveAspectRatio="none" tabindex="1">
<path></path>
</svg>
</div>
<slot></slot>
</main>
</template>
`;
class XAccordionElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this["#arrow"].addEventListener("keydown", (event) => this._onArrowKeyDown(event));
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "expanded") {
this._updateArrowPosition();
}
}
async connectedCallback() {
// Replace this lame code with ResizeObserver when it becomes available
await sleep(100);
this._updateArrowPosition();
await sleep(400);
this._updateArrowPosition();
await sleep(1000);
this._updateArrowPosition();
await sleep(2000);
this._updateArrowPosition();
await sleep(4000);
this._updateArrowPosition();
await sleep(7000);
this._updateArrowPosition();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["expanded"];
}
get expanded() {
return this.hasAttribute("expanded");
}
set expanded(expanded) {
expanded ? this.setAttribute("expanded", "") : this.removeAttribute("expanded");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onArrowKeyDown(event) {
if (event.key === "Enter") {
this.querySelector("header").click();
}
}
async _onPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return;
}
let header = this.querySelector("header");
let closestFocusableElement = event.target.closest("[tabindex]");
if (header.contains(event.target) && this.contains(closestFocusableElement) === false) {
let triggerEffect = getComputedStyle(this).getPropertyValue("--trigger-effect").trim();
// Ripple
if (triggerEffect === "ripple") {
let rect = this["#ripples"].getBoundingClientRect();
let size = max(rect.width, rect.height) * 1.5;
let top = pointerDownEvent.clientY - rect.y - size/2;
let left = pointerDownEvent.clientX - rect.x - size/2;
let whenLostPointerCapture = new Promise((r) => this.addEventListener("lostpointercapture", r, {once: true}));
this.setPointerCapture(pointerDownEvent.pointerId);
let ripple = html`<div></div>`;
ripple.setAttribute("class", "ripple pointer-down-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
this["#ripples"].style.contain = "strict";
let inAnimation = ripple.animate(
{ transform: ["scale3d(0, 0, 0)", "none"]},
{ duration: 300, easing }
);
await whenLostPointerCapture;
await inAnimation.finished;
let outAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"]},
{ duration: 300, easing }
);
await outAnimation.finished;
ripple.remove();
}
}
}
async _onClick(event) {
let header = this.querySelector("header");
let closestFocusableElement = event.target.closest("[tabindex]");
if (header.contains(event.target) && this.contains(closestFocusableElement) === false) {
// Collapse
if (this.expanded) {
let startBBox = this.getBoundingClientRect();
if (this._animation) {
this._animation.finish();
}
this.expanded = false;
this.removeAttribute("animating");
let endBBox = this.getBoundingClientRect();
this.setAttribute("animating", "");
let animation = this.animate(
{
height: [startBBox.height + "px", endBBox.height + "px"],
},
{
duration: 300,
easing
}
);
this._animation = animation;
await animation.finished;
if (this._animation === animation) {
this.removeAttribute("animating");
}
}
// Expand
else {
let startBBox = this.getBoundingClientRect();
if (this._animation) {
this._animation.finish();
}
this.expanded = true;
this.removeAttribute("animating");
let endBBox = this.getBoundingClientRect();
this.setAttribute("animating", "");
let animation = this.animate(
{
height: [startBBox.height + "px", endBBox.height + "px"],
},
{
duration: 300,
easing
}
);
this._animation = animation;
await animation.finished;
if (this._animation === animation) {
this.removeAttribute("animating");
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async _updateArrowPosition() {
let header = this.querySelector(":scope > header");
if (header) {
this["#arrow-container"].style.height = header.getBoundingClientRect().height + "px";
}
else {
this["#arrow-container"].style.height = null;
}
}
}
customElements.define("x-accordion", XAccordionElement);
// @info
// Same as <div>, but have default stylings suitable for flexbox container.
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$1 = html`
<template>
<style>:host{box-sizing:border-box;display:flex;align-items:center;justify-content:flex-start}:host([vertical]){flex-flow:column;align-items:flex-start;justify-content:center}</style>
<slot></slot>
</template>
`;
class XBoxElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$1.content, true));
}
// @info
// Whether to use vertical (rather than horizontal) layout.
// @type
// boolean
// @default
// false
get vertical() {
return this.hasAttribute("vertical");
}
set vertical(vertical) {
vertical ? this.setAttribute("vertical", "") : this.removeAttribute("vertical");
}
}
customElements.define("x-box", XBoxElement);
// @doc
// http://w3c.github.io/aria-practices/#button
// @copyright
// © 2016-2017 Jarosław Foksa
let {max: max$1} = Math;
let easing$1 = "cubic-bezier(0.4, 0, 0.2, 1)";
let shadowTemplate$2 = html`
<template>
<style>:host{display:flex;align-items:center;justify-content:center;width:fit-content;height:fit-content;box-sizing:border-box;opacity:1;position:relative;--trigger-effect: none;--ripple-background: currentColor;--ripple-opacity: 0.2;--arrow-width: 8px;--arrow-height: 8px;--arrow-margin: 0 0 0 4px;--arrow-d: path("M 11.699 19.846 L 49.822 57.886 L 87.945 19.846 L 99.657 31.557 L 49.822 81.392 L -0.013 31.557 Z")}:host(:focus){outline:0}:host([mixed]){opacity:.75}:host([disabled]){pointer-events:none;opacity:.5}:host([hidden]){display:none}#arrow{width:var(--arrow-width);height:var(--arrow-height);min-width:var(--arrow-width);margin:var(--arrow-margin);color:currentColor;d:var(--arrow-d)}#arrow path{fill:currentColor;d:inherit}#arrow[hidden]{display:none}#ripples,#ripples .ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;border-radius:inherit}#ripples .ripple{width:200px;height:200px;background:var(--ripple-background);opacity:var(--ripple-opacity);border-radius:999px;transform:none;transition:all 800ms cubic-bezier(.4,0,.2,1);will-change:opacity,transform}</style>
<div id="ripples"></div>
<slot></slot>
<svg id="arrow" viewBox="0 0 100 100" preserveAspectRatio="none">
<path id="arrow-path"></path>
</svg>
</template>
`;
// @events
// toggle
class XButtonElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$2.content, true));
this["#overlay"] = createElement("x-overlay");
this["#overlay"].style.background = "rgba(0, 0, 0, 0)";
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "button");
this.setAttribute("aria-disabled", this.disabled);
if (this.parentElement && this.parentElement.localName === "a" && this.parentElement.tabIndex !== -1) {
this.parentElement.tabIndex = -1;
}
this._updateArrowVisibility();
}
attributeChangedCallback(name) {
if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["disabled"];
}
// @info
// Direct ancestor <x-buttons> element.
// @type
// XButtonsElement?
get ownerButtons() {
if (this.parentElement) {
if (this.parentElement.localName === "x-buttons") {
return this.parentElement;
}
else if (this.parentElement.localName === "x-box") {
if (this.parentElement.parentElement.localName === "x-buttons") {
return this.parentElement.parentElement;
}
}
}
return null;
}
// @info
// Values associated with this button.
// @type
// string
// @default
// ""
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : null;
}
set value(value) {
value === null ? this.removeAttribute("value") : this.setAttribute("value", value);
}
// @info
// Whether this button is toggled.
// @type
// boolean
// @default
// false
// @attribute
get toggled() {
return this.hasAttribute("toggled");
}
set toggled(toggled) {
toggled ? this.setAttribute("toggled", "") : this.removeAttribute("toggled");
}
// @info
// Whether this button can be toggled on/off by the user (e.g. by clicking the button).
// @type
// boolean
// @default
// false
// @attribute
get togglable() {
return this.hasAttribute("togglable");
}
set togglable(togglable) {
togglable ? this.setAttribute("togglable", "") : this.removeAttribute("togglable");
}
// @info
// CSS skin to be used by this button.
// @type
// string
// @default
// ""
// @attribute
get skin() {
return this.getAttribute("skin");
}
set skin(skin) {
skin === null ? this.removeAttribute("skin") : this.setAttribute("skin", skin);
}
// @info
// Whether the this button has "mixed" state.
// @type
// boolean
// @default
// false
// @attribute
get mixed() {
return this.hasAttribute("mixed");
}
set mixed(mixed) {
mixed ? this.setAttribute("mixed", "") : this.removeAttribute("mixed");
}
// @info
// Whether this button is disabled.
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
// @info
// Whether the menu or popup associated with this button is opened.
// @type
// boolean
// @attribute
// read-only
get expanded() {
return this.hasAttribute("expanded");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
_onPointerDown(event) {
let popup = this.querySelector(":scope > x-menu, :scope > x-popover");
if (popup && (popup.hasAttribute("closing") || popup.contains(event.target))) {
return;
}
else if (event.target === this["#overlay"]) {
this._onOverlayPointerDown(event);
}
else {
this._onButtonPointerDown(event);
}
}
_onClick(event) {
let childPopup = this.querySelector(":scope > x-menu, :scope > x-popover");
let closestMenu = event.target.closest("x-menu");
let closestMenuItem = event.target.closest("x-menuitem");
let closestPopover = event.target.closest("x-popover");
if (childPopup && childPopup.hasAttribute("closing")) {
return;
}
else if (event.target === this["#overlay"]) {
return;
}
else if (closestMenu) {
if (closestMenuItem) {
this._onMenuItemClick(event);
}
}
else if (closestPopover && this.contains(closestPopover)) {
return;
}
else {
this._onButtonClick(event);
}
}
_onOverlayPointerDown(pointerDownEvent) {
this.collapse();
}
async _onButtonPointerDown(pointerDownEvent) {
// Don't focus the widget with pointer, instead focus the closest ancestor focusable element
if (this.matches(":focus") === false) {
event.preventDefault();
let ancestorFocusableElement = closest(this.parentNode, "*[tabindex]:not(a)");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
if (pointerDownEvent.button !== 0) {
return;
}
this.expand();
this.setPointerCapture(pointerDownEvent.pointerId);
// Provide "pressed" attribute for theming purposes which acts like :active pseudo-class, but is guaranteed
// to last at least 100ms.
if (this.isExpandable() === false) {
let pointerDownTimeStamp = Date.now();
let isDown = true;
let release = async () => {
window.removeEventListener("pointerup", release);
this.removeEventListener("lostpointercapture", release);
isDown = false;
let pressedTime = Date.now() - pointerDownTimeStamp;
let minPressedTime = 100;
if (pressedTime < minPressedTime) {
await sleep(minPressedTime - pressedTime);
}
this.removeAttribute("pressed");
};
window.addEventListener("pointerup", release);
this.addEventListener("lostpointercapture", release);
(async () => {
if (this.ownerButtons) {
if (this.ownerButtons.tracking === 0 || this.ownerButtons.tracking === 2) {
await sleep(10);
}
else if (this.ownerButtons.tracking === 1 && (this.toggled === false || this.mixed)) {
await sleep(10);
}
}
else if (this.togglable) {
await sleep(10);
}
if (isDown) {
this.setAttribute("pressed", "");
}
})();
}
// Ripple
{
let triggerEffect = getComputedStyle(this).getPropertyValue("--trigger-effect").trim();
if (triggerEffect === "ripple") {
let rect = this["#ripples"].getBoundingClientRect();
let size = max$1(rect.width, rect.height) * 1.5;
let top = pointerDownEvent.clientY - rect.y - size/2;
let left = pointerDownEvent.clientX - rect.x - size/2;
let whenLostPointerCapture = new Promise((r) => this.addEventListener("lostpointercapture", r, {once: true}));
let delay = true;
if (this.isExpandable() === false) {
if (this.ownerButtons) {
if (this.ownerButtons.tracking === 0 || this.ownerButtons.tracking === 2) {
delay = false;
}
else if (this.ownerButtons.tracking === 1 && this.toggled === false) {
delay = false;
}
}
else if (this.togglable) {
delay = false;
}
}
let ripple = createElement("div");
ripple.setAttribute("class", "ripple pointer-down-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
this["#ripples"].style.contain = "strict";
let inAnimation = ripple.animate(
{ transform: ["scale3d(0, 0, 0)", "none"]},
{ duration: 300, easing: easing$1 }
);
await whenLostPointerCapture;
if (delay) {
await inAnimation.finished;
let outAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"]},
{ duration: 300, easing: easing$1 }
);
await outAnimation.finished;
}
ripple.remove();
}
else if (triggerEffect === "unbounded-ripple") {
let bounds = this["#ripples"].getBoundingClientRect();
let size = bounds.height * 1.25;
let top = (bounds.y + bounds.height/2) - bounds.y - size/2;
let left = (bounds.x + bounds.width/2) - bounds.x - size/2;
let whenLostPointerCapture = new Promise((r) => this.addEventListener("lostpointercapture", r, {once: true}));
let ripple = createElement("div");
ripple.setAttribute("class", "ripple pointer-down-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
this["#ripples"].style.contain = "none";
// Workaround for buttons that change their color when toggled on/off.
ripple.hidden = true;
await sleep(20);
ripple.hidden = false;
let inAnimation = ripple.animate(
{ transform: ["scale(0)", "scale(1)"] },
{ duration: 200, easing: easing$1 }
);
await whenLostPointerCapture;
await inAnimation.finished;
let outAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 200, easing: easing$1 }
);
await outAnimation.finished;
ripple.remove();
}
}
}
async _onButtonClick(event) {
let popup = this.querySelector(":scope > x-menu, :scope > x-popover");
if (popup && popup.hasAttribute("closing")) {
return;
}
// Toggle the button
if (this.togglable) {
this.removeAttribute("pressed");
this.toggled = !this.toggled;
this.dispatchEvent(new CustomEvent("toggle"));
}
// Ripple
if (this["#ripples"].querySelector(".pointer-down-ripple") === null) {
let triggerEffect = getComputedStyle(this).getPropertyValue("--trigger-effect").trim();
if (triggerEffect === "ripple") {
let rect = this["#ripples"].getBoundingClientRect();
let size = max$1(rect.width, rect.height) * 1.5;
let top = (rect.y + rect.height/2) - rect.y - size/2;
let left = (rect.x + rect.width/2) - rect.x - size/2;
let delay = true;
if (this.ownerButtons) {
if (this.ownerButtons.tracking === 0 || this.ownerButtons.tracking === 2) {
delay = false;
}
else if (this.ownerButtons.tracking === 1 && this.toggled === true) {
delay = false;
}
}
else if (this.togglable) {
delay = false;
}
let ripple = createElement("div");
ripple.setAttribute("class", "ripple click-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
this["#ripples"].style.contain = "strict";
let inAnimation = ripple.animate(
{ transform: ["scale3d(0, 0, 0)", "none"]},
{ duration: 300, easing: easing$1 }
);
if (delay) {
await inAnimation.finished;
let outAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 300, easing: easing$1 }
);
await outAnimation.finished;
}
ripple.remove();
}
else if (triggerEffect === "unbounded-ripple") {
let rect = this["#ripples"].getBoundingClientRect();
let size = rect.height * 1.35;
let top = (rect.y + rect.height/2) - rect.y - size/2;
let left = (rect.x + rect.width/2) - rect.x - size/2;
let ripple = createElement("div");
ripple.setAttribute("class", "ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
this["#ripples"].style.contain = "none";
await ripple.animate(
{ transform: ["scale3d(0, 0, 0)", "none"] },
{ duration: 300, easing: easing$1 }
).finished;
await ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 300, easing: easing$1 }
).finished;
ripple.remove();
}
}
}
async _onMenuItemClick(event) {
let item = event.target.closest("x-menuitem");
let menu = this.querySelector(":scope > x-menu");
if (!menu.hasAttribute("closing")) {
this.collapse(item.whenTriggerEnd);
}
}
_onKeyDown(event) {
if (event.code === "Enter" || event.code === "Space" || event.code === "ArrowDown") {
let menu = this.querySelector("x-menu");
let popover = this.querySelector("x-popover");
if (menu) {
if (menu.opened === false) {
event.preventDefault();
this.expand().then(() => menu.focusFirstMenuItem());
}
}
else if (popover) {
if (popover.opened === false) {
event.preventDefault();
this.expand();
}
}
else {
event.preventDefault();
this.click();
}
}
else if (event.code === "Escape") {
let menu = this.querySelector("x-menu");
let popover = this.querySelector("x-popover");
if (menu) {
if (menu.opened) {
event.preventDefault();
this.collapse();
}
}
else if (popover) {
if (popover.opened) {
event.preventDefault();
this.collapse();
}
}
}
else if (event.code === "ArrowUp") {
let menu = this.querySelector("x-menu");
let popover = this.querySelector("x-popover");
if (menu) {
event.preventDefault();
this.expand().then(() => menu.focusLastMenuItem());
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Show the menu or popover associated with this button.
expand() {
return new Promise( async (resolve) => {
if (this._canExpandMenu()) {
let menu = this.querySelector("x-menu");
if (menu) {
this._wasFocusedBeforeExpanding = this.matches(":focus");
this.setAttribute("expanded", "");
this["#overlay"].ownerElement = menu;
this["#overlay"].show(false);
await menu.openNextToElement(this, "vertical", 3);
menu.focus();
}
}
else if (this._canExpandPopover()) {
let popover = this.querySelector("x-popover");
if (popover) {
this._wasFocusedBeforeExpanding = this.matches(":focus");
this.setAttribute("expanded", "");
this["#overlay"].ownerElement = popover;
this["#overlay"].show(false);
await popover.open(this);
popover.focus();
}
}
resolve();
});
}
// @info
// Hide the menu or popover associated with this button.
collapse(delay = null) {
return new Promise(async (resolve) => {
let popup = null;
if (this._canCollapseMenu()) {
popup = this.querySelector("x-menu");
}
else if (this._canCollapsePopover()) {
popup = this.querySelector("x-popover");
}
if (popup) {
popup.setAttribute("closing", "");
await delay;
await popup.close();
this["#overlay"].hide(false);
this.removeAttribute("expanded");
if (this._wasFocusedBeforeExpanding) {
this.focus();
}
else {
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
popup.removeAttribute("closing");
}
resolve();
});
}
isExpandable() {
return this.querySelector(":scope > x-menu x-menuitem, :scope > x-popover") !== null;
}
_canExpandMenu() {
if (this.disabled) {
return false;
}
else {
let menu = this.querySelector(":scope > x-menu");
let item = this.querySelector(":scope > x-menu x-menuitem");
return menu !== null && item !== null && !menu.hasAttribute("opened") && !menu.hasAttribute("closing");
}
}
_canExpandPopover() {
if (this.disabled) {
return false;
}
else {
let popover = this.querySelector("x-popover");
return popover !== null && !popover.hasAttribute("opened") && !popover.hasAttribute("closing");
}
}
_canCollapseMenu() {
if (this.disabled) {
return false;
}
else {
let menu = this.querySelector(":scope > x-menu");
return menu !== null && menu.opened; /* && menu.hasAttribute("closing") === false; */
}
}
_canCollapsePopover() {
if (this.disabled) {
return false;
}
else {
let popover = this.querySelector("x-popover");
return popover !== null && popover.opened === true; /* && popover.hasAttribute("closing") === false; */
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_updateArrowVisibility() {
let menu = this.querySelector("x-menu");
let popover = this.querySelector("x-popover");
this["#arrow"].style.display = (menu === null && popover === null) ? "none" : null;
}
}
customElements.define("x-button", XButtonElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let {isArray} = Array;
let shadowTemplate$3 = html`
<template>
<style>:host{display:flex;flex-flow:row;align-items:center;justify-content:flex-start;box-sizing:border-box;width:fit-content}:host([hidden]){display:none}</style>
<slot></slot>
</template>
`;
// @events
// toggle
class XButtonsElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$3.content, true));
this.addEventListener("click", (event) => this._onClick(event), true);
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
for (let child of this.children) {
if (child.localName === "x-button") {
let boxShadow = getComputedStyle(child).boxShadow;
if (boxShadow !== "none") {
this.setAttribute("hasboxshadow", "");
}
else {
this.removeAttribute("hasboxshadow");
}
break;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Specifies what should happen when user clicks a button:
// -1 - Do not toggle any buttons
// 0 - Toggle the clicked button on/off and other buttons off
// 1 - Toggle the clicked button on and other buttons off
// 2 - Toggle the clicked button on/off
// @type
// number
// @default
// -1
// @attribute
get tracking() {
return this.hasAttribute("tracking") ? parseInt(this.getAttribute("tracking")) : -1;
}
set tracking(tracking) {
this.setAttribute("tracking", tracking);
}
// @info
// Get/set the buttons that should have toggled state.
// @type
// string || Array || null
get value() {
if (this.tracking === 2) {
let buttons = this._getButtons().filter(button => button.toggled);
return buttons.map(button => button.value).filter(value => value != undefined);
}
else if (this.tracking === 1 || this.tracking === 0) {
let button = this._getButtons().find(button => button.toggled);
return button && button.value !== undefined ? button.value : null;
}
else if (this.tracking === -1) {
return null;
}
}
set value(value) {
if (this.tracking === 2) {
let buttons = this._getButtons();
if (isArray(value)) {
for (let button of buttons) {
button.toggled = (value.includes(button.value));
}
}
else {
for (let button of buttons) {
button.toggled = button.value === value;
}
}
}
else if (this.tracking === 1 || this.tracking === 0) {
let buttons = this._getButtons();
let matchedButton = buttons.find(button => button.value === value);
for (let button of buttons) {
button.toggled = (button === matchedButton);
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onClick(event) {
if (event.button !== 0) {
return;
}
let clickedButton = event.target.closest("x-button");
let canToggle = (clickedButton && clickedButton.disabled === false && clickedButton.isExpandable() === false);
if (canToggle) {
let otherButtons = this._getButtons().filter(button => button !== clickedButton);
if (this.tracking === 0) {
if (clickedButton.mixed) {
clickedButton.mixed = false;
}
else {
clickedButton.toggled = !clickedButton.toggled;
clickedButton.mixed = false;
}
for (let button of otherButtons) {
button.toggled = false;
button.mixed = false;
}
this.dispatchEvent(new CustomEvent("toggle", {bubbles: true, detail: clickedButton}));
}
else if (this.tracking === 1) {
if (clickedButton.toggled === false || clickedButton.mixed === true) {
clickedButton.toggled = true;
clickedButton.mixed = false;
for (let button of otherButtons) {
button.toggled = false;
button.mixed = false;
}
this.dispatchEvent(new CustomEvent("toggle", {bubbles: true, detail: clickedButton}));
}
}
else if (this.tracking === 2) {
if (clickedButton.mixed) {
clickedButton.mixed = false;
}
else {
clickedButton.toggled = !clickedButton.toggled;
}
this.dispatchEvent(new CustomEvent("toggle", {bubbles: true, detail: clickedButton}));
}
}
}
_onKeyDown(event) {
let {key} = event;
if (key === "ArrowRight") {
let element = [...this.children].find(child => child.matches(":focus"));
if (element.nextElementSibling) {
element.nextElementSibling.focus();
}
else if (element !== element.parentElement.firstElementChild) {
element.parentElement.firstElementChild.focus();
}
}
else if (key === "ArrowLeft") {
let element = [...this.children].find(child => child.matches(":focus"));
if (element.previousElementSibling) {
element.previousElementSibling.focus();
}
else if (element !== element.parentElement.lastElementChild) {
element.parentElement.lastElementChild.focus();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_getButtons() {
return [...this.querySelectorAll(":scope > x-button, :scope > x-box > x-button")];
}
}
customElements.define("x-buttons", XButtonsElement);
// @info
// A card is a sheet of material that serves as an entry point to more detailed information.
// @doc
// https://youtu.be/oujlrIZkyYY?t=16382
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$4 = html`
<template>
<style>:host{display:block;width:100%;min-width:20px;min-height:48px;box-sizing:border-box;margin:30px 0}</style>
<slot></slot>
</template>
`;
class XCardElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$4.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
}
customElements.define("x-card", XCardElement);
// @info
// Checkbox widget.
// @doc
// http://w3c.github.io/aria-practices/#checkbox
// @copyright
// © 2016-2017 Jarosław Foksa
let easing$2 = "cubic-bezier(0.4, 0, 0.2, 1)";
let shadowTemplate$5 = html`
<template>
<style>:host{display:block;position:relative;margin:0 8px 0 0;width:24px;height:24px;box-sizing:border-box;border:2px solid currentColor;--checkmark-width: 100%;--checkmark-height: 100%;--checkmark-opacity: 0;--checkmark-d: path("M 0.01 0 L 100 0 L 100 100 L 0 100 L 0.01 0 Z M 95.364 22.552 L 86.211 12.636 L 36.933 65.648 L 13.621 40.644 L 4.467 50.49 L 36.933 85.416 L 95.364 22.552 Z");--ripple-type: none;--ripple-background: currentColor;--ripple-opacity: 0.15}:host([mixed]),:host([toggled]){--checkmark-opacity: 1}:host([mixed]){--checkmark-d: path("M 0 0 L 100 0 L 100 100 L 0 100 Z M 87.033 42.592 L 12.967 42.592 L 12.967 57.408 L 87.033 57.408 Z")}:host([disabled]){opacity:.4;pointer-events:none}:host([hidden]){display:none}:host(:focus){outline:0}#checkmark{position:absolute;top:0;left:0;width:var(--checkmark-width);height:var(--checkmark-height);opacity:var(--checkmark-opacity);d:var(--checkmark-d);transition-property:opacity;transition-timing-function:inherit;transition-duration:inherit}#checkmark path{fill:currentColor;d:inherit}#ripples,#ripples .ripple{position:absolute;top:0;left:0;width:100%;height:100%}#ripples{pointer-events:none}#ripples .ripple{background:var(--ripple-background);opacity:var(--ripple-opacity);z-index:-1;will-change:opacity,transform;border-radius:999px;transform:scale(2.6)}</style>
<div id="ripples"></div>
<svg id="checkmark" viewBox="0 0 100 100" preserveAspectRatio="none">
<path></path>
</svg>
</template>
`;
// @events
// change
class XCheckboxElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$5.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "checkbox");
this.setAttribute("aria-checked", this.mixed ? "mixed" : this.toggled);
this.setAttribute("aria-disabled", this.disabled);
}
attributeChangedCallback(name) {
if (name === "toggled") {
this._onToggledAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["toggled", "disabled"];
}
// @type
// boolean
// @default
// false
// @attribute
get toggled() {
return this.hasAttribute("toggled");
}
set toggled(toggled) {
toggled ? this.setAttribute("toggled", "") : this.removeAttribute("toggled");
}
// @type
// boolean
// @default
// false
// @attribute
get mixed() {
return this.hasAttribute("mixed");
}
set mixed(mixed) {
mixed ? this.setAttribute("mixed", "") : this.removeAttribute("mixed");
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onToggledAttributeChange() {
this.setAttribute("aria-toggled", this.mixed ? "mixed" : this.toggled);
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
_onPointerDown(event) {
// Don't focus the widget with pointer, instead focus the closest ancestor focusable element
if (this.matches(":focus") === false) {
event.preventDefault();
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
// Ripple
{
let rippleType = getComputedStyle(this).getPropertyValue("--ripple-type").trim();
if (rippleType === "unbounded") {
let ripple = createElement("div");
ripple.setAttribute("class", "ripple pointer-down-ripple");
this["#ripples"].append(ripple);
let transformAnimation = ripple.animate(
{ transform: ["scale(0)", "scale(2.6)"] },
{ duration: 200, easing: easing$2 }
);
this.setPointerCapture(event.pointerId);
this.addEventListener("lostpointercapture", async () => {
await transformAnimation.finished;
let opacityAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 200, easing: easing$2 }
);
await opacityAnimation.finished;
ripple.remove();
}, {once: true});
}
}
}
async _onClick(event) {
// Update state
{
if (this.mixed) {
this.mixed = false;
}
else {
this.toggled = !this.toggled;
}
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
// Ripple
if (this["#ripples"].querySelector(".pointer-down-ripple") === null) {
let rippleType = getComputedStyle(this).getPropertyValue("--ripple-type").trim();
if (rippleType === "unbounded") {
let ripple = createElement("div");
ripple.setAttribute("class", "ripple");
this["#ripples"].append(ripple);
await ripple.animate(
{ transform: ["scale(0)", "scale(2.6)"] },
{ duration: 300, easing: easing$2 }
).finished;
await ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 300, easing: easing$2 }
).finished;
ripple.remove();
}
}
}
_onKeyDown(event) {
if (event.code === "Enter" || event.code === "Space") {
event.preventDefault();
this.click();
}
}
}
customElements.define("x-checkbox", XCheckboxElement);
// @copyright
// © 2016-2017 Jarosław Foksa
class StringScanner {
// @type
// (string) => void
constructor(text) {
this.text = text;
this.cursor = 0;
this.line = 1;
this.column = 1;
this._storedPosition = {cursor: 0, line: 1, column: 1};
}
// @info
// Read given number of chars.
// @type
// (number) => string?
read(i = 1) {
let string = "";
let initialCursor = this.cursor;
for (let j = 0; j < i; j += 1) {
let c = this.text[initialCursor + j];
if (c === undefined) {
break;
}
else {
string += c;
this.cursor += 1;
if (c === "\n"){
this.line += 1;
this.column = 1;
}
else {
this.column += 1;
}
}
}
return (string === "" ? null : string);
}
// @info
// Read given number of chars without advancing the cursor.
// @type
// (number) => string?
peek(i = 1) {
let string = "";
for (let j = 0; j < i; j += 1) {
let c = this.text[this.cursor + j];
if (c === undefined) {
break;
}
else {
string += c;
}
}
return (string === "" ? null : string);
}
// @type
// () => void
storePosition() {
let {cursor, line, column} = this;
this._storedPosition = {cursor, line, column};
}
// @type
// () => void
restorePosition() {
let {cursor, line, column} = this._storedPosition;
this.cursor = cursor;
this.line = line;
this.column = column;
}
}
// @copyright
// © 2016-2017 Jarosław Foksa
let {max: max$3, pow: pow$1, sqrt: sqrt$1, PI: PI$1} = Math;
// @info
// Round given number to the fixed number of decimal places.
// @type
// (number, number) => number
let round = (number, precision = 0) => {
let coefficient = pow$1(10, precision);
return Math.round(number * coefficient) / coefficient;
};
// @type
// (number, number, number, number?) => number
let normalize = (number, min, max = Infinity, precision = null) => {
if (precision !== null) {
number = round(number, precision);
}
if (number < min) {
number = min;
}
else if (number > max) {
number = max;
}
return number;
};
// @type
// (number) => number
let getPrecision = (number) => {
if (!isFinite(number)) {
return 0;
}
else {
let e = 1;
let p = 0;
while (Math.round(number * e) / e !== number) {
e *= 10;
p += 1;
}
return p;
}
};
// @info
// Get distance between two points.
// @type
// (DOMPoint, DOMPoint) => number
let getDistanceBetweenPoints = (point1, point2) => {
let x = point2.x - point1.x;
x = x * x;
let y = point2.y - point1.y;
y = y * y;
let distance = sqrt$1(x+y);
return distance;
};
// @type
// (number) => number
let degToRad = (degrees) => {
let radians = (PI$1 * degrees) / 180;
return radians;
};
// @copyright
// © 2016-2017 Jarosław Foksa
let {min, max: max$2, floor} = Math;
let {parseFloat: parseFloat$1, parseInt: parseInt$1} = Number;
// @info
// A list of named colors and their corresponding RGB values.
// @doc
// http://www.w3.org/TR/css3-color/#svg-color
let namedColors = {
// R, G, B
aliceblue: [240, 248, 255],
antiquewhite: [250, 235, 215],
aqua: [ 0, 255, 255],
aquamarine: [127, 255, 212],
azure: [240, 255, 255],
beige: [245, 245, 220],
bisque: [255, 228, 196],
black: [ 0, 0 ,0],
blanchedalmond: [255, 235, 205],
blue: [ 0, 0, 255],
blueviolet: [138, 43, 226],
brown: [165, 42, 42],
burlywood: [222, 184, 135],
cadetblue: [ 95, 158, 160],
chartreuse: [127, 255, 0],
chocolate: [210, 105, 30],
coral: [255, 127, 80],
cornflowerblue: [100, 149, 237],
cornsilk: [255, 248, 220],
crimson: [220, 20, 60],
cyan: [ 0, 255, 255],
darkblue: [ 0, 0, 139],
darkcyan: [ 0, 139, 139],
darkgoldenrod: [184, 134, 11],
darkgray: [169, 169, 169],
darkgreen: [ 0, 100, 0],
darkgrey: [169, 169, 169],
darkkhaki: [189, 183, 107],
darkmagenta: [139, 0, 139],
darkolivegreen: [ 85, 107, 47],
darkorange: [255, 140, 0],
darkorchid: [153, 50, 204],
darkred: [139, 0, 0],
darksalmon: [233, 150, 122],
darkseagreen: [143, 188, 143],
darkslateblue: [ 72, 61, 139],
darkslategray: [ 47, 79, 79],
darkslategrey: [ 47, 79, 79],
darkturquoise: [ 0, 206, 209],
darkviolet: [148, 0, 211],
deeppink: [255, 20, 147],
deepskyblue: [ 0, 191, 255],
dimgray: [105, 105, 105],
dimgrey: [105, 105, 105],
dodgerblue: [ 30, 144, 255],
firebrick: [178, 34, 34],
floralwhite: [255, 250, 240],
forestgreen: [ 34, 139, 34],
fuchsia: [255, 0, 255],
gainsboro: [220, 220, 220],
ghostwhite: [248, 248, 255],
gold: [255, 215, 0],
goldenrod: [218, 165, 32],
gray: [128, 128, 128],
green: [ 0, 128, 0],
greenyellow: [173, 255, 47],
grey: [128, 128, 128],
honeydew: [240, 255, 240],
hotpink: [255, 105, 180],
indianred: [205, 92, 92],
indigo: [ 75, 0, 130],
ivory: [255, 255, 240],
khaki: [240, 230, 140],
lavender: [230, 230, 250],
lavenderblush: [255, 240, 245],
lawngreen: [124, 252, 0],
lemonchiffon: [255, 250, 205],
lightblue: [173, 216, 230],
lightcoral: [240, 128, 128],
lightcyan: [224, 255, 255],
lightgoldenrodyellow: [250, 250, 210],
lightgray: [211, 211, 211],
lightgreen: [144, 238, 144],
lightgrey: [211, 211, 211],
lightpink: [255, 182, 193],
lightsalmon: [255, 160, 122],
lightseagreen: [32, 178, 170],
lightskyblue: [135, 206, 250],
lightslategray: [119, 136, 153],
lightslategrey: [119, 136, 153],
lightsteelblue: [176, 196, 222],
lightyellow: [255, 255, 224],
lime: [ 0, 255, 0],
limegreen: [ 50, 205, 50],
linen: [250, 240, 230],
magenta: [255, 0 ,255],
maroon: [128, 0, 0],
mediumaquamarine: [102, 205, 170],
mediumblue: [ 0, 0, 205],
mediumorchid: [186, 85, 211],
mediumpurple: [147, 112, 219],
mediumseagreen: [ 60, 179, 113],
mediumslateblue: [123, 104, 238],
mediumspringgreen: [ 0, 250, 154],
mediumturquoise: [ 72, 209, 204],
mediumvioletred: [199, 21, 133],
midnightblue: [ 25, 25, 112],
mintcream: [245, 255, 250],
mistyrose: [255, 228, 225],
moccasin: [255, 228, 181],
navajowhite: [255, 222, 173],
navy: [ 0, 0, 128],
oldlace: [253, 245, 230],
olive: [128, 128, 0],
olivedrab: [107, 142, 35],
orange: [255, 165, 0],
orangered: [255, 69, 0],
orchid: [218, 112, 214],
palegoldenrod: [238, 232, 170],
palegreen: [152, 251, 152],
paleturquoise: [175, 238, 238],
palevioletred: [219, 112, 147],
papayawhip: [255, 239, 213],
peachpuff: [255, 218, 185],
peru: [205, 133, 63],
pink: [255, 192, 203],
plum: [221, 160, 221],
powderblue: [176, 224, 230],
purple: [128, 0, 128],
red: [255, 0, 0],
rosybrown: [188, 143, 143],
royalblue: [ 65, 105, 225],
saddlebrown: [139, 69, 19],
salmon: [250, 128, 114],
sandybrown: [244, 164, 96],
seagreen: [46, 139, 87],
seashell: [255, 245, 238],
sienna: [160, 82, 45],
silver: [192, 192, 192],
skyblue: [135, 206, 235],
slateblue: [106, 90, 205],
slategray: [112, 128, 144],
slategrey: [112, 128, 144],
snow: [255, 250, 250],
springgreen: [ 0, 255, 127],
steelblue: [ 70, 130, 180],
tan: [210, 180, 140],
teal: [ 0, 128, 128],
thistle: [216, 191, 216],
tomato: [255, 99, 71],
turquoise: [ 64, 224, 208],
violet: [238, 130, 238],
wheat: [245, 222, 179],
white: [255, 255, 255],
whitesmoke: [245, 245, 245],
yellow: [255, 255, 0],
yellowgreen: [154, 205, 50]
};
// @info
// Convert color from RGB to HSL space. R, G and B components on input must be in 0-255 range.
// @src
// http://goo.gl/J9ra3
// @type
// (number, number, number) => [number, number, number]
let rgbToHsl = (r, g, b) => {
r = r / 255;
g = g / 255;
b = b / 255;
let maxValue = max$2(r, g, b);
let minValue = min(r, g, b);
let h;
let s;
let l;
h = s = l = (maxValue + minValue) / 2;
if (maxValue === minValue) {
h = s = 0;
}
else {
let d = maxValue - minValue;
if (l > 0.5) {
s = d / (2 - maxValue - minValue);
}
else {
s = d / (maxValue + minValue);
}
if (maxValue === r) {
let z;
if (g < b) {
z = 6;
}
else {
z = 0;
}
h = (g - b) / d + z;
}
else if (maxValue === g) {
h = (b - r) / d + 2;
}
else if (maxValue === b) {
h = (r - g) / d + 4;
}
}
h = normalize((h / 6) * 360, 0, 360, 0);
s = normalize(s * 100, 0, 100, 1);
l = normalize(l * 100, 0, 100, 1);
return [h, s, l];
};
// @info
// Convert color from HSL to RGB space. Input H must be in 0-360 range, S and L must be in
// 0-100 range.
// @src
// http://goo.gl/J9ra3
// @type
// (number, number, number) => [number, number, number]
let hslToRgb = (h, s, l) => {
h = h / 360;
s = s / 100;
l = l / 100;
let r;
let g;
let b;
if (s === 0) {
r = g = b = l;
}
else {
let hue2rgb = (p, q, t) => {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1/6) {
return p + (q - p) * 6 * t;
}
if (t < 1/2) {
return q;
}
if (t < 2/3) {
return p + (q - p) * (2/3 - t) * 6;
}
return p;
};
let q;
let p;
if (l < 0.5) {
q = l * (1 + s);
}
else {
q = l + s - l * s;
}
p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
r = normalize(255 * r, 0, 255, 0);
g = normalize(255 * g, 0, 255, 0);
b = normalize(255 * b, 0, 255, 0);
return [r, g, b];
};
// @info
// Convert color from RGB to HSV space.
// @src
// http://goo.gl/J9ra3
// @type
// (number, number, number) => [number, number, number]
let rgbToHsv = (r, g, b) => {
r = r / 255;
g = g / 255;
b = b / 255;
let maxValue = max$2(r, g, b);
let minValue = min(r, g, b);
let h = 0;
let s = 0;
let v = maxValue;
let d = maxValue - minValue;
if (maxValue === 0) {
s = 0;
}
else {
s = d / maxValue;
}
if (maxValue === minValue) {
h = 0;
}
else {
if (maxValue === r) {
h = (g - b) / d + (g < b ? 6 : 0);
}
else if (maxValue === g) {
h = (b - r) / d + 2;
}
else if (maxValue === b) {
h = (r - g) / d + 4;
}
h = h / 6;
}
h = h * 360;
s = s * 100;
v = v * 100;
return [h, s, v];
};
// @info
// Convert color from HSV to RGB space.
// @src
// http://goo.gl/J9ra3
// @type
// (number, number, number) => [number, number, number]
let hsvToRgb = (h, s, v) => {
h = h / 360;
s = s / 100;
v = v / 100;
let i = floor(h * 6);
let f = (h * 6) - i;
let p = v * (1 - s);
let q = v * (1 - (f * s));
let t = v * (1 - (1 - f) * s);
let r = 0;
let g = 0;
let b = 0;
if (i % 6 === 0) {
r = v;
g = t;
b = p;
}
else if (i % 6 === 1) {
r = q;
g = v;
b = p;
}
else if (i % 6 === 2) {
r = p;
g = v;
b = t;
}
else if (i % 6 === 3) {
r = p;
g = q;
b = v;
}
else if (i % 6 === 4) {
r = t;
g = p;
b = v;
}
else if (i % 6 === 5) {
r = v;
g = p;
b = q;
}
r = r * 255;
g = g * 255;
b = b * 255;
return [r, g, b];
};
// @info
// Convert color from HSL to HSV space.
// @src
// http://ariya.blogspot.com/2008/07/converting-between-hsl-and-hsv.html
// @type
// (number, number, number) => [number, number, number]
let hslToHsv = (h, s, l) => {
h = h / 360;
s = s / 100;
l = (l / 100) * 2;
if (l <= 1) {
s = s * l;
}
else {
s = s * (2 - l);
}
let hh = h;
let ss;
let vv;
if ((l + s) === 0) {
ss = 0;
}
else {
ss = (2 * s) / (l + s);
}
vv = (l + s) / 2;
hh = 360 * hh;
ss = max$2(0, min(1, ss)) * 100;
vv = max$2(0, min(1, vv)) * 100;
return [hh, ss, vv];
};
// @info
// Convert color from HSV to HSL space.
// @src
// http://ariya.blogspot.com/2008/07/converting-between-hsl-and-hsv.html
// @type
// (number, number, number) => [number, number, number]
let hsvToHsl = (h, s, v) => {
h = h / 360;
s = s / 100;
v = v / 100;
let hh = h;
let ll = (2 - s) * v;
let ss = s * v;
if (ll <= 1) {
if (ll === 0) {
ss = 0;
}
else {
ss = ss / ll;
}
}
else if (ll === 2) {
ss = 0;
}
else {
ss = ss / (2 - ll);
}
ll = ll / 2;
hh = 360 * hh;
ss = max$2(0, min(1, ss)) * 100;
ll = max$2(0, min(1, ll)) * 100;
return [hh, ss, ll];
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Parse given CSS color string into corresponding RGBA, HSLA or HSVA components.
// @type
// outputModel = "rgba" || "hsla" || "hsva"
// components = Array<number, number, number, number>
// (string, outputModel) => components
let parseColor = (colorString, outputModel = "rgba") => {
colorString = colorString.trim();
let tokens = tokenizeColor(colorString);
let rgbaComponents = null;
let hslaComponents = null;
// RGB, e.g. rgb(100, 100, 100)
if (
tokens.length === 7 &&
tokens[0].text === "rgb(" &&
tokens[1].type === "NUM" &&
tokens[2].text === "," &&
tokens[3].type === "NUM" &&
tokens[4].text === "," &&
tokens[5].type === "NUM" &&
tokens[6].text === ")"
) {
rgbaComponents = [
parseFloat$1(tokens[1].text),
parseFloat$1(tokens[3].text),
parseFloat$1(tokens[5].text),
1,
];
}
// RGB with percentages, e.g. rgb(50%, 50%, 50%)
else if (
tokens.length === 7 &&
tokens[0].text === "rgb(" &&
tokens[1].type === "PERCENTAGE" &&
tokens[2].text === "," &&
tokens[3].type === "PERCENTAGE" &&
tokens[4].text === "," &&
tokens[5].type === "PERCENTAGE" &&
tokens[6].text === ")"
) {
rgbaComponents = [
(parseFloat$1(tokens[1].text)/100) * 255,
(parseFloat$1(tokens[3].text)/100) * 255,
(parseFloat$1(tokens[5].text)/100) * 255,
1,
];
}
// RGBA, e.g. rgba(100, 100, 100, 0.5)
else if (
tokens.length === 9 &&
tokens[0].text === "rgba(" &&
tokens[1].type === "NUM" &&
tokens[2].text === "," &&
tokens[3].type === "NUM" &&
tokens[4].text === "," &&
tokens[5].type === "NUM" &&
tokens[6].text === "," &&
tokens[7].type === "NUM" &&
tokens[8].text === ")"
) {
rgbaComponents = [
parseFloat$1(tokens[1].text),
parseFloat$1(tokens[3].text),
parseFloat$1(tokens[5].text),
parseFloat$1(tokens[7].text),
];
}
// RGBA with percentages, e.g. rgba(50%, 50%, 50%, 0.5)
else if (
tokens.length === 9 &&
tokens[0].text === "rgb(" &&
tokens[1].type === "PERCENTAGE" &&
tokens[2].text === "," &&
tokens[3].type === "PERCENTAGE" &&
tokens[4].text === "," &&
tokens[5].type === "PERCENTAGE" &&
tokens[6].text === ","&&
tokens[7].type === "NUM" &&
tokens[8].text === ")"
) {
rgbaComponents = [
(parseFloat$1(tokens[1].text)/100) * 255,
(parseFloat$1(tokens[3].text)/100) * 255,
(parseFloat$1(tokens[5].text)/100) * 255,
parseFloat$1(tokens[7].text),
];
}
// HSL, e.g. hsl(360, 100%, 100%)
else if (
tokens.length === 7 &&
tokens[0].text === "hsl(" &&
tokens[1].type === "NUM" &&
tokens[2].text === "," &&
tokens[3].type === "PERCENTAGE" &&
tokens[4].text === "," &&
tokens[5].type === "PERCENTAGE" &&
tokens[6].text === ")"
) {
hslaComponents = [
parseFloat$1(tokens[1].text),
parseFloat$1(tokens[3].text),
parseFloat$1(tokens[5].text),
1,
];
}
// HSLA, e.g. hsla(360, 100%, 100%, 1)
else if (
tokens.length === 9 &&
tokens[0].text === "hsla(" &&
tokens[1].type === "NUM" &&
tokens[2].text === "," &&
tokens[3].type === "PERCENTAGE" &&
tokens[4].text === "," &&
tokens[5].type === "PERCENTAGE" &&
tokens[6].text === "," &&
tokens[7].type === "NUM" &&
tokens[8].text === ")"
) {
hslaComponents = [
parseFloat$1(tokens[1].text),
parseFloat$1(tokens[3].text),
parseFloat$1(tokens[5].text),
parseFloat$1(tokens[7].text),
];
}
// HEX, e.g. "#fff"
else if (tokens[0].type === "HEX" && tokens[1] === undefined) {
let hexString = tokens[0].text.substring(1); // get rid of leading "#"
let hexRed;
let hexGreen;
let hexBlue;
if (hexString.length === 3) {
hexRed = hexString[0] + hexString[0];
hexGreen = hexString[1] + hexString[1];
hexBlue = hexString[2] + hexString[2];
}
else {
hexRed = hexString[0] + hexString[1];
hexGreen = hexString[2] + hexString[3];
hexBlue = hexString[4] + hexString[5];
}
rgbaComponents = [
parseInt$1(hexRed, 16),
parseInt$1(hexGreen, 16),
parseInt$1(hexBlue, 16),
1,
];
}
// Named color, e.g. "white"
else if (namedColors[colorString]) {
rgbaComponents = [
namedColors[colorString][0],
namedColors[colorString][1],
namedColors[colorString][2],
1,
];
}
// Finalize
if (rgbaComponents) {
let [r, g, b, a] = rgbaComponents;
r = normalize(r, 0, 255, 0);
g = normalize(g, 0, 255, 0);
b = normalize(b, 0, 255, 0);
a = normalize(a, 0, 1, 2);
if (outputModel === "hsla") {
let [h, s, l] = rgbToHsl(r, g, b);
return [h, s, l, a];
}
else if (outputModel === "hsva") {
let [h, s, v] = rgbToHsv(r, g, b);
return [h, s, v, a];
}
else {
return [r, g, b, a];
}
}
else if (hslaComponents) {
let [h, s, l, a] = hslaComponents;
h = normalize(h, 0, 360, 0);
s = normalize(s, 0, 100, 1);
l = normalize(l, 0, 100, 1);
a = normalize(a, 0, 1, 2);
if (outputModel === "hsla") {
return [h, s, l, a];
}
else if (outputModel === "hsva") {
let [hh, ss, vv] = hslToHsv(h, s, l);
return [hh, ss, vv, a];
}
else {
let [r, g, b] = hslToRgb(h, s, l);
return [r, g, b, a];
}
}
else {
throw new Error(`Invalid color string: "${colorString}"`);
return null;
}
};
// @type
// components = Array<number, number, number, number>
// inputModel = "rgba" || "hsla" || "hsva"
// outputFormat = "rgb" || "rgba" || "rgb%" || "rgba%" || "hex" || "hsl" || "hsla"
// (components, inputModel, outputFormat) => string
let serializeColor = (components, inputModel = "rgba", outputFormat = "hex") => {
let string = null;
// RGB(A) output
if (["rgb", "rgba", "rgb%", "rgba%", "hex"].includes(outputFormat)) {
let r;
let g;
let b;
let a;
if (inputModel === "rgba") {
[r, g, b, a] = components;
}
else if (inputModel === "hsla") {
[r, g, b] = hslToRgb(...components);
a = components[3];
}
else if (inputModel === "hsva") {
[r, g, b] = hsvToRgb(...components);
a = components[3];
}
if (outputFormat === "rgb%" || outputFormat === "rgba%") {
r = normalize((r/255) * 100, 0, 100, 1);
g = normalize((g/255) * 100, 0, 100, 1);
b = normalize((b/255) * 100, 0, 100, 1);
a = normalize(a, 0, 1, 2);
}
else {
r = normalize(r, 0, 255, 0);
g = normalize(g, 0, 255, 0);
b = normalize(b, 0, 255, 0);
a = normalize(a, 0, 1, 2);
}
if (outputFormat === "rgb") {
string = `rgb(${r}, ${g}, ${b})`;
}
else if (outputFormat === "rgba") {
string = `rgba(${r}, ${g}, ${b}, ${a})`;
}
else if (outputFormat === "rgb%") {
string = `rgb(${r}%, ${g}%, ${b}%)`;
}
else if (outputFormat === "rgba%") {
string = `rgb(${r}%, ${g}%, ${b}%, ${a})`;
}
else if (outputFormat === "hex") {
let hexRed = r.toString(16);
let hexGreen = g.toString(16);
let hexBlue = b.toString(16);
if (hexRed.length === 1) {
hexRed = "0" + hexRed;
}
if (hexGreen.length === 1) {
hexGreen = "0" + hexGreen;
}
if (hexBlue.length === 1) {
hexBlue = "0" + hexBlue;
}
string = "#" + hexRed + hexGreen + hexBlue;
}
}
// HSL(A) space
else if (outputFormat === "hsl" || outputFormat === "hsla") {
let h;
let s;
let l;
let a;
if (inputModel === "hsla") {
[h, s, l, a] = components;
}
else if (inputModel === "hsva") {
[h, s, l] = hsvToHsl(...components);
a = components[3];
}
else if (inputModel === "rgba") {
[h, s, l] = rgbToHsl(...components);
a = components[3];
}
h = normalize(h, 0, 360, 0);
s = normalize(s, 0, 100, 1);
l = normalize(l, 0, 100, 1);
a = normalize(a, 0, 1, 2);
if (outputFormat === "hsl") {
string = `hsl(${h}, ${s}%, ${l}%)`;
}
else if (outputFormat === "hsla") {
string = `hsla(${h}, ${s}%, ${l}%, ${a})`;
}
}
return string;
};
// @info
// Convert CSS color string into an array of tokens.
// -----------------------------------
// Token type Sample token text
// -----------------------------------
// "FUNCTION" "rgb(", "hsla("
// "HEX" "#000", "#bada55"
// "NUMBER" "100", ".2", "10.3234"
// "PERCENTAGE" "100%", "0.2%"
// "CHAR" ")", ","
// @type
// type Token = {type: string, text: string}
// (string) => Array<Token>
let tokenizeColor = (cssText) => {
let tokens = [];
let scanner = new StringScanner(cssText.toLowerCase());
while (scanner.peek() !== null) {
let char = scanner.read();
(() => {
// FUNCTION
if (char === "r" || char === "h") {
let text = char;
if (char + scanner.peek(3) === "rgb(") {
text += scanner.read(3);
}
else if (char + scanner.peek(4) === "rgba(") {
text += scanner.read(4);
}
else if (char + scanner.peek(3) === "hsl(") {
text += scanner.read(3);
}
else if (char + scanner.peek(4) === "hsla(") {
text += scanner.read(4);
}
if (text !== char) {
tokens.push({type: "FUNCTION", text: text});
return;
}
}
// HEX
if (char === "#") {
if (isHexColorString(char + scanner.peek(6))) {
let text = char + scanner.read(6);
tokens.push({type: "HEX", text: text});
return;
}
else if (isHexColorString(char + scanner.peek(3))) {
text = char + scanner.read(3);
tokens.push({type: "HEX", text: text});
return;
}
}
// NUMBER
// PERCENTAGE
if (["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "-"].includes(char)) {
let text = char;
while (true) {
if (["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."].includes(scanner.peek())) {
text += scanner.read();
}
else {
break;
}
}
if (scanner.peek() === "%") {
text += scanner.read();
tokens.push({type: "PERCENTAGE", text: text});
}
else {
tokens.push({type: "NUM", text: text});
}
return;
}
// S
if (/\u0009|\u000a|\u000c|\u000d|\u0020/.test(char)) {
// Don't tokenize whitespace as it's meaningless
return;
}
// CHAR
tokens.push({type: "CHAR", text: char});
return;
})();
}
return tokens;
};
// @type
// format = "rgb" || "rgba" || "rgb%" || "rgba%" || "hex" || "hsl" || "hsla"
// (string, format) => string
let formatColorString = (colorString, format) => {
let model = format.startsWith("hsl") ? "hsla" : "rgba";
let components = parseColor(colorString, model);
let formattedColorString = serializeColor(components, model, format);
return formattedColorString;
};
// @info
// Check if string represents a valid hex color, e.g. "#fff", "#bada55".
// @type
// (string) => boolean
let isHexColorString = (string) => {
string = string.toLowerCase();
if (string[0] !== "#") {
return false;
}
else if (string.length !== 4 && string.length !== 7) {
return false;
}
else {
string = string.substring(1); // get rid of "#"
}
let hexDigits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"];
for (let digit of string) {
if (!hexDigits.includes(digit)) {
return false;
}
}
return true;
};
// @info
// Check if string contains valid CSS3 color, e.g. "blue", "#fff", "rgb(50, 50, 100)".
// @type
// (string) => boolean
let isValidColorString = (string) => {
try {
parseColor(string);
}
catch (error) {
return false;
}
return true;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Creates HSV spectrum wheel image used by the color pickers.
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowHTML = `
<style>:host{display:block;height:24px;width:40px;box-sizing:border-box;border:1px solid #969696;position:relative}:host([hidden]){display:none}:host([disabled]){pointer-events:none;opacity:.4}::slotted(x-popover){width:190px;height:auto;padding:12px}#input{display:flex;width:100%;height:100%;box-sizing:border-box;border:0;background:0 0;padding:0;opacity:0;-webkit-appearance:none}#input::-webkit-color-swatch-wrapper{padding:0}#input::-webkit-color-swatch{border:0}</style>
<style>:host { background: url(node_modules/xel/images/checkboard.png) repeat 0 0; }</style>
<input tabindex="-1" id="input" type="color" value="#ffffff">
<slot></slot>
`;
// @events
// change
// changestart
// changeend
class XColorSelectElement extends HTMLElement {
constructor() {
super();
this._inputChangeStarted = false;
this._onInputChangeDebouonced = debounce(this._onInputChangeDebouonced, 400, this);
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.innerHTML = shadowHTML;
this["#overlay"] = createElement("x-overlay");
this["#overlay"].style.background = "rgba(0, 0, 0, 0)";
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("change", (event) => this._onChange(event));
this["#input"].addEventListener("change", (event) => this._onInputChange());
}
attributeChangedCallback(name) {
if (name === "value") {
this._onValueAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
let picker = this.querySelector("x-wheelcolorpicker, x-rectcolorpicker");
if (picker) {
picker.setAttribute("value", formatColorString(this.value, "rgba"));
}
this._updateInput();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["value", "disabled"];
}
// @type
// string
// @default
// #000000
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : "#ffffff";
}
set value(value) {
this.setAttribute("value", value);
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onValueAttributeChange() {
if (!this._inputChangeStarted) {
this._updateInput();
}
let picker = this.querySelector("x-wheelcolorpicker, x-rectcolorpicker");
if (picker && picker.getAttribute("value") !== this.getAttribute("value")) {
picker.setAttribute("value", this.getAttribute("value"));
}
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
_onChange(event) {
if (event.target !== this) {
this.value = formatColorString(event.target.value, "rgba");
this._updateInput();
}
}
_onInputChange() {
if (this._inputChangeStarted === false) {
this._inputChangeStarted = true;
this.dispatchEvent(new CustomEvent("changestart"));
}
this.dispatchEvent(new CustomEvent("change"));
this._onInputChangeDebouonced();
}
_onInputChangeDebouonced() {
if (this._inputChangeStarted) {
this.value = this["#input"].value;
this._inputChangeStarted = false;
this.dispatchEvent(new CustomEvent("changeend"));
}
}
_onPointerDown(event) {
event.preventDefault();
}
_onClick(event) {
if (event.target === this["#overlay"]) {
this._collapse();
}
else {
let popover = this.querySelector("x-popover");
if (popover) {
event.preventDefault();
}
if (popover && popover.hasAttribute("opened") === false) {
event.preventDefault();
this._expand();
}
}
}
_onKeyDown(event) {
if (event.code === "Enter" || event.code === "Space") {
let popover = this.querySelector("x-popover");
event.preventDefault();
event.stopPropagation();
if (popover) {
if (this.hasAttribute("expanded")) {
this._collapse();
}
else {
this._expand();
}
}
else {
this["#input"].click();
}
}
else if (event.code === "Escape") {
let popover = this.querySelector("x-popover");
if (popover) {
if (this.hasAttribute("expanded")) {
this._collapse();
}
}
}
else if (event.code === "Tab") {
if (this.hasAttribute("expanded")) {
event.preventDefault();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async _expand() {
let popover = this.querySelector("x-popover");
if (popover) {
this._wasFocusedBeforeExpanding = this.matches(":focus");
this.setAttribute("expanded", "");
this["#overlay"].ownerElement = popover;
this["#overlay"].show(false);
await popover.open(this);
popover.focus();
}
}
async _collapse(delay = null) {
let popover = this.querySelector("x-popover");
if (popover) {
popover.setAttribute("closing", "");
await popover.close();
this["#overlay"].hide(false);
this.removeAttribute("expanded");
if (this._wasFocusedBeforeExpanding) {
this.focus();
}
else {
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
popover.removeAttribute("closing");
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_updateInput() {
let [r, g, b, a] = parseColor(this.value, "rgba");
this["#input"].value = serializeColor([r, g, b, a], "rgba", "hex");
this["#input"].style.opacity = a;
}
}
customElements.define("x-colorselect", XColorSelectElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let debug = true;
let shadowTemplate$6 = html`
<template>
<style>:host{display:fixed;width:0;height:0;z-index:1001}</style>
<slot></slot>
</template>
`;
class XContextMenuElement extends HTMLElement {
constructor() {
super();
this._parentElement = null;
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$6.content, true));
this["#overlay"] = createElement("x-overlay");
this["#overlay"].style.background = "rgba(0, 0, 0, 0)";
this["#overlay"].addEventListener("contextmenu", (event) => this._onOverlayContextMenu(event));
this["#overlay"].addEventListener("pointerdown", (event) => this._onOverlayPointerDown(event));
window.addEventListener("blur", () => this._onBlur());
this.addEventListener("blur", () => this._onBlur());
this.addEventListener("keydown", (event) => this._onKeyDown(event), true);
this.addEventListener("click", (event) => this._onClick(event));
}
connectedCallback() {
this._parentElement = this.parentElement || this.parentNode.host;
this._parentElement.addEventListener("contextmenu", this._parentContextMenuListener = (event) => {
this._onParentContextMenu(event);
});
}
disconnectedCallback() {
this._parentElement.removeEventListener("contextmenu", this._parentContextMenuListener);
this._parentElement = null;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onBlur() {
if (debug === false) {
this.close();
}
}
_onParentContextMenu(event) {
if (this.disabled === false) {
event.preventDefault();
this.open(event.clientX, event.clientY);
}
}
_onOverlayContextMenu(event) {
event.preventDefault();
event.stopImmediatePropagation();
this.close().then(() => {
let target = this.parentElement.getRootNode().elementFromPoint(event.clientX, event.clientY);
if (target && this.parentElement.contains(target)) {
this.open(event.clientX, event.clientY);
}
});
}
_onOverlayPointerDown(event) {
if (event.button === 0) {
event.preventDefault();
this.close();
}
}
async _onClick() {
let item = event.target.closest("x-menuitem");
if (item && item.disabled === false) {
let submenu = item.querySelector("x-menu");
if (submenu) {
if (submenu.opened) {
submenu.close();
}
else {
submenu.openNextToElement(item, "horizontal");
}
}
else {
this.setAttribute("closing", "");
await item.whenTriggerEnd;
await this.close();
this.removeAttribute("closing");
}
}
}
_onKeyDown(event) {
if (event.key === "Escape") {
let menu = this.querySelector("x-menu");
if (menu.opened) {
event.preventDefault();
this.close();
}
}
else if (event.key === "Tab") {
event.preventDefault();
event.stopPropagation();
let menu = this.querySelector("x-menu");
menu.focusNextMenuItem();
}
}
///////////////////////////////////'/////////////////////////////////////////////////////////////////////////////
open(clientX, clientY) {
let menu = this.querySelector("x-menu");
if (menu.opened === false) {
menu.openAtPoint(clientX, clientY);
this["#overlay"].ownerElement = menu;
this["#overlay"].show(false);
menu.focus();
}
}
close() {
return new Promise(async (resolve) => {
let menu = this.querySelector("x-menu");
await menu.close();
this["#overlay"].hide(false);
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
resolve();
});
}
}
customElements.define("x-contextmenu", XContextMenuElement);
// @copyright
// © 2016-2017 Jarosław Foksa
// @doc
// http://w3c.github.io/aria-practices/#dialog_modal
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_dialog_role
// https://www.marcozehe.de/2015/02/05/advanced-aria-tip-2-accessible-modal-dialogs/
let shadowTemplate$7 = html`
<template>
<style>:host{position:fixed;left:50%;top:0;width:600px;height:auto;min-height:100px;padding:0;box-sizing:border-box;z-index:999999;display:block;outline:0;--backdrop-color: rgba(0, 0, 0, 0.5);--origin: center}:host([offscreen]){opacity:0;pointer-events:none;top:-10000}</style>
<slot></slot>
</template>
`;
// @events
// close
class XDialogElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$7.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this["#overlay"] = createElement("x-overlay");
this["#overlay"].ownerElement = this;
this["#overlay"].addEventListener("click", (event) => this._onOverlayClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
this.setAttribute("role", "dialog");
this.setAttribute("tabindex", "-1");
if (this.opened === false) {
this.setAttribute("offscreen", "");
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "opened") {
this._onOpenedAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["opened"];
}
// @type
// boolean
// @default
// false
// @attribute
get opened() {
return this.hasAttribute("opened");
}
set opened(opened) {
opened ? this.setAttribute("opened", "") : this.removeAttribute("opened");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onOpenedAttributeChange() {
if (this.opened) {
this._open();
}
else {
this._close();
}
}
_onOverlayClick() {
this.opened = false;
}
_onKeyDown(event) {
if (event.key === "Escape") {
this.opened = false;
}
else if (event.key === "Tab") {
// Prevent user from moving focus outside the dialog
let focusableElements = [...this.querySelectorAll("*")].filter($0 => $0.tabIndex >= 0);
if (focusableElements.length > 0) {
let firstFocusableElement = focusableElements[0];
let lastFocusableElement = focusableElements[focusableElements.length-1];
if (event.shiftKey === false) {
if (event.target === lastFocusableElement) {
event.preventDefault();
firstFocusableElement.focus();
}
}
else if (event.shiftKey === true) {
if (event.target === firstFocusableElement) {
event.preventDefault();
lastFocusableElement.focus();
}
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async _open() {
this._initiallyFocusedElement = this.getRootNode().querySelector(":focus");
let computedStyle = getComputedStyle(this);
let origin = computedStyle.getPropertyValue("--origin").trim();
let backdropColor = computedStyle.getPropertyValue("--backdrop-color");
if (origin === "center") {
/* http://zerosixthree.se/vertical-align-anything-with-just-3-lines-of-css/ */
this.style.transform = "perspective(1px) translate(-50%, -50%)";
this.style.top = "50%";
}
else if (origin === "top") {
this.style.transform = "perspective(1px) translate(-50%, -0%)";
}
this["#overlay"].style.background = backdropColor;
this.setAttribute("opened", "");
this.removeAttribute("offscreen");
this["#overlay"].show(true);
let bbox = this.getBoundingClientRect();
let animation = this.animate(
{
top: [`-${bbox.height}px`, computedStyle.top],
},
{
duration: 300,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)"
}
);
await animation.finished;
let descendants = [...this.querySelectorAll("*")];
let lastFocusableDescendant = descendants.reverse().find($0 => $0.tabIndex >= 0);
if (lastFocusableDescendant) {
lastFocusableDescendant.focus();
}
}
async _close() {
let computedStyle = getComputedStyle(this);
let origin = computedStyle.getPropertyValue("--origin").trim();
let bbox = this.getBoundingClientRect();
this.removeAttribute("opened");
let animation = this.animate(
{
top: [computedStyle.top, `-${bbox.height + 20}px`],
},
{
duration: 300,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)"
}
);
await animation.finished;
this.setAttribute("offscreen", "");
this["#overlay"].hide(true);
// Resotre focus to the element that was focused before we opened the dialog
if (this._initiallyFocusedElement) {
this._initiallyFocusedElement.focus();
}
this.dispatchEvent(new CustomEvent("close"));
}
}
customElements.define("x-dialog", XDialogElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let {max: max$4} = Math;
let easing$3 = "cubic-bezier(0.4, 0, 0.2, 1)";
let shadowTemplate$8 = html`
<template>
<style>:host{display:flex;align-items:center;position:relative;width:100%;height:100%;max-width:200px;flex:1 0 0;transition-property:max-width,padding,order;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);cursor:default;padding:0 4px 0 16px;user-select:none;touch-action:pan-y;box-sizing:border-box;overflow:hidden;contain:strict;will-change:max-width;z-index:0;-webkit-app-region:no-drag;--ripple-type: none;--ripple-background: currentColor;--ripple-opacity: 0.2;--selection-indicator-height: 3px;--selection-indicator-color: var(--accent-color);--close-button-position: static;--close-button-left: 0;--close-button-right: initial;--close-button-width: 18px;--close-button-height: 18px;--close-button-margin: 0 0 0 auto;--close-button-opacity: 0.8;--close-button-path-d: path("M 74.355 30.551 L 69.449 25.645 L 50 45.094 L 30.551 25.645 L 25.645 30.551 L 45.094 50 L 25.645 69.449 L 30.551 74.355 L 50 54.906 L 69.449 74.355 L 74.355 69.449 L 54.906 50 Z")}:host([edited]){--close-button-path-d: path("M 68.107 50 C 68.107 60 60 68.107 50 68.107 C 40 68.107 31.893 60 31.893 50 C 31.893 40 40 31.893 50 31.893 C 60 31.893 68.107 40 68.107 50 Z")}:host(:focus){outline:0}:host([closing]){pointer-events:none}:host([selected]){z-index:1}:host([disabled]){pointer-events:none;opacity:.5}#close-button{display:flex;align-items:center;justify-content:center;position:var(--close-button-position);left:var(--close-button-left);right:var(--close-button-right);width:var(--close-button-width);height:var(--close-button-height);margin:var(--close-button-margin);opacity:var(--close-button-opacity);padding:1px}#close-button:hover{background:rgba(0,0,0,.08);opacity:1}#close-button-path{pointer-events:none;d:var(--close-button-path-d);fill:currentColor}#ripples{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;z-index:-1;contain:strict}#ripples .ripple,#selection-indicator{position:absolute;left:0;pointer-events:none}#ripples .ripple{top:0;height:200px;background:var(--ripple-background);opacity:var(--ripple-opacity);border-radius:999px;will-change:opacity,transform;width:200px}#selection-indicator{display:none;bottom:0;width:100%;height:var(--selection-indicator-height);background:var(--selection-indicator-color)}:host([selected]) #selection-indicator{display:block}:host-context(x-doctabs[animatingindicator]) #selection-indicator{display:none}</style>
<div id="ripples"></div>
<div id="selection-indicator"></div>
<slot></slot>
<svg id="close-button" viewBox="0 0 100 100" preserveAspectRatio="none">
<path id="close-button-path"></path>
</svg>
</template>
`;
// @events
// close
class XDocTabElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$8.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this["#close-button"].addEventListener("pointerdown", (event) => this._onCloseButtonPointerDown(event));
this["#close-button"].addEventListener("click", (event) => this._onCloseButtonClick(event));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("click", (event) => this._onClick(event));
}
connectedCallback() {
this.setAttribute("tabindex", this.selected ? "0" : "-1");
this.setAttribute("role", "tab");
this.setAttribute("aria-selected", this.selected);
this.setAttribute("aria-disabled", this.disabled);
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "selected") {
this._onSelectedAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["selected", "disabled"];
}
// @type
// XDocTabsElement
// @readOnly
get ownerTabs() {
return this.closest("x-doctabs");
}
// @info
// Value associated with this tab.
// @type
// string
// @default
// ""
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : "";
}
set value(value) {
this.setAttribute("value", value);
}
// @property
// reflected
// @type
// boolean
// @default
// false
get selected() {
return this.hasAttribute("selected");
}
set selected(selected) {
selected ? this.setAttribute("selected", "") : this.removeAttribute("selected");
}
// @property
// reflected
// @type
// boolean
// @default
// false
get edited() {
return this.hasAttribute("edited");
}
set edited(edited) {
edited ? this.setAttribute("edited", "") : this.removeAttribute("edited");
}
// @property
// reflected
// @type
// boolean
// @default
// false
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled === true ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onSelectedAttributeChange() {
this.setAttribute("aria-selected", this.selected);
this.setAttribute("tabindex", this.selected ? "0" : "-1");
}
_onDisabledAttributeChange() {
this.setAttribute("aria-disabled", this.disabled);
this.setAttribute("tabindex", this.selected ? "0" : "-1");
}
_onPointerDown(pointerDownEvent) {
// Don't focus the widget with pointer, instead focus the closest ancestor focusable element
if (this.matches(":focus") === false) {
event.preventDefault();
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
if (pointerDownEvent.button !== 0) {
return;
}
// Provide "pressed" attribute for theming purposes
{
let pointerDownTimeStamp = Date.now();
this.setAttribute("pressed", "");
this.setPointerCapture(pointerDownEvent.pointerId);
this.addEventListener("lostpointercapture", async (event) => {
if (this.selected === true) {
let pressedTime = Date.now() - pointerDownTimeStamp;
let minPressedTime = 100;
if (pressedTime < minPressedTime) {
await sleep(minPressedTime - pressedTime);
}
}
this.removeAttribute("pressed");
}, {once: true});
}
// Ripple
{
let rippleType = getComputedStyle(this).getPropertyValue("--ripple-type").trim();
if (rippleType === "bounded") {
let rect = this["#ripples"].getBoundingClientRect();
let size = max$4(rect.width, rect.height) * 1.5;
let top = pointerDownEvent.clientY - rect.y - size/2;
let left = pointerDownEvent.clientX - rect.x - size/2;
let ripple = createElement("div");
ripple.setAttribute("class", "ripple pointer-down-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
let inAnimation = ripple.animate({ transform: ["scale(0)", "scale(1)"]}, { duration: 300, easing: easing$3 });
// Pointer capture is set on the owner tabs rather than this tab intentionally. Owner tabs might be
// already capturing the pointer and hijacking it would disrupt the currently performed tab move
// operation.
this.ownerTabs.setPointerCapture(pointerDownEvent.pointerId);
this.ownerTabs.addEventListener("lostpointercapture", async () => {
await inAnimation.finished;
let fromOpacity = getComputedStyle(ripple).opacity;
let outAnimation = ripple.animate({ opacity: [fromOpacity, "0"]}, { duration: 300, easing: easing$3 });
await outAnimation.finished;
ripple.remove();
}, {once: true});
}
}
}
async _onClick(event) {
if (event.button !== 0) {
return;
}
// Ripple
if (this["#ripples"].querySelector(".pointer-down-ripple") === null) {
let rippleType = getComputedStyle(this).getPropertyValue("--ripple-type").trim();
if (rippleType === "bounded") {
let rect = this["#ripples"].getBoundingClientRect();
let size = max$4(rect.width, rect.height) * 1.5;
let top = (rect.y + rect.height/2) - rect.y - size/2;
let left = (rect.x + rect.width/2) - rect.x - size/2;
let ripple = createElement("div");
ripple.setAttribute("class", "ripple click-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
let inAnimation = ripple.animate({ transform: ["scale(0)", "scale(1)"]}, { duration: 300, easing: easing$3 });
await inAnimation.finished;
let fromOpacity = getComputedStyle(ripple).opacity;
let outAnimation = ripple.animate({ opacity: [fromOpacity, "0"] }, { duration: 300, easing: easing$3 });
await outAnimation.finished;
ripple.remove();
}
}
}
_onCloseButtonPointerDown(event) {
if (event.button !== 0) {
return;
}
event.stopPropagation();
}
_onCloseButtonClick(event) {
if (event.button !== 0) {
return;
}
event.stopPropagation();
let customEvent = new CustomEvent("close", {bubbles: true, cancelable: true, detail: this});
this.dispatchEvent(customEvent);
if (customEvent.defaultPrevented === false) {
this.ownerTabs.closeTab(this);
}
}
}
customElements.define("x-doctab", XDocTabElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let {abs} = Math;
let {parseInt: parseInt$2} = Number;
let shadowTemplate$9 = html`
<template>
<style>:host{display:flex;align-items:center;width:100%;position:relative;--open-button-width: 24px;--open-button-height: 24px;--open-button-margin: 0 10px;--open-button-path-d: path("M 79 54 L 54 54 L 54 79 L 46 79 L 46 54 L 21 54 L 21 46 L 46 46 L 46 21 L 54 21 L 54 46 L 79 46 L 79 54 Z")}:host(:focus){outline:0}:host([disabled]){opacity:.5;pointer-events:none}#open-button{display:flex;align-items:center;justify-content:center;width:var(--open-button-width);height:var(--open-button-height);margin:var(--open-button-margin);order:9999;opacity:.7;color:inherit;-webkit-app-region:no-drag}#open-button:hover{opacity:1}#open-button-path{d:var(--open-button-path-d);fill:currentColor}#selection-indicator-container{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;pointer-events:none}#selection-indicator{position:absolute;width:100%;bottom:0;left:0}</style>
<slot></slot>
<div id="selection-indicator-container">
<div id="selection-indicator" hidden></div>
</div>
<svg id="open-button" viewBox="0 0 100 100" preserveAspectRatio="none">
<path id="open-button-path"></path>
</svg>
</template>
`;
// @events
// open
// close
// select
// rearrange
class XDocTabsElement extends HTMLElement {
constructor() {
super();
this._waitingForTabToClose = false;
this._waitingForPointerMoveAfterClosingTab = false;
this._shadowRoot = this.attachShadow({mode: "closed", delegatesFocus: true});
this._shadowRoot.append(document.importNode(shadowTemplate$9.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this["#open-button"].addEventListener("click", (event) => this._onOpenButtonClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled === true ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
// @info
// Maximal number of tambs that can be opened
// @type
// number
// @default
// 20
// @attribute
get maxTabs() {
return this.hasAttribute("maxtabs") ? parseInt$2(this.getAttribute("maxtabs")) : 20;
}
set maxTabs(maxTabs) {
this.setAttribute("maxtabs", maxTabs);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
openTab(tab, animate = true) {
return new Promise( async (resolve, reject) => {
let tabs = this.querySelectorAll("x-doctab");
if (tabs.length >= this.maxTabs) {
reject(`Can't open more than ${this.maxTabs} tabs.`);
}
else {
let maxOrder = 0;
for (let tab of this.children) {
let order = parseInt$2(tab.style.order);
if (!Number.isNaN(order) && order > maxOrder) {
maxOrder = order;
}
}
tab.style.order = maxOrder;
if (animate === false) {
tab.style.transition = "none";
tab.style.maxWidth = null;
tab.style.padding = null;
this.append(tab);
tab.focus();
resolve(tab);
}
else if (animate === true) {
tab.style.transition = null;
tab.style.maxWidth = "0px";
tab.style.padding = "0px";
tab.setAttribute("opening", "");
this.append(tab);
await sleep(30);
tab.addEventListener("transitionend", (event) => {
tab.removeAttribute("opening");
resolve(tab);
}, {once: true});
tab.style.maxWidth = null;
tab.style.padding = null;
tab.focus();
}
}
});
}
closeTab(tab, animate = true) {
return new Promise( async (resolve) => {
let tabs = this.getTabsByScreenIndex().filter(tab => tab.hasAttribute("closing") === false);
let tabWidth = tab.getBoundingClientRect().width;
let tabScreenIndex = this._getTabScreenIndex(tab);
tab.setAttribute("closing", "");
if (tabScreenIndex < tabs.length - 1) {
for (let tab of this.children) {
if (tab.hasAttribute("closing") === false) {
tab.style.transition = "none";
tab.style.maxWidth = tabWidth + "px";
}
}
}
if (animate) {
tab.style.transition = null;
}
else {
tab.style.transition = "none";
}
tab.style.maxWidth = "0px";
tab.style.pointerEvents = "none";
this._waitingForTabToClose = true;
if (tab.selected) {
let previousTab = tabs[tabs.indexOf(tab) - 1];
let nextTab = tabs[tabs.indexOf(tab) + 1];
tab.selected = false;
if (nextTab) {
nextTab.selected = true;
}
else if (previousTab) {
previousTab.selected = true;
}
}
if (tab.matches(":focus")) {
let selectedTab = this.querySelector("x-doctab[selected]");
if (selectedTab) {
selectedTab.focus();
}
else {
this.focus();
}
}
tab.style.maxWidth = "0px";
tab.style.padding = "0px";
if (animate) {
await sleep(150);
}
tab.remove();
this._waitingForTabToClose = false;
tab.removeAttribute("closing");
resolve();
if (!this._waitingForPointerMoveAfterClosingTab) {
this._waitingForPointerMoveAfterClosingTab = true;
await this._whenPointerMoved(3);
this._waitingForPointerMoveAfterClosingTab = false;
for (let tab of this.children) {
tab.style.transition = null;
tab.style.maxWidth = null;
tab.style.order = this._getTabScreenIndex(tab);
}
}
});
}
selectPreviousTab() {
let tabs = this.getTabsByScreenIndex();
let currentTab = this.querySelector(`x-doctab[selected]`) || this.querySelector("x-doctab");
let previousTab = this._getPreviousTabOnScreen(currentTab);
if (currentTab && previousTab) {
currentTab.tabIndex = -1;
currentTab.selected = false;
previousTab.tabIndex = 0;
previousTab.selected = true;
return previousTab;
}
return null;
}
selectNextTab() {
let tabs = this.getTabsByScreenIndex();
let currentTab = this.querySelector(`x-doctab[selected]`) || this.querySelector("x-doctab:last-of-type");
let nextTab = this._getNextTabOnScreen(currentTab);
if (currentTab && nextTab) {
currentTab.tabIndex = -1;
currentTab.selected = false;
nextTab.tabIndex = 0;
nextTab.selected = true;
return nextTab;
}
return null;
}
moveSelectedTabLeft() {
let selectedTab = this.querySelector("x-doctab[selected]");
let selectedTabScreenIndex = this._getTabScreenIndex(selectedTab);
for (let tab of this.children) {
tab.style.order = this._getTabScreenIndex(tab);
}
if (parseInt$2(selectedTab.style.order) === 0) {
for (let tab of this.children) {
if (tab === selectedTab) {
tab.style.order = this.childElementCount - 1;
}
else {
tab.style.order = parseInt$2(tab.style.order) - 1;
}
}
}
else {
let otherTab = this._getTabWithScreenIndex(selectedTabScreenIndex - 1);
otherTab.style.order = parseInt$2(otherTab.style.order) + 1;
selectedTab.style.order = parseInt$2(selectedTab.style.order) - 1;
}
}
moveSelectedTabRight() {
let selectedTab = this.querySelector("x-doctab[selected]");
let selectedTabScreenIndex = this._getTabScreenIndex(selectedTab);
for (let tab of this.children) {
tab.style.order = this._getTabScreenIndex(tab);
}
if (parseInt$2(selectedTab.style.order) === this.childElementCount - 1) {
for (let tab of this.children) {
if (tab === selectedTab) {
tab.style.order = 0;
}
else {
tab.style.order = parseInt$2(tab.style.order) + 1;
}
}
}
else {
let otherTab = this._getTabWithScreenIndex(selectedTabScreenIndex + 1);
otherTab.style.order = parseInt$2(otherTab.style.order) - 1;
selectedTab.style.order = parseInt$2(selectedTab.style.order) + 1;
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onPointerDown(event) {
if (event.button === 0 && !this._waitingForTabToClose && event.target.closest("x-doctab")) {
this._onTabPointerDown(event);
}
}
_onTabPointerDown(pointerDownEvent) {
if (pointerDownEvent.isPrimary === false) {
return;
}
let pointerMoveListener, lostPointerCaptureListener;
let pointerDownTab = pointerDownEvent.target.closest("x-doctab");
let selectedTab = this.querySelector("x-doctab[selected]");
for (let tab of this.querySelectorAll("x-doctab")) {
if (tab === pointerDownTab) {
if (tab.selected === false) {
tab.selected = true;
this.dispatchEvent(new CustomEvent("select"));
}
}
else {
tab.selected = false;
}
tab.tabIndex = (tab === pointerDownTab) ? 0 : -1;
}
let selectionIndicatorAnimation = this._animateSelectionIndicator(selectedTab, pointerDownTab);
this.setPointerCapture(pointerDownEvent.pointerId);
this.addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
if (pointerMoveEvent.isPrimary && abs(pointerMoveEvent.clientX - pointerDownEvent.clientX) > 1) {
this.removeEventListener("pointermove", pointerMoveListener);
this.removeEventListener("lostpointercapture", lostPointerCaptureListener);
selectionIndicatorAnimation.finish();
this._onTabDragStart(pointerDownEvent, pointerDownTab);
}
});
this.addEventListener("lostpointercapture", lostPointerCaptureListener = () => {
this.removeEventListener("pointermove", pointerMoveListener);
this.removeEventListener("lostpointercapture", lostPointerCaptureListener);
});
}
_onTabDragStart(firstPointerMoveEvent, draggedTab) {
let tabBounds = draggedTab.getBoundingClientRect();
let tabsBounds = this.getBoundingClientRect();
let $initialScreenIndex = Symbol();
let $screenIndex = Symbol();
let $flexOffset = Symbol();
draggedTab.style.zIndex = 999;
this["#open-button"].style.opacity = "0";
for (let tab of this.children) {
let screenIndex = this._getTabScreenIndex(tab);
tab[$screenIndex] = screenIndex;
tab[$initialScreenIndex] = screenIndex;
tab[$flexOffset] = tab.getBoundingClientRect().left - tabsBounds.left;
if (tab !== draggedTab) {
tab.style.transition = "transform 0.15s cubic-bezier(0.4, 0, 0.2, 1)";
}
}
let onDraggedTabScreenIndexChange = (fromScreenIndex, toScreenIndex) => {
if (toScreenIndex > fromScreenIndex + 1) {
for (let i = fromScreenIndex; i < toScreenIndex; i += 1) {
onDraggedTabScreenIndexChange(i, i + 1);
}
}
else if (toScreenIndex < fromScreenIndex - 1) {
for (let i = fromScreenIndex; i > toScreenIndex; i -= 1) {
onDraggedTabScreenIndexChange(i, i - 1);
}
}
else {
for (let tab of this.children) {
if (tab !== draggedTab) {
if (tab[$screenIndex] === toScreenIndex) {
tab[$screenIndex] = fromScreenIndex;
}
let translateX = -tab[$flexOffset];
for (let i = 0; i < tab[$screenIndex]; i += 1) {
translateX += tabBounds.width;
}
if (translateX === 0) {
tab.style.transform = null;
}
else {
tab.style.transform = "translate(" + translateX + "px)";
}
}
}
}
};
let pointerMoveListener = (pointerMoveEvent) => {
if (pointerMoveEvent.isPrimary) {
let dragOffset = pointerMoveEvent.clientX - firstPointerMoveEvent.clientX;
if (dragOffset + draggedTab[$flexOffset] <= 0) {
dragOffset = -draggedTab[$flexOffset];
}
else if (dragOffset + draggedTab[$flexOffset] + tabBounds.width > tabsBounds.width) {
dragOffset = tabsBounds.width - draggedTab[$flexOffset] - tabBounds.width;
}
draggedTab.style.transform = "translate(" + dragOffset + "px)";
let screenIndex = this._getTabScreenIndex(draggedTab);
if (screenIndex !== draggedTab[$screenIndex]) {
let previousTabScreenIndex = draggedTab[$screenIndex];
draggedTab[$screenIndex] = screenIndex;
onDraggedTabScreenIndexChange(previousTabScreenIndex, draggedTab[$screenIndex]);
}
}
};
let lostPointerCaptureListener = async (dragEndEvent) => {
this.removeEventListener("pointermove", pointerMoveListener);
this.removeEventListener("lostpointercapture", lostPointerCaptureListener);
let translateX = -draggedTab[$flexOffset];
for (let i = 0; i < draggedTab[$screenIndex]; i += 1) {
translateX += tabBounds.width;
}
draggedTab.style.transition = "transform 0.15s cubic-bezier(0.4, 0, 0.2, 1)";
draggedTab.style.transform = "translate(" + translateX + "px)";
if (draggedTab[$initialScreenIndex] !== draggedTab[$screenIndex]) {
this.dispatchEvent(
new CustomEvent("rearrange")
);
}
await sleep(150);
draggedTab.style.zIndex = null;
this["#open-button"].style.opacity = null;
for (let tab of this.children) {
tab.style.transition = "none";
tab.style.transform = "translate(0px, 0px)";
tab.style.order = tab[$screenIndex];
}
};
this.addEventListener("pointermove", pointerMoveListener);
this.addEventListener("lostpointercapture", lostPointerCaptureListener);
}
_onOpenButtonClick(clickEvent) {
if (clickEvent.button === 0) {
let customEvent = new CustomEvent("open", {cancelable: true});
this.dispatchEvent(customEvent);
if (customEvent.defaultPrevented === false) {
let openedTab = html`<x-doctab><x-label>Untitled</x-label></x-doctab>`;
openedTab.style.order = this.childElementCount;
this.openTab(openedTab);
for (let tab of this.children) {
tab.selected = (tab === openedTab);
}
}
}
}
_onKeyDown(event) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
return;
}
else if (event.code === "Enter" || event.code === "Space") {
let currentTab = this.querySelector(`x-doctab[tabindex="0"]`);
let selectedTab = this.querySelector(`x-doctab[selected]`);
event.preventDefault();
currentTab.click();
if (currentTab !== selectedTab) {
selectedTab.selected = false;
currentTab.selected = true;
this._animateSelectionIndicator(selectedTab, currentTab);
}
}
else if (event.code === "ArrowLeft") {
let tabs = this.getTabsByScreenIndex();
let currentTab = this.querySelector(`x-doctab[tabindex="0"]`);
let previousTab = this._getPreviousTabOnScreen(currentTab);
if (previousTab) {
event.preventDefault();
currentTab.tabIndex = -1;
previousTab.tabIndex = 0;
previousTab.focus();
}
}
else if (event.code === "ArrowRight") {
let tabs = this.getTabsByScreenIndex();
let currentTab = this.querySelector(`x-doctab[tabindex="0"]`);
let nextTab = this._getNextTabOnScreen(currentTab);
if (nextTab) {
event.preventDefault();
currentTab.tabIndex = -1;
nextTab.tabIndex = 0;
nextTab.focus();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Returns a promise that is resolved when the pointer is moved by at least the given distance.
// @type
// (number) => Promise
_whenPointerMoved(distance = 3) {
return new Promise((resolve) => {
let pointerMoveListener, pointerOutListener, blurListener;
let fromPoint = null;
let removeListeners = () => {
window.removeEventListener("pointermove", pointerMoveListener);
window.removeEventListener("pointerout", pointerOutListener);
window.removeEventListener("blur", blurListener);
};
window.addEventListener("pointermove", pointerMoveListener = (event) => {
if (fromPoint === null) {
fromPoint = {x: event.clientX, y: event.clientY};
}
else {
let toPoint = {x: event.clientX, y: event.clientY};
if (getDistanceBetweenPoints(fromPoint, toPoint) >= distance) {
removeListeners();
resolve();
}
}
});
window.addEventListener("pointerout", pointerOutListener = (event) => {
if (event.toElement === null) {
removeListeners();
resolve();
}
});
window.addEventListener("blur", blurListener = () => {
removeListeners();
resolve();
});
});
}
_animateSelectionIndicator(fromTab, toTab) {
let mainBBox = this.getBoundingClientRect();
let startBBox = fromTab ? fromTab.getBoundingClientRect() : null;
let endBBox = toTab.getBoundingClientRect();
let computedStyle = getComputedStyle(toTab);
if (startBBox === null) {
startBBox = DOMRect.fromRect(endBBox);
startBBox.x += startBBox.width / 2;
startBBox.width = 0;
}
this["#selection-indicator"].style.height = computedStyle.getPropertyValue("--selection-indicator-height");
this["#selection-indicator"].style.background = computedStyle.getPropertyValue("--selection-indicator-color");
this["#selection-indicator"].hidden = false;
this.setAttribute("animatingindicator", "");
let animation = this["#selection-indicator"].animate(
[
{
bottom: (startBBox.bottom - mainBBox.bottom) + "px",
left: (startBBox.left - mainBBox.left) + "px",
width: startBBox.width + "px",
},
{
bottom: (endBBox.bottom - mainBBox.bottom) + "px",
left: (endBBox.left - mainBBox.left) + "px",
width: endBBox.width + "px",
}
],
{
duration: 200,
iterations: 1,
delay: 0,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)"
}
);
animation.finished.then(() => {
this["#selection-indicator"].hidden = true;
this.removeAttribute("animatingindicator");
});
return animation;
}
_moveSelectedTabRight() {
}
getTabsByScreenIndex() {
let $screenIndex = Symbol();
for (let tab of this.children) {
tab[$screenIndex] = this._getTabScreenIndex(tab);
}
return [...this.children].sort((tab1, tab2) => tab1[$screenIndex] > tab2[$screenIndex]);
}
_getTabScreenIndex(tab) {
let tabBounds = tab.getBoundingClientRect();
let tabsBounds = this.getBoundingClientRect();
if (tabBounds.left - tabsBounds.left < tabBounds.width / 2) {
return 0;
}
else {
let offset = (tabBounds.width / 2);
for (let i = 1; i < this.maxTabs; i += 1) {
if (tabBounds.left - tabsBounds.left >= offset &&
tabBounds.left - tabsBounds.left < offset + tabBounds.width) {
if (i > this.childElementCount - 1) {
return this.childElementCount - 1;
}
else {
return i;
}
}
else {
offset += tabBounds.width;
}
}
}
}
_getTabWithScreenIndex(screenIndex) {
for (let tab of this.children) {
if (this._getTabScreenIndex(tab) === screenIndex) {
return tab;
}
}
return null;
}
_getPreviousTabOnScreen(tab, skipDisabled = true, wrapAround = true) {
let tabs = this.getTabsByScreenIndex();
let tabScreenIndex = tabs.indexOf(tab);
let previousTab = null;
for (let i = tabScreenIndex - 1; i >= 0; i -= 1) {
let tab = tabs[i];
if (skipDisabled && tab.disabled) {
continue;
}
else {
previousTab = tab;
break;
}
}
if (wrapAround) {
if (previousTab === null) {
for (let i = tabs.length - 1; i > tabScreenIndex; i -= 1) {
let tab = tabs[i];
if (skipDisabled && tab.disabled) {
continue;
}
else {
previousTab = tab;
break;
}
}
}
}
return previousTab;
}
// @info
// Get previous tab on screen.
_getNextTabOnScreen(tab, skipDisabled = true, wrapAround = true) {
let tabs = this.getTabsByScreenIndex();
let tabScreenIndex = tabs.indexOf(tab);
let nextTab = null;
for (let i = tabScreenIndex + 1; i < tabs.length; i += 1) {
let tab = tabs[i];
if (skipDisabled && tab.disabled) {
continue;
}
else {
nextTab = tab;
break;
}
}
if (wrapAround) {
if (nextTab === null) {
for (let i = 0; i < tabScreenIndex; i += 1) {
let tab = tabs[i];
if (skipDisabled && tab.disabled) {
continue;
}
else {
nextTab = tab;
break;
}
}
}
}
return nextTab;
}
}
customElements.define("x-doctabs", XDocTabsElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let {parseFloat: parseFloat$2} = Number;
let shadowTemplate$10 = html`
<template>
<style>:host{display:block;position:fixed!important;box-sizing:border-box;margin:auto;will-change:transform;z-index:1001;transition-property:transform;transition-duration:.25s;transition-timing-function:cubic-bezier(0,0,.2,1);--backdrop-color: rgba(0, 0, 0, 0)}:host([offscreen]){display:none!important}:host([position="left"]),:host([position="right"]){width:280px;height:100%;top:0;bottom:0;left:0;right:auto}:host([position="right"]){left:auto;right:0}:host([position="bottom"]),:host([position="top"]){width:100%;height:280px;top:0;bottom:auto;left:0;right:0}:host([position="bottom"]){top:auto;bottom:0}</style>
<slot></slot>
</template>
`;
class XDrawerElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$10.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this["#overlay"] = html`<x-overlay id="overlay"></x-overlay>`;
this["#overlay"].ownerElement = this;
this["#overlay"].addEventListener("click", (event) => this._onOverlayClick(event));
}
connectedCallback() {
if (this.hasAttribute("position") === false) {
this.setAttribute("position", "left");
}
if (this.opened === false) {
this.setAttribute("offscreen", "");
}
}
attributeChangedCallback(name) {
if (name === "opened") {
this._onOpenedAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["opened"];
}
// @type
// boolean
// @default
// false
// @attribute
get opened() {
return this.hasAttribute("opened");
}
set opened(opened) {
opened ? this.setAttribute("opened", "") : this.removeAttribute("opened");
}
// @type
// string
// @default
// "left"
// @attribute
get position() {
return this.getAttribute("position") || "left";
}
set position(position) {
this.setAttribute("position", position);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onOpenedAttributeChange() {
if (this.opened) {
this._open();
}
else {
this._close();
}
}
_onOverlayClick() {
this.opened = false;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_open() {
return new Promise( async (resolve) => {
// Overlay
{
this["#overlay"].style.background = getComputedStyle(this).getPropertyValue("--backdrop-color");
this["#overlay"].show(true);
}
// Drawer
{
this.removeAttribute("offscreen");
let computedStyle = getComputedStyle(this);
let {width, height, marginLeft} = computedStyle;
let keyframes = null;
if (marginLeft === "auto") {
marginLeft = "0px";
}
if (this.position === "left") {
keyframes = [
{ transform: `translateX(-${width})` },
{ transform: "translateX(0px)" },
];
}
else if (this.position === "right") {
keyframes = [
{ transform: `translateX(${width})` },
{ transform: "translateX(0px)" },
];
}
else if (this.position === "top") {
keyframes = [
{ transform: `translateY(-${height})` },
{ transform: "translateY(0px)" },
];
}
else if (this.position === "bottom") {
keyframes = [
{ transform: `translateY(${height})` },
{ transform: "translateY(0px)" },
];
}
if (keyframes) {
let {transitionTimingFunction, transitionDuration, transitionDelay} = computedStyle;
let animation = this.animate(keyframes, {
duration: parseFloat$2(transitionDuration) * 1000,
delay: parseFloat$2(transitionDelay) * 1000,
easing: transitionTimingFunction,
iterations: 1
});
this._currentAnimation = animation;
await animation.finished;
}
}
resolve();
});
}
_close() {
return new Promise( async (resolve) => {
// Overlay
{
this["#overlay"].hide(true);
}
// Drawer
{
let computedStyle = getComputedStyle(this);
let {width, height, marginLeft} = computedStyle;
let keyframes = null;
if (this.position === "left") {
keyframes = [
{ transform: "translateX(0px)" },
{ transform: `translateX(-${width})` },
];
}
else if (this.position === "right") {
keyframes = [
{ transform: "translateX(0px)" },
{ transform: `translateX(${width})` },
];
}
else if (this.position === "top") {
keyframes = [
{ transform: "translateY(0px)" },
{ transform: `translateY(-${height})` },
];
}
else if (this.position === "bottom") {
keyframes = [
{ transform: "translateY(0px)" },
{ transform: `translateY(${height})` },
];
}
if (keyframes) {
let {transitionTimingFunction, transitionDuration, transitionDelay} = computedStyle;
let animation = this.animate(keyframes, {
duration: parseFloat$2(transitionDuration) * 1000,
delay: parseFloat$2(transitionDelay) * 1000,
easing: transitionTimingFunction,
iterations: 1
});
this._currentAnimation = animation;
await animation.finished;
if (this._currentAnimation === animation) {
this.setAttribute("offscreen", "");
}
}
}
resolve();
});
}
}
customElements.define("x-drawer", XDrawerElement);
// @doc
// https://material.google.com/style/icons.html#icons-system-icons
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$11 = html`
<template>
<style>:host{display:block;color:currentColor;display:flex;align-items:center;justify-content:center;width:24px;height:24px}:host([disabled]){opacity:.5}:host([hidden]){display:none}#svg{width:100%;height:100%;fill:currentColor;stroke:none;pointer-events:none}</style>
<svg id="svg" preserveAspectRatio="none" viewBox="0 0 100 100" width="0px" height="0px"></svg>
</template>
`;
class XIconElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$11.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "name") {
this._update();
}
else if (name === "iconset") {
this._update();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["name", "iconset"];
}
// @type
// string
// @default
// ""
// @attribute
get name() {
return this.hasAttribute("name") ? this.getAttribute("name") : "";
}
set name(name) {
this.setAttribute("name", name);
}
// @type
// string
// @default
// "node_modules/xel/images/icons.svg"
// @attribute
get iconset() {
if (this.hasAttribute("iconset") === false || this.getAttribute("iconset").trim() === "") {
return "node_modules/xel/images/icons.svg";
}
else {
return this.getAttribute("iconset");
}
}
set iconset(iconset) {
this.setAttribute("iconset", iconset);
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async _update() {
if (this.name === "") {
this["#svg"].innerHTML = "";
}
else {
if (this.iconset.startsWith("http") && new URL(this.iconset).origin !== window.location.origin) {
let symbol = await this._getSymbol(this.name, this.iconset);
if (symbol) {
this["#svg"].innerHTML = `${symbol.outerHTML}<use href="#${this.name}" width="100%" height="100%"></use>`;
}
}
else {
let href = `${this.iconset}#${this.name}`;
this["#svg"].innerHTML = `<use href="${href}" width="100%" height="100%"></use>`;
}
}
}
_getSymbol(name, url) {
return new Promise(async (resolve) => {
let iconset = null;
let cache = XIconElement._cache || [];
if (!XIconElement._cache) {
XIconElement._cache = cache;
}
if (cache[url]) {
iconset = cache[url];
}
else {
let fetchResponse;
try {
fetchResponse = await fetch(url);
}
catch (error) {
fetchResponse = null;
}
if (fetchResponse && fetchResponse.ok) {
try {
let iconsetSVG = await fetchResponse.text();
iconset = svg`${iconsetSVG}`;
cache[url] = iconset;
}
catch (error) {
}
}
}
if (iconset) {
let symbol = iconset.querySelector("#" + CSS.escape(name));
if (symbol) {
resolve(symbol);
}
}
});
}
}
customElements.define("x-icon", XIconElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$12 = html`
<template>
<style>:host{display:block;position:relative;max-width:140px;height:24px;box-sizing:border-box;color:#000;background:#fff;--selection-color: currentColor;--selection-background: #B2D7FD;--inner-padding: 0}:host(:focus){z-index:10}:host(:hover){cursor:text}:host([invalid]){--selection-color: white;--selection-background: #d50000}:host([disabled]){pointer-events:none;opacity:.5}:host([hidden]){display:none}::selection{color:var(--selection-color);background:var(--selection-background)}#input,#main{width:100%;height:100%}#main{display:flex;align-items:center}#input{padding:var(--inner-padding);box-sizing:border-box;color:inherit;background:0 0;border:0;outline:0;font-family:inherit;font-size:inherit;font-weight:inherit;text-align:inherit;cursor:inherit}:host(:not(:focus)) ::selection{color:inherit;background:0 0}:host([invalid])::before{position:absolute;left:0;top:26px;box-sizing:border-box;color:#d50000;font-family:inherit;font-size:11px;line-height:1.2;white-space:pre;content:attr(invalid-hint)}</style>
<main id="main">
<slot></slot>
<input id="input" spellcheck="false"></input>
</main>
</template>
`;
// @events
// input
// change
// textinputmodestart
// textinputmodeend
class XInputElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed", delegatesFocus: true});
this._shadowRoot.append(document.importNode(shadowTemplate$12.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("focusin", () => this._onFocusIn());
this.addEventListener("focusout", () => this._onFocusOut());
this.addEventListener("keydown", (event) => this._onKeyDown(event));
this["#input"].addEventListener("change", () => this._onInputChange());
this["#input"].addEventListener("input", (event) => this._onInputInput(event));
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "input");
this.setAttribute("aria-disabled", this.disabled);
this._update();
}
attributeChangedCallback(name) {
if (name === "type") {
this._onTypeAttributeChange();
}
else if (name === "value") {
this._onValueAttributeChange();
}
else if (name === "spellcheck") {
this._onSpellcheckAttributeChange();
}
else if (name === "maxlength") {
this._onMaxLengthAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
selectAll() {
this["#input"].select();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["type", "value", "spellcheck", "maxlength", "disabled"];
}
// @type
// "text" || "email" || "password" || "url" || "color"
// @default
// "text"
// @attribute
get type() {
return this.hasAttribute("type") ? this.getAttribute("type") : "text";
}
set type(type) {
this.setAttribute("type", type);
}
// @type
// string
// @default
// ""
// @inertAttribute
get value() {
return this["#input"].value;
}
set value(value) {
if (this["#input"].value !== value) {
if (this.matches(":focus")) {
// https://goo.gl/s1UnHh
this["#input"].selectionStart = 0;
this["#input"].selectionEnd = this["#input"].value.length;
document.execCommand("insertText", false, value);
}
else {
this["#input"].value = value;
}
this._update();
}
}
// @info
// Whether the input value should be validated instantly on each key press rather than when user confirms
// the value by pressing the enter key or moving focus away from the input.
// @default
// false
// @attribute
get instantValidation() {
return this.hasAttribute("instantvalidation");
}
set instantValidation(value) {
value === true ? this.setAttribute("instantvalidation", "") : this.removeAttribute("instantvalidation");
}
// @type
// boolean
// @default
// false
// @attribute
get spellcheck() {
return this.hasAttribute("spellcheck");
}
set spellcheck(spellcheck) {
spellcheck ? this.setAttribute("spellcheck", "") : this.removeAttribute("spellcheck");
}
// @type
// number
// @default
// 0
// @attribute
get minLength() {
return this.hasAttribute("minlength") ? parseInt(this.getAttribute("minlength")) : 0;
}
set minLength(minLength) {
this.setAttribute("minlength", minLength);
}
// @type
// number || Infinity
// @default
// 0
// @attribute
get maxLength() {
return this.hasAttribute("maxlength") ? parseInt(this.getAttribute("maxlength")) : Infinity;
}
set maxLength(maxLength) {
this.setAttribute("maxlength", maxLength);
}
// @type
// boolean
// @default
// false
// @attribute
get required() {
return this.hasAttribute("required");
}
set required(required) {
required ? this.setAttribute("required", "") : this.removeAttribute("required");
}
// @info
// Validation hints are not shown unless user focuses the element for the first time. Set this attribute to
// true to show the hints immediately.
// @type
// boolean
// @default
// false
// @attribute
get visited() {
return this.hasAttribute("visited");
}
set visited(visited) {
visited ? this.setAttribute("visited", "") : this.removeAttribute("visited");
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
// @info
// Whether the input is in invalid state.
// @type
// boolean
// @readOnly
get invalid() {
return this.hasAttribute("invalid");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Override this method to validate the input value manually.
// @type
// {valid:boolean, hint:string}
validate() {
let valid = true;
let hint = "";
if (this.value.length < this.minLength) {
valid = false;
hint = "Entered text is too short";
}
else if (this.value.length > this.maxLength) {
valid = false;
hint = "Entered text is too long";
}
else if (this.required && this.value.length === 0 && this.visited === true) {
valid = false;
hint = "This field is required";
}
else if (this.type === "email" && this["#input"].validity.valid === false) {
valid = false;
hint = "Invalid e-mail address";
}
else if (this.type === "url" && this["#input"].validity.valid === false) {
valid = false;
hint = "Invalid URL";
}
else if (this.type === "color" && isValidColorString(this["#input"].value) === false) {
valid = false;
hint = "Invalid color";
}
return {valid, hint};
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onTypeAttributeChange() {
if (this.type === "color") {
this["#input"].type = "text";
}
else {
this["#input"].type = this.type;
}
}
_onValueAttributeChange() {
this.value = this.hasAttribute("value") ? this.getAttribute("value") : "";
if (this.matches(":focus")) {
document.execCommand("selectAll");
}
this._update();
}
_onSpellcheckAttributeChange() {
this["#input"].spellcheck = this.spellcheck;
}
_onMaxLengthAttributeChange() {
this["#input"].maxLength = this.maxLength;
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
this["#input"].disabled = this.disabled;
}
_onFocusIn() {
this.visited = true;
this.dispatchEvent(new CustomEvent("textinputmodestart", {bubbles: true, composed: true}));
}
_onFocusOut() {
this.dispatchEvent(new CustomEvent("textinputmodeend", {bubbles: true, composed: true}));
this._updateInvalidState();
}
_onKeyDown(event) {
if (event.key === "Enter") {
document.execCommand("selectAll");
this._updateInvalidState();
}
}
_onInputInput(event) {
if (this.instantValidation) {
this._updateInvalidState();
}
else if (this.invalid) {
this._updateInvalidState();
}
event.stopPropagation();
this._updateEmptyState();
this.dispatchEvent(new CustomEvent("input", {bubbles: true}));
}
_onInputChange() {
this._updateInvalidState();
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
this._updateInvalidState();
this._updateEmptyState();
}
_updateInvalidState() {
if (this.visited) {
let {valid, hint} = this.validate();
if (valid) {
this.removeAttribute("invalid");
this.removeAttribute("invalid-hint");
}
else {
this.setAttribute("invalid", "");
this.setAttribute("invalid-hint", hint);
}
}
}
_updateEmptyState() {
if (this.value.length === 0) {
this.setAttribute("empty", "");
}
else {
this.removeAttribute("empty");
}
}
}
customElements.define("x-input", XInputElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$13 = html`
<template>
<style>:host{display:block;line-height:1.2;user-select:none;box-sizing:border-box}:host(:hover){cursor:default}:host([disabled]){opacity:.5}:host([hidden]){display:none}</style>
<slot></slot>
</template>
`;
class XLabelElement extends HTMLElement {
static get observedAttributes() {
return ["for"];
}
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$13.content, true));
this.addEventListener("click", (event) => this._onClick(event));
}
attributeChangedCallback(name) {
if (name === "for") {
this._onForAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Values associated with this label.
// @type
// string
// @default
// ""
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : null;
}
set value(value) {
value === null ? this.removeAttribute("value") : this.setAttribute("value", value);
}
// @info
// Source of the icon to show.
// @type
// string
// @attribute
get for() {
return this.getAttribute("for");
}
set for(value) {
this.setAttribute("for", value);
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onClick(event) {
if (this.for && this.disabled === false) {
let target = this.getRootNode().querySelector("#" + CSS.escape(this.for));
if (target) {
target.click();
}
}
}
_onForAttributeChange() {
let rootNode = this.getRootNode();
let target = rootNode.querySelector("#" + CSS.escape(this.for));
if (target) {
if (!this.id) {
this.id = generateUniqueID(rootNode, "label-");
}
target.setAttribute("aria-labelledby", this.id);
}
}
}
customElements.define("x-label", XLabelElement);
// @copyright
// © 2016-2017 Jarosław Foksa
// @doc
// http://w3c.github.io/aria-practices/#menu
let {abs: abs$1} = Math;
let windowWhitespace = 7;
let shadowTemplate$14 = html`
<template>
<style>:host{display:none;top:0;left:0;width:fit-content;z-index:1001;box-sizing:border-box;background:#fff;cursor:default;-webkit-app-region:no-drag;--scrollbar-background: rgba(0, 0, 0, 0.2);--scrollbar-width: 6px;--open-transition: 100 transform cubic-bezier(0.4, 0, 0.2, 1);--close-transition: 200 opacity cubic-bezier(0.4, 0, 0.2, 1)}:host([animating]),:host([opened]){display:block}:host(:focus){outline:0}:host-context([debug]):host(:focus){outline:2px solid red}#main{width:100%;height:100%;overflow:auto;display:flex;flex-direction:column}::-webkit-scrollbar{max-width:var(--scrollbar-width);background:0 0}::-webkit-scrollbar-thumb{background-color:var(--scrollbar-background)}::-webkit-scrollbar-corner{display:none}</style>
<main id="main" role="presentation">
<slot id="slot"></slot>
</main>
</template>
`;
// @events
// open XMenu
// close XMenu
class XMenuElement extends HTMLElement {
constructor() {
super();
this._delayPoints = [];
this._delayTimeoutID = null;
this._lastDelayPoint = null;
this._lastScrollTop = 0;
this._isPointerOverMenuBlock = false;
this._expandWhenScrolled = false;
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$14.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("pointerover", (event) => this._onPointerOver(event));
this.addEventListener("pointerout", (event) => this._onPointerOut(event));
this.addEventListener("pointermove", (event) => this._onPointerMove(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
this.addEventListener("wheel", (event) => this._onWheel(event), {passive: true});
this["#main"].addEventListener("scroll", (event) => this._onScroll(event), {passive: true});
}
connectedCallback() {
this.setAttribute("role", "menu");
this.setAttribute("aria-hidden", !this.opened);
this.setAttribute("tabindex", "0");
}
attributeChangedCallback(name) {
if (name === "opened") {
this._onOpenedAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["opened"];
}
// @info
// Whether the menu is shown on screen.
// @type
// boolean
// @readonly
// @attribute
get opened() {
return this.hasAttribute("opened");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onOpenedAttributeChange() {
this.setAttribute("aria-hidden", !this.opened);
}
_onPointerDown(event) {
if (event.target === this || event.target.localName === "hr") {
event.stopPropagation();
}
}
_onPointerOver(event) {
if (this._isClosing() || event.pointerType !== "mouse") {
return;
}
if (event.target.closest("x-menu") === this) {
if (this._isPointerOverMenuBlock === false) {
this._onMenuBlockPointerEnter();
}
// Focus and expand the menu item under pointer and collapse other items
{
let item = event.target.closest("x-menuitem");
if (item && item.disabled === false && item.closest("x-menu") === this) {
if (item.matches(":focus") === false) {
this._delay( async () => {
let otherItem = this.querySelector(":scope > x-menuitem:focus");
if (otherItem) {
let otherSubmenu = otherItem.querySelector("x-menu");
if (otherSubmenu) {
// otherItem.removeAttribute("expanded");
otherSubmenu.close();
}
}
item.focus();
let menu = item.closest("x-menu");
let submenu = item.querySelector("x-menu");
let otherItems = [...this.querySelectorAll(":scope > x-menuitem")].filter($0 => $0 !== item);
if (submenu) {
await sleep(60);
if (item.matches(":focus") && submenu.opened === false) {
submenu.openNextToElement(item, "horizontal");
}
}
for (let otherItem of otherItems) {
let otherSubmenu = otherItem.querySelector("x-menu");
if (otherSubmenu) {
otherSubmenu.close();
}
}
});
}
}
else {
this._delay(() => {
this.focus();
});
}
}
}
}
_onPointerOut(event) {
// @bug: event.relatedTarget leaks shadowDOM, so we have to use closest() utility function
if (!event.relatedTarget || closest(event.relatedTarget, "x-menu") !== this) {
if (this._isPointerOverMenuBlock === true) {
this._onMenuBlockPointerLeave();
}
}
}
_onMenuBlockPointerEnter() {
if (this._isClosing()) {
return;
}
this._isPointerOverMenuBlock = true;
this._clearDelay();
}
_onMenuBlockPointerLeave() {
if (this._isClosing()) {
return;
}
this._isPointerOverMenuBlock = false;
this._clearDelay();
this.focus();
}
_onPointerMove(event) {
this._delayPoints.push({
x: event.clientX,
y: event.clientY
});
if (this._delayPoints.length > 3) {
this._delayPoints.shift();
}
}
_onWheel(event) {
if (event.target.closest("x-menu") === this) {
this._isPointerOverMenuBlock = true;
}
else {
this._isPointerOverMenuBlock = false;
}
}
_onScroll(event) {
if (this._expandWhenScrolled) {
let delta = this["#main"].scrollTop - this._lastScrollTop;
this._lastScrollTop = this["#main"].scrollTop;
if (getTimeStamp() - this._openedTimestamp > 100) {
let menuRect = this.getBoundingClientRect();
if (delta < 0) {
if (menuRect.bottom + abs$1(delta) <= window.innerHeight - windowWhitespace) {
this.style.height = (menuRect.height + abs$1(delta)) + "px";
}
else {
this.style.height = (window.innerHeight - menuRect.top - windowWhitespace) + "px";
}
}
else if (delta > 0) {
let {top, left, height} = getComputedStyle(this);
if (menuRect.top - abs$1(delta) >= windowWhitespace) {
this.style.top = (parseFloat(top) - abs$1(delta)) + "px";
this.style.height = (parseFloat(height) + abs$1(delta)) + "px";
this["#main"].scrollTop = 0;
this._lastScrollTop = 0;
}
else {
this.style.top = windowWhitespace + "px";
this.style.height = (window.innerHeight - menuRect.top - windowWhitespace) + "px";
}
}
}
}
}
_onKeyDown(event) {
if (this._isClosing()) {
event.preventDefault();
event.stopPropagation();
}
else if (event.key === "ArrowUp") {
event.preventDefault();
event.stopPropagation();
this.focusPreviousMenuItem();
}
else if (event.key === "ArrowDown") {
event.preventDefault();
event.stopPropagation();
this.focusNextMenuItem();
}
else if (event.code === "ArrowRight" || event.code === "Enter" || event.code === "Space") {
let focusedItem = this.querySelector("x-menuitem:focus");
if (focusedItem) {
let submenu = focusedItem.querySelector("x-menu");
if (submenu) {
event.preventDefault();
event.stopPropagation();
if (submenu.opened === false) {
submenu.openNextToElement(focusedItem, "horizontal");
}
let submenuFirstItem = submenu.querySelector("x-menuitem:not([disabled]):not([hidden])");
if (submenuFirstItem) {
submenuFirstItem.focus();
}
}
}
}
else if (event.code === "ArrowLeft") {
let focusedItem = this.querySelector("x-menuitem:focus");
if (focusedItem) {
let parentMenu = focusedItem.closest("x-menu");
let parentItem = parentMenu.closest("x-menuitem");
if (parentItem && parentItem.closest("x-menu")) {
event.preventDefault();
event.stopPropagation();
parentItem.focus();
this.close();
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Open the menu so that overElement (belonging to the menu) is positioned directly over underElement.
// Returns a promise that is resolved when the menu finishes animating.
// @type
// (HTMLElement, HTMLElement) => Promise<>
openOverElement(underElement, overElement) {
return new Promise( async (resolve) => {
let items = this.querySelectorAll(":scope > x-menuitem");
if (items.length > 0) {
this._expandWhenScrolled = true;
this._openedTimestamp = getTimeStamp();
this._resetInlineStyles();
this.setAttribute("opened", "");
let menuItem = [...items].find((item) => item.contains(overElement)) || items[0];
let menuBounds = this.getBoundingClientRect();
let underElementBounds = underElement.getBoundingClientRect();
let overElementBounds = overElement.getBoundingClientRect();
let extraLeft = 0; // Extra offset needed when menu has fixed-positioned ancestor(s)
let extraTop = 0; // Extra offset needed when menu has fixed-positioned ancestor(s)
menuItem.focus();
// Determine extraLeft and extraTop which represent the extra offset when the menu is inside another
// fixed-positioned element such as a popover.
{
if (menuBounds.top !== 0 || menuBounds.left !== 0) {
extraLeft = -menuBounds.left;
extraTop = -menuBounds.top;
}
}
// Position the menu so that the underElement is directly above the overLabel
{
this.style.left = (underElementBounds.x - (overElementBounds.x - menuBounds.x) + extraLeft) + "px";
this.style.top = (underElementBounds.y - (overElementBounds.y - menuBounds.y) + extraTop) + "px";
menuBounds = this.getBoundingClientRect();
}
// Move the menu right if it overflows the left client bound
{
if (menuBounds.left < windowWhitespace) {
this.style.left = (windowWhitespace + extraLeft) + "px";
menuBounds = this.getBoundingClientRect();
}
}
// Reduce the menu height if it overflows the top client bound
{
let overflowTop = windowWhitespace - menuBounds.top;
if (overflowTop > 0) {
this.style.height = (menuBounds.bottom - windowWhitespace) + "px";
this.style.top = (windowWhitespace + extraTop) + "px";
this["#main"].scrollTop = 9999;
menuBounds = this.getBoundingClientRect();
}
}
// Reduce menu height if it overflows the bottom client bound
// Reduce menu width if it overflows the right client bound
{
if (menuBounds.bottom + windowWhitespace > window.innerHeight) {
let overflow = menuBounds.bottom - window.innerHeight;
let height = menuBounds.height - overflow - windowWhitespace;
this.style.height = height + "px";
}
if (menuBounds.right + windowWhitespace > window.innerWidth) {
let overflow = menuBounds.right - window.innerWidth;
let width = menuBounds.width - overflow - windowWhitespace;
this.style.width = `${width}px`;
}
}
// Animate the menu block
{
let transition = getComputedStyle(this).getPropertyValue("--open-transition");
let [property, duration, easing] = this._parseTransistion(transition);
if (property === "transform") {
let blockBounds = this.getBoundingClientRect();
let originY = underElementBounds.y + underElementBounds.height/2 - blockBounds.top;
await this.animate(
{
transform: ["scaleY(0)", "scaleY(1)"],
transformOrigin: [`0 ${originY}px`, `0 ${originY}px`]
},
{ duration, easing }
).finished;
}
}
this.dispatchEvent(new CustomEvent("open", {bubbles: true, detail: this}));
}
resolve();
});
}
// @info
// Open the menu over the given <x-label> element.
// Returns a promise that is resolved when the menu finishes animating.
// @type
// (XMenuItem) => Promise<>
openOverLabel(underLabel) {
return new Promise( async (resolve) => {
let items = this.querySelectorAll(":scope > x-menuitem");
if (items.length > 0) {
this._resetInlineStyles();
this.setAttribute("opened", "");
this._expandWhenScrolled = true;
this._openedTimestamp = getTimeStamp();
let item = [...items].find((item) => {
let itemLabel = item.querySelector("x-label");
return (itemLabel && itemLabel.textContent === underLabel.textContent) ? true : false;
});
if (!item) {
item = items[0];
}
let overLabel = item.querySelector("x-label");
await this.openOverElement(underLabel, overLabel);
}
resolve();
});
}
// @info
// Open the menu next the given menu item.
// Returns a promise that is resolved when the menu finishes animating.
// @type
// (XMenuItem, string) => Promise
async openNextToElement(element, direction = "horizontal", elementWhitespace = 0) {
return new Promise(async (resolve) => {
this._expandWhenScrolled = false;
this._openedTimestamp = getTimeStamp();
this._resetInlineStyles();
this.setAttribute("opened", "");
this.dispatchEvent(new CustomEvent("open", {bubbles: true, detail: this}));
if (element.localName === "x-menuitem") {
element.setAttribute("expanded", "");
}
let elementBounds = element.getBoundingClientRect();
let menuBounds = this.getBoundingClientRect();
let extraLeft = 0; // Extra offset needed when menu has fixed-positioned ancestor(s)
let extraTop = 0; // Extra offset needed when menu has fixed-positioned ancestor(s)
// Determine extraLeft and extraTop which represent the extra offset when the menu is inside another
// fixed-positioned element such as a popover.
{
if (menuBounds.top !== 0 || menuBounds.left !== 0) {
extraLeft = -menuBounds.left;
extraTop = -menuBounds.top;
}
}
if (direction === "horizontal") {
this.style.top = (elementBounds.top + extraTop) + "px";
this.style.left = (elementBounds.left + elementBounds.width + elementWhitespace + extraLeft) + "px";
let side = "right";
// Reduce menu size if it does not fit on screen
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.width > window.innerWidth - 10) {
this.style.width = (window.innerWidth - 10) + "px";
}
if (menuBounds.height > window.innerHeight - 10) {
this.style.height = (window.innerHeight - 10) + "px";
}
}
// Move the menu horizontally if it overflows the right screen edge
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.left + menuBounds.width + windowWhitespace > window.innerWidth) {
// Move menu to the left side of the element if there is enough space to fit it in
if (elementBounds.left > menuBounds.width + windowWhitespace) {
this.style.left = (elementBounds.left - menuBounds.width + extraLeft) + "px";
side = "left";
}
// ... otherwise move menu to the screen edge
else {
// Move menu to the left screen edge
if (elementBounds.left > window.innerWidth - (elementBounds.left + elementBounds.width)) {
this.style.left = (windowWhitespace + extraLeft) + "px";
side = "left";
}
// Move menu to the right screen edge
else {
this.style.left = (window.innerWidth - menuBounds.width - windowWhitespace + extraLeft) + "px";
side = "right";
}
}
}
}
// Move the menu vertically it overflows the bottom screen edge
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.top + menuBounds.height + windowWhitespace > window.innerHeight) {
let bottomOverflow = (menuBounds.top + menuBounds.height + windowWhitespace) - window.innerHeight;
this.style.top = (menuBounds.top - bottomOverflow + extraTop) + "px";
}
}
// Animate the menu
{
let transition = getComputedStyle(this).getPropertyValue("--open-transition");
let [property, duration, easing] = this._parseTransistion(transition);
if (property === "transform") {
await this.animate(
{
transform: ["scale(0, 0)", "scale(1, 1)"],
transformOrigin: [side === "left" ? "100% 0" : "0 0", side === "left" ? "100% 0" : "0 0"]
},
{ duration, easing }
).finished;
}
}
}
else if (direction === "vertical") {
this.style.top = (elementBounds.top + elementBounds.height + elementWhitespace + extraTop) + "px";
this.style.left = (elementBounds.left + extraLeft) + "px";
let side = "bottom";
// Reduce menu size if it does not fit on screen
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.width > window.innerWidth - 10) {
this.style.width = (window.innerWidth - 10) + "px";
}
if (menuBounds.height > window.innerHeight - 10) {
this.style.height = (window.innerHeight - 10) + "px";
}
}
if (element.parentElement.localName === "x-menubar") {
let menuBounds = this.getBoundingClientRect();
// Reduce menu height if it overflows bottom screen edge
if (menuBounds.top + menuBounds.height + windowWhitespace > window.innerHeight) {
this.style.height = (window.innerHeight - (elementBounds.top + elementBounds.height) - 10) + "px";
}
}
else {
// Move the menu vertically if it overflows the bottom screen edge
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.top + menuBounds.height + windowWhitespace > window.innerHeight) {
// Move menu to the top side of the element if there is enough space to fit it in
if (elementBounds.top > menuBounds.height + windowWhitespace) {
this.style.top = (elementBounds.top - menuBounds.height - elementWhitespace + extraTop) + "px";
side = "top";
}
// ... otherwise move menu to the screen edge
else {
// Move menu to the top screen edge
if (elementBounds.top > window.innerHeight - (elementBounds.top + elementBounds.height)) {
this.style.top = (windowWhitespace + extraTop) + "px";
side = "top";
}
// Move menu to the bottom screen edge
else {
this.style.top = (window.innerHeight - menuBounds.height - windowWhitespace + extraTop) + "px";
side = "bottom";
}
}
}
}
}
// Float the menu to the right element edge if the menu overflows right screen edge
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.left + menuBounds.width + windowWhitespace > window.innerWidth) {
this.style.left = (elementBounds.left + elementBounds.width - menuBounds.width + extraLeft) + "px";
}
}
// Float the menu to the left screen edge if it overflows the left screen edge
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.left < windowWhitespace) {
this.style.left = (windowWhitespace + extraLeft) + "px";
}
}
// Animate the menu
{
let transition = getComputedStyle(this).getPropertyValue("--open-transition");
let [property, duration, easing] = this._parseTransistion(transition);
if (property === "transform") {
await this.animate(
{
transform: ["scale(1, 0)", "scale(1, 1)"],
transformOrigin: [side === "top" ? "0 100%" : "0 0", side === "top" ? "0 100%" : "0 0"]
},
{ duration, easing }
).finished;
}
}
}
resolve();
});
}
// @info
// Open the menu at given client point.
// Returns a promise that is resolved when the menu finishes animating.
// @type
// (number, number) => Promise
openAtPoint(left, top) {
return new Promise( async (resolve) => {
this._expandWhenScrolled = false;
this._openedTimestamp = getTimeStamp();
this._resetInlineStyles();
this.setAttribute("opened", "");
this.dispatchEvent(new CustomEvent("open", {bubbles: true, detail: this}));
let menuBounds = this.getBoundingClientRect();
let extraLeft = 0; // Extra offset needed when menu has fixed-positioned ancestor(s)
let extraTop = 0; // Extra offset needed when menu has fixed-positioned ancestor(s)
// Determine extraLeft and extraTop which represent the extra offset when the menu is inside another
// fixed-positioned element such as a popover.
{
if (menuBounds.top !== 0 || menuBounds.left !== 0) {
extraLeft = -menuBounds.left;
extraTop = -menuBounds.top;
}
}
// Position the menu at given point
{
this.style.left = (left + extraLeft) + "px";
this.style.top = (top + extraTop) + "px";
menuBounds = this.getBoundingClientRect();
}
// If menu overflows right screen border then move it to the opposite side
if (menuBounds.right + windowWhitespace > window.innerWidth) {
left = left - menuBounds.width;
this.style.left = (left + extraLeft) + "px";
menuBounds = this.getBoundingClientRect();
}
// If menu overflows bottom screen border then move it up
if (menuBounds.bottom + windowWhitespace > window.innerHeight) {
top = top + window.innerHeight - (menuBounds.top + menuBounds.height) - windowWhitespace;
this.style.top = (top + extraTop) + "px";
menuBounds = this.getBoundingClientRect();
// If menu now overflows top screen border then make it stretch to the whole available vertical space
if (menuBounds.top < windowWhitespace) {
top = windowWhitespace;
this.style.top = (top + extraTop) + "px";
this.style.height = (window.innerHeight - windowWhitespace - windowWhitespace) + "px";
}
}
// Animate the menu
{
let transition = getComputedStyle(this).getPropertyValue("--open-transition");
let [property, duration, easing] = this._parseTransistion(transition);
if (property === "transform") {
await this.animate(
{
transform: ["scale(0)", "scale(1)"],
transformOrigin: ["0 0", "0 0"]
},
{
duration: 80,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)"
}
).finished;
}
}
resolve();
});
}
// @info
// Close the menu.
// Returns a promise that is resolved when the menu finishes animating.
// @type
// (boolean) => Promise
close(animate = true) {
return new Promise(async (resolve) => {
if (this.opened) {
this.removeAttribute("opened");
this.dispatchEvent(new CustomEvent("close", {bubbles: true, detail: this}));
let item = this.closest("x-menuitem");
if (item) {
item.removeAttribute("expanded");
}
if (animate) {
this.setAttribute("animating", "");
let transition = getComputedStyle(this).getPropertyValue("--close-transition");
let [property, duration, easing] = this._parseTransistion(transition);
if (property === "opacity") {
await this.animate({ opacity: ["1", "0"] }, { duration, easing }).finished;
}
this.removeAttribute("animating");
}
for (let item of this.querySelectorAll(":scope > x-menuitem")) {
let submenu = item.querySelector("x-menu[opened]");
if (submenu) {
submenu.close();
}
}
}
resolve();
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
focusNextMenuItem() {
let refItem = this.querySelector(":scope > x-menuitem:focus, :scope > x-menuitem[expanded]");
if (refItem) {
let nextItem = null;
for (let item = refItem.nextElementSibling; item; item = item.nextElementSibling) {
if (item.localName === "x-menuitem" && item.disabled === false && item.hidden === false) {
nextItem = item;
break;
}
}
if (nextItem === null) {
for (let item of refItem.parentElement.children) {
if (item.localName === "x-menuitem" && item.disabled === false && item.hidden === false) {
nextItem = item;
break;
}
}
}
if (nextItem) {
nextItem.focus();
let menu = refItem.querySelector("x-menu");
if (menu) {
menu.close();
}
}
}
else {
this.focusFirstMenuItem();
}
}
focusPreviousMenuItem() {
let refItem = this.querySelector(":scope > x-menuitem:focus, :scope > x-menuitem[expanded]");
if (refItem) {
let previousItem = null;
for (let item = refItem.previousElementSibling; item; item = item.previousElementSibling) {
if (item.localName === "x-menuitem" && item.disabled === false && item.hidden === false) {
previousItem = item;
break;
}
}
if (previousItem === null) {
for (let item of [...refItem.parentElement.children].reverse()) {
if (item.localName === "x-menuitem" && item.disabled === false && item.hidden === false) {
previousItem = item;
break;
}
}
}
if (previousItem) {
previousItem.focus();
let menu = refItem.querySelector("x-menu");
if (menu) {
menu.close();
}
}
}
else {
this.focusLastMenuItem();
}
}
focusFirstMenuItem() {
let items = this.querySelectorAll("x-menuitem:not([disabled]):not([hidden])");
let firstItem = items[0] || null;
if (firstItem) {
firstItem.focus();
}
}
focusLastMenuItem() {
let items = this.querySelectorAll("x-menuitem:not([disabled]):not([hidden])");
let lastItem = (items.length > 0) ? items[items.length-1] : null;
if (lastItem) {
lastItem.focus();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @doc
// http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown
_delay(callback) {
let tolerance = 75;
let fullDelay = 300;
let delay = 0;
let direction = "right";
{
let point = this._delayPoints[this._delayPoints.length - 1];
let prevPoint = this._delayPoints[0];
let openedSubmenu = this.querySelector("x-menu[opened]");
if (openedSubmenu && point) {
if (!prevPoint) {
prevPoint = point;
}
let bounds = this.getBoundingClientRect();
let upperLeftPoint = {x: bounds.left, y: bounds.top - tolerance };
let upperRightPoint = {x: bounds.left + bounds.width, y: upperLeftPoint.y };
let lowerLeftPoint = {x: bounds.left, y: bounds.top + bounds.height + tolerance};
let lowerRightPoint = {x: bounds.left + bounds.width, y: lowerLeftPoint.y };
let proceed = true;
if (
prevPoint.x < bounds.left || prevPoint.x > lowerRightPoint.x ||
prevPoint.y < bounds.top || prevPoint.y > lowerRightPoint.y
) {
proceed = false;
}
if (
this._lastDelayPoint &&
point.x === this._lastDelayPoint.x &&
point.y === this._lastDelayPoint.y
) {
proceed = false;
}
if (proceed) {
let decreasingCorner;
let increasingCorner;
if (direction === "right") {
decreasingCorner = upperRightPoint;
increasingCorner = lowerRightPoint;
}
else if (direction === "left") {
decreasingCorner = lowerLeftPoint;
increasingCorner = upperLeftPoint;
}
else if (direction === "below") {
decreasingCorner = lowerRightPoint;
increasingCorner = lowerLeftPoint;
}
else if (direction === "above") {
decreasingCorner = upperLeftPoint;
increasingCorner = upperRightPoint;
}
let getSlope = (a, b) => (b.y - a.y) / (b.x - a.x);
let decreasingSlope = getSlope(point, decreasingCorner);
let increasingSlope = getSlope(point, increasingCorner);
let prevDecreasingSlope = getSlope(prevPoint, decreasingCorner);
let prevIncreasingSlope = getSlope(prevPoint, increasingCorner);
if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) {
this._lastDelayPoint = point;
delay = fullDelay;
}
else {
this._lastDelayPoint = null;
}
}
}
}
if (delay > 0) {
this._delayTimeoutID = setTimeout(() => {
this._delay(callback);
}, delay);
}
else {
callback();
}
}
_clearDelay() {
if (this._delayTimeoutID) {
clearTimeout(this._delayTimeoutID);
this._delayTimeoutID = null;
}
}
_resetInlineStyles() {
this.style.position = "fixed";
this.style.top = "0px";
this.style.left = "0px";
this.style.width = null;
this.style.height = null;
this.style.minWidth = null;
this.style.maxWidth = null;
}
// @info
// Whether this or any ancestor menu is closing
// @type
// Boolean
_isClosing() {
return this.matches("*[closing], *[closing] x-menu");
}
// @info
// Parse the value of CSS transition property.
// @type
// (string) => [string, number, string]
_parseTransistion(string) {
let [rawDuration, property, ...rest] = string.trim().split(" ");
let duration = parseFloat(rawDuration);
let easing = rest.join(" ");
return [property, duration, easing];
}
}
customElements.define("x-menu", XMenuElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let debug$1 = false;
let shadowTemplate$15 = html`
<template>
<style>:host{display:flex;align-items:center;width:100%;height:fit-content;overflow:auto;box-sizing:border-box}:host([disabled]){pointer-events:none;opacity:.6}#overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;pointer-events:none;touch-action:none}#overlay[hidden]{display:none}#overlay path{fill:red;fill-rule:evenodd;opacity:0;pointer-events:all}</style>
<svg id="overlay" hidden>
<path id="overlay-path"></path>
</svg>
<slot></slot>
</template>
`;
class XMenuBarElement extends HTMLElement {
constructor() {
super();
this._expanded = false;
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$15.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("focusout", (event) => this._onFocusOut(event));
this._shadowRoot.addEventListener("pointerover", (event) => this._onShadowRootPointerOver(event));
this._shadowRoot.addEventListener("click", (event) => this._onShadowRootClick(event));
this._shadowRoot.addEventListener("wheel", (event) => this._onShadowRootWheel(event));
this._shadowRoot.addEventListener("keydown", (event) => this._onShadowRootKeyDown(event));
}
connectedCallback() {
this.setAttribute("role", "menubar");
this.setAttribute("aria-disabled", this.disabled);
window.addEventListener("orientationchange", this._orientationChangeListener = () => {
this._onOrientationChange();
});
}
disconnectedCallback() {
window.removeEventListener("orientationchange", this._orientationChangeListener);
}
attributeChangedCallback(name) {
if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["disabled"];
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onDisabledAttributeChange() {
this.setAttribute("aria-disabled", this.disabled);
}
_onFocusOut(event) {
if ((event.relatedTarget === null || this.contains(event.relatedTarget) === false) && debug$1 === false) {
this._collapseMenubarItems();
}
}
_onOrientationChange() {
this._collapseMenubarItems();
}
_onShadowRootWheel(event) {
let openedMenu = this.querySelector("x-menu[opened]");
if (openedMenu && openedMenu.contains(event.target) === false) {
event.preventDefault();
}
}
async _onShadowRootClick(event) {
if (this.hasAttribute("closing")) {
return;
}
let item = event.target.closest("x-menuitem");
let ownerMenu = event.target.closest("x-menu");
if (item && item.disabled === false && (!ownerMenu || ownerMenu.contains(item))) {
let menu = item.querySelector("x-menu");
if (item.parentElement === this) {
if (menu) {
menu.opened ? this._collapseMenubarItems() : this._expandMenubarItem(item);
}
}
else {
if (menu) {
if (menu.opened && menu.opened === false) {
menu.openNextToElement(item, "horizontal");
}
}
else {
this.setAttribute("closing", "");
await item.whenTriggerEnd;
await this._collapseMenubarItems();
this.removeAttribute("closing");
}
}
}
else if (event.target === this["#overlay-path"]) {
this._collapseMenubarItems();
event.preventDefault();
event.stopPropagation();
}
}
_onShadowRootPointerOver(event) {
if (this.hasAttribute("closing")) {
return;
}
let item = event.target.closest("x-menuitem");
if (event.target.closest("x-menu") === null && item && item.parentElement === this) {
if (this._expanded && event.pointerType === "mouse") {
if (item.hasAttribute("expanded") === false) {
this._expandMenubarItem(item);
}
else {
item.focus();
}
}
}
}
_onShadowRootKeyDown(event) {
if (this.hasAttribute("closing")) {
event.stopPropagation();
event.preventDefault();
}
else if (event.code === "Enter" || event.code === "Space") {
let focusedMenubarItem = this.querySelector(":scope > x-menuitem:focus");
if (focusedMenubarItem) {
event.preventDefault();
focusedMenubarItem.click();
}
}
else if (event.code === "Escape") {
if (this._expanded) {
event.preventDefault();
this._collapseMenubarItems();
}
}
else if (event.code === "Tab") {
let refItem = this.querySelector(":scope > x-menuitem:focus, :scope > x-menuitem[expanded]");
if (refItem) {
refItem.focus();
let menu = refItem.querySelector(":scope > x-menu");
if (menu) {
menu.tabIndex = -1;
menu.close().then(() => {
menu.tabIndex = -1;
});
}
}
}
else if (event.code === "ArrowRight") {
this._expandNextMenubarItem();
}
else if (event.code === "ArrowLeft") {
this._expandPreviousMenubarItem();
}
else if (event.code === "ArrowDown") {
let menu = this.querySelector("x-menuitem:focus > x-menu");
if (menu) {
event.preventDefault();
menu.focusFirstMenuItem();
}
}
else if (event.code === "ArrowUp") {
let menu = this.querySelector("x-menuitem:focus > x-menu");
if (menu) {
event.preventDefault();
menu.focusLastMenuItem();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_expandMenubarItem(item) {
let menu = item.querySelector(":scope > x-menu");
if (menu && menu.opened === false) {
item.focus();
this._expanded = true;
this.style.touchAction = "none";
// Open item's menu and close other menus
{
menu.openNextToElement(item, "vertical");
let menus = this.querySelectorAll(":scope > x-menuitem > x-menu");
let otherMenus = [...menus].filter($0 => $0 !== menu);
for (let otherMenu of otherMenus) {
if (otherMenu) {
otherMenu.close(false);
}
}
}
// Show the overlay
{
let {x, y, width, height} = this.getBoundingClientRect();
this["#overlay-path"].setAttribute("d", `
M 0 0
L ${window.innerWidth} 0
L ${window.innerWidth} ${window.innerHeight}
L 0 ${window.innerHeight}
L 0 0
M ${x} ${y}
L ${x + width} ${y}
L ${x + width} ${y + height}
L ${x} ${y + height}
`);
this["#overlay"].removeAttribute("hidden");
}
}
}
_collapseMenubarItems() {
return new Promise( async (resolve) => {
this._expanded = false;
this.style.touchAction = null;
// Hide the overlay
{
this["#overlay"].setAttribute("hidden", "");
this["#overlay-path"].setAttribute("d", "");
}
// Close all opened menus
{
let menus = this.querySelectorAll(":scope > x-menuitem > x-menu[opened]");
for (let menu of menus) {
await menu.close(true);
}
}
let focusedMenuItem = this.querySelector("x-menuitem:focus");
if (focusedMenuItem) {
focusedMenuItem.blur();
}
resolve();
});
}
_expandPreviousMenubarItem() {
let items = [...this.querySelectorAll(":scope > x-menuitem:not([disabled])")];
let focusedItem = this.querySelector(":focus").closest("x-menubar > x-menuitem");
if (items.length > 1 && focusedItem) {
let i = items.indexOf(focusedItem);
let previousItem = items[i - 1] || items[items.length-1];
this._expandMenubarItem(previousItem);
}
}
_expandNextMenubarItem() {
let items = [...this.querySelectorAll(":scope > x-menuitem:not([disabled])")];
let focusedItem = this.querySelector(":focus").closest("x-menubar > x-menuitem");
if (focusedItem && items.length > 1) {
let i = items.indexOf(focusedItem);
let nextItem = items[i + 1] || items[0];
this._expandMenubarItem(nextItem);
}
}
}
customElements.define("x-menubar", XMenuBarElement);
// @doc
// http://w3c.github.io/aria/aria/aria.html#menuitem
// @copyright
// © 2016-2017 Jarosław Foksa
let {max: max$5} = Math;
let easing$4 = "cubic-bezier(0.4, 0, 0.2, 1)";
let shadowTemplate$16 = html`
<template>
<style>:host{display:flex;flex-flow:row;align-items:center;position:relative;box-sizing:border-box;cursor:default;user-select:none;contain:layout style;--trigger-effect: ripple;--ripple-background: currentColor;--ripple-opacity: 0.1;--checkmark-d: path("M 37.497 65.117 L 21.228 48.849 L 15.689 54.35 L 37.497 76.158 L 84.311 29.343 L 78.811 23.842 Z");--checkmark-width: 24px;--checkmark-height: 24px;--checkmark-margin: 0 12px 0 0}:host([hidden]){display:none}:host([disabled]){pointer-events:none;opacity:.6}:host(:focus){outline:0}:host-context([debug]):host(:focus){outline:2px solid red}#ripples,#ripples .ripple{position:absolute;top:0;left:0;pointer-events:none}#ripples{z-index:0;contain:strict;overflow:hidden;width:100%;height:100%}#ripples .ripple{width:200px;height:200px;background:var(--ripple-background);opacity:var(--ripple-opacity);border-radius:999px;transform:none;transition:all 800ms cubic-bezier(.4,0,.2,1);will-change:opacity,transform}#checkmark{color:inherit;display:none;transition:transform .2s cubic-bezier(.4,0,.2,1);align-self:center;width:var(--checkmark-width);height:var(--checkmark-height);margin:var(--checkmark-margin);d:var(--checkmark-d)}:host([selected]) #checkmark{display:flex;transform:scale(0);transform-origin:50% 50%}:host([selected="true"]) #checkmark{display:flex;transform:scale(1)}#checkmark path{d:inherit;fill:currentColor}#arrow-icon{display:flex;width:16px;height:16px;transform:scale(1.1);align-self:center;margin-left:8px;color:inherit}#arrow-icon[hidden]{display:none}</style>
<div id="ripples"></div>
<svg id="checkmark" viewBox="0 0 100 100" preserveAspectRatio="none">
<path></path>
</svg>
<slot></slot>
<x-icon id="arrow-icon" name="play-arrow" hidden></x-icon>
</template>
`;
class XMenuItemElement extends HTMLElement {
constructor() {
super();
this._observer = new MutationObserver(() => this._update());
this._blinking = false;
this._triggerEndCallbacks = [];
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$16.content, true));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
connectedCallback() {
this._observer.observe(this, {childList: true, attributes: false, characterData: false, subtree: false});
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "menuitem");
this.setAttribute("aria-disabled", this.disabled);
this._update();
}
disconnectedCallback() {
this._observer.disconnect();
}
attributeChangedCallback(name) {
if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["disabled"];
}
// @info
// Value associated with this menu item (usually the command name).
// @type
// string?
// @default
// null
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : null;
}
set value(value) {
if (this.value !== value) {
value === null ? this.removeAttribute("value") : this.setAttribute("value", value);
}
}
// @type
// boolean?
// @default
// null
get selected() {
if (this.hasAttribute("selected") === false) {
return null;
}
else if (this.getAttribute("selected") === "false") {
return false;
}
else {
return true;
}
}
set selected(selected) {
if (this.selected !== selected) {
if (selected === null) {
this.removeAttribute("selected");
}
else if (selected === false) {
this.setAttribute("selected", "false");
}
else {
this.setAttribute("selected", "true");
}
}
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
// @info
// Promise that is resolved when any trigger effects (such ripples or blinking) are finished.
// @type
// Promise
get whenTriggerEnd() {
return new Promise((resolve) => {
if (this["#ripples"].childElementCount === 0 && this._blinking === false) {
resolve();
}
else {
this._triggerEndCallbacks.push(resolve);
}
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
async _onPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return false;
}
if (this.matches("[closing] x-menuitem")) {
event.preventDefault();
event.stopPropagation();
return;
}
event.stopPropagation();
// Trigger effect
{
let triggerEffect = getComputedStyle(this).getPropertyValue("--trigger-effect").trim();
if (triggerEffect === "ripple") {
let rect = this["#ripples"].getBoundingClientRect();
let size = max$5(rect.width, rect.height) * 1.5;
let top = pointerDownEvent.clientY - rect.y - size/2;
let left = pointerDownEvent.clientX - rect.x - size/2;
let whenLostPointerCapture = new Promise((r) => this.addEventListener("lostpointercapture", r, {once: true}));
let ripple = createElement("div");
ripple.setAttribute("class", "ripple pointer-down-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
this.setPointerCapture(pointerDownEvent.pointerId);
let inAnimation = ripple.animate(
{ transform: ["scale3d(0, 0, 0)", "none"]},
{ duration: 300, easing: easing$4 }
);
await whenLostPointerCapture;
await inAnimation.finished;
let outAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"]},
{ duration: 300, easing: easing$4 }
);
await outAnimation.finished;
ripple.remove();
if (this["#ripples"].childElementCount === 0) {
for (let callback of this._triggerEndCallbacks) {
callback();
}
}
}
}
}
async _onClick(event) {
if (
event.button > 0 ||
event.target.closest("x-menuitem") !== this ||
event.target.closest("x-menu") !== this.closest("x-menu") ||
this.matches("[closing] x-menuitem")
) {
return;
}
// Trigger effect
{
let triggerEffect = getComputedStyle(this).getPropertyValue("--trigger-effect").trim();
if (triggerEffect === "ripple") {
if (this["#ripples"].querySelector(".pointer-down-ripple") === null) {
let rect = this["#ripples"].getBoundingClientRect();
let size = max$5(rect.width, rect.height) * 1.5;
let top = (rect.y + rect.height/2) - rect.y - size/2;
let left = (rect.x + rect.width/2) - rect.x - size/2;
let ripple = createElement("div");
ripple.setAttribute("class", "ripple click-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
let inAnimation = ripple.animate(
{ transform: ["scale3d(0, 0, 0)", "none"]},
{ duration: 300, easing: easing$4 }
);
await inAnimation.finished;
let outAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 300, easing: easing$4 }
);
await outAnimation.finished;
ripple.remove();
if (this["#ripples"].childElementCount === 0) {
for (let callback of this._triggerEndCallbacks) {
callback();
}
}
}
}
else if (triggerEffect === "blink") {
this._blinking = true;
this.parentElement.focus();
await sleep(150);
this.focus();
await sleep(150);
this._blinking = true;
for (let callback of this._triggerEndCallbacks) {
callback();
}
}
}
}
_onKeyDown(event) {
if (event.code === "Enter" || event.code === "Space") {
event.preventDefault();
if (!this.querySelector("x-menu")) {
event.stopPropagation();
this.click();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
// Update arrow icon visibility
{
if (this.parentElement.localName === "x-menubar") {
this["#arrow-icon"].hidden = true;
}
else {
let menu = this.querySelector("x-menu");
this["#arrow-icon"].hidden = menu ? false : true;
}
}
}
}
customElements.define("x-menuitem", XMenuItemElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let {isFinite: isFinite$2, isNaN, parseFloat: parseFloat$3} = Number;
// @info
// Convert the first letter in the given string from lowercase to uppercase.
// @type
// (string) => void
let capitalize = (string) => {
return string.charAt(0).toUpperCase() + string.substr(1);
};
// @info
// Replace every occurance of string A with string B.
// @type
// (string, string, string) => string
let replaceAll = (text, a, b) => {
return text.split(a).join(b);
};
// @info
// Check if given string is a whitespace string as defined by DOM spec.
// @type
// (string) => boolean
// @src
// https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Whitespace_in_the_DOM
let isDOMWhitespace = (string) => {
return !(/[^\t\n\r ]/.test(string));
};
// @info
// Returns true if the passed argument is either a number or a string that represents a number.
// @type
// (any) => boolean
let isNumeric = (value) => {
let number = parseFloat$3(value);
return isNaN(number) === false && isFinite$2(number);
};
// @copyright
// © 2016-2017 Jarosław Foksa
let {isFinite: isFinite$1} = Number;
let numericKeys = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "+", ",", "."];
let shadowTemplate$17 = html`
<template>
<style>:host{display:block;position:relative;width:100px;height:24px;box-sizing:border-box;color:#000;--selection-color: currentColor;--selection-background: #B2D7FD;--inner-padding: 0}:host(:hover){cursor:text}:host([invalid]){--selection-color: white;--selection-background: #d50000}:host([disabled]){pointer-events:none;opacity:.5}:host([hidden]){display:none}::selection{color:var(--selection-color);background:var(--selection-background)}#editor-container,#main{display:flex;align-items:center;height:100%}#editor-container{width:100%;padding:var(--inner-padding);box-sizing:border-box;overflow:hidden}#editor{width:100%;overflow:auto;color:inherit;background:0 0;border:0;outline:0;font-family:inherit;font-size:inherit;line-height:10;white-space:nowrap}#editor::-webkit-scrollbar{display:none}#editor::after,#editor::before{content:attr(data-prefix);pointer-events:none}#editor::after{content:attr(data-suffix)}:host(:focus) #editor::after,:host(:focus) #editor::before,:host([empty]) #editor::after,:host([empty]) #editor::before{content:""}</style>
<main id="main">
<div id="editor-container">
<div id="editor" contenteditable="plaintext-only" spellcheck="false"></div>
</div>
<slot></slot>
</main>
</template>
`;
// @events
// change
// changestart
// changeend
// textinputmodestart
// textinputmodeend
class XNumberInputElement extends HTMLElement {
constructor() {
super();
this._isDragging = false;
this._isChangeStart = false;
this._isArrowKeyDown = false;
this._isBackspaceKeyDown = false;
this._isStepperButtonDown = false;
this._maybeDispatchChangeEndEvent = debounce(this._maybeDispatchChangeEndEvent, 500, this);
this._shadowRoot = this.attachShadow({mode: "closed", delegatesFocus: true});
this._shadowRoot.append(document.importNode(shadowTemplate$17.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this._shadowRoot.addEventListener("pointerdown", (event) => this._onShadowRootPointerDown(event));
this._shadowRoot.addEventListener("wheel", (event) => this._onWheel(event));
this["#editor"].addEventListener("paste", (event) => this._onPaste(event));
this["#editor"].addEventListener("input", (event) => this._onEditorInput(event));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
this.addEventListener("keyup", (event) => this._onKeyUp(event));
this.addEventListener("keypress", (event) => this._onKeyPress(event));
this.addEventListener("incrementstart", (event) => this._onStepperIncrementStart(event));
this.addEventListener("decrementstart", (event) => this._onStepperDecrementStart(event));
this.addEventListener("focusin", (event) => this._onFocusIn(event));
this.addEventListener("focusout", (event) => this._onFocusOut(event));
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "input");
this.setAttribute("aria-disabled", this.disabled);
this._update();
}
attributeChangedCallback(name) {
if (name === "value") {
this._onValueAttributeChange();
}
else if (name === "min") {
this._onMinAttributeChange();
}
else if (name === "max") {
this._onMaxAttributeChange();
}
else if (name === "prefix") {
this._onPrefixAttributeChange();
}
else if (name === "suffix") {
this._onSuffixAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["value", "min", "max", "prefix", "suffix", "disabled"];
}
// @type
// number?
// @default
// null
// @attribute
get value() {
return this.hasAttribute("value") ? parseFloat(this.getAttribute("value")) : null;
}
set value(value) {
value === null ? this.removeAttribute("value") : this.setAttribute("value", value);
}
// @type
// number
// @default
// -Infinity
// @attribute
get min() {
return this.hasAttribute("min") ? parseFloat(this.getAttribute("min")) : -Infinity;
}
set min(min) {
isFinite$1(min) ? this.setAttribute("min", min) : this.removeAttribute("min");
}
// @type
// number
// @default
// Infinity
// @attribute
get max() {
return this.hasAttribute("max") ? parseFloat(this.getAttribute("max")) : Infinity;
}
set max(max) {
isFinite$1(max) ? this.setAttribute("max", max) : this.removeAttribute("max");
}
// @type
// boolean
// @default
// false
// @attribute
get mixed() {
return this.hasAttribute("mixed");
}
set mixed(mixed) {
mixed ? this.setAttribute("mixed", "") : this.removeAttribute("mixed");
}
// @info
// Maximal number of digits to be shown after the dot. This setting affects only the display value.
// @type
// number
// @default
// 20
// @attribute
get precision() {
return this.hasAttribute("precision") ? parseFloat(this.getAttribute("precision")) : 20;
}
set precision(value) {
this.setAttribute("precision", value);
}
// @info
// Number by which value should be incremented or decremented when up or down arrow key is pressed.
// @type
// number
// @default
// 1
// @attribute
get step() {
return this.hasAttribute("step") ? parseFloat(this.getAttribute("step")) : 1;
}
set step(value) {
this.setAttribute("step", step);
}
// @type
// string
// @default
// ""
// @attribute
get prefix() {
return this.hasAttribute("prefix") ? this.getAttribute("prefix") : "";
}
set prefix(prefix) {
this.setAttribute("prefix", prefix);
}
// @type
// string
// @default
// ""
// @attribute
get suffix() {
return this.hasAttribute("suffix") ? this.getAttribute("suffix") : "";
}
set suffix(suffix) {
this.setAttribute("suffix", suffix);
}
// @type
// boolean
// @default
// false
// @attribute
get required() {
return this.hasAttribute("required");
}
set required(required) {
required ? this.setAttribute("required", "") : this.removeAttribute("required");
}
// @info
// Validation hints are not shown unless user focuses the element for the first time. Set this attribute to
// true to show the hints immediately.
// @type
// boolean
// @default
// false
// @attribute
get visited() {
return this.hasAttribute("visited");
}
set visited(visited) {
visited ? this.setAttribute("visited", "") : this.removeAttribute("visited");
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onValueAttributeChange() {
this._update();
}
_onMinAttributeChange() {
this._updateStepper();
}
_onMaxAttributeChange() {
this._updateStepper();
}
_onPrefixAttributeChange() {
this["#editor"].setAttribute("data-prefix", this.prefix);
}
_onSuffixAttributeChange() {
this["#editor"].setAttribute("data-suffix", this.suffix);
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
this["#editor"].disabled = this.disabled;
}
_onFocusIn() {
document.execCommand("selectAll");
this.dispatchEvent(new CustomEvent("textinputmodestart", {bubbles: true, composed: true}));
this.visited = true;
}
_onFocusOut() {
this._shadowRoot.getSelection().collapse(this["#main"]);
this._commitEditorChanges();
this.dispatchEvent(new CustomEvent("textinputmodeend", {bubbles: true, composed: true}));
}
_onEditorInput() {
this._updateState();
this._updateStepper();
}
_onWheel(event) {
if (this.matches(":focus")) {
event.preventDefault();
this._maybeDispatchChangeStartEvent();
if (event.wheelDeltaX > 0 || event.wheelDeltaY > 0) {
this._increment(event.shiftKey);
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
else {
this._decrement(event.shiftKey);
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
this._maybeDispatchChangeEndEvent();
}
}
_onClick(event) {
event.preventDefault();
}
_onPointerDown(pointerDownEvent) {
if (pointerDownEvent.target.localName === "x-stepper") {
// Don't focus the input when user clicks stepper
pointerDownEvent.preventDefault();
}
}
_onShadowRootPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0 || pointerDownEvent.isPrimary === false) {
pointerDownEvent.preventDefault();
return;
}
if (pointerDownEvent.target === this["#editor"]) {
if (this["#editor"].matches(":focus") === false) {
pointerDownEvent.preventDefault();
let initialValue = this.value;
let cachedClientX = null;
let pointerMoveListener, lostPointerCaptureListener;
this.style.cursor = "col-resize";
this["#editor"].setPointerCapture(pointerDownEvent.pointerId);
this["#editor"].addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
if (pointerMoveEvent.clientX === cachedClientX || pointerMoveEvent.isPrimary === false) {
return;
}
if (this._isDragging === false) {
this._isDragging = true;
this._isChangeStart = true;
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
}
cachedClientX = pointerMoveEvent.clientX;
let dragOffset = pointerMoveEvent.clientX - pointerDownEvent.clientX;
let value = initialValue + (dragOffset * this.step);
value = normalize(value, this.min, this.max, getPrecision(this.step));
this.value = value;
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
});
this["#editor"].addEventListener("lostpointercapture", lostPointerCaptureListener = () => {
this["#editor"].removeEventListener("pointermove", pointerMoveListener);
this["#editor"].removeEventListener("lostpointercapture", lostPointerCaptureListener);
this.style.cursor = null;
if (this._isDragging === true) {
this._isDragging = false;
this._isChangeStart = false;
this.dispatchEvent(new CustomEvent("changeend", {detail: this.value !== initialValue, bubbles: true}));
}
else {
this["#editor"].focus();
document.execCommand("selectAll");
}
});
}
}
}
_onStepperIncrementStart(event) {
let incrementListener, incrementEndListener;
this._isStepperButtonDown = true;
this.addEventListener("increment", incrementListener = (event) => {
this._maybeDispatchChangeStartEvent();
this._increment(event.detail.shiftKey);
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
this._maybeDispatchChangeEndEvent();
this._update();
});
this.addEventListener("incrementend", incrementEndListener = (event) => {
this._isStepperButtonDown = false;
this.removeEventListener("increment", incrementListener);
this.removeEventListener("incrementend", incrementEndListener);
});
}
_onStepperDecrementStart(event) {
let decrementListener, decrementEndListener;
this._isStepperButtonDown = true;
this.addEventListener("decrement", decrementListener = (event) => {
this._maybeDispatchChangeStartEvent();
this._decrement(event.detail.shiftKey);
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
this._maybeDispatchChangeEndEvent();
this._update();
});
this.addEventListener("decrementend", decrementEndListener = (event) => {
this._isStepperButtonDown = false;
this.removeEventListener("decrement", decrementListener);
this.removeEventListener("decrementend", decrementEndListener);
});
}
_onKeyDown(event) {
if (event.code === "ArrowDown") {
event.preventDefault();
this._isArrowKeyDown = true;
this._maybeDispatchChangeStartEvent();
this._decrement(event.shiftKey);
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
this._maybeDispatchChangeEndEvent();
this._update();
}
else if (event.code === "ArrowUp") {
event.preventDefault();
this._isArrowKeyDown = true;
this._maybeDispatchChangeStartEvent();
this._increment(event.shiftKey);
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
this._maybeDispatchChangeEndEvent();
this._update();
}
else if (event.code === "Backspace") {
this._isBackspaceKeyDown = true;
}
else if (event.code === "Enter") {
this._commitEditorChanges();
document.execCommand("selectAll");
}
}
_onKeyUp(event) {
if (event.code === "ArrowDown") {
this._isArrowKeyDown = false;
this._maybeDispatchChangeEndEvent();
}
else if (event.code === "ArrowUp") {
this._isArrowKeyDown = false;
this._maybeDispatchChangeEndEvent();
}
else if (event.code === "Backspace") {
this._isBackspaceKeyDown = false;
}
}
_onKeyPress(event) {
if (numericKeys.includes(event.key) === false) {
event.preventDefault();
}
}
async _onPaste(event) {
// Allow only for pasting numeric text
event.preventDefault();
let content = event.clipboardData.getData("text/plain").trim();
if (isNumeric(content)) {
// @bugfix: https://github.com/nwjs/nw.js/issues/3403
await sleep(1);
document.execCommand("insertText", false, content);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Override this method to validate the input value manually.
// @type
// {valid:boolean, hint:string}
validate() {
let valid = true;
if (this.value < this.min) {
valid = false;
}
else if (this.value > this.max) {
valid = false;
}
else if (this.required && this.value === null && this.visited === true) {
valid = false;
}
return valid;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_maybeDispatchChangeStartEvent() {
if (!this._isChangeStart) {
this._isChangeStart = true;
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
}
}
_maybeDispatchChangeEndEvent() {
if (this._isChangeStart && !this._isArrowKeyDown && !this._isBackspaceKeyDown && !this._isStepperButtonDown) {
this._isChangeStart = false;
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
}
}
_commitEditorChanges() {
let editorValue = this["#editor"].textContent.trim() === "" ? null : parseFloat(this["#editor"].textContent);
if (editorValue !== this.value) {
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
this.value = editorValue;
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_increment(large = false) {
let oldValue = this.value;
let newValue = this.value;
if (large) {
newValue += this.step * 10;
}
else {
newValue += this.step;
}
newValue = normalize(newValue, this.min, this.max, getPrecision(this.step));
if (oldValue !== newValue) {
this.value = newValue;
}
if (this.matches(":focus")) {
document.execCommand("selectAll");
}
this._updateState();
}
_decrement(large = false) {
let oldValue = this.value;
let newValue = this.value;
if (large) {
newValue -= this.step * 10;
}
else {
newValue -= this.step;
}
newValue = normalize(newValue, this.min, this.max, getPrecision(this.step));
if (oldValue !== newValue) {
this.value = newValue;
}
if (this.matches(":focus")) {
document.execCommand("selectAll");
}
this._updateState();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
this._updateEditorTextContent();
this._updateState();
this._updateStepper();
}
_updateEditorTextContent() {
if (this.hasAttribute("value")) {
this["#editor"].textContent = this.getAttribute("value").trim();
}
else {
this["#editor"].textContent = "";
}
}
_updateState() {
let isValid = this.validate();
if (isValid) {
this.removeAttribute("invalid");
}
else {
this.setAttribute("invalid", "");
}
if (this.value === null) {
this.setAttribute("empty", "");
}
else {
this.removeAttribute("empty");
}
}
_updateStepper() {
let stepper = this.querySelector("x-stepper");
if (stepper) {
let canDecrement = (this.value > this.min);
let canIncrement = (this.value < this.max);
if (canIncrement === true && canDecrement === true) {
stepper.removeAttribute("disabled");
}
else if (canIncrement === false && canDecrement === false) {
stepper.setAttribute("disabled", "");
}
else if (canIncrement === false) {
stepper.setAttribute("disabled", "increment");
}
else if (canDecrement === false) {
stepper.setAttribute("disabled", "decrement");
}
}
}
}
customElements.define("x-numberinput", XNumberInputElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$18 = html`
<template>
<style>
:host {
display: block;
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
touch-action: none;
will-change: opacity;
cursor: default;
background: rgba(0, 0, 0, 0.5);
}
:host([hidden]) {
display: none;
}
</style>
</template>
`;
class XOverlayElement extends HTMLElement {
constructor() {
super();
this._ownerElement = null;
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$18.content, true));
this.addEventListener("wheel", (event) => event.preventDefault());
this.addEventListener("pointerdown", (event) => event.preventDefault()); // Don't steal the focus
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Element below which the overlay should be placed.
// @type
// HTMLElement
get ownerElement() {
return this._ownerElement ? this._ownerElement : document.body.firstElementChild;
}
set ownerElement(ownerElement) {
this._ownerElement = ownerElement;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
show(animate = true) {
this.style.top = "0px";
this.style.left = "0px";
this.ownerElement.before(this);
this.hidden = false;
let bounds = this.getBoundingClientRect();
let extraTop = 0;
let extraLeft = 0;
// Determine extraLeft and extraTop which represent the extra offset needed when the overlay is inside another
// fixed-positioned element such as a popover
{
if (bounds.top !== 0 || bounds.left !== 0) {
extraTop = -bounds.top;
extraLeft = -bounds.left;
}
}
// Prevent the document body from being scrolled
{
if (document.body.scrollHeight > document.body.clientHeight) {
document.body.style.overflow = "hidden";
}
}
// Ensure the overlay is stacked directly below the ref element
{
let zIndex = parseFloat(getComputedStyle(this.ownerElement).zIndex);
this.style.zIndex = zIndex - 1;
}
this.style.top = (extraTop) + "px";
this.style.left = (extraLeft) + "px";
// Animate the overlay
if (animate) {
let overlayAnimation = this.animate(
{
opacity: ["0", "1"]
},
{
duration: 100,
easing: "ease-out"
}
);
return overlayAnimation.finished;
}
}
hide(animate = true) {
if (animate) {
let overlayAnimation = this.animate(
{
opacity: ["1", "0"]
},
{
duration: 100,
easing: "ease-in"
}
);
overlayAnimation.finished.then(() => {
document.body.style.overflow = null;
this.remove();
});
return overlayAnimation.finished;
}
else {
document.body.style.overflow = null;
this.remove();
}
}
}
customElements.define("x-overlay", XOverlayElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$19 = html`
<template>
<style>:host{position:fixed;display:none;top:0;left:0;min-height:30px;z-index:1001;box-sizing:border-box;background:#fff;-webkit-app-region:no-drag;--orientation: vertical;--align: bottom;--arrow-size: 20px;--open-transition: 900 transform cubic-bezier(0.4, 0, 0.2, 1);--close-transition: 200 opacity cubic-bezier(0.4, 0, 0.2, 1)}:host(:focus){outline:0}:host([animating]),:host([opened]){display:flex}#arrow{position:fixed;box-sizing:border-box;content:""}#arrow[data-align=bottom],#arrow[data-align=top]{width:var(--arrow-size);height:calc(var(--arrow-size)*.6);transform:translate(-50%,0)}#arrow[data-align=left],#arrow[data-align=right]{width:calc(var(--arrow-size)*.6);height:var(--arrow-size);transform:translate(0,-50%)}#arrow path{fill:pink;vector-effect:non-scaling-stroke;stroke-width:1}</style>
<svg id="arrow" viewBox="0 0 100 100" preserveAspectRatio="none">
<path id="arrow-path"></path>
</svg>
<slot></slot>
</template>
`;
// @events
// open
// close
class XPopoverElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$19.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
connectedCallback() {
this.tabIndex = -1;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// boolean
// @readonly
// @attribute
get opened() {
return this.hasAttribute("opened");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Open the popover next to the given element.
// Returns a promise that is resolved when the popover finishes animating.
// @type
// (XButtonElement, string) => Promise
open(button) {
return new Promise( async (resolve) => {
let computedStyle = getComputedStyle(this);
let align = computedStyle.getPropertyValue("--align").trim();
let marginTop = parseFloat(computedStyle.marginTop);
let extraLeft = 0; // Extra offset needed when popover has fixed-positioned ancestor(s)
let extraTop = 0; // Extra offset needed when popover has fixed-positioned ancestor(s)
let windowWhitespace = 5; // Minimal whitespace between popover and window bounds
let arrowWhitespace = 2; // Minimal whitespace between popover and arrow
this.style.left = "0px";
this.style.top = "0px";
this.style.width = null;
this.style.height = null;
this.setAttribute("opened", "");
// Determine extraLeft and extraTop which represent the extra offset when the popover is inside another
// fixed-positioned element.
{
let popoverBounds = this.getBoundingClientRect();
if (popoverBounds.top !== 0 || popoverBounds.left !== 0) {
extraLeft = -popoverBounds.left;
extraTop = -popoverBounds.top;
}
}
// Make the arrow look consistentaly with the popover
{
let {backgroundColor, borderColor, borderWidth} = getComputedStyle(this);
this["#arrow"].setAttribute("data-align", align);
this["#arrow-path"].style.fill = backgroundColor;
this["#arrow-path"].style.stroke = borderColor;
this["#arrow-path"].style.strokeWidth = borderWidth + "px";
}
if (align === "bottom") {
let buttonBounds = button.getBoundingClientRect();
let popoverBounds = this.getBoundingClientRect();
let arrowBounds = this["#arrow"].getBoundingClientRect();
let borderWidth = parseFloat(getComputedStyle(this).borderWidth);
// Place the popover below the button
{
this.style.top = (buttonBounds.bottom + arrowBounds.height + arrowWhitespace + extraTop) + "px";
this["#arrow"].style.top = (buttonBounds.bottom + arrowWhitespace + borderWidth + extraTop) + "px";
this["#arrow-path"].style.d = `path("M 0 100, L 50 0, L 100 100")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows bottom client bound, reduce its height (respecting min-height)
if (popoverBounds.bottom + windowWhitespace > window.innerHeight) {
let reducedHeight = window.innerHeight - popoverBounds.top - windowWhitespace;
let minHeight = parseFloat(getComputedStyle(this).minHeight);
if (reducedHeight >= minHeight) {
this.style.height = reducedHeight + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popover still overflows bottom client bound, place it above the button
if (popoverBounds.bottom + windowWhitespace > window.innerHeight) {
this.style.top = (
buttonBounds.top - arrowWhitespace - arrowBounds.height - popoverBounds.height + extraTop
) + "px";
this["#arrow"].style.top = (
buttonBounds.top - arrowWhitespace - arrowBounds.height - borderWidth + extraTop
) + "px";
this["#arrow-path"].style.d = `path("M 0 0, L 50 100, L 100 0")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows top client bound, reduce its height (respecting min-height)
if (popoverBounds.top - windowWhitespace < 0) {
let reducedHeight = buttonBounds.top - arrowWhitespace - arrowBounds.height - windowWhitespace;
let minHeight = parseFloat(getComputedStyle(this).minHeight);
if (reducedHeight >= minHeight) {
this.style.top = (windowWhitespace + extraTop) + "px";
this.style.height = reducedHeight + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popoever still overflows top client bound, place it back below the button
if (popoverBounds.top - windowWhitespace < 0) {
this.style.top = (buttonBounds.bottom + arrowBounds.height + arrowWhitespace + extraTop) + "px";
this["#arrow"].style.top = (buttonBounds.bottom + arrowWhitespace + borderWidth + extraTop) + "px";
this["#arrow-path"].style.d = `path("M 0 100, L 50 0, L 100 100")`;
}
}
else if (align === "top") {
let buttonBounds = button.getBoundingClientRect();
let popoverBounds = this.getBoundingClientRect();
let arrowBounds = this["#arrow"].getBoundingClientRect();
let borderWidth = parseFloat(getComputedStyle(this).borderWidth);
// Place the popover above the button
{
this.style.top = (
buttonBounds.top - arrowWhitespace - arrowBounds.height - popoverBounds.height + extraTop
) + "px";
this["#arrow"].style.top = (
buttonBounds.top - arrowWhitespace - arrowBounds.height - borderWidth + extraTop
) + "px";
this["#arrow-path"].style.d = `path("M 0 0, L 50 100, L 100 0")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows top client bound, reduce its height (respecting min-height)
if (popoverBounds.top - windowWhitespace < 0) {
let reducedHeight = buttonBounds.top - arrowWhitespace - arrowBounds.height - windowWhitespace;
let minHeight = parseFloat(getComputedStyle(this).minHeight);
if (reducedHeight >= minHeight) {
this.style.top = (windowWhitespace + extraTop) + "px";
this.style.height = reducedHeight + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popoever still overflows top client bound, place it below the button
if (popoverBounds.top - windowWhitespace < 0) {
this.style.top = (buttonBounds.bottom + arrowBounds.height + arrowWhitespace + extraTop) + "px";
this["#arrow"].style.top = (buttonBounds.bottom + arrowWhitespace + borderWidth + extraTop) + "px";
this["#arrow-path"].style.d = `path("M 0 100, L 50 0, L 100 100")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows bottom client bound, reduce its height (respecting min-height)
if (popoverBounds.bottom + windowWhitespace > window.innerHeight) {
let reducedHeight = window.innerHeight - popoverBounds.top - windowWhitespace;
let minHeight = parseFloat(getComputedStyle(this).minHeight);
if (reducedHeight >= minHeight) {
this.style.height = reducedHeight + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popover still overflows bottom client bound, move it back above the button
if (popoverBounds.bottom + windowWhitespace > window.innerHeight) {
this.style.top = (
buttonBounds.top - arrowWhitespace - arrowBounds.height - popoverBounds.height + extraTop
) + "px";
this["#arrow"].style.top = (
buttonBounds.top - arrowWhitespace - arrowBounds.height - borderWidth + extraTop
) + "px";
this["#arrow-path"].style.d = `path("M 0 0, L 50 100, L 100 0")`;
popoverBounds = this.getBoundingClientRect();
}
}
else if (align === "left") {
let buttonBounds = button.getBoundingClientRect();
let popoverBounds = this.getBoundingClientRect();
let arrowBounds = this["#arrow"].getBoundingClientRect();
let borderWidth = parseFloat(getComputedStyle(this).borderWidth);
// Place the popover on the left side of the button
{
this.style.left = (
buttonBounds.left - arrowWhitespace - arrowBounds.width - popoverBounds.width + extraLeft
) + "px";
this["#arrow"].style.left = (
buttonBounds.left - arrowBounds.width - arrowWhitespace - borderWidth + extraLeft
) + "px";
this["#arrow-path"].style.d = `path("M 0 0, L 100 50, L 00 100")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows left client bound, reduce its width (respecting min-width)
if (popoverBounds.left - windowWhitespace < 0) {
let reducedWidth = buttonBounds.left - arrowWhitespace - arrowBounds.height - windowWhitespace;
let minWidth = parseFloat(getComputedStyle(this).minWidth);
if (reducedWidth >= minWidth) {
this.style.left = (windowWhitespace + extraLeft) + "px";
this.style.width = reducedWidth + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popoever still overflows left client bound, place it on the right side of the button
if (popoverBounds.left - windowWhitespace < 0) {
this.style.left = (buttonBounds.right + arrowBounds.height + arrowWhitespace + extraLeft) + "px";
this["#arrow"].style.top = (buttonBounds.right + arrowWhitespace + borderWidth + extraLeft) + "px";
this["#arrow-path"].style.d = `path("M 0 100, L 50 0, L 100 100")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows right client bound, reduce its width (respecting min-width)
if (popoverBounds.right + windowWhitespace > window.innerWidth) {
let reducedWidth = window.innerWidth - popoverBounds.left - windowWhitespace;
let minWidth = parseFloat(getComputedStyle(this).minWidth);
if (reducedWidth >= minWidth) {
this.style.width = reducedWidth + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popover still overflows right client bound, move it back to the left side of the button
if (popoverBounds.right + windowWhitespace > window.innerWidth) {
this.style.left = (
buttonBounds.left - arrowWhitespace - arrowBounds.width - popoverBounds.width + extraLeft
) + "px";
this["#arrow"].style.elft = (
buttonBounds.left - arrowWhitespace - arrowBounds.width - borderWidth + extraLeft
) + "px";
this["#arrow-path"].style.d = `path("M 0 0, L 100 50, L 00 100")`;
popoverBounds = this.getBoundingClientRect();
}
}
else if (align === "right") {
let buttonBounds = button.getBoundingClientRect();
let popoverBounds = this.getBoundingClientRect();
let arrowBounds = this["#arrow"].getBoundingClientRect();
let borderWidth = parseFloat(getComputedStyle(this).borderWidth);
// Place the popover on the right side of the button
{
this.style.left = (buttonBounds.right + arrowBounds.width + arrowWhitespace + extraLeft) + "px";
this["#arrow"].style.left = (buttonBounds.right + arrowWhitespace + borderWidth + extraLeft) + "px";
this["#arrow-path"].style.d = `path("M 100 0, L 0 50, L 100 100")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows right client bound, reduce its width (respecting min-width)
if (popoverBounds.right + windowWhitespace > window.innerWidth) {
let reducedWidth = window.innerWidth - popoverBounds.left - windowWhitespace;
let minWidth = parseFloat(getComputedStyle(this).minWidth);
if (reducedWidth >= minWidth) {
this.style.width = reducedWidth + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popover still overflows right client bound, place it on the left side of the button
if (popoverBounds.right + windowWhitespace > window.innerWidth) {
this.style.left = (
buttonBounds.left - arrowWhitespace - arrowBounds.width - popoverBounds.width + extraLeft
) + "px";
this["#arrow"].style.left = (
buttonBounds.left - arrowWhitespace - arrowBounds.width - borderWidth + extraLeft
) + "px";
this["#arrow-path"].style.d = `path("M 0 0, L 50 100, L 100 0")`;
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows left client bound, reduce its width (respecting min-width)
if (popoverBounds.left - windowWhitespace < 0) {
let reducedWidth = buttonBounds.left - arrowWhitespace - arrowBounds.width - windowWhitespace;
let minWidth = parseFloat(getComputedStyle(this).minWidth);
if (reducedWidth >= minWidth) {
this.style.left = (windowWhitespace + extraLeft) + "px";
this.style.width = reducedWidth + "px";
popoverBounds = this.getBoundingClientRect();
}
}
// If popoever still overflows left client bound, place it back on the right side of the button
if (popoverBounds.left - windowWhitespace < 0) {
this.style.left = (buttonBounds.right + arrowBounds.width + arrowWhitespace + extraLeft) + "px";
this["#arrow"].style.left = (buttonBounds.right + arrowWhitespace + borderWidth + extraLeft) + "px";
this["#arrow-path"].style.d = `path("M 100 0, L 0 50, L 100 100")`;
}
}
if (align === "bottom" || align === "top") {
let buttonBounds = button.getBoundingClientRect();
let popoverBounds = this.getBoundingClientRect();
let arrowBounds = this["#arrow"].getBoundingClientRect();
// Place the popover along the same X-axis as the button
{
this.style.left = (buttonBounds.left + buttonBounds.width/2 - popoverBounds.width/2 + extraLeft) + "px";
this["#arrow"].style.left = (buttonBounds.left + buttonBounds.width/2 + extraLeft) + "px";
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows left client bound, move it right
if (popoverBounds.left - windowWhitespace < 0) {
this.style.left = (windowWhitespace + extraLeft) + "px";
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows right client bound, move it left
if (popoverBounds.right + windowWhitespace > window.innerWidth) {
this.style.left = (window.innerWidth - windowWhitespace - popoverBounds.width + extraLeft) + "px";
popoverBounds = this.getBoundingClientRect();
}
// If popover still overflows left client bound, reduce its width
if (popoverBounds.left < windowWhitespace) {
this.style.left = (windowWhitespace + extraLeft) + "px";
this.style.width = (window.innerWidth - windowWhitespace - windowWhitespace) + "px";
}
}
else if (align === "left" || align === "right") {
let buttonBounds = button.getBoundingClientRect();
let popoverBounds = this.getBoundingClientRect();
// Place the popover along the same Y-axis as the button
{
this.style.top = (buttonBounds.top + buttonBounds.height/2 - popoverBounds.height/2 + extraTop) + "px";
this["#arrow"].style.top = (buttonBounds.top + buttonBounds.height/2 + extraTop + marginTop) + "px";
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows top client bound, move it down
if (popoverBounds.top - windowWhitespace < 0) {
this.style.top = (windowWhitespace + extraTop + marginTop) + "px";
popoverBounds = this.getBoundingClientRect();
}
// If popover overflows bottom client bound, move it up
if (popoverBounds.bottom + windowWhitespace > window.innerHeight) {
let overflowBottom = popoverBounds.bottom + windowWhitespace - window.innerHeight;
this.style.top = (popoverBounds.top - overflowBottom + extraTop) + "px";
popoverBounds = this.getBoundingClientRect();
}
// If popover still overflows top client bound, reduce its size
if (popoverBounds.top < windowWhitespace) {
this.style.top = (windowWhitespace + extraTop) + "px";
this.style.height = (window.innerHeight - windowWhitespace - windowWhitespace) + "px";
}
}
// Animate the popover
{
let transition = getComputedStyle(this).getPropertyValue("--open-transition");
let [property, duration, easing] = this._parseTransistion(transition);
if (property === "transform") {
await this.animate(
{
transform: ["scale(1, 0)", "scale(1, 1)"],
transformOrigin: ["0 0", "0 0"]
},
{ duration, easing }
).finished;
}
}
this.dispatchEvent(new CustomEvent("open", {bubbles: true, detail: this}));
resolve();
});
}
// @info
// Close the popover.
// Returns a promise that is resolved when the popover finishes animating.
// @type
// (boolean) => Promise
close() {
return new Promise(async (resolve) => {
if (this.opened) {
this.removeAttribute("opened");
this.setAttribute("animating", "");
this.dispatchEvent(new CustomEvent("close", {bubbles: true, detail: this}));
let transition = getComputedStyle(this).getPropertyValue("--close-transition");
let [property, duration, easing] = this._parseTransistion(transition);
if (property === "opacity") {
await this.animate({ opacity: ["1", "0"] }, { duration, easing }).finished;
}
this.removeAttribute("animating");
}
resolve();
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Parse the value of CSS transition property.
// @type
// (string) => [string, number, string]
_parseTransistion(string) {
let [rawDuration, property, ...rest] = string.trim().split(" ");
let duration = parseFloat(rawDuration);
let easing = rest.join(" ");
return [property, duration, easing];
}
}
customElements.define("x-popover", XPopoverElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$20 = html`
<template>
<style>:host{display:block;box-sizing:border-box;height:4px;width:100%;position:relative;contain:strict;overflow:hidden;background:#acece6;cursor:default;--bar-background: #3B99FB;--bar-box-shadow: 0px 0px 0px 1px #3385DB}#indeterminate-bars{width:100%;height:100%}#determinate-bar{width:0%;box-shadow:var(--bar-box-shadow);transition:width .4s ease-in-out}#determinate-bar,#primary-indeterminate-bar,#secondary-indeterminate-bar{position:absolute;top:0;left:0;bottom:0;height:100%;background:var(--bar-background);will-change:left,right}</style>
<div id="determinate-bar"></div>
<div id="indeterminate-bars">
<div id="primary-indeterminate-bar"></div>
<div id="secondary-indeterminate-bar"></div>
</div>
</template>
`;
class XProgressbarElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$20.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
connectedCallback() {
this._update();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "value") {
this._update();
}
else if (name === "disabled") {
this._update();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["value", "max", "disabled"];
}
// @info
// Current progress, in procentages.
// @type
// number?
// @default
// null
// @attribute
get value() {
return this.hasAttribute("value") ? parseFloat(this.getAttribute("value")) : null;
}
set value(value) {
value === null ? this.removeAttribute("value") : this.setAttribute("value", value);
}
// @type
// number?
// @default
// null
// @attribute
get max() {
return this.hasAttribute("max") ? parseFloat(this.getAttribute("max")) : 1;
}
set max(max) {
this.setAttribute("max", max);
}
// @info
// Whether this button is disabled.
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
// Determinate bar
{
// Hide
if (this.value === null || this.value === -1 || this.disabled) {
this["#determinate-bar"].style.width = "0%";
}
// Show
else {
this["#determinate-bar"].style.width = ((this.value / this.max) * 100) + "%";
}
}
// Indeterminate bars
{
// Hide
if ((this.value !== null && this.value !== -1) || this.disabled) {
if (this._indeterminateAnimations) {
for (let animation of this._indeterminateAnimations) {
animation.cancel();
}
this._indeterminateAnimations = null;
}
}
// Show
else {
if (!this._indeterminateAnimations) {
this._indeterminateAnimations = [
this["#primary-indeterminate-bar"].animate(
[
{ left: "-35%", right: "100%", offset: 0.0 },
{ left: "100%", right: "-90%", offset: 0.6 },
{ left: "100%", right: "-90%", offset: 1.0 }
],
{
duration: 2000,
easing: "ease-in-out",
iterations: Infinity
}
),
this["#secondary-indeterminate-bar"].animate(
[
{ left: "-100%", right: "100%", offset: 0.0 },
{ left: "110%", right: "-30%", offset: 0.8 },
{ left: "110%", right: "-30%", offset: 1.0 }
],
{
duration: 2000,
delay: 1000,
easing: "ease-in-out",
iterations: Infinity
}
)
];
}
}
}
}
}
customElements.define("x-progressbar", XProgressbarElement);
// @info
// Radio widget.
// @doc
// http://w3c.github.io/aria/aria/aria.html#radio
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$21 = html`
<template>
<style>:host{display:block;position:relative;border:3px solid #000;width:20px;height:20px;border-radius:99px;--dot-color: black;--dot-transform: scale(0);--dot-box-shadow: none}:host([toggled]){--dot-transform: scale(0.6)}:host(:focus){outline:0}:host([disabled]){opacity:.4;pointer-events:none}:host([hidden]){display:none}#dot,#main{width:100%;height:100%;border-radius:99px}#main{display:flex;align-items:center;justify-content:center}#dot{background:var(--dot-color);box-shadow:var(--dot-box-shadow);transform:var(--dot-transform);transition:all .15s ease-in-out}:host([mixed][toggled]) #dot{height:33%;border-radius:0}</style>
<main id="main">
<div id="dot"></div>
</main>
</template>
`;
// @events
// change
class XRadioElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$21.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
if (!this.closest("x-radios")) {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
}
this.setAttribute("role", "radio");
this.setAttribute("aria-checked", this.toggled);
this.setAttribute("aria-disabled", this.disabled);
}
attributeChangedCallback(name) {
if (name === "toggled") {
this._onToggledAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["toggled", "disabled"];
}
// @info
// Values associated with this widget.
// @type
// string
// @default
// ""
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : null;
}
set value(value) {
value === null ? this.removeAttribute("value") : this.setAttribute("value", value);
}
// @type
// boolean
// @default
// false
// @attribute
get toggled() {
return this.hasAttribute("toggled");
}
set toggled(toggled) {
toggled ? this.setAttribute("toggled", "") : this.removeAttribute("toggled");
}
// @type
// boolean
// @default
// false
// @attribute
get mixed() {
return this.hasAttribute("mixed");
}
set mixed(mixed) {
mixed ? this.setAttribute("mixed", "") : this.removeAttribute("mixed");
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onToggledAttributeChange() {
this.setAttribute("aria-checked", this.toggled);
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
_onClick(event) {
if (!this.closest("x-radios")) {
if (this.toggled && this.mixed) {
this.mixed = false;
}
else {
this.mixed = false;
this.toggled = !this.toggled;
}
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
}
_onPointerDown(event) {
// Don't focus the widget with pointer, instead focus the closest ancestor focusable element
if (this.matches(":focus") === false) {
event.preventDefault();
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
}
_onKeyDown(event) {
if (event.code === "Enter" || event.code === "Space") {
event.preventDefault();
this.click();
}
}
}
customElements.define("x-radio", XRadioElement);
// @copyright
// © 2016-2017 Jarosław Foksa
// @doc
// https://www.youtube.com/watch?v=uCIC2LNt0bk
class XRadiosElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.innerHTML = `<slot></slot>`;
this.addEventListener("click", (event) => this._onClick(event), true);
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
this.setAttribute("role", "radiogroup");
let radios = [...this.querySelectorAll("x-radio")].filter(radio => radio.closest("x-radios") === this);
let defaultRadio = radios.find($0 => $0.toggled && !$0.disabled) || radios.find($0 => !$0.disabled);
for (let radio of radios) {
radio.setAttribute("tabindex", radio === defaultRadio ? "0 ": "-1");
radio.setAttribute("aria-checked", radio === defaultRadio);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// string?
// @default
// null
get value() {
let radio = this.querySelector(`x-radio[toggled]`);
return radio ? radio.value : null;
}
set value(value) {
for (let radio of this.querySelectorAll("x-radio")) {
radio.toggled = (radio.value === value && value !== null);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onClick(event) {
let clickedRadio = event.target.localName === "x-radio" ? event.target : null;
if (clickedRadio && !clickedRadio.toggled && !clickedRadio.disabled && event.button === 0) {
let radios = [...this.querySelectorAll("x-radio")];
let otherRadios = radios.filter(radio => radio.closest("x-radios") === this && radio !== clickedRadio);
if (clickedRadio.toggled === false || clickedRadio.mixed === true) {
clickedRadio.toggled = true;
clickedRadio.mixed = false;
clickedRadio.tabIndex = 0;
for (let radio of otherRadios) {
radio.toggled = false;
radio.tabIndex = -1;
}
this.dispatchEvent(new CustomEvent("toggle", {bubbles: true, detail: clickedRadio}));
}
}
}
_onKeyDown(event) {
let {key} = event;
if (key === "ArrowDown" || key === "ArrowRight") {
let radios = [...this.querySelectorAll("x-radio")];
let contextRadios = radios.filter($0 => $0.disabled === false && $0.closest("x-radios") === this);
let focusedRadio = radios.find(radio => radio.matches(":focus"));
if (focusedRadio) {
let focusedRadioIndex = contextRadios.indexOf(focusedRadio);
let nextRadio = contextRadios.length > 1 ? contextRadios[focusedRadioIndex+1] || contextRadios[0] : null;
if (nextRadio) {
event.preventDefault();
nextRadio.focus();
nextRadio.tabIndex = 0;
focusedRadio.tabIndex = -1;
}
}
}
else if (key === "ArrowUp" || key === "ArrowLeft") {
let radios = [...this.querySelectorAll("x-radio")];
let contextRadios = radios.filter($0 => $0.disabled === false && $0.closest("x-radios") === this);
let focusedRadio = radios.find(radio => radio.matches(":focus"));
if (focusedRadio) {
let focusedRadioIndex = contextRadios.indexOf(focusedRadio);
let lastRadio = contextRadios[contextRadios.length-1];
let prevRadio = contextRadios.length > 1 ? contextRadios[focusedRadioIndex-1] || lastRadio : null;
if (prevRadio) {
event.preventDefault();
prevRadio.focus();
prevRadio.tabIndex = 0;
focusedRadio.tabIndex = -1;
}
}
}
}
}
customElements.define("x-radios", XRadiosElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let debug$2 = false;
let shadowHTML$1 = `
<style>:host{display:block;width:100%;user-select:none}:host([hidden]){display:none}#hue-slider{width:100%;height:28px;padding:0 calc(var(--marker-width)/2);margin-bottom:14px;box-sizing:border-box;border-radius:2px;touch-action:pan-y;--marker-width: 18px;background:red}#hue-slider-track{width:100%;height:100%;position:relative;display:flex;align-items:center;background:linear-gradient(to right,red ,#ff0 ,#0f0 ,#0ff ,#00f ,#f0f ,red)}#hue-slider-marker{left:0%;background:rgba(0,0,0,.2);box-shadow:0 0 3px #000;box-sizing:border-box;transform:translateX(calc((var(--marker-width)/2)*-1));border:3px solid #fff;width:var(--marker-width);height:32px;position:absolute}#satlight-slider{width:100%;height:174px;border-radius:2px;position:relative;touch-action:pinch-zoom}#satlight-marker{position:absolute;top:0%;left:0%;width:var(--marker-size);height:var(--marker-size);transform:translate(calc(var(--marker-size)/-2),calc(var(--marker-size)/-2));box-sizing:border-box;background:rgba(0,0,0,.3);border:3px solid #fff;border-radius:999px;box-shadow:0 0 3px #000;--marker-size: 20px}#alpha-slider{display:none;width:100%;height:28px;margin-top:14px;padding:0 calc(var(--marker-width)/2);box-sizing:border-box;border:1px solid #cecece;border-radius:2px;touch-action:pan-y;--marker-width: 18px}:host([alphaslider]) #alpha-slider{display:block}#alpha-slider-track{width:100%;height:100%;position:relative;display:flex;align-items:center}#alpha-slider-marker{left:0%;background:rgba(0,0,0,.2);box-shadow:0 0 3px #000;box-sizing:border-box;transform:translateX(calc((var(--marker-width)/2)*-1));border:3px solid #fff;width:var(--marker-width);height:32px;position:absolute}</style>
<x-box vertical>
<div id="hue-slider">
<div id="hue-slider-track">
<div id="hue-slider-marker"></div>
</div>
</div>
<div id="satlight-slider">
<div id="satlight-marker"></div>
</div>
<div id="alpha-slider">
<div id="alpha-slider-track">
<div id="alpha-slider-marker"></div>
</div>
</div>
</x-box>
`;
// @events
// change
// changestart
// changeend
class XRectColorPickerElement extends HTMLElement {
static get observedAttributes() {
return ["value"];
}
constructor() {
super();
// Note that HSVA color model is used only internally
this._h = 0; // Hue (0 ~ 360)
this._s = 0; // Saturation (0 ~ 100)
this._v = 100; // Value (0 ~ 100)
this._a = 1; // Alpha (0 ~ 1)
this._isDraggingHueSliderMarker = false;
this._isDraggingSatlightMarker = false;
this._isDraggingAlphaSliderMarker = false;
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.innerHTML = shadowHTML$1;
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this["#hue-slider"].addEventListener("pointerdown", (event) => this._onHueSliderPointerDown(event));
this["#satlight-slider"].addEventListener("pointerdown", (event) => this._onSatlightSliderPointerDown(event));
this["#alpha-slider"].addEventListener("pointerdown", (event) => this._onAlphaSliderPointerDown(event));
}
connectedCallback() {
this._update();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "value") {
this._onValueAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// string
// @default
// "hsla(0, 0%, 100%, 1)"
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : "hsla(0, 0%, 100%, 1)";
}
set value(value) {
this.setAttribute("value", value);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onValueAttributeChange() {
if (
this._isDraggingHueSliderMarker === false &&
this._isDraggingSatlightMarker === false &&
this._isDraggingAlphaSliderMarker === false
) {
let [h, s, v, a] = parseColor(this.value, "hsva");
this._h = h;
this._s = s;
this._v = v;
this._a = a;
this._update();
}
if (debug$2) {
console.log(`%c ${this.value}`, `background: ${this.value};`);
}
}
_onSatlightSliderPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return;
}
let pointerMoveListener, lostPointerCaptureListener;
let sliderBounds = this["#satlight-slider"].getBoundingClientRect();
this._isDraggingSatlightMarker = true;
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
this["#satlight-slider"].setPointerCapture(pointerDownEvent.pointerId);
let onPointerMove = (clientX, clientY) => {
let x = ((clientX - sliderBounds.left) / sliderBounds.width) * 100;
let y = ((clientY - sliderBounds.top) / sliderBounds.height) * 100;
x = normalize(x, 0, 100, 2);
y = normalize(y, 0, 100, 2);
this._s = x;
this._v = 100 - y;
this.value = serializeColor([this._h, this._s, this._v, this._a], "hsva", "hsla");
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
this._updateSatlightSliderMarker();
this._updateSatlightSliderBackground();
this._updateAlphaSliderBackground();
};
onPointerMove(pointerDownEvent.clientX, pointerDownEvent.clientY);
this["#satlight-slider"].addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
onPointerMove(pointerMoveEvent.clientX, pointerMoveEvent.clientY);
});
this["#satlight-slider"].addEventListener("lostpointercapture", lostPointerCaptureListener = (event) => {
this["#satlight-slider"].removeEventListener("pointermove", pointerMoveListener);
this["#satlight-slider"].removeEventListener("lostpointercapture", lostPointerCaptureListener);
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
this._isDraggingSatlightMarker = false;
});
}
_onHueSliderPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return;
}
let trackBounds = this["#hue-slider-track"].getBoundingClientRect();
let pointerMoveListener, lostPointerCaptureListener;
this._isDraggingHueSliderMarker = true;
this["#hue-slider"].setPointerCapture(pointerDownEvent.pointerId);
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
let onPointerMove = (clientX) => {
let h = ((clientX - trackBounds.x) / trackBounds.width) * 360;
h = normalize(h, 0, 360, 0);
if (h !== this._h) {
this._h = h;
this.value = serializeColor([this._h, this._s, this._v, this._a], "hsva", "hsla");
this._updateHueSliderMarker();
this._updateSatlightSliderBackground();
this._updateSatlightSliderMarker();
this._updateAlphaSliderBackground();
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
};
onPointerMove(pointerDownEvent.clientX);
this["#hue-slider"].addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
onPointerMove(pointerMoveEvent.clientX);
});
this["#hue-slider"].addEventListener("lostpointercapture", lostPointerCaptureListener = () => {
this["#hue-slider"].removeEventListener("pointermove", pointerMoveListener);
this["#hue-slider"].removeEventListener("lostpointercapture", lostPointerCaptureListener);
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
this._isDraggingHueSliderMarker = false;
});
}
_onAlphaSliderPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return;
}
let trackBounds = this["#alpha-slider-track"].getBoundingClientRect();
let pointerMoveListener, lostPointerCaptureListener;
this._isDraggingAlphaSliderMarker = true;
this["#alpha-slider"].setPointerCapture(pointerDownEvent.pointerId);
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
let onPointerMove = (clientX) => {
let a = 1 - ((clientX - trackBounds.x) / trackBounds.width);
a = normalize(a, 0, 1, 2);
if (a !== this._a) {
this._a = a;
this.value = serializeColor([this._h, this._s, this._v, this._a], "hsva", "hsla");
this._updateAlphaSliderMarker();
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
};
onPointerMove(pointerDownEvent.clientX);
this["#alpha-slider"].addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
onPointerMove(pointerMoveEvent.clientX);
});
this["#alpha-slider"].addEventListener("lostpointercapture", lostPointerCaptureListener = () => {
this["#alpha-slider"].removeEventListener("pointermove", pointerMoveListener);
this["#alpha-slider"].removeEventListener("lostpointercapture", lostPointerCaptureListener);
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
this._isDraggingAlphaSliderMarker = false;
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
this._updateHueSliderMarker();
this._updateSatlightSliderMarker();
this._updateSatlightSliderBackground();
this._updateAlphaSliderMarker();
this._updateAlphaSliderBackground();
}
_updateHueSliderMarker() {
this["#hue-slider-marker"].style.left = ((normalize(this._h, 0, 360, 0) / 360) * 100) + "%";
}
_updateSatlightSliderMarker() {
let left = (this._s / 100) * 100;
let top = 100 - ((this._v / 100) * 100);
this["#satlight-marker"].style.left = `${left}%`;
this["#satlight-marker"].style.top = `${top}%`;
}
_updateSatlightSliderBackground() {
let background1 = serializeColor([this._h, 100, 50, 1], "hsla", "hex");
let background2 = "linear-gradient(to left, rgba(255,255,255,0), rgba(255,255,255,1))";
let background3 = "linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1))";
this["#satlight-slider"].style.background = `${background3}, ${background2}, ${background1}`;
}
_updateAlphaSliderMarker() {
this["#alpha-slider-marker"].style.left = normalize((1 - this._a) * 100, 0, 100, 2) + "%";
}
_updateAlphaSliderBackground() {
let [r, g, b] = hsvToRgb(this._h, this._s, this._v).map($0 => round($0, 0));
let backroundA = `url(node_modules/xel/images/checkboard.png) repeat 0 0`;
let background = `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 1), rgba(${r}, ${g}, ${b}, 0))`;
this["#alpha-slider"].style.background = background + "," + backroundA;
}
}
customElements.define("x-rectcolorpicker", XRectColorPickerElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let debug$3 = false;
let windowPadding = 7;
let $itemChild = Symbol();
let shadowTemplate$22 = html`
<template>
<style>:host{display:block;width:fit-content;height:fit-content;max-width:100%;box-sizing:border-box;outline:0;font-size:15px;user-select:none;--arrow-width: 13px;--arrow-height: 13px;--arrow-min-width: 13px;--arrow-margin: 0 2px 0 11px;--arrow-color: currentColor;--arrow-d: path("M 24.75 41.33 L 50 16.14 L 75.25 41.33 L 83.01 33.68 L 50 0.63 L 17 33.628 Z M 17 66.372 L 50 99.372 L 83.001 66.372 L 75.245 58.617 L 50 83.807 L 24.755 58.617 Z")}:host([disabled]){pointer-events:none;opacity:.5}:host([hidden]){display:none}:host(:hover){cursor:default}#button{display:flex;flex-flow:row;align-items:center;justify-content:flex-start;flex:1;width:100%;height:100%}#button>x-label{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}#button>#arrow-container{margin:0 0 0 auto;z-index:999}#button>#arrow-container #arrow{display:flex;width:var(--arrow-width);height:var(--arrow-height);min-width:var(--arrow-min-width);margin:var(--arrow-margin);color:var(--arrow-color);d:var(--arrow-d)}#button>#arrow-container #arrow path{fill:currentColor;d:inherit}</style>
<div id="button">
<div id="arrow-container">
<svg id="arrow" viewBox="0 0 100 100" preserveAspectRatio="none">
<path></path>
</svg>
</div>
</div>
<slot></slot>
</template>
`;
// @event
// change {oldValue: string?, newValue: string?}
class XSelectElement extends HTMLElement {
constructor() {
super();
this._wasFocusedBeforeExpanding = false;
this._observer = new MutationObserver((args) => this._onMutation(args));
this._updateButtonTh300 = throttle(this._updateButton, 300, this);
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$22.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this["#overlay"] = createElement("x-overlay");
this["#overlay"].style.opacity = "0";
this["#overlay"].ownerElement = this;
this["#overlay"].addEventListener("click", (event) => this._onOverlayClick(event));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
this._observer.observe(this, {childList: true, attributes: true, characterData: true, subtree: true});
this._updateAccessabiltyAttributes();
if (debug$3) {
this.setAttribute("debug", "");
}
sleep(500).then(() => this._updateButtonTh300());
}
disconnectedCallback() {
this._observer.disconnect();
}
attributeChangedCallback(name) {
if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["disabled"];
}
// @type
// string?
// @default
// null
get value() {
let item = this.querySelector(`x-menuitem[selected="true"]`);
return item ? item.value : null;
}
set value(value) {
for (let item of this.querySelectorAll("x-menuitem")) {
item.selected = (item.value === value && value !== null);
}
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
_onMutation(records) {
for (let record of records) {
if (record.type === "attributes" && record.target.localName === "x-menuitem" && record.attributeName === "selected") {
this._updateButtonTh300();
}
}
}
_onPointerDown(event) {
// Don't focus the widget with pointer
if (!event.target.closest("x-menu") && this.matches(":focus") === false) {
event.preventDefault();
}
}
_onClick(event) {
if (event.button !== 0) {
return;
}
if (this._canExpand()) {
this._expand();
}
else if (this._canCollapse()) {
let clickedItem = event.target.closest("x-menuitem");
if (clickedItem) {
let oldValue = this.value;
let newValue = clickedItem.value;
for (let item of this.querySelectorAll("x-menuitem")) {
item.selected = (item === clickedItem);
}
if (oldValue !== newValue) {
this.dispatchEvent(new CustomEvent("change", {bubbles: true, detail: {oldValue, newValue}}));
}
this._collapse(clickedItem.whenTriggerEnd);
}
}
}
_onOverlayClick(event) {
this._collapse();
}
_onKeyDown(event) {
let menu = this.querySelector(":scope > x-menu");
if (event.key === "Enter" || event.key === "Space" || event.key === "ArrowUp" || event.key === "ArrowDown") {
if (this._canExpand()) {
event.preventDefault();
this._expand();
}
}
else if (event.key === "Escape") {
if (this._canCollapse()) {
event.preventDefault();
this._collapse();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_expand() {
if (this._canExpand() === false) {
return;
}
this._wasFocusedBeforeExpanding = this.matches(":focus");
this["#overlay"].show(false);
window.addEventListener("resize", this._resizeListener = () => {
this._collapse();
});
window.addEventListener("blur", this._blurListener = () => {
if (debug$3 === false) {
this._collapse();
}
});
let menu = this.querySelector(":scope > x-menu");
// Ensure there is at most one selected menu item and all other items are unselected
{
let selectedItem = null;
for (let item of menu.querySelectorAll("x-menuitem")) {
if (item.selected === null) {
item.selected = false;
}
else if (item.selected === true) {
if (selectedItem === null) {
selectedItem = item;
}
else {
item.selected = false;
}
}
}
}
// Open the menu
{
let selectedItem = menu.querySelector(`x-menuitem[selected="true"]`);
if (selectedItem) {
let buttonChild = this["#button"].querySelector("x-label") || this["#button"].firstElementChild;
let itemChild = buttonChild[$itemChild];
menu.openOverElement(buttonChild, itemChild);
}
else {
let item = menu.querySelector("x-menuitem").firstElementChild;
menu.openOverElement(this["#button"], item);
}
}
// Increase menu width if it is narrower than the button
{
let menuBounds = menu.getBoundingClientRect();
let buttonBounds = this["#button"].getBoundingClientRect();
let hostPaddingRight = parseFloat(getComputedStyle(this).paddingRight);
if (menuBounds.right - hostPaddingRight < buttonBounds.right) {
menu.style.minWidth = (buttonBounds.right - menuBounds.left + hostPaddingRight) + "px";
}
}
// Reduce menu width if it oveflows the right client bound
{
let menuBounds = this.getBoundingClientRect();
if (menuBounds.right + windowPadding > window.innerWidth) {
this.style.maxWidth = (window.innerWidth - menuBounds.left - windowPadding) + "px";
}
}
}
async _collapse(whenTriggerEnd = null) {
if (this._canCollapse() === false) {
return;
}
let menu = this.querySelector(":scope > x-menu");
menu.setAttribute("closing", "");
await whenTriggerEnd;
this["#overlay"].hide(false);
if (this._wasFocusedBeforeExpanding) {
this.focus();
}
else {
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
window.removeEventListener("resize", this._resizeListener);
window.removeEventListener("blur", this._blurListener);
await menu.close();
menu.removeAttribute("closing");
}
_canExpand() {
if (this.disabled) {
return false;
}
else {
let menu = this.querySelector(":scope > x-menu");
let item = menu.querySelector("x-menuitem");
return menu !== null && menu.opened === false && menu.hasAttribute("closing") === false && item !== null;
}
}
_canCollapse() {
if (this.disabled) {
return false;
}
else {
let menu = this.querySelector(":scope > x-menu");
let item = menu.querySelector("x-menuitem");
return menu !== null && menu.opened === true && menu.hasAttribute("closing") === false;
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_updateButton() {
let selectedItem = this.querySelector(`:scope > x-menu x-menuitem[selected="true"]`);
let arrowContainer = this["#arrow-container"];
this["#button"].innerHTML = "";
if (selectedItem) {
for (let itemChild of selectedItem.children) {
let buttonChild = itemChild.cloneNode(true);
buttonChild[$itemChild] = itemChild;
buttonChild.removeAttribute("id");
buttonChild.removeAttribute("style");
buttonChild.style.marginLeft = getComputedStyle(itemChild).marginLeft;
if (["x-icon", "x-swatch", "img", "svg"].includes(itemChild.localName)) {
let {width, height, border} = getComputedStyle(itemChild);
buttonChild.style.width = width;
buttonChild.style.height = height;
buttonChild.style.minWidth = width;
buttonChild.style.border = border;
}
this["#button"].append(buttonChild);
}
}
this["#button"].append(arrowContainer);
}
_updateAccessabiltyAttributes() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "button");
this.setAttribute("aria-disabled", this.disabled);
let menu = this.querySelector(":scope > x-menu");
if (menu) {
menu.setAttribute("role", "listbox");
for (let item of menu.querySelectorAll("x-listitem")) {
item.setAttribute("role", "option");
}
}
}
}
customElements.define("x-select", XSelectElement);
// @info
// Element responsible for displaying a platform-agnostic keyboard shortcut.
// @copyright
// © 2016-2017 Jarosław Foksa
let isAppleDevice = navigator.platform.startsWith("Mac") || ["iPhone", "iPad"].includes(navigator.platform);
// @doc
// https://www.w3.org/TR/uievents-key/#keys-modifier
let modKeys = [
"Alt",
"AltGraph",
"CapsLock",
"Control",
"Fn",
"FnLock",
"Meta",
"NumLock",
"ScrollLock",
"Shift",
"Symbol",
"SymbolLock"
];
let shadowTemplate$23 = html`
<template>
<style>:host{display:inline-block;box-sizing:border-box;font-size:14px;line-height:1}</style>
<main id="main"></main>
</template>
`;
class XShortcutElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$23.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
attributeChangedCallback(name) {
if (name === "value") {
this._update();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["value"];
}
// @type
// Array<string>
// @default
// []
// @attribute
get value() {
let value = [];
if (this.hasAttribute("value")) {
let parts = this.getAttribute("value").replace("++", "+PLUS").split("+");
parts = parts.map($0 => $0.trim().replace("PLUS", "+")).filter($0 => $0 !== "");
value = parts;
}
return value;
}
set value(value) {
this.setAttribute("value", value.join("+"));
}
// @type
// Array<string>
get modKeys() {
return this.value.filter(key => modKeys.includes(key));
}
// @type
// String?
get normalKey() {
let key = this.value.find(key => modKeys.includes(key) === false);
return key === undefined ? null : key;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
let displayValue = "";
let modKeys = this.modKeys;
let normalKey = this.normalKey;
if (isAppleDevice) {
if (modKeys.includes("Meta")) {
displayValue += "^";
}
if (modKeys.includes("Alt")) {
displayValue += "⌥";
}
if (modKeys.includes("Shift")) {
displayValue += "⇧";
}
if (modKeys.includes("Control")) {
displayValue += "⌘";
}
if (modKeys.includes("Symbol")) {
displayValue += "☺";
}
let mappings = {
"ArrowUp": "↑",
"ArrowDown": "↓",
"ArrowLeft": "←",
"ArrowRight": "→",
"Backspace": "⌦"
};
if (normalKey !== undefined) {
displayValue += mappings[normalKey] || normalKey;
}
}
else {
let parts = [];
if (modKeys.includes("Control")) {
parts.push("Ctrl");
}
if (modKeys.includes("Alt")) {
parts.push("Alt");
}
if (modKeys.includes("Meta")) {
parts.push("Meta");
}
if (modKeys.includes("Shift")) {
parts.push("Shift");
}
if (modKeys.includes("Symbol")) {
parts.push("Symbol");
}
let mappings = {
"ArrowUp": "Up",
"ArrowDown": "Down",
"ArrowLeft": "Left",
"ArrowRight": "Right"
};
if (normalKey !== null) {
parts.push(mappings[normalKey] || normalKey);
}
displayValue = parts.join("+");
}
this["#main"].textContent = displayValue;
}
}
customElements.define("x-shortcut", XShortcutElement);
// @copyright
// © 2016-2017 Jarosław Foksa
// @doc
// Material Design - https://material.google.com/components/sliders.html
// MacOS - https://goo.gl/KBmOG3
// Windows - https://metroui.org.ua/sliders.html
// HTML - http://thenewcode.com/757/Playing-With-The-HTML5-range-Slider-Input
// ARIA - http://w3c.github.io/aria-practices/#slider, http://w3c.github.io/aria-practices/#slidertwothumb
let getClosestMultiple = (number, step) => round(round(number / step) * step, getPrecision(step));
let shadowTemplate$24 = html`
<template>
<style>:host{display:block;width:100%;position:relative;box-sizing:border-box;--focus-ring-color: currentColor;--focus-ring-opacity: 1;--focus-ring-width: 10px;--focus-ring-transition-duration: 0.15s;--thumb-width: 20px;--thumb-height: 20px;--thumb-d: path("M 50 50 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0");--thumb-transform: none;--thumb-color: gray;--thumb-border-width: 1px;--thumb-border-color: rgba(0, 0, 0, 0.2);--tick-color: rgba(0, 0, 0, 0.4);--track-height: 2px;--track-color: gray;--track-tint-color: black}:host(:focus){outline:0}:host(:hover){cursor:default}:host([disabled]){pointer-events:none;opacity:.4}#tracks{position:absolute;width:100%;height:var(--track-height);top:calc((var(--thumb-height)/2) - var(--track-height)/2)}#tracks #normal-track{position:absolute;width:100%;height:100%;background:var(--track-color);border-radius:10px}#tracks #tint-track{position:absolute;width:0%;height:100%;background:var(--track-tint-color)}#thumbs{position:relative;width:calc(100% - var(--thumb-width));height:100%}#thumbs .thumb{position:relative;left:0;width:var(--thumb-width);height:var(--thumb-height);display:block;box-sizing:border-box;overflow:visible;transform:var(--thumb-transform);transition:transform .2s ease-in-out;will-change:transform;d:var(--thumb-d)}#thumbs .thumb .shape{d:inherit;fill:var(--thumb-color);stroke:var(--thumb-border-color);stroke-width:var(--thumb-border-width);vector-effect:non-scaling-stroke}#thumbs .thumb .focus-ring{d:inherit;fill:none;stroke:var(--focus-ring-color);stroke-width:0;opacity:var(--focus-ring-opacity);vector-effect:non-scaling-stroke;transition:stroke-width var(--focus-ring-transition-duration) cubic-bezier(.4,0,.2,1)}:host(:focus) #thumbs .thumb .focus-ring{stroke-width:var(--focus-ring-width)}#ticks{width:calc(100% - var(--thumb-width));height:5px;margin:0 0 3px;position:relative;margin-left:calc(var(--thumb-width)/2)}#ticks:empty{display:none}#ticks .tick{position:absolute;width:1px;height:100%;background:var(--tick-color)}#labels{position:relative;width:calc(100% - var(--thumb-width));height:14px;margin-left:calc(var(--thumb-width)/2);font-size:12px}:host(:empty) #labels{display:none}::slotted(x-label){position:absolute;transform:translateX(-50%)}</style>
<div id="tracks">
<div id="normal-track"></div>
<div id="tint-track"></div>
</div>
<div id="thumbs">
<svg id="start-thumb" class="thumb" viewBox="0 0 100 100" preserveAspectRatio="none" style="left: 0%;">
<path class="focus-ring"></path>
<path class="shape"></path>
</svg>
</div>
<div id="ticks"></div>
<div id="labels">
<slot></slot>
</div>
</template>
`;
// @events
// change
// changestart
// changeend
class XSliderElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$24.content, true));
this._observer = new MutationObserver((args) => this._onMutation(args));
this._updateTicks500ms = throttle(this._updateTicks, 500, this);
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this._shadowRoot.addEventListener("pointerdown", (event) => this._onShadowRootPointerDown(event));
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "value") {
this._onValueAttributeChange();
}
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
this.setAttribute("value", this.value);
this._observer.observe(this, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["value"],
characterData: false
});
this._update();
}
disconnectedCallback() {
this._observer.disconnect();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["value"];
}
// @type
// number
// @default
// 0
// @attribute
get min() {
return this.hasAttribute("min") ? parseFloat(this.getAttribute("min")) : 0;
}
set min(min) {
this.setAttribute("min", min);
}
// @type
// number
// @default
// 100
// @attribute
get max() {
return this.hasAttribute("max") ? parseFloat(this.getAttribute("max")) : 100;
}
set max(max) {
this.setAttribute("max", max);
}
// @type
// number
// @attribute
get value() {
if (this.hasAttribute("value")) {
return parseFloat(this.getAttribute("value"));
}
else {
return this.max >= this.min ? this.min + (this.max - this.min) / 2 : this.min;
}
}
set value(value) {
value = normalize(value, this.min, this.max);
this.setAttribute("value", value);
}
// @type
// number
// @default
// 1
// @attribute
get step() {
return this.hasAttribute("step") ? parseFloat(this.getAttribute("step")) : 1;
}
set step(step) {
this.setAttribute("step", step);
}
// @info
// Whether this button is disabled.
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onValueAttributeChange() {
this._updateTracks();
this._updateThumbs();
}
_onMutation(records) {
for (let record of records) {
if (record.type === "attributes" && record.target === this) {
return;
}
else {
this._updateTicks500ms();
}
}
}
_onPointerDown(event) {
// Don't focus the widget with pointer, instead focus the closest ancestor focusable element
if (this.matches(":focus") === false) {
event.preventDefault();
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
}
_onShadowRootPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0 || pointerDownEvent.isPrimary === false) {
return;
}
let containerBounds = this["#thumbs"].getBoundingClientRect();
let thumb = this["#start-thumb"];
let thumbBounds = thumb.getBoundingClientRect();
let pointerMoveListener, lostPointerCaptureListener;
let changeStarted = false;
this.setPointerCapture(pointerDownEvent.pointerId);
let updateValue = (clientX, animate) => {
let x = clientX - containerBounds.x - thumbBounds.width/2;
x = normalize(x, 0, containerBounds.width);
let value = (x / containerBounds.width) * (this.max - this.min) + this.min;
value = getClosestMultiple(value, this.step);
if (this.value !== value) {
this.value = value;
if (changeStarted === false) {
changeStarted = true;
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
}
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
};
if (event.target.closest(".thumb") !== thumb) {
updateValue(pointerDownEvent.clientX, true);
}
this.addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
if (pointerMoveEvent.isPrimary) {
updateValue(pointerMoveEvent.clientX, false);
}
});
this.addEventListener("lostpointercapture", lostPointerCaptureListener = () => {
this.removeEventListener("pointermove", pointerMoveListener);
this.removeEventListener("lostpointercapture", lostPointerCaptureListener);
if (changeStarted) {
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
}
});
}
_onKeyDown(event) {
if (event.code === "ArrowLeft" || event.code === "ArrowDown") {
event.preventDefault();
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
let oldValue = this.value;
if (event.shiftKey) {
this.value -= this.step * 10;
}
else {
this.value -= this.step;
}
if (oldValue !== this.value) {
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
}
else if (event.code === "ArrowRight" || event.code === "ArrowUp") {
event.preventDefault();
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
let oldValue = this.value;
if (event.shiftKey) {
this.value += this.step * 10;
}
else {
this.value += this.step;
}
if (oldValue !== this.value) {
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
this._updateTracks();
this._updateThumbs();
this._updateTicks();
}
_updateTracks() {
let left = (((this.value - this.min) / (this.max - this.min)) * 100);
let originLeft = (((this.min > 0 ? this.min : 0) - this.min) / (this.max - this.min)) * 100;
if (left >= originLeft) {
this["#tint-track"].style.left = `${originLeft}%`;
this["#tint-track"].style.width = (left - originLeft) + "%";
}
else {
this["#tint-track"].style.left = `${left}%`;
this["#tint-track"].style.width = `${originLeft - left}%`;
}
}
_updateThumbs(animate) {
this["#start-thumb"].style.left = (((this.value - this.min) / (this.max - this.min)) * 100) + "%";
}
async _updateTicks() {
await customElements.whenDefined("x-label");
this["#ticks"].innerHTML = "";
for (let label of this.querySelectorAll(":scope > x-label")) {
label.style.left = (((label.value - this.min) / (this.max - this.min)) * 100) + "%";
this["#ticks"].insertAdjacentHTML("beforeend", `<div class="tick" style="left: ${label.style.left}"></div>`);
}
}
}
customElements.define("x-slider", XSliderElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$25 = html`
<template>
<style>:host{display:flex;flex-flow:row;align-items:center;justify-content:center;height:100%;width:fit-content;--button-color: rgba(0, 0, 0, 0.6);--button-border-left: none;--pressed-button-color: white;--pressed-button-background: rgba(0, 0, 0, 0.3);--increment-arrow-width: 11px;--increment-arrow-height: 11px;--increment-arrow-path-d: path("M 24.381 69.236 L 49.998 43.258 L 76.104 69.128 L 68.759 76.474 L 49.995 57.708 L 31.242 76.46 L 24.381 69.236 Z" );--decrement-arrow-width: 11px;--decrement-arrow-height: 11px;--decrement-arrow-path-d: path("M 24.381 32.156 L 49.998 58.134 L 76.104 32.264 L 68.759 24.918 L 49.995 43.684 L 31.242 24.932 L 24.381 32.156 Z" )}:host(:hover){cursor:default}#decrement-button,#increment-button{display:flex;align-items:center;justify-content:center;width:100%;height:100%;user-select:none;box-sizing:border-box;color:var(--button-color);border-left:var(--button-border-left)}#decrement-button[data-pressed],#increment-button[data-pressed]{color:var(--pressed-button-color);background:var(--pressed-button-background)}:host([disabled=""]) #decrement-button,:host([disabled=""]) #increment-button,:host([disabled="decrement"]) #decrement-button,:host([disabled="increment"]) #increment-button{opacity:.3;pointer-events:none}#decrement-arrow,#increment-arrow{width:var(--increment-arrow-width);height:var(--increment-arrow-height);pointer-events:none}#decrement-arrow{width:var(--decrement-arrow-width);height:var(--decrement-arrow-height)}#increment-arrow-path{d:var(--increment-arrow-path-d);fill:currentColor}#decrement-arrow-path{d:var(--decrement-arrow-path-d);fill:currentColor}</style>
<div id="decrement-button" class="button">
<svg id="decrement-arrow" viewBox="0 0 100 100" preserveAspectRatio="none">
<path id="decrement-arrow-path"></path>
</svg>
</div>
<div id="increment-button" class="button">
<svg id="increment-arrow" viewBox="0 0 100 100" preserveAspectRatio="none">
<path id="increment-arrow-path"></path>
</svg>
</div>
</template>
`;
// @events
// increment
// incrementstart
// incrementend
// decrement
// decrementstart
// decrementend
class XStepperElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$25.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this._shadowRoot.addEventListener("pointerdown", (event) => this._onPointerDown(event));
}
attributeChangedCallback(name) {
if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["disabled"];
}
// @type
// true || false || "increment" || "decrement"
// @default
// "false"
get disabled() {
if (this.hasAttribute("disabled")) {
if (this.getAttribute("disabled") === "increment") {
return "increment";
}
else if (this.getAttribute("disabled") === "decrement") {
return "decrement";
}
else {
return true;
}
}
else {
return false;
}
}
set disabled(disabled) {
if (disabled === true) {
this.setAttribute("disabled", "");
}
else if (disabled === false) {
this.removeAttribute("disabled");
}
else {
this.setAttribute("disabled", disabled);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onDisabledAttributeChange() {
if (this.hasAttribute("disabled")) {
this["#increment-button"].removeAttribute("data-pressed");
this["#decrement-button"].removeAttribute("data-pressed");
}
}
async _onPointerDown(pointerDownEvent) {
let button = pointerDownEvent.target.closest(".button");
let action = null;
if (button === this["#increment-button"]) {
action = "increment";
}
else if (button === this["#decrement-button"]) {
action = "decrement";
}
if (pointerDownEvent.button !== 0 || action === null) {
return;
}
// Provide "pressed" attribute for theming purposes which acts like :active pseudo-class, but is guaranteed
// to last at least 100ms.
{
let pointerDownTimeStamp = Date.now();
button.setAttribute("data-pressed", "");
this.setPointerCapture(pointerDownEvent.pointerId);
this.addEventListener("lostpointercapture", async (event) => {
let pressedTime = Date.now() - pointerDownTimeStamp;
let minPressedTime = 100;
if (pressedTime < minPressedTime) {
await sleep(minPressedTime - pressedTime);
}
button.removeAttribute("data-pressed");
}, {once: true});
}
// Dispatch events
{
let intervalID = null;
let pointerDownTimeStamp = Date.now();
let {shiftKey} = pointerDownEvent;
this.dispatchEvent(new CustomEvent(action + "start", {bubbles: true}));
this.dispatchEvent(new CustomEvent(action, {bubbles: true, detail: {shiftKey}}));
this.addEventListener("lostpointercapture", async (event) => {
clearInterval(intervalID);
this.dispatchEvent(new CustomEvent(action + "end", {bubbles: true}));
}, {once: true});
intervalID = setInterval(() => {
if (Date.now() - pointerDownTimeStamp > 500) {
this.dispatchEvent(new CustomEvent(action, {bubbles: true, detail: {shiftKey}}));
}
}, 100);
}
}
}
customElements.define("x-stepper", XStepperElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$26 = html`
<template>
<style>:host{display:block;width:22px;height:22px;cursor:default;box-sizing:border-box;overflow:hidden}#main{width:100%;height:100%;position:relative}#selected-icon{display:none;position:absolute;left:calc(50% - 8px);top:calc(50% - 8px);width:16px;height:16px;color:#fff}:host([showicon]:hover) #selected-icon{display:block;opacity:.6}:host([showicon][selected]) #selected-icon{display:block;opacity:1}:host([showicon][value="#FFFFFF"]) #selected-icon{fill:gray}</style>
<main id="main">
<x-icon id="selected-icon" name="send"></x-icon>
</main>
</template>
`;
class XSwatchElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$26.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
connectedCallback() {
this._update();
}
attributeChangedCallback(name) {
if (name === "value") {
this._update();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["disabled"];
}
// @info
// Value associated with this button.
// @type
// string
// @default
// "white"
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : "white";
}
set value(value) {
this.setAttribute("value", value);
}
// @type
// boolean
// @default
// false
// @attribute
get selected() {
return this.hasAttribute("selected");
}
set selected(selected) {
selected ? this.setAttribute("selected", "") : this.removeAttribute("selected");
}
// @info
// Whether to show selection icon on hover and when the swatch is selected.
// @type
// boolean
// @default
// false
// @attribute
get showicon() {
return this.hasAttribute("showicon");
}
set showicon(showicon) {
showicon ? this.setAttribute("showicon", "") : this.removeAttribute("showicon");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
this["#main"].style.background = this.value;
}
}
customElements.define("x-swatch", XSwatchElement);
// @info
// Switch widget.
// @copyright
// © 2016-2017 Jarosław Foksa
let easing$5 = "cubic-bezier(0.4, 0, 0.2, 1)";
let shadowTemplate$27 = html`
<template>
<style>:host{display:block;width:30px;height:18px;margin:0 8px 0 0;box-sizing:border-box;display:flex;--focus-ring-color: currentColor;--focus-ring-opacity: 0.2;--focus-ring-width: 10px;--focus-ring-transition-duration: 0.15s;--ripple-type: none;--ripple-background: currentColor;--ripple-opacity: 0.2;--thumb-color: currentColor;--thumb-size: 20px;--thumb-border-radius: 999px;--track-height: 65%;--track-color: currentColor;--track-opacity: 0.5;--track-border-radius: 999px}:host([disabled]){opacity:.5;pointer-events:none}:host(:focus){outline:0}#main{width:100%;height:100%;position:relative}#track{width:100%;height:var(--track-height);background:var(--track-color);opacity:var(--track-opacity);border-radius:var(--track-border-radius)}#thumb{position:absolute;left:0;width:var(--thumb-size);height:var(--thumb-size);background:var(--thumb-color);border-radius:var(--thumb-border-radius);transition:left .2s cubic-bezier(.4,0,.2,1)}:host([toggled]) #thumb{left:calc(100% - var(--thumb-size))}:host([mixed]) #thumb{left:calc(50% - var(--thumb-size)/2)}#focus-ring{position:absolute;top:50%;left:50%;width:var(--thumb-size);height:var(--thumb-size);transform:translate(-50%,-50%);background:0 0;border:0 solid var(--focus-ring-color);border-radius:999px;opacity:var(--focus-ring-opacity);transition:border-width var(--focus-ring-transition-duration) cubic-bezier(.4,0,.2,1)}:host(:focus) #thumb #focus-ring{border-width:var(--focus-ring-width)}#ripples .ripple{position:absolute;top:50%;left:50%;width:calc(var(--thumb-size) + 22px);height:calc(var(--thumb-size) + 22px);transform:translate(-50%,-50%);background:var(--ripple-background);border-radius:999px;opacity:var(--ripple-opacity)}</style>
<x-box id="main">
<div id="track"></div>
<div id="thumb">
<div id="focus-ring"></div>
<div id="ripples"></div>
</div>
</x-box>
</template>
`;
// @events
// change
class XSwitchElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$27.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "switch");
this.setAttribute("aria-checked", this.mixed ? "mixed" : this.toggled);
this.setAttribute("aria-disabled", this.disabled);
}
attributeChangedCallback(name) {
if (name === "toggled") {
this._onToggledAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["toggled", "disabled"];
}
// @type
// boolean
// @default
// false
// @attribute
get toggled() {
return this.hasAttribute("toggled");
}
set toggled(toggled) {
toggled ? this.setAttribute("toggled", "") : this.removeAttribute("toggled");
}
// @type
// boolean
// @default
// false
// @attribute
get mixed() {
return this.hasAttribute("mixed");
}
set mixed(mixed) {
mixed ? this.setAttribute("mixed", "") : this.removeAttribute("mixed");
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onToggledAttributeChange() {
this.setAttribute("aria-checked", this.mixed ? "mixed" : this.toggled);
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
_onPointerDown(event) {
// Don't focus the widget with pointer, instead focus the closest ancestor focusable element
if (this.matches(":focus") === false) {
event.preventDefault();
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
// Ripple
{
let rippleType = getComputedStyle(this).getPropertyValue("--ripple-type").trim();
if (rippleType === "unbounded") {
let ripple = createElement("div");
ripple.setAttribute("class", "ripple");
this["#ripples"].append(ripple);
let transformAnimation = ripple.animate(
{ transform: ["translate(-50%, -50%) scale(0)", "translate(-50%, -50%) scale(1)"] },
{ duration: 200, easing: easing$5 }
);
this.setPointerCapture(event.pointerId);
this.addEventListener("lostpointercapture", async () => {
await transformAnimation.finished;
let opacityAnimation = ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 200, easing: easing$5 }
);
await opacityAnimation.finished;
ripple.remove();
}, {once: true});
}
}
}
async _onClick(event) {
// Update state
{
if (this.mixed) {
this.mixed = false;
}
else {
this.toggled = !this.toggled;
}
this.dispatchEvent(new CustomEvent("change"));
}
// Ripple
if (event.isTrusted === false) {
let rippleType = getComputedStyle(this).getPropertyValue("--ripple-type").trim();
if (rippleType === "unbounded") {
let ripple = createElement("div");
ripple.setAttribute("class", "ripple");
this["#ripples"].append(ripple);
await ripple.animate(
{ transform: ["translate(-50%, -50%) scale(0)", "translate(-50%, -50%) scale(1)"] },
{ duration: 200, easing: easing$5 }
).finished;
await ripple.animate(
{ opacity: [getComputedStyle(ripple).opacity, "0"] },
{ duration: 200, easing: easing$5 }
).finished;
ripple.remove();
}
}
}
_onKeyDown(event) {
if (event.code === "Enter" || event.code === "Space") {
event.preventDefault();
this.click();
}
}
}
customElements.define("x-switch", XSwitchElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let {max: max$6} = Math;
let easing$6 = "cubic-bezier(0.4, 0, 0.2, 1)";
let shadowTemplate$28 = html`
<template>
<style>:host{position:relative;display:flex;align-items:center;justify-content:center;height:100%;box-sizing:border-box;cursor:default;user-select:none;--menu-position: below;--trigger-effect: none;--ripple-background: currentColor;--ripple-opacity: 0.2;--arrow-width: 9px;--arrow-height: 9px;--arrow-margin: 1px 0 0 3px;--arrow-d: path("M 11.699 19.846 L 49.822 57.886 L 87.945 19.846 L 99.657 31.557 L 49.822 81.392 L -0.013 31.557 Z");--selection-indicator-height: 3px;--selection-indicator-background: white}:host(:focus){outline:0}#content{display:inherit;flex-flow:inherit;align-items:inherit;z-index:100}#arrow{width:var(--arrow-width);height:var(--arrow-height);margin:var(--arrow-margin);color:currentColor;d:var(--arrow-d)}#arrow-path{fill:currentColor;d:inherit}#ripples,#ripples .ripple,#selection-indicator{width:100%;position:absolute;left:0}#ripples{z-index:0;top:0;height:100%;overflow:hidden;pointer-events:none}#ripples .ripple{top:0;width:200px;height:200px;background:var(--ripple-background);opacity:var(--ripple-opacity);border-radius:999px;transform:none;transition:all 800ms cubic-bezier(.4,0,.2,1);will-change:opacity,transform;pointer-events:none}#selection-indicator{display:none;height:var(--selection-indicator-height);background:var(--selection-indicator-background);bottom:0}:host([selected]) #selection-indicator{display:block}:host-context([animatingindicator]) #selection-indicator{display:none}</style>
<div id="ripples"></div>
<div id="selection-indicator"></div>
<div id="content">
<slot></slot>
<svg id="arrow" viewBox="0 0 100 100" preserveAspectRatio="none" hidden>
<path id="arrow-path"></path>
</svg>
</div>
</template>
`;
class XTabElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$28.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("pointerdown", (event) => this._onPointerDown(event));
this.addEventListener("click", (event) => this._onClick(event));
}
connectedCallback() {
this.setAttribute("tabindex", this.selected ? "0" : "-1");
this.setAttribute("role", "tab");
this.setAttribute("aria-selected", this.selected);
this.setAttribute("aria-disabled", this.disabled);
this._updateArrowVisibility();
}
attributeChangedCallback(name) {
if (name === "selected") {
this._onSelectedAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["selected", "disabled"];
}
// @info
// Value associated with this tab.
// @type
// string
// @default
// ""
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : "";
}
set value(value) {
this.setAttribute("value", value);
}
// @type
// boolean
// @default
// false
// @attribute
get selected() {
return this.hasAttribute("selected");
}
set selected(selected) {
selected ? this.setAttribute("selected", "") : this.removeAttribute("selected");
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onSelectedAttributeChange() {
this.setAttribute("aria-selected", this.selected);
this.setAttribute("tabindex", this.selected ? "0" : "-1");
}
_onDisabledAttributeChange() {
this.setAttribute("aria-disabled", this.disabled);
this.setAttribute("tabindex", this.selected ? "0" : "-1");
}
async _onPointerDown(pointerDownEvent) {
// Don't focus the tab with pointer
if (this.matches(":focus") === false && !event.target.closest("x-menu, x-popup")) {
event.preventDefault();
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
if (pointerDownEvent.button !== 0 || this.querySelector("x-menu")) {
return;
}
// Provide "pressed" attribute for theming purposes
{
let pointerDownTimeStamp = Date.now();
this.setAttribute("pressed", "");
this.setPointerCapture(pointerDownEvent.pointerId);
this.addEventListener("lostpointercapture", async (event) => {
if (this.selected === true) {
let pressedTime = Date.now() - pointerDownTimeStamp;
let minPressedTime = 100;
if (pressedTime < minPressedTime) {
await sleep(minPressedTime - pressedTime);
}
}
this.removeAttribute("pressed");
}, {once: true});
}
// Ripple
{
let triggerEffect = getComputedStyle(this).getPropertyValue("--trigger-effect").trim();
if (triggerEffect === "ripple") {
let bounds = this["#ripples"].getBoundingClientRect();
let size = max$6(bounds.width, bounds.height) * 1.5;
let top = pointerDownEvent.clientY - bounds.y - size/2;
let left = pointerDownEvent.clientX - bounds.x - size/2;
let whenLostPointerCapture = new Promise((r) => this.addEventListener("lostpointercapture", r, {once: true}));
let ripple = createElement("div");
ripple.setAttribute("class", "ripple pointer-down-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
this.setPointerCapture(pointerDownEvent.pointerId);
// Workaround for tabs that that change their color when selected
ripple.hidden = true;
await sleep(10);
ripple.hidden = false;
let inAnimation = ripple.animate({ transform: ["scale(0)", "scale(1)"]}, { duration: 300, easing: easing$6 });
await whenLostPointerCapture;
await inAnimation.finished;
let fromOpacity = getComputedStyle(ripple).opacity;
let outAnimation = ripple.animate({ opacity: [fromOpacity, "0"]}, { duration: 300, easing: easing$6 });
await outAnimation.finished;
ripple.remove();
}
}
}
async _onClick(event) {
// Ripple
if (this["#ripples"].querySelector(".pointer-down-ripple") === null && !this.querySelector("x-menu")) {
let triggerEffect = getComputedStyle(this).getPropertyValue("--trigger-effect").trim();
if (triggerEffect === "ripple") {
let bounds = this["#ripples"].getBoundingClientRect();
let size = max$6(bounds.width, bounds.height) * 1.5;
let top = (bounds.y + bounds.height/2) - bounds.y - size/2;
let left = (bounds.x + bounds.width/2) - bounds.x - size/2;
let ripple = createElement("div");
ripple.setAttribute("class", "ripple click-ripple");
ripple.setAttribute("style", `width: ${size}px; height: ${size}px; top: ${top}px; left: ${left}px;`);
this["#ripples"].append(ripple);
let inAnimation = ripple.animate({ transform: ["scale(0)", "scale(1)"]}, { duration: 300, easing: easing$6 });
await inAnimation.finished;
let fromOpacity = getComputedStyle(ripple).opacity;
let outAnimation = ripple.animate({ opacity: [fromOpacity, "0"] }, { duration: 300, easing: easing$6 });
await outAnimation.finished;
ripple.remove();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_updateArrowVisibility() {
let menu = this.querySelector("x-menu");
let popover = this.querySelector("x-popover");
this["#arrow"].style.display = (menu === null && popover === null) ? "none" : null;
}
}
customElements.define("x-tab", XTabElement);
// @info
// Tabs make it easy to explore and switch between different views.
// @doc
// http://w3c.github.io/aria-practices/#tabpanel
// http://accessibility.athena-ict.com/aria/examples/tabpanel2.shtml
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$29 = html`
<template>
<style>:host{position:relative;display:flex;width:100%;height:100%;box-sizing:border-box;justify-content:flex-start}:host([centered]){margin:0 auto;justify-content:center}:host([centered]) ::slotted(x-tab){flex:0}#selection-indicator{position:absolute;width:100%;height:fit-content;bottom:0;left:0;pointer-events:none}</style>
<slot></slot>
<div id="selection-indicator" hidden></div>
</template>
`;
// @events
// change
class XTabsElement extends HTMLElement {
constructor() {
super();
this._wasFocusedBeforeExpanding = false;
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$29.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this["#overlay"] = createElement("x-overlay");
this["#overlay"].style.background = "rgba(0, 0, 0, 0)";
this.addEventListener("click", (event) => this._onClick(event));
this.addEventListener("keydown", (event) => this._onKeyDown(event));
}
connectedCallback() {
this.setAttribute("role", "tablist");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// string?
// @default
// null
get value() {
let selectedTab = this.querySelector("x-tab[selected]");
return selectedTab ? selectedTab.value : null;
}
set value(value) {
let tabs = [...this.querySelectorAll("x-tab")];
let selectedTab = (value === null) ? null : tabs.find(tab => tab.value === value);
for (let tab of tabs) {
tab.selected = (tab === selectedTab);
}
}
// @property
// reflected
// @type
// boolean
// @default
// false
get centered() {
return this.hasAttribute("centered");
}
set centered(centered) {
centered === true ? this.setAttribute("centered", "") : this.removeAttribute("centered");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onClick(event) {
if (event.button !== 0) {
return;
}
if (event.target.closest("x-overlay")) {
this._collapse();
}
else if (event.target.closest("x-menu")) {
let clickedMenuItem = event.target.closest("x-menuitem");
if (clickedMenuItem && clickedMenuItem.disabled === false) {
let submenu = clickedMenuItem.querySelector("x-menu");
if (submenu) {
if (submenu.opened) {
submenu.close();
}
else {
submenu.openNextToElement(clickedMenuItem, "horizontal");
}
}
else {
this._collapse(clickedMenuItem.whenTriggerEnd);
}
}
}
else if (event.target.closest("x-tab")) {
let tabs = this.querySelectorAll("x-tab");
let clickedTab = event.target.closest("x-tab");
let selectedTab = this.querySelector("x-tab[selected]");
let submenu = clickedTab.querySelector(":scope > x-menu");
if (clickedTab !== selectedTab) {
// Open a popup menu
if (submenu) {
this._expand(clickedTab);
}
// Select the tab
else {
for (let tab of tabs) {
tab.selected = (tab === clickedTab);
}
this._animateSelectionIndicator(selectedTab, clickedTab);
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
}
}
}
_onKeyDown(event) {
if (event.code === "Enter" || event.code === "Space") {
let tab = event.target;
let menu = tab.querySelector("x-menu");
let label = tab.querySelector("x-label");
if (menu) {
if (menu.opened) {
this._collapse();
event.preventDefault();
}
else {
this._expand(tab);
event.preventDefault();
}
}
else {
event.preventDefault();
tab.click();
}
}
else if (event.code === "Escape") {
let tab = event.target.closest("x-tab");
let menu = tab.querySelector("x-menu");
if (menu) {
this._collapse();
}
}
else if (event.code === "ArrowLeft") {
let tabs = [...this.querySelectorAll("x-tab:not([disabled])")];
let currentTab = this.querySelector(`x-tab[tabindex="0"]`);
let openedTabMenu = this.querySelector("x-menu[opened]");
event.preventDefault();
if (openedTabMenu) {
}
else if (currentTab && tabs.length > 0) {
let currentTabIndex = tabs.indexOf(currentTab);
let previousTab = tabs[currentTabIndex - 1] || tabs[tabs.length - 1];
currentTab.tabIndex = -1;
previousTab.tabIndex = 0;
previousTab.focus();
}
}
else if (event.code === "ArrowRight") {
let tabs = [...this.querySelectorAll("x-tab:not([disabled])")];
let currentTab = this.querySelector(`x-tab[tabindex="0"]`);
let openedTabMenu = this.querySelector("x-menu[opened]");
event.preventDefault();
if (openedTabMenu) {
}
else if (currentTab && tabs.length > 0) {
let currentTabIndex = tabs.indexOf(currentTab);
let nextTab = tabs[currentTabIndex + 1] || tabs[0];
currentTab.tabIndex = -1;
nextTab.tabIndex = 0;
nextTab.focus();
}
}
else if (event.code === "ArrowUp") {
let tab = event.target.closest("x-tab");
let menu = tab.querySelector("x-menu");
if (menu) {
event.preventDefault();
if (menu.opened) {
let lastMenuItem = menu.querySelector(":scope > x-menuitem:last-of-type:not([disabled])");
if (lastMenuItem) {
lastMenuItem.focus();
}
}
else {
this._expand(tab);
}
}
}
else if (event.code === "ArrowDown") {
let tab = event.target.closest("x-tab");
let menu = tab.querySelector("x-menu");
if (menu) {
event.preventDefault();
if (menu.opened) {
let firstMenuItem = menu.querySelector(":scope > x-menuitem:not([disabled])");
if (firstMenuItem) {
firstMenuItem.focus();
}
}
else {
this._expand(tab);
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Expands given tab by opening its menu.
_expand(tab) {
return new Promise( async (resolve) => {
let menu = tab.querySelector(":scope > x-menu");
let label = tab.querySelector("x-label");
if (menu) {
this._wasFocusedBeforeExpanding = this.querySelector("*:focus") !== null;
let over = getComputedStyle(tab).getPropertyValue("--menu-position").trim() === "over";
let whenOpened = over ? menu.openOverLabel(label) : menu.openNextToElement(tab, "vertical", 3);
tab.setAttribute("expanded", "");
// When menu closes, focus the tab
menu.addEventListener("close", () => {
let tabs = this.querySelectorAll("x-tab");
let closedTab = tab;
if (this._wasFocusedBeforeExpanding) {
for (let tab of tabs) {
tab.tabIndex = (tab === closedTab ? 0 : -1);
}
closedTab.focus();
}
else {
for (let tab of tabs) {
tab.tabIndex = (tab.selected ? 0 : -1);
}
let ancestorFocusableElement = closest(this.parentNode, "[tabindex]");
if (ancestorFocusableElement) {
ancestorFocusableElement.focus();
}
}
}, {once: true});
await whenOpened;
if (!tab.querySelector("*:focus")) {
menu.focus();
}
this["#overlay"].ownerElement = menu;
this["#overlay"].show(false);
}
resolve();
});
}
// @info
// Collapses currently expanded tab by closing its menu.
_collapse(delay) {
return new Promise( async (resolve) => {
let menu = this.querySelector("x-menu[opened]");
if (menu && !menu.hasAttribute("closing")) {
let tabs = this.querySelectorAll("x-tab");
let closedTab = menu.closest("x-tab");
menu.setAttribute("closing", "");
await delay;
await menu.close();
this["#overlay"].hide(false);
menu.removeAttribute("closing");
closedTab.removeAttribute("expanded");
}
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_animateSelectionIndicator(startTab, endTab) {
return new Promise( async (resolve) => {
let mainBBox = this.getBoundingClientRect();
let startBBox = startTab ? startTab.getBoundingClientRect() : null;
let endBBox = endTab.getBoundingClientRect();
let computedStyle = getComputedStyle(endTab);
if (startBBox === null) {
startBBox = DOMRect.fromRect(endBBox);
startBBox.x += startBBox.width / 2;
startBBox.width = 0;
}
this["#selection-indicator"].style.height = computedStyle.getPropertyValue("--selection-indicator-height");
if (this["#selection-indicator"].style.height !== "0px") {
this["#selection-indicator"].style.background = computedStyle.getPropertyValue("--selection-indicator-background");
this["#selection-indicator"].hidden = false;
this.setAttribute("animatingindicator", "");
let animation = this["#selection-indicator"].animate(
[
{
bottom: (startBBox.bottom - mainBBox.bottom) + "px",
left: (startBBox.left - mainBBox.left) + "px",
width: startBBox.width + "px",
},
{
bottom: (endBBox.bottom - mainBBox.bottom) + "px",
left: (endBBox.left - mainBBox.left) + "px",
width: endBBox.width + "px",
}
],
{
duration: 100,
iterations: 1,
delay: 0,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)"
}
);
await animation.finished;
this["#selection-indicator"].hidden = true;
this.removeAttribute("animatingindicator");
}
resolve();
});
}
}
customElements.define("x-tabs", XTabsElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$30 = html`
<template>
<style>:host{display:flex;align-items:center;position:relative;box-sizing:border-box;min-height:24px;background:#fff;border:1px solid #bfbfbf;font-size:12px;--close-button-path-d: path("M 25 16 L 50 41 L 75 16 L 84 25 L 59 50 L 84 75 L 75 84 L 50 59 L 25 84 L 16 75 L 41 50 L 16 25 Z");--selection-color: currentColor;--selection-background: #B2D7FD;--tag-background: rgba(0, 0, 0, 0.04);--tag-border: 1px solid #cccccc;--tag-color: currentColor}:host(:focus){outline:1px solid #00f}:host([invalid]){--selection-color: white;--selection-background: #d50000}:host([disabled]){pointer-events:none;opacity:.5}:host([hidden]){display:none}::selection{color:var(--selection-color);background:var(--selection-background)}#items,#main{display:flex;flex-wrap:wrap}#main{width:100%;height:100%;min-height:inherit;justify-content:flex-start;align-items:flex-start;align-content:flex-start;cursor:text}#items{padding:2px}.item{height:100%;margin:2px;padding:0 3px 0 6px;display:flex;line-height:1.2;align-items:center;justify-content:center;background:var(--tag-background);border:var(--tag-border);color:var(--tag-color);font-size:inherit;cursor:default;user-select:none}.item#editable-item{color:inherit;outline:0;background:0 0;border:1px solid transparent;flex-grow:1;align-items:center;justify-content:flex-start;white-space:pre;cursor:text;user-select:text}.item .close-button{color:inherit;opacity:.8;width:11px;height:11px;vertical-align:middle;margin-left:4px}.item .close-button:hover{background:rgba(0,0,0,.1);opacity:1}.item .close-button-path{fill:currentColor;d:var(--close-button-path-d)}</style>
<main id="main">
<div id="items">
<span id="editable-item" class="item" spellcheck="false"></span>
</div>
<slot></slot>
</main>
</template>
`;
// @events
// input
// change
// textinputmodestart
// textinputmodeend
class XTagInputElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed", delegatesFocus: true});
this._shadowRoot.append(document.importNode(shadowTemplate$30.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("focusin", () => this._onFocusIn());
this.addEventListener("focusout", () => this._onFocusOut());
this._shadowRoot.addEventListener("pointerdown", (event) => this._onShadowRootPointerDown(event));
this._shadowRoot.addEventListener("click", (event) => this._onShadowRootClick(event));
this["#editable-item"].addEventListener("keydown", (event) => this._onInputKeyDown(event));
this["#editable-item"].addEventListener("input", () => this._onInputInput());
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "input");
this.setAttribute("aria-disabled", this.disabled);
this._update();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "value") {
this._onValueAttributeChange();
}
else if (name === "spellcheck") {
this._onSpellcheckAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
// @info
// Override this method if you want the entered tags to match specific criteria.
// @type
// (string) => boolean
validateTag(tag) {
return true;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["value", "spellcheck", "disabled"];
}
// @type
// Array<string>
// @default
// []
// @attribute
get value() {
if (this.hasAttribute("value")) {
return this.getAttribute("value").split(this.delimiter).map($0 => $0.trim()).filter($0 => $0 !== "");
}
else {
return [];
}
}
set value(value) {
if (value.length === 0) {
this.removeAttribute("value");
}
else {
this.setAttribute("value", value.join(this.delimiter));
}
}
// @type
// string
get delimiter() {
return this.hasAttribute("delimiter") ? this.getAttribute("delimiter") : ",";
}
set delimiter(delimiter) {
this.setAttribute("delimiter", delimiter);
}
// @type
// boolean
// @default
// false
// @attribute
get spellcheck() {
return this.hasAttribute("spellcheck");
}
set spellcheck(spellcheck) {
spellcheck ? this.setAttribute("spellcheck", "") : this.removeAttribute("spellcheck");
}
// @type
// string
get prefix() {
return this.hasAttribute("prefix") ? this.getAttribute("prefix") : "";
}
set prefix(prefix) {
prefix === "" ? this.removeAttribute("prefix") : this.setAttribute("prefix", prefix);
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onValueAttributeChange() {
this._update();
}
_onSpellcheckAttributeChange() {
this["#editable-item"].spellcheck = this.spellcheck;
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onFocusIn() {
this.dispatchEvent(new CustomEvent("textinputmodestart", {bubbles: true, composed: true}));
}
_onFocusOut() {
this._commitInput();
this["#editable-item"].removeAttribute("contenteditable");
this.dispatchEvent(new CustomEvent("textinputmodeend", {bubbles: true, composed: true}));
if (this.hasAttribute("invalid")) {
this["#editable-item"].textContent = "";
this.removeAttribute("invalid");
}
}
_onShadowRootPointerDown(event) {
if (event.target === this["#main"] || event.target === this["#items"]) {
event.preventDefault();
this["#editable-item"].setAttribute("contenteditable", "");
let range = new Range();
range.selectNodeContents(this["#editable-item"]);
range.collapse(false);
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
else if (event.target.matches(`.item, .item > *`)) {
let item = event.target.closest(".item");
let closeButton = event.target.closest(".close-button");
if (item !== this["#editable-item"] && !closeButton) {
event.preventDefault();
event.stopPropagation();
this["#editable-item"].focus();
this._commitInput();
}
}
}
_onShadowRootClick(event) {
if (event.target.closest(".close-button")) {
this._onCloseButtonClick(event);
}
}
_onCloseButtonClick(event) {
let item = event.target.closest(".item");
this.value = this.value.filter(tag => tag !== item.getAttribute("data-tag"));
this.dispatchEvent(new CustomEvent("change"));
}
_onInputKeyDown(event) {
if (event.key === "Enter") {
event.preventDefault();
this._commitInput();
}
else if (event.key === "Backspace") {
let value = this["#editable-item"].textContent;
if (value.length === 0) {
this.value = this.value.slice(0, this.value.length - 1);
this.dispatchEvent(new CustomEvent("change"));
}
}
}
_onInputInput() {
let value = this["#editable-item"].textContent;
if (value.includes(this.delimiter)) {
this._commitInput();
}
this._updatePlaceholderVisibility();
if (this.hasAttribute("invalid")) {
this._updateValidityState();
}
this.dispatchEvent(new CustomEvent("input"));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_commitInput() {
this._updateValidityState();
if (this.hasAttribute("invalid") === false) {
let tag = this["#editable-item"].textContent.trim();
this["#editable-item"].textContent = "";
if (tag.length > 0) {
if (this.value.includes(tag) === false) {
let value = this.value.filter($0 => $0 !== tag);
this.value = [...value, tag];
this.dispatchEvent(new CustomEvent("change"));
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
for (let item of [...this["#items"].children]) {
if (item !== this["#editable-item"]) {
item.remove();
}
}
for (let tag of this.value) {
this["#editable-item"].insertAdjacentHTML("beforebegin", `
<div class="item" data-tag="${tag}">
<label>${this.prefix}${tag}</label>
<svg class="close-button" viewBox="0 0 100 100"><path class="close-button-path"></path></svg>
</div>
`);
}
this._updatePlaceholderVisibility();
}
_updateValidityState() {
let tag = this["#editable-item"].textContent.trim();
if (this.validateTag(tag) === true || tag.length === 0) {
this.removeAttribute("invalid");
}
else {
this.setAttribute("invalid", "");
}
}
_updatePlaceholderVisibility() {
let placeholder = this.querySelector(":scope > x-label");
if (placeholder) {
placeholder.hidden = (this.value.length > 0 || this["#editable-item"].textContent.length > 0);
}
}
}
customElements.define("x-taginput", XTagInputElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$31 = html`
<template>
<style>:host{display:block;position:relative;width:100%;min-height:100px;box-sizing:border-box;background:#fff;color:#000;--selection-color: currentColor;--selection-background: #B2D7FD;--inner-padding: 0}:host(:hover){cursor:text}:host([invalid]){--selection-color: white;--selection-background: #d50000}:host([disabled]){pointer-events:none;opacity:.5}:host([hidden]){display:none}::selection{color:var(--selection-color);background:var(--selection-background)}::-webkit-scrollbar{max-width:6px;max-height:6px;background:0 0}::-webkit-scrollbar-track{border-radius:25px}::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.2);border-radius:25px}::-webkit-scrollbar-corner{display:none}#main{display:flex;flex-flow:column;height:100%;min-height:inherit}#editor{flex:1;padding:var(--inner-padding);box-sizing:border-box;color:inherit;background:0 0;border:0;outline:0;font-family:inherit;font-size:inherit;overflow:auto}:host([invalid])::before{position:absolute;left:0;bottom:-20px;box-sizing:border-box;color:#d50000;font-family:inherit;font-size:11px;line-height:1.2;white-space:pre;content:attr(invalid-hint) " "}</style>
<main id="main">
<slot></slot>
<div id="editor" contenteditable="plaintext-only" spellcheck="false"></div>
</main>
</template>
`;
// @events
// input
// change
// textinputmodestart
// textinputmodeend
class XTextareaElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed", delegatesFocus: true});
this._shadowRoot.append(document.importNode(shadowTemplate$31.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this.addEventListener("focusin", () => this._onFocusIn());
this.addEventListener("focusout", () => this._onFocusOut());
this["#editor"].addEventListener("click", (event) => this._onEditorClick(event));
this["#editor"].addEventListener("input", () => this._onEditorInput());
}
connectedCallback() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("role", "input");
this.setAttribute("aria-disabled", this.disabled);
this._updateEmptyState();
}
attributeChangedCallback(name) {
if (name === "value") {
this._onValueAttributeChange();
}
else if (name === "spellcheck") {
this._onSpellcheckAttributeChange();
}
else if (name === "disabled") {
this._onDisabledAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["value", "spellcheck", "disabled"];
}
// @type
// string
// @default
// ""
// @attribute
get value() {
return this["#editor"].textContent;
}
set value(value) {
this["#editor"].textContent = value;
}
// @type
// boolean
// @default
// false
// @attribute
get spellcheck() {
return this.hasAttribute("spellcheck");
}
set spellcheck(spellcheck) {
spellcheck ? this.setAttribute("spellcheck", "") : this.removeAttribute("spellcheck");
}
// @type
// number
// @default
// 0
// @attribute
get minLength() {
return this.hasAttribute("minlength") ? parseInt(this.getAttribute("minlength")) : 0;
}
set minLength(minLength) {
this.setAttribute("minlength", minLength);
}
// @type
// number || Infinity
// @default
// 0
// @attribute
get maxLength() {
return this.hasAttribute("maxlength") ? parseInt(this.getAttribute("maxlength")) : Infinity;
}
set maxLength(maxLength) {
this.setAttribute("maxlength", maxLength);
}
// @type
// boolean
// @default
// false
// @attribute
get required() {
return this.hasAttribute("required");
}
set required(required) {
required ? this.setAttribute("required", "") : this.removeAttribute("required");
}
// @info
// Validation hints are not shown unless user focuses the element for the first time. Set this attribute to
// true to show the hints immediately.
// @type
// boolean
// @default
// false
// @attribute
get visited() {
return this.hasAttribute("visited");
}
set visited(visited) {
visited ? this.setAttribute("visited", "") : this.removeAttribute("visited");
}
// @info
// Whether the current value is valid.
// @type
// boolean
// @readOnly
get invalid() {
return this.hasAttribute("invalid");
}
// @type
// boolean
// @default
// false
// @attribute
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Override this method to validate the input value manually.
// @type
// {valid:boolean, hint:string}
validate() {
let valid = true;
let hint = "";
if (this.value.length < this.minLength) {
valid = false;
hint = "Entered text is too short";
}
else if (this.value.length > this.maxLength) {
valid = false;
hint = "Entered text is too long";
}
else if (this.required && this.value.length === 0 && this.visited === true) {
valid = false;
hint = "This field is required";
}
return {valid, hint};
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onValueAttributeChange() {
this.value = this.hasAttribute("value") ? this.getAttribute("value") : "";
if (this.matches(":focus")) {
document.execCommand("selectAll");
}
}
_onSpellcheckAttributeChange() {
this["#editor"].spellcheck = this.spellcheck;
}
_onDisabledAttributeChange() {
this.setAttribute("tabindex", this.disabled ? "-1" : "0");
this.setAttribute("aria-disabled", this.disabled);
this["#editor"].disabled = this.disabled;
}
_onEditorClick(event) {
if (event.detail >= 4) {
document.execCommand("selectAll");
}
}
_onEditorInput(event) {
this.dispatchEvent(new CustomEvent("input", {bubbles: true}));
this._updateEmptyState();
if (this.invalid) {
this._updateValidityState();
}
}
_onFocusIn() {
this.visited = true;
this._focusInValue = this.value;
this.dispatchEvent(new CustomEvent("textinputmodestart", {bubbles: true, composed: true}));
}
_onFocusOut() {
this.dispatchEvent(new CustomEvent("textinputmodeend", {bubbles: true, composed: true}));
this._shadowRoot.getSelection().collapse(this["#main"]);
this._updateValidityState();
if (this.invalid === false && this.value !== this._focusInValue) {
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_updateValidityState() {
let {valid, hint} = this.validate();
if (valid) {
this.removeAttribute("invalid");
this.removeAttribute("invalid-hint");
}
else {
this.setAttribute("invalid", "");
this.setAttribute("invalid-hint", hint);
}
}
_updateEmptyState() {
if (this.value.length === 0) {
this.setAttribute("empty", "");
}
else {
this.removeAttribute("empty");
}
}
}
customElements.define("x-textarea", XTextareaElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$32 = html`
<template>
<style>:host{display:block;width:30px;height:30px;box-sizing:border-box}:host([type="ring"]){color:#4285f4}:host([type="spin"]){color:#404040}#main,svg{width:100%;height:100%}svg{color:inherit}</style>
<main id="main"></main>
</template>
`;
class XThrobberElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$32.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
connectedCallback() {
this._update();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "type") {
this._update();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static get observedAttributes() {
return ["type"];
}
// @type
// "ring" || "spin"
// @default
// "ring"
// @attribute
get type() {
return this.hasAttribute("type") ? this.getAttribute("type") : "ring";
}
set type(type) {
this.setAttribute("type", type);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async _update() {
let response = await fetch(`node_modules/xel/images/${this.type}-throbber.svg`);
let artworkSVG = await response.text();
this["#main"].innerHTML = artworkSVG;
if (this.hasAttribute("type") === false) {
this.setAttribute("type", this.type);
}
}
}
customElements.define("x-throbber", XThrobberElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let {PI: PI$2, sqrt: sqrt$2, atan2: atan2$1, sin, cos, pow: pow$2} = Math;
let debug$4 = false;
let shadowHTML$2 = `
<style>:host{display:block;width:100%;user-select:none}:host([hidden]){display:none}#huesat-slider{display:flex;position:relative;width:100%;height:auto;touch-action:pinch-zoom}#huesat-image{width:100%;height:100%;border-radius:999px;pointer-events:none}#huesat-marker{position:absolute;top:0%;left:0%;width:var(--marker-size);height:var(--marker-size);transform:translate(calc(var(--marker-size)/-2),calc(var(--marker-size)/-2));box-sizing:border-box;background:rgba(0,0,0,.3);border:3px solid #fff;border-radius:999px;box-shadow:0 0 3px #000;--marker-size: 20px}#value-slider{width:100%;height:28px;margin-top:10px;padding:0 calc(var(--marker-width)/2);box-sizing:border-box;border:1px solid #cecece;border-radius:2px;touch-action:pan-y;--marker-width: 18px}#value-slider-track{width:100%;height:100%;position:relative;display:flex;align-items:center}#value-slider-marker{left:0%;background:rgba(0,0,0,.2);box-shadow:0 0 3px #000;box-sizing:border-box;transform:translateX(calc((var(--marker-width)/2)*-1));border:3px solid #fff;width:var(--marker-width);height:32px;position:absolute}#alpha-slider{display:none;width:100%;height:28px;margin-top:14px;padding:0 calc(var(--marker-width)/2);box-sizing:border-box;border:1px solid #cecece;border-radius:2px;touch-action:pan-y;--marker-width: 18px}:host([alphaslider]) #alpha-slider{display:block}#alpha-slider-track{width:100%;height:100%;position:relative;display:flex;align-items:center}#alpha-slider-marker{left:0%;background:rgba(0,0,0,.2);box-shadow:0 0 3px #000;box-sizing:border-box;transform:translateX(calc((var(--marker-width)/2)*-1));border:3px solid #fff;width:var(--marker-width);height:32px;position:absolute}</style>
<x-box vertical>
<div id="huesat-slider">
<img id="huesat-image" src="node_modules/xel/images/wheel-spectrum.png"></img>
<div id="huesat-marker"></div>
</div>
<div id="value-slider">
<div id="value-slider-track">
<div id="value-slider-marker"></div>
</div>
</div>
<div id="alpha-slider">
<div id="alpha-slider-track">
<div id="alpha-slider-marker"></div>
</div>
</div>
</x-box>
`;
// @events
// change
// changestart
// changeend
class XWheelColorPickerElement extends HTMLElement {
static get observedAttributes() {
return ["value"];
}
constructor() {
super();
// Note that HSVA color model is used only internally
this._h = 0; // Hue (0 ~ 360)
this._s = 0; // Saturation (0 ~ 100)
this._v = 100; // Value (0 ~ 100)
this._a = 1; // Alpha (0 ~ 1)
this._isDraggingHuesatMarker = false;
this._isDraggingValueSliderMarker = false;
this._isDraggingAlphaSliderMarker = false;
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.innerHTML = shadowHTML$2;
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
this["#huesat-slider"].addEventListener("pointerdown", (event) => this._onHuesatSliderPointerDown(event));
this["#value-slider"].addEventListener("pointerdown", (event) => this._onValueSliderPointerDown(event));
this["#alpha-slider"].addEventListener("pointerdown", (event) => this._onAlphaSliderPointerDown(event));
}
connectedCallback() {
this._update();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
else if (name === "value") {
this._onValueAttributeChange();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// string
// @default
// "hsla(0, 0%, 100%, 1)"
// @attribute
get value() {
return this.hasAttribute("value") ? this.getAttribute("value") : "hsla(0, 0%, 100%, 1)";
}
set value(value) {
this.setAttribute("value", value);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_onValueAttributeChange() {
if (
this._isDraggingHuesatMarker === false &&
this._isDraggingValueSliderMarker === false &&
this._isDraggingAlphaSliderMarker === false
) {
let [h, s, v, a] = parseColor(this.value, "hsva");
this._h = h;
this._s = s;
this._v = v;
this._a = a;
this._update();
}
if (debug$4) {
console.log(`%c ${this.value}`, `background: ${this.value};`);
}
}
_onHuesatSliderPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return;
}
let pointerMoveListener, lostPointerCaptureListener;
let wheelBounds = this["#huesat-slider"].getBoundingClientRect();
this._isDraggingHuesatMarker = true;
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
this["#huesat-slider"].style.cursor = "default";
this["#huesat-slider"].setPointerCapture(pointerDownEvent.pointerId);
let onPointerMove = (clientX, clientY) => {
let radius = wheelBounds.width / 2;
let x = clientX - wheelBounds.left - radius;
let y = clientY - wheelBounds.top - radius;
let d = pow$2(x, 2) + pow$2(y, 2);
let theta = atan2$1(y, x);
if (d > pow$2(radius, 2)) {
x = radius * cos(theta);
y = radius * sin(theta);
d = pow$2(x, 2) + pow$2(y, 2);
theta = atan2$1(y, x);
}
this._h = round(((theta + PI$2) / (PI$2 * 2)) * 360, 3);
this._s = round((sqrt$2(d) / radius) * 100, 3);
this.value = serializeColor([this._h, this._s, this._v, this._a], "hsva", "hsla");
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
this._updateHuesatMarker();
this._updateValueSliderBackground();
this._updateAlphaSliderBackground();
};
onPointerMove(pointerDownEvent.clientX, pointerDownEvent.clientY);
this["#huesat-slider"].addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
onPointerMove(pointerMoveEvent.clientX, pointerMoveEvent.clientY);
});
this["#huesat-slider"].addEventListener("lostpointercapture", lostPointerCaptureListener = (event) => {
this["#huesat-slider"].removeEventListener("pointermove", pointerMoveListener);
this["#huesat-slider"].removeEventListener("lostpointercapture", lostPointerCaptureListener);
this["#huesat-slider"].style.cursor = null;
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
this._isDraggingHuesatMarker = false;
});
}
_onValueSliderPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return;
}
let trackBounds = this["#value-slider-track"].getBoundingClientRect();
let pointerMoveListener, lostPointerCaptureListener;
this._isDraggingValueSliderMarker = true;
this["#value-slider"].style.cursor = "default";
this["#value-slider"].setPointerCapture(pointerDownEvent.pointerId);
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
let onPointerMove = (clientX) => {
let v = 100 - ((clientX - trackBounds.x) / trackBounds.width) * 100;
v = normalize(v, 0, 100, 2);
if (v !== this._v) {
this._v = v;
this.value = serializeColor([this._h, this._s, this._v, this._a], "hsva", "hsla");
this._updateValueSliderMarker();
this._updateAlphaSliderBackground();
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
};
onPointerMove(pointerDownEvent.clientX);
this["#value-slider"].addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
onPointerMove(pointerMoveEvent.clientX);
});
this["#value-slider"].addEventListener("lostpointercapture", lostPointerCaptureListener = () => {
this["#value-slider"].removeEventListener("pointermove", pointerMoveListener);
this["#value-slider"].removeEventListener("lostpointercapture", lostPointerCaptureListener);
this["#value-slider"].style.cursor = null;
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
this._isDraggingValueSliderMarker = false;
});
}
_onAlphaSliderPointerDown(pointerDownEvent) {
if (pointerDownEvent.button !== 0) {
return;
}
let trackBounds = this["#alpha-slider-track"].getBoundingClientRect();
let pointerMoveListener, lostPointerCaptureListener;
this._isDraggingAlphaSliderMarker = true;
this["#alpha-slider"].style.cursor = "default";
this["#alpha-slider"].setPointerCapture(pointerDownEvent.pointerId);
this.dispatchEvent(new CustomEvent("changestart", {bubbles: true}));
let onPointerMove = (clientX) => {
let a = 1 - ((clientX - trackBounds.x) / trackBounds.width);
a = normalize(a, 0, 1, 2);
if (a !== this._a) {
this._a = a;
this.value = serializeColor([this._h, this._s, this._v, this._a], "hsva", "hsla");
this._updateAlphaSliderMarker();
this.dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
};
onPointerMove(pointerDownEvent.clientX);
this["#alpha-slider"].addEventListener("pointermove", pointerMoveListener = (pointerMoveEvent) => {
onPointerMove(pointerMoveEvent.clientX);
});
this["#alpha-slider"].addEventListener("lostpointercapture", lostPointerCaptureListener = () => {
this["#alpha-slider"].removeEventListener("pointermove", pointerMoveListener);
this["#alpha-slider"].removeEventListener("lostpointercapture", lostPointerCaptureListener);
this["#alpha-slider"].style.cursor = null;
this.dispatchEvent(new CustomEvent("changeend", {bubbles: true}));
this._isDraggingAlphaSliderMarker = false;
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
this._updateHuesatMarker();
this._updateValueSliderMarker();
this._updateValueSliderBackground();
this._updateAlphaSliderMarker();
this._updateAlphaSliderBackground();
}
_updateHuesatMarker() {
let h = this._h;
let s = this._s;
let wheelSize = 100;
let angle = degToRad(h);
let radius = (s / 100) * wheelSize/2;
let centerPoint = {x: wheelSize/2, y: wheelSize/2};
let x = ((wheelSize - (centerPoint.x + (radius * cos(angle)))) / wheelSize) * 100;
let y = ((centerPoint.y - (radius * sin(angle))) / wheelSize) * 100;
this["#huesat-marker"].style.left = x + "%";
this["#huesat-marker"].style.top = y + "%";
}
_updateValueSliderMarker() {
this["#value-slider-marker"].style.left = (100 - normalize(this._v, 0, 100, 2)) + "%";
}
_updateValueSliderBackground() {
let gradientBackground = "linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1))";
let solidBackground = serializeColor([this._h, this._s, 100, 1], "hsva", "hex");
this["#value-slider"].style.background = `${gradientBackground}, ${solidBackground}`;
}
_updateAlphaSliderMarker() {
this["#alpha-slider-marker"].style.left = normalize((1 - this._a) * 100, 0, 100, 2) + "%";
}
_updateAlphaSliderBackground() {
let [r, g, b] = hsvToRgb(this._h, this._s, this._v).map($0 => round($0, 0));
let backroundA = `url(node_modules/xel/images/checkboard.png) repeat 0 0`;
let background = `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 1), rgba(${r}, ${g}, ${b}, 0))`;
this["#alpha-slider"].style.background = background + "," + backroundA;
}
}
customElements.define("x-wheelcolorpicker", XWheelColorPickerElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let theme = document.querySelector('link[href*=".theme.css"]').getAttribute("href");
let colorSchemesByTheme = {
material: {},
macos: {
blue: "hsl(211, 96.7%, 52.9%)",
green: "hsl(88, 35%, 46%)",
red: "hsl(344, 65%, 45%)",
purple: "hsl(290, 40%, 46%)",
yellowgreen: "hsl(61, 28%, 45%)"
},
vanilla: {
blue: "hsl(211, 86%, 57%)",
green: "hsl(88, 35%, 46%)",
red: "hsl(344, 65%, 45%)",
purple: "hsl(290, 40%, 46%)",
yellowgreen: "hsl(61, 28%, 45%)"
},
};
let shadowTemplate$33 = html`
<template>
<link rel="stylesheet" href="${theme}">
<style>:host{width:100%;height:100%;display:block}#main,#sidebar{position:relative}#main{display:flex;flex-flow:row;height:100%;width:100%}#sidebar{width:270px;overflow:auto;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);z-index:100}#sidebar #header{padding:20px 0}#sidebar #header+hr{margin-top:-1px}#sidebar h1{margin:0 22px 0 104px;line-height:1}#sidebar #nav{margin-bottom:20px;width:100%}#sidebar #nav .external-link-icon{margin:0;width:20px;height:20px}#sidebar #nav x-button{width:calc(100% + 60px);margin-left:-30px;padding:8px 30px;--ripple-background: white}#sidebar #nav x-button x-label{font-size:15px}#hide-sidebar-button,#show-sidebar-button{position:absolute;top:18px;left:11px;padding:0;width:32px;height:32px;min-height:32px}#show-sidebar-button{top:20px;z-index:10}#theme-section{padding:10px 0}#theme-section #theme-heading,#views #faq-view h4{margin-top:0}#theme-section x-select{width:100%}#theme-section #theme-select{margin-bottom:14px}#views,#views>.view{display:block;width:100%;height:100%}#views{min-width:20px;min-height:20px;position:relative;flex:1}#views>.view{box-sizing:border-box;overflow:auto}#views>.view:not([selected]){display:none!important}#views #about-view,#views>.view>article{padding:0 70px;margin:0 auto;max-width:780px;box-sizing:border-box}#views section{margin-bottom:35px}#views section[data-last-visible]+hr,#views section[hidden]+hr{display:none}#views section h3,#views section h4,#views section h5{position:relative}#views #about-view{color:#fff;width:100%;height:100vh;display:flex;align-items:center;padding:0 100px;margin:0;max-width:none}#about-view h1{font-size:170px;font-weight:700;margin:0 0 50px;padding:0;line-height:1}@media screen and (max-width:880px){#about-view h1{font-size:120px}}#about-view h2{font-size:27px;font-weight:400;line-height:1.05;color:rgba(255,255,255,.8);margin:0 0 20px;text-transform:none}#about-view h2 em{color:rgba(255,255,255,.95);font-style:normal;font-weight:700}#views #setup-view h3{margin-bottom:0}#views #setup-view h3 x-icon{width:40px;height:auto;display:inline-block;vertical-align:middle}#views #setup-view pre{display:block;white-space:pre;overflow:auto}#views #setup-view dd{margin:0 0 18px}#views #setup-view dd:last-of-type{margin:0}</style>
<main id="main">
<x-button id="show-sidebar-button" icon="menu" skin="textured">
<x-icon name="menu"></x-icon>
</x-button>
<sidebar id="sidebar">
<header id="header">
<h1 id="logo">Xel</h1>
<x-button id="hide-sidebar-button" skin="textured">
<x-icon name="chevron-left"></x-icon>
</x-button>
</header>
<hr/>
<nav id="nav">
<section>
<a href="/">
<x-button skin="nav">
<x-icon name="info"></x-icon>
<x-label>About</x-label>
</x-button>
</a>
<a href="/setup">
<x-button skin="nav">
<x-icon name="build"></x-icon>
<x-label>Setup</x-label>
</x-button>
</a>
<a href="/faq">
<x-button skin="nav">
<x-icon name="question-answer"></x-icon>
<x-label>FAQ</x-label>
</x-button>
</a>
</section>
<hr/>
<section>
<a href="https://github.com/jarek-foksa/xel" target="_blank">
<x-button skin="nav">
<x-icon name="code"></x-icon>
<x-label>Source Code</x-label>
<x-icon class="external-link-icon" name="exit-to-app"></x-icon>
</x-button>
</a>
<a href="https://github.com/jarek-foksa/xel/issues" target="_blank">
<x-button skin="nav">
<x-icon name="bug-report"></x-icon>
<x-label>Bugs</x-label>
<x-icon class="external-link-icon" name="exit-to-app"></x-icon>
</x-button>
</a>
<a href="https://github.com/jarek-foksa/xel/commits" target="_blank">
<x-button skin="nav">
<x-icon name="event"></x-icon>
<x-label>Changelog</x-label>
<x-icon class="external-link-icon" name="exit-to-app"></x-icon>
</x-button>
</a>
</section>
<hr/>
<section id="theme-section">
<div id="theme-subsection">
<h3 id="theme-heading">Theme</h3>
<x-select id="theme-select">
<x-menu>
<x-menuitem value="macos">
<x-label>MacOS</x-label>
</x-menuitem>
<x-menuitem value="material" selected="true">
<x-label>Material</x-label>
</x-menuitem>
<x-menuitem value="vanilla">
<x-label>Vanilla</x-label>
</x-menuitem>
</x-menu>
</x-select>
</div>
<div id="accent-color-subsection">
<h3>Accent color</h3>
<x-select id="accent-color-select">
<x-menu id="accent-color-menu"></x-menu>
</x-select>
</div>
</section>
<hr/>
<section>
<h3>Primitives</h3>
<a href="/elements/x-box">
<x-button skin="nav">
<x-label>x-box</x-label>
</x-button>
</a>
<a href="/elements/x-card">
<x-button skin="nav">
<x-label>x-card</x-label>
</x-button>
</a>
<a href="/elements/x-accordion">
<x-button skin="nav">
<x-label>x-accordion</x-label>
</x-button>
</a>
<a href="/elements/x-icon">
<x-button skin="nav">
<x-label>x-icon</x-label>
</x-button>
</a>
<a href="/elements/x-label">
<x-button skin="nav">
<x-label>x-label</x-label>
</x-button>
</a>
<a href="/elements/x-shortcut">
<x-button skin="nav">
<x-label>x-shortcut</x-label>
</x-button>
</a>
<a href="/elements/x-stepper">
<x-button skin="nav">
<x-label>x-stepper</x-label>
</x-button>
</a>
<a href="/elements/x-swatch">
<x-button skin="nav">
<x-label>x-swatch</x-label>
</x-button>
</a>
</section>
<hr/>
<section>
<h3>Buttons</h3>
<a href="/elements/x-button">
<x-button skin="nav">
<x-label>x-button</x-label>
</x-button>
</a>
<a href="/elements/x-buttons">
<x-button skin="nav">
<x-label>x-buttons</x-label>
</x-button>
</a>
</section>
<hr/>
<section>
<h3>Tabs</h3>
<a href="/elements/x-tabs">
<x-button skin="nav">
<x-label>x-tabs</x-label>
</x-button>
</a>
<a href="/elements/x-doctabs">
<x-button skin="nav">
<x-label>x-doctabs</x-label>
</x-button>
</a>
</section>
<hr/>
<section>
<h3>Menus</h3>
<a href="/elements/x-menu">
<x-button skin="nav">
<x-label>x-menu</x-label>
</x-button>
</a>
<a href="/elements/x-menuitem">
<x-button skin="nav">
<x-label>x-menuitem</x-label>
</x-button>
</a>
<a href="/elements/x-menubar">
<x-button skin="nav">
<x-label>x-menubar</x-label>
</x-button>
</a>
<a href="/elements/x-contextmenu">
<x-button skin="nav">
<x-label>x-contextmenu</x-label>
</x-button>
</a>
</section>
<hr/>
<section>
<h3>Modals</h3>
<a href="/elements/x-dialog">
<x-button skin="nav">
<x-label>x-dialog</x-label>
</x-button>
</a>
<a href="/elements/x-drawer">
<x-button skin="nav">
<x-label>x-drawer</x-label>
</x-button>
</a>
<a href="/elements/x-popover">
<x-button skin="nav">
<x-label>x-popover</x-label>
</x-button>
</a>
</section>
<hr/>
<section>
<h3>Forms</h3>
<a href="/elements/x-checkbox">
<x-button skin="nav">
<x-label>x-checkbox</x-label>
</x-button>
</a>
<a href="/elements/x-radio">
<x-button skin="nav">
<x-label>x-radio</x-label>
</x-button>
</a>
<a href="/elements/x-switch">
<x-button skin="nav">
<x-label>x-switch</x-label>
</x-button>
</a>
<a href="/elements/x-select">
<x-button skin="nav">
<x-label>x-select</x-label>
</x-button>
</a>
<a href="/elements/x-colorselect">
<x-button skin="nav">
<x-label>x-colorselect</x-label>
</x-button>
</a>
<a href="/elements/x-input">
<x-button skin="nav">
<x-label>x-input</x-label>
</x-button>
</a>
<a href="/elements/x-numberinput">
<x-button skin="nav">
<x-label>x-numberinput</x-label>
</x-button>
</a>
<a href="/elements/x-taginput">
<x-button skin="nav">
<x-label>x-taginput</x-label>
</x-button>
</a>
<a href="/elements/x-textarea">
<x-button skin="nav">
<x-label>x-textarea</x-label>
</x-button>
</a>
<a href="/elements/x-slider">
<x-button skin="nav">
<x-label>x-slider</x-label>
</x-button>
</a>
</section>
<hr/>
<section>
<h3>Progress</h3>
<a href="/elements/x-progressbar">
<x-button skin="nav">
<x-label>x-progressbar</x-label>
</x-button>
</a>
<a href="/elements/x-throbber">
<x-button skin="nav">
<x-label>x-throbber</x-label>
</x-button>
</a>
</section>
</nav>
</sidebar>
<div id="views"></div>
</main>
</template>
`;
class XelAppElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$33.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
window.addEventListener("load", () => this._onWindowLoad());
window.addEventListener("popstate", (event) => this._onPopState(event));
window.addEventListener("beforeunload", (event) => this._onWindowUnload(event));
this._shadowRoot.addEventListener("click", (event) => this._onShadowRootClick(event));
this["#hide-sidebar-button"].addEventListener("click", (event) => this._onHideNavButtonClick(event));
this["#show-sidebar-button"].addEventListener("click", (event) => this._onShowNavButtonClick(event));
this["#theme-select"].addEventListener("change", () => this._onThemeSelectChange());
this["#accent-color-select"].addEventListener("change", () => this._onAccentColorSelectChange());
}
connectedCallback() {
history.scrollRestoration = "manual";
if (history.state === null) {
history.replaceState(null, null, window.location.href);
}
this._updateNavButtons();
this._updateViews();
this._updateThemeSection();
this._applyAccentColor();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async _onThemeSelectChange() {
sessionStorage.setItem("theme", this["#theme-select"].value);
await sleep(800);
location.reload();
}
_onAccentColorSelectChange() {
sessionStorage.setItem("accentColorName", this["#accent-color-select"].value);
this._applyAccentColor();
}
_onWindowLoad() {
let scrollTop = parseInt(sessionStorage.getItem("selectedViewScrollTop") || "0");
let selectedView = this["#views"].querySelector(".view[selected]");
if (selectedView) {
selectedView.scrollTop = scrollTop;
}
else {
sleep(100).then(() => {
selectedView = this["#views"].querySelector(".view[selected]");
selectedView.scrollTop = scrollTop;
});
}
}
_onWindowUnload(event) {
let selectedView = this["#views"].querySelector(".view[selected]");
sessionStorage.setItem("selectedViewScrollTop", selectedView.scrollTop);
}
_onPopState(event) {
this._updateNavButtons();
this._updateViews();
}
_onShadowRootClick(event) {
let {ctrlKey, shiftKey, metaKey, target} = event;
if (ctrlKey === false && shiftKey === false && metaKey === false) {
let anchor = target.closest("a");
if (anchor) {
let url = new URL(anchor.href);
if (location.origin === url.origin) {
event.preventDefault();
if (location.pathname !== url.pathname) {
history.pushState(null, null, anchor.href);
this._updateNavButtons();
this._updateViews();
}
}
}
}
}
_onHideNavButtonClick(event) {
if (event.button === 0) {
this._hideSidebar();
}
}
_onShowNavButtonClick(event) {
if (event.button === 0) {
this._showSidebar();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_showSidebar() {
return new Promise(async (resolve) => {
this["#sidebar"].hidden = false;
let {width, height, marginLeft} = getComputedStyle(this["#sidebar"]);
let fromMarginLeft = (marginLeft === "0px" && width !== "auto" ? `-${width}` : marginLeft);
let toMarginLeft = "0px";
let animation = this["#sidebar"].animate(
{
marginLeft: [fromMarginLeft, toMarginLeft]
},
{
duration: 250,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)"
}
);
this["#sidebar"].style.marginLeft = "0";
this._currentAnimation = animation;
});
}
_hideSidebar() {
return new Promise(async (resolve) => {
this["#sidebar"].hidden = false;
let {width, height, marginLeft} = getComputedStyle(this["#sidebar"]);
let fromMarginLeft = (marginLeft === "0px" && width !== "auto" ? "0px" : marginLeft);
let toMarginLeft = `-${width}`;
let animation = this["#sidebar"].animate(
{
marginLeft: [fromMarginLeft, toMarginLeft]
},
{
duration: 250,
easing: "cubic-bezier(0.4, 0.0, 0.2, 1)",
}
);
this["#sidebar"].style.marginLeft = toMarginLeft;
this._currentAnimation = animation;
await animation.finished;
if (this._currentAnimation === animation) {
this["#sidebar"].hidden = true;
}
});
}
_applyAccentColor() {
let accentColorName = sessionStorage.getItem("accentColorName");
if (accentColorName !== null) {
let themePath = document.querySelector('link[href*=".theme.css"]').getAttribute('href');
let theme = themePath.substring(themePath.lastIndexOf("/") + 1, themePath.length - 10);
let accentColor = colorSchemesByTheme[theme][accentColorName];
if (!accentColor) {
let names = Object.keys(colorSchemesByTheme[theme]);
if (names.length > 0) {
accentColor = colorSchemesByTheme[theme][names[0]];
}
}
if (accentColor) {
let [h, s, l] = parseColor(accentColor, "hsla");
document.body.style.setProperty("--accent-color-h", h);
document.body.style.setProperty("--accent-color-s", s + "%");
document.body.style.setProperty("--accent-color-l", l + "%");
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Update selected nav button to match current location.
_updateNavButtons() {
for (let button of this["#nav"].querySelectorAll("x-button")) {
let anchor = button.closest("a");
if (anchor) {
let url = new URL(anchor);
if (url.origin === location.origin && url.pathname === location.pathname) {
button.setAttribute("toggled", "");
}
else {
button.removeAttribute("toggled");
}
}
}
}
// @info
// Update displayed view to match current location
async _updateViews() {
let selectedView = this["#views"].querySelector(".view[selected]");
if (!selectedView || selectedView.dataset.pathname !== location.pathname) {
let view = this["#views"].querySelector(`[data-pathname="${location.pathname}"]`);
// If the view does not exist, try to create it
if (!view) {
let $0 = (location.pathname === "/") ? "/about" : location.pathname;
let url = `/docs` + $0 + `.html`;
let result = await fetch(url);
let viewHTML = await result.text();
view = html`${viewHTML}`;
view.setAttribute("data-pathname", location.pathname);
this["#views"].append(view);
}
if (location.pathname === "/") {
document.querySelector("title").textContent = "Xel";
}
else {
document.querySelector("title").textContent = "Xel - " + view.querySelector("h2").textContent;
}
// Toggle view
{
let view = this["#views"].querySelector(`[data-pathname="${location.pathname}"]`);
let otherView = this["#views"].querySelector(`.view[selected]`);
if (otherView) {
if (otherView === view) {
return;
}
else {
otherView.removeAttribute("selected");
}
}
view.setAttribute("selected", "");
}
// Hide theme-specific sections that don't match the current theme
{
let theme = document.querySelector('link[href*=".theme.css"]').getAttribute('href');
let themeName = theme.substring(theme.lastIndexOf("/") + 1, theme.length - 10);
for (let section of view.querySelectorAll("section")) {
if (section.hasAttribute("data-themes")) {
if (section.getAttribute("data-themes").includes(themeName) === false) {
section.hidden = true;
}
}
}
let visibleSections = view.querySelectorAll("section:not([hidden])");
if (visibleSections.length > 0) {
let lastVisibleSection = visibleSections[visibleSections.length-1];
lastVisibleSection.setAttribute("data-last-visible", "");
}
}
// Remove offscreen views
{
for (let view of [...this["#views"].children]) {
if (view.hasAttribute("animating") === false && view.hasAttribute("selected") === false) {
view.remove();
}
}
}
}
}
_updateThemeSection() {
let themePath = document.querySelector('link[href*=".theme.css"]').getAttribute('href');
let theme = themePath.substring(themePath.lastIndexOf("/") + 1, themePath.length - 10);
// Update theme subsection
{
for (let item of this["#theme-select"].querySelectorAll("x-menuitem")) {
item.setAttribute("selected", (item.getAttribute("value") === theme) ? "true" : "false");
}
}
// Update accent color subsection
{
if (theme === "material") {
this["#accent-color-subsection"].hidden = true;
}
else {
let accentColorName = sessionStorage.getItem("accentColorName");
let supportedAccentColorNames = Object.keys(colorSchemesByTheme[theme]);
let itemsHTML = "";
for (let [colorName, colorValue] of Object.entries(colorSchemesByTheme[theme])) {
itemsHTML += `
<x-menuitem value="${colorName}" selected="true">
<x-swatch value="${colorValue}"></x-swatch>
<x-label>${capitalize(colorName)}</x-label>
</x-menuitem>
`;
}
this["#accent-color-menu"].innerHTML = itemsHTML;
if (accentColorName === null) {
if (supportedAccentColorNames.length > 0) {
accentColorName = supportedAccentColorNames[0];
sessionStorage.setItem("accentColorName", accentColorName);
}
}
if (supportedAccentColorNames.includes(accentColorName) === false) {
if (supportedAccentColorNames.length > 0) {
accentColorName = supportedAccentColorNames[0];
sessionStorage.setItem("accentColorName", accentColorName);
}
else {
accentColorName = null;
}
}
for (let item of this["#accent-color-select"].querySelectorAll("x-menuitem")) {
item.setAttribute("selected", (item.getAttribute("value") === accentColorName) ? "true" : "false");
}
this["#accent-color-subsection"].hidden = false;
}
}
}
}
customElements.define("xel-app", XelAppElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let shadowTemplate$34 = html`
<template>
<style>:host{display:block;width:100%;box-sizing:border-box;background:#fff;padding:14px;--selection-background: #B2D7FD}::selection{background:var(--selection-background)}#code{display:block;white-space:pre-wrap;overflow-x:auto;font-size:13px;line-height:18px;outline:0;background:0 0;padding:0}</style>
<link id="prism-theme" rel="stylesheet">
<code id="code" class="language-html"></code>
</template>
`;
class XelCodeViewElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$34.content, true));
this._value = "";
this._observer = new MutationObserver(() => this._update());
this._observer.observe(this, {childList: true, attributes: false, characterData: true, subtree: true});
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
connectedCallback() {
let theme = document.querySelector('link[href*=".theme.css"]').getAttribute("href");
this["#prism-theme"].setAttribute("href", `node_modules/prismjs/themes/prism-coy.css`);
this._update();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @type
// string
// @default
// ""
get value() {
return this._value;
}
set value(value) {
this._value = value;
//this._update();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_update() {
this["#code"].textContent = this.textContent;
if (this["#code"].textContent !== "") {
Prism.highlightElement(this["#code"], true);
}
}
}
customElements.define("xel-codeview", XelCodeViewElement);
// @copyright
// © 2016-2017 Jarosław Foksa
let theme$1 = document.querySelector('link[href*=".theme.css"]').getAttribute("href");
let counter = 0;
let shadowTemplate$35 = html`
<template>
<link rel="stylesheet" href="${theme$1}">
<style>:host{display:block}#code-view{margin-top:25px}:host([compact]) #code-view{max-height:350px;overflow:scroll}</style>
<main>
<div id="live-view"></div>
<xel-codeview id="code-view"></xel-codeview>
</main>
</template>
`;
class XelDemoElement extends HTMLElement {
static get observedAttributes() {
return ["name"];
}
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "closed"});
this._shadowRoot.append(document.importNode(shadowTemplate$35.content, true));
for (let element of this._shadowRoot.querySelectorAll("[id]")) {
this["#" + element.id] = element;
}
}
connectedCallback() {
this["#code-view"].textContent = this._getDemoHTML();
}
attributeChangedCallback(name) {
if (name === "name") {
this._update();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @info
// Compact demo has a scrollable code view with limited max height.
// @type
// boolean
// @default
// false
// @attribute
get compact() {
return this.hasAttribute("compact");
}
set compact(compact) {
compact ? this.setAttribute("compact", "") : this.removeAttribute("compact");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
_getDemoHTML() {
let container = document.createElement("div");
let template = this.querySelector("template");
if (!template) {
return "";
}
let content = document.importNode(template.content, true);
{
let liveViewContent = content.cloneNode(true);
window["shadowRoot" + counter] = this["#live-view"];
let script = liveViewContent.querySelector("script");
if (script) {
let textContent = replaceAll(script.textContent, "document", `window.shadowRoot${counter}`);
textContent = "{" + textContent + "}";
script.textContent = textContent;
}
counter += 1;
this["#live-view"].append(liveViewContent);
}
for (let child of content.childNodes) {
container.append(child.cloneNode(true));
}
// Remove dynamically added attributes
for (let element of container.querySelectorAll("*")) {
if (element.localName.startsWith("x-")) {
for (let {name, value} of [...element.attributes]) {
if (name === "tabindex" || name === "role" || name.startsWith("aria")) {
element.removeAttribute(name);
}
}
}
}
let textContent = container.innerHTML;
// Simplify boolean attributes
textContent = replaceAll(textContent, `=""`, "");
textContent = replaceAll(textContent, "demo", "document");
let lines = textContent.split("\n");
// Remove leading and trailing empty lines
{
if (isDOMWhitespace(lines[0])) {
lines.shift();
}
if (isDOMWhitespace(lines[lines.length - 1])) {
lines.pop();
}
}
// Remove excesive indentation
{
let minIndent = Infinity;
for (let line of lines) {
if (isDOMWhitespace(line) === false) {
let indent = 0;
for (let char of line) {
if (char === " ") {
indent += 1;
}
else {
break;
}
}
if (indent < minIndent) {
minIndent = indent;
}
}
}
lines = lines.map(line => line.substring(minIndent));
}
let innerHTML = lines.join("\n");
return innerHTML;
}
}
customElements.define("xel-demo", XelDemoElement);
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment