Add initial versions

This commit is contained in:
trading_peter 2022-03-11 23:42:26 +01:00
parent 28442b85fb
commit cab71980d8
11 changed files with 500 additions and 0 deletions

16
closest.js Normal file
View File

@ -0,0 +1,16 @@
export const closest = (node, selector, pierce) => {
const matches = node.matches || node.msMatchesSelector || node.oMatchesSelector;
while (node) {
if (matches.call(node, selector)) {
return node;
}
if (pierce && !node.parentElement) {
node = node.getRootNode();
if (node) {
node = node.host;
}
} else {
node = node.parentElement;
}
}
};

52
control-state.js Normal file
View File

@ -0,0 +1,52 @@
/**
@license
Copyright (c) 2022 trading_peter
This program is available under Apache License Version 2.0
*/
export const ControlState = function(superClass) {
return class extends superClass {
static get properties() {
return {
focused: { type: Boolean, reflect: true },
disabled: { type: Boolean, reflect: true },
};
}
constructor() {
super();
this._boundFocus = this._focusHandler.bind(this);
}
firstUpdated() {
super.firstUpdated();
this.addEventListener('focus', this._boundFocus, true);
this.addEventListener('blur', this._boundFocus, true);
}
_focusHandler(e) {
this.focused = e.type === 'focus';
}
_disabledChanged(disabled) {
this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
this.style.pointerEvents = disabled ? 'none' : '';
if (disabled) {
// Read the `tabindex` attribute instead of the `tabIndex` property.
// The property returns `-1` if there is no `tabindex` attribute.
// This distinction is important when restoring the value because
// leaving `-1` hides shadow root children from the tab order.
this._prevTabIndex = this.getAttribute('tabindex');
this.focused = false;
this.tabIndex = -1;
this.blur();
} else if (this._prevTabIndex !== undefined) {
if (this._prevTabIndex === null) {
this.removeAttribute('tabindex');
} else {
this.setAttribute('tabindex', this._prevTabIndex);
}
}
}
};
}

29
dom-query.js Normal file
View File

@ -0,0 +1,29 @@
/**
@license
Copyright (c) 2022 trading_peter
This program is available under Apache License Version 2.0
*/
/**
* Helps to automatically query elements in the shadow dom of the extended element.
*/
export const DomQuery = function(superClass) {
return class extends superClass {
constructor() {
super();
const handler = {
get: (o, selector) => {
const root = this.shadowRoot || this;
const el = selector[0] === '?' ? root.querySelector(selector.substring(1)) : root.querySelector(`#${selector}`);
if (el !== null) {
return el;
}
}
};
this.$ = new Proxy({}, handler);
}
};
}

57
event-helpers.js Normal file
View File

@ -0,0 +1,57 @@
/**
* Add an event listener bound to the context of the superClass.
*
* @param {HTMLElement} node Element to attach the event listener to.
* @param {String} eventName Name of the event.
* @param {String} cbName Name of the handler.
*/
export const EventHelpers = function(superClass) {
return class extends superClass {
listen(node, eventName, cbName) {
this.__boundEventListeners = this.__boundEventListeners || new WeakMap();
const boundListener = this[cbName].bind(this);
const eventKey = `${eventName}_${cbName}`;
let listeners = this.__boundEventListeners.get(node);
// If there is already a handler for the event assigned we stop here.
if (listeners && typeof listeners[eventKey] === 'function') return;
if (!listeners) {
listeners = {};
}
listeners[eventKey] = boundListener;
this.__boundEventListeners.set(node, listeners);
node.addEventListener(eventName, boundListener);
}
/**
* Remove an event listener bound to the context of the superClass.
*
* @param {HTMLElement} node Element to attach the event listener to.
* @param {String} eventName Name of the event.
* @param {String} cbName Name of the handler.
*/
unlisten(node, eventName, cbName) {
this.__boundEventListeners = this.__boundEventListeners || new WeakMap();
const listeners = this.__boundEventListeners.get(node);
const eventKey = `${eventName}_${cbName}`;
if (listeners && typeof listeners[eventKey]) {
node.removeEventListener(eventName, listeners[eventKey]);
listeners[eventKey] = null;
}
}
once(node, eventName, cbName) {
const wrappedCbName = `__onceCb__${cbName}`;
this[wrappedCbName] = (...args) => {
this[cbName](...args);
this.unlisten(node, eventName, wrappedCbName);
};
this.listen(node, eventName, wrappedCbName);
}
}
}

