Add initial versions
This commit is contained in:
parent
28442b85fb
commit
cab71980d8
16
closest.js
Normal file
16
closest.js
Normal 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
52
control-state.js
Normal 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
29
dom-query.js
Normal 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
57
event-helpers.js
Normal 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
71
fetch-mixin.js
Normal 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
51
form-element.js
Normal 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
44
inert.js
Normal 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
3
isDefined.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const isDefined = val => {
|
||||
return val !== null && val !== undefined;
|
||||
};
|
75
lazy-imports.js
Normal file
75
lazy-imports.js
Normal 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
15
package.json
Normal 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
87
position.js
Normal 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
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user