71
fetch-mixin.js Normal file
View File

@ -0,0 +1,71 @@
export const fetchMixin = function(superClass) {
return class extends superClass {
constructor() {
super();
this.__abortControllers = new Map();
}
get(url) {
return fetch(url).then(response => response.json());
}
head(url) {
return fetch(url, { method: 'HEAD' });
}
async post(url, data, overwrite = true) {
this.__cancelRunningRequest(url);
if (overwrite === true) {
const ac = new AbortController();
this.__abortControllers.set(url, ac);
}
try {
const reqOptions = {
method: 'POST',
signal: this.__abortControllers.get(url).signal,
mode: 'cors',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
referrer: 'no-referrer',
body: JSON.stringify(data)
};
document.dispatchEvent(new CustomEvent('before-request', { detail: reqOptions, bubbles: true, composed: true }));
const result = await fetch(url, reqOptions).then(response => response.json());
this.__abortControllers.delete(url);
if (result.statusCode === 500) {
console.error(result);
}
return result;
} catch (err) {
if (err.name === 'AbortError') {
return { statusCode: -1, error: err };
} else {
this.__abortControllers.delete(url);
return { statusCode: null, error: err };
}
}
}
isInFlight(url) {
return Boolean(this.__abortControllers.get(url));
}
__cancelRunningRequest(url) {
if (this.__abortControllers.has(url)) {
try {
this.__abortControllers.get(url).abort();
} catch (err) { }
this.__abortControllers.delete(url);
}
}
};
};

51
form-element.js Normal file
View File

@ -0,0 +1,51 @@
/**
@license
Copyright (c) 2022 trading_peter
This program is available under Apache License Version 2.0
*/
export const FormElement = function(superClass) {
return class extends superClass {
static get properties() {
return {
// The name of this element.
name: { type: String },
// The value for this element.
value: { type: String },
// Set to true to mark the input as required. Element needs to provide a "validate()" function tha returns a boolean.
required: { type: Boolean },
// The form that the element is registered to. Set by the form that got the registration.
parentForm: { type: Object }
};
}
connectedCallback() {
super.connectedCallback();
// Prevent that child elements register.
this.addEventListener('form-element-register', this._onChildRegister.bind(this));
}
firstUpdated() {
super.firstUpdated();
this.dispatchEvent(new CustomEvent('form-element-register', { bubbles: true, composed: true }));
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._parentForm) {
this._parentForm.dispatchEvent(new CustomEvent('form-element-unregister', { detail: { target: this }, bubbles: true, composed: true }));
}
}
// Prevent that child elements register themselves to the form element.
_onChildRegister(e) {
if (e.composedPath()[0].tagName !== this.tagName) {
e.stopPropagation();
}
}
};
}

44
inert.js Normal file
View File

@ -0,0 +1,44 @@
/**
@license
Copyright (c) 2022 trading_peter
This program is available under Apache License Version 2.0
*/
/**
* This helper provides sort of a inert polyfill.
* The `inert` property and a helper method is added that can be used
* to update tabindex for example. The implementing element also is styled with
* pointer-events: none if the inert property is set true.
*/
export const Inert = function(superClass) {
return class extends superClass {
static get properties() {
return {
inert: {
type: Boolean,
value: false,
observer: '_inertChanged',
reflectToAttribute: true
}
};
}
updated(changes) {
if (changes.has('inert')) {
this._inertChanged(this.inert);
}
}
_inertChanged(state) {
if (state) {
this.style.pointerEvents = 'none';
this._inertTapIndex = this.getAttribute('tabindex');
} else {
this.style.pointerEvents = '';
if (this._inertTapIndex) {
this.setAttribute('tabindex', this._inertTapIndex);
}
}
}
};
}

3
isDefined.js Normal file
View File

@ -0,0 +1,3 @@
export const isDefined = val => {
return val !== null && val !== undefined;
};

75
lazy-imports.js Normal file
View File

@ -0,0 +1,75 @@
/**
@license
Copyright (c) 2022 trading_peter
This program is available under Apache License Version 2.0
*/
/**
* Used to import other scripts dynamically based on a route change from the-router for example.
* Do this by defining lazyMap for the importer and then calling `import` with the new path segments.
*
* ## Example lazyMp
*
* ```js
* [
* {
* match: /^user$/, // Would match /user/
* imports: [ 'user-element-1.html', 'user-element-2.html' ],
* map: [
* {
* match: /^settings$/, // Would match /user/settings
* imports: [ 'settings.html' ]
* }
* ]
* }
* ]
* ```
*
* Use the _lazyImport method to start processing the map.
* _lazyImport needs an array of strings that describe the path segments it should
* match the map and sub maps against.
*
* So, if for example your route is /user/settings you need to split
* the path to [ 'user', 'settings' ] and feed it to the _lazyImport function.
*/
export default class {
constructor(lazyMap) {
this.lazyMap = lazyMap;
}
import(segments) {
if (typeof segments === 'object' && !Array.isArray(segments)) {
segments = Object.values(segments).filter(v => v !== undefined);
}
if (!this.lazyMap || !Array.isArray(segments)) return;
const imports = [];
this.__processLazyMap(this.lazyMap, segments, 0, imports);
const promises = imports.map(url => {
return import(url);
});
return promises.length === 1 ? promises[0] : Promise.all(promises);
}
__processLazyMap(map, segments, level, list) {
const segment = segments[level];
if (segment === undefined) return [];
map.forEach(entry => {
if (!entry.match.test(segment)) return;
if (Array.isArray(entry.imports)) {
if (entry.match.test(segment)) {
list.push(...entry.imports);
}
}
if (Array.isArray(entry.map)) {
this.__processLazyMap(entry.map, segments, level + 1, list);
}
});
}
}

15
package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "helpers",
"version": "1.0.0",
"description": "",
"main": "closest.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gitea.codeblob.work/tp-elements/helpers.git"
},
"author": "trading_peter",
"license": "Apache-2.0"
}

87
position.js Normal file
View File

@ -0,0 +1,87 @@
/**
@license
Copyright (c) 2022 trading_peter
This program is available under Apache License Version 2.0
*/
/**
* Helps to position elements relative to each other.
*/
export const Position = function(superClass) {
return class extends superClass {
_posFixed(anchor, el, options) {
options = Object.assign({
valign: 'top',
halign: 'middle',
spacing: 0
}, options);
let top, left, fixLeft = 0, fixTop = 0, compLeft = 0, compTop = 0;
el.style.position = 'fixed';
el.style.zIndex = 1001;
// Test if the target is in a different stacking context.
el.style.left = '0px';
el.style.top = '0px';
const elRect = el.getBoundingClientRect();
if (elRect.left > 0 || elRect.top > 0) {
fixLeft = elRect.left;
fixTop = elRect.top;
}
const anchorRect = anchor.getBoundingClientRect();
if (options.valign === 'top') {
top = anchorRect.top - elRect.height - options.spacing;
// Move popup down a little bit if there issn't enough room over the anchor.
if (top < 0) {
compTop = Math.abs(top);
top = 0;
}
}
if (options.valign === 'bottom') {
top = anchorRect.top + anchorRect.height + options.spacing;
// Move popup up a little bit if there issn't enough room under the anchor.
if (top + elRect.height > window.innerHeight) {
compTop = top + elRect.height - window.innerHeight;
top -= compTop;
}
}
if (options.halign === 'left') {
left = anchorRect.left;
}
if (options.halign === 'middle') {
left = anchorRect.left - elRect.width / 2 + anchorRect.width / 2;
}
if (options.halign === 'right') {
left = anchorRect.left + anchorRect.width - elRect.width;
}
if (left + elRect.width > window.innerWidth) {
compLeft = left + elRect.width - window.innerWidth;
left -= compLeft;
}
if (left < 0) {
compLeft = Math.abs(left);
left = 0;
}
el.style.top = (top - fixTop) + 'px';
el.style.left = (left - fixLeft) + 'px';
// Return info object about how much the position had to compensate to fit on the page.
return {
compLeft: compLeft,
compTop: compTop
};
}
};
}