Compare commits
11 Commits
5dca00d772
...
master
Author | SHA1 | Date | |
---|---|---|---|
de558561b6 | |||
ab914d8bf2 | |||
e75b7b8efe | |||
28fbac0c6d | |||
9164bae268 | |||
0c154c23d0 | |||
9c1ee77a20 | |||
9cf25a6666 | |||
69042b8ebe | |||
cd4698d73b | |||
78d8f658e9 |
75
color.js
Normal file
75
color.js
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Calculate light or darker color based on the luminance arg.
|
||||
*
|
||||
* @param {String} hex Color hex string to calculate on.
|
||||
* @param {Number} lum Luminance.
|
||||
* @return {String} The new color hex string.
|
||||
*/
|
||||
export const colorLuminance = (hex, lum) => {
|
||||
// validate hex string
|
||||
hex = String(hex).replace(/[^0-9a-f]/gi, '');
|
||||
if (hex.length < 6) {
|
||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||
}
|
||||
lum = lum || 0;
|
||||
// convert to decimal and change luminosity
|
||||
let rgb = '#';
|
||||
let c;
|
||||
let i;
|
||||
for (i = 0; i < 3; i++) {
|
||||
c = parseInt(hex.substr(i * 2, 2), 16);
|
||||
c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
|
||||
rgb += ('00' + c).substr(c.length);
|
||||
}
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an appropriate contrast color.
|
||||
*
|
||||
* @param {String} hex Hex string of the color we're searching for the right contrast.
|
||||
* @param {String} dark Dark color to return. Defaults to #000000.
|
||||
* @param {String} light Light color to return. Defaults to #ffffff.
|
||||
* @return {String} Hex color string with the appropriate contrast color.
|
||||
*/
|
||||
export const contrastTextColor = (hex, dark, light) => {
|
||||
dark = dark || '#000000';
|
||||
light = light || '#ffffff';
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (!result) {
|
||||
return dark;
|
||||
}
|
||||
const red = parseInt(result[1], 16);
|
||||
const green = parseInt(result[2], 16);
|
||||
const blue = parseInt(result[3], 16);
|
||||
let brightness;
|
||||
brightness = (red * 299) + (green * 587) + (blue * 114);
|
||||
brightness /= 255000;
|
||||
// values range from 0 to 1
|
||||
// anything greater than 0.6 should be bright enough for dark text
|
||||
if (brightness >= 0.6) {
|
||||
return dark;
|
||||
}
|
||||
|
||||
return light;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert rgb(r, g, b) to hex.
|
||||
*
|
||||
* @param {String} rgb Rgb string to convert.
|
||||
* @return {String} The hex string.
|
||||
*/
|
||||
export const rgbToHex = rgb => {
|
||||
const matches = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/);
|
||||
if (matches.length !== 4) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// From https://gist.github.com/lrvick/2080648.
|
||||
const bin = matches[1] << 16 | matches[2] << 8 | matches[3];
|
||||
return (function(h) {
|
||||
return '#' + new Array(7 - h.length).join('0') + h;
|
||||
})(bin.toString(16).toLowerCase());
|
||||
}
|
103
event-helpers.js
103
event-helpers.js
@ -7,7 +7,47 @@
|
||||
*/
|
||||
export const EventHelpers = function(superClass) {
|
||||
return class extends superClass {
|
||||
listen(node, eventName, cbName) {
|
||||
listen(node, eventName, cbName, options) {
|
||||
const boundListener = this.__registerListener(cbName, node, eventName);
|
||||
|
||||
if (eventName === 'track') {
|
||||
this._track(node, boundListener);
|
||||
return;
|
||||
}
|
||||
|
||||
node.addEventListener(eventName, boundListener, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, options) {
|
||||
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], options);
|
||||
listeners[eventKey] = null;
|
||||
}
|
||||
}
|
||||
|
||||
once(node, eventName, cbName, options) {
|
||||
const wrappedCbName = `__onceCb__${cbName}`;
|
||||
|
||||
this[wrappedCbName] = (...args) => {
|
||||
this[cbName](...args);
|
||||
this.unlisten(node, eventName, wrappedCbName, options);
|
||||
};
|
||||
|
||||
this.listen(node, eventName, wrappedCbName, options);
|
||||
}
|
||||
|
||||
__registerListener(cbName, node, eventName) {
|
||||
this.__boundEventListeners = this.__boundEventListeners || new WeakMap();
|
||||
const boundListener = this[cbName].bind(this);
|
||||
const eventKey = `${eventName}_${cbName}`;
|
||||
@ -22,36 +62,49 @@ export const EventHelpers = function(superClass) {
|
||||
|
||||
listeners[eventKey] = boundListener;
|
||||
this.__boundEventListeners.set(node, listeners);
|
||||
node.addEventListener(eventName, boundListener);
|
||||
|
||||
return 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}`;
|
||||
_track(node, boundListener) {
|
||||
this._boundTrackingListener = boundListener;
|
||||
this.listen(node, 'mousedown', '_trackMouseDown');
|
||||
}
|
||||
|
||||
if (listeners && typeof listeners[eventKey]) {
|
||||
node.removeEventListener(eventName, listeners[eventKey]);
|
||||
listeners[eventKey] = null;
|
||||
_trackMouseDown(e) {
|
||||
this._trackInfo = {
|
||||
originX: e.clientX,
|
||||
originY: e.clientY,
|
||||
started: false
|
||||
};
|
||||
|
||||
this.listen(window, 'mousemove', '_trackMouseMove', true);
|
||||
this.once(window, 'mouseup', '_trackMouseUp');
|
||||
}
|
||||
|
||||
_trackMouseMove(e) {
|
||||
if (!this._trackInfo.started && trackHasMovedEnough(this._trackInfo, e.clientX, e.clientY)) {
|
||||
this._trackInfo.started = true;
|
||||
this._boundTrackingListener(new CustomEvent('track', { detail: { state: 'start', dx: e.clientX - this._trackInfo.originX, dy: e.clientY - this._trackInfo.originY }, bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
if (this._trackInfo.started) {
|
||||
this._boundTrackingListener(new CustomEvent('track', { detail: { state: 'track', dx: e.clientX - this._trackInfo.originX, dy: e.clientY - this._trackInfo.originY }, bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
once(node, eventName, cbName) {
|
||||
const wrappedCbName = `__onceCb__${cbName}`;
|
||||
|
||||
this[wrappedCbName] = (...args) => {
|
||||
this[cbName](...args);
|
||||
this.unlisten(node, eventName, wrappedCbName);
|
||||
};
|
||||
|
||||
this.listen(node, eventName, wrappedCbName);
|
||||
_trackMouseUp(e) {
|
||||
this._boundTrackingListener(new CustomEvent('track', { detail: { state: 'end', dx: e.clientX - this._trackInfo.originX, dy: e.clientY - this._trackInfo.originY }, bubbles: true, composed: true }));
|
||||
this.unlisten(window, 'mousemove', '_trackMouseMove', true);
|
||||
this._trackInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TRACK_DISTANCE = 5;
|
||||
|
||||
function trackHasMovedEnough(info, x, y) {
|
||||
let dx = Math.abs(info.originX - x);
|
||||
let dy = Math.abs(info.originY - y);
|
||||
return (dx >= TRACK_DISTANCE || dy >= TRACK_DISTANCE);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tp/helpers",
|
||||
"version": "2.2.0",
|
||||
"version": "2.7.1",
|
||||
"description": "",
|
||||
"main": "closest.js",
|
||||
"scripts": {
|
||||
|
17
position.js
17
position.js
@ -21,6 +21,10 @@ export const Position = function(superClass) {
|
||||
el.style.position = 'fixed';
|
||||
el.style.zIndex = 1001;
|
||||
|
||||
// Reset max-height and overflow in case the element has been positioned before.
|
||||
el.style.maxHeight = '';
|
||||
el.style.overflowY = '';
|
||||
|
||||
// Test if the target is in a different stacking context.
|
||||
el.style.left = '0px';
|
||||
el.style.top = '0px';
|
||||
@ -52,6 +56,12 @@ export const Position = function(superClass) {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure top never goes below 0
|
||||
if (top < 0) {
|
||||
compTop += Math.abs(top);
|
||||
top = 0;
|
||||
}
|
||||
|
||||
if (options.halign === 'left') {
|
||||
left = anchorRect.left;
|
||||
}
|
||||
@ -74,6 +84,13 @@ export const Position = function(superClass) {
|
||||
left = 0;
|
||||
}
|
||||
|
||||
// Constrain height to viewport - calculate available height from final top position
|
||||
const availableHeight = window.innerHeight - top;
|
||||
if (elRect.height > availableHeight) {
|
||||
el.style.maxHeight = availableHeight + 'px';
|
||||
el.style.overflowY = 'auto';
|
||||
}
|
||||
|
||||
el.style.top = (top - fixTop) + 'px';
|
||||
el.style.left = (left - fixLeft) + 'px';
|
||||
|
||||
|
2
reach.js
2
reach.js
@ -1,4 +1,6 @@
|
||||
export const reach = (path, data) => {
|
||||
if (!data) return;
|
||||
|
||||
const parts = path.split('.');
|
||||
let part;
|
||||
while (part = parts.shift()) {
|
||||
|
104
sort.js
Normal file
104
sort.js
Normal file
@ -0,0 +1,104 @@
|
||||
import { reach } from './reach.js';
|
||||
|
||||
/**
|
||||
* Sort collection asc by some field.
|
||||
*
|
||||
* @param {String} field Field to sort by.
|
||||
* @param {Array} collection Collection to sort.
|
||||
* @return {Array} The sorted collection.
|
||||
*/
|
||||
const sortAsc = (field, collection) => {
|
||||
return collection.sort(function(a, b) {
|
||||
let s1 = reach(field, a);
|
||||
let s2 = reach(field, b);
|
||||
|
||||
if (typeof s1 === 'string') {
|
||||
s1 = s1.toLowerCase();
|
||||
}
|
||||
|
||||
if (typeof s2 === 'string') {
|
||||
s2 = s2.toLowerCase();
|
||||
}
|
||||
|
||||
return naturalCompare(s1, s2);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort collection desc by some field.
|
||||
*
|
||||
* @param {String} field Field to sort by.
|
||||
* @param {Array} collection Collection to sort.
|
||||
* @return {Array} The sorted collection.
|
||||
*/
|
||||
const sortDesc = (field, collection) => {
|
||||
return collection.sort(function(a, b) {
|
||||
let s1 = reach(field, a);
|
||||
let s2 = reach(field, b);
|
||||
|
||||
if (typeof s1 === 'string') {
|
||||
s1 = s1.toLowerCase();
|
||||
}
|
||||
|
||||
if (typeof s2 === 'string') {
|
||||
s2 = s2.toLowerCase();
|
||||
}
|
||||
|
||||
return naturalCompare(s2, s1);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort strings naturally.
|
||||
*
|
||||
* @version 1.4.0
|
||||
* @date 2015-10-26
|
||||
* @stability 3 - Stable
|
||||
* @author Lauri Rooden (https://github.com/litejs/natural-compare-lite)
|
||||
* @license MIT License
|
||||
*
|
||||
* @param {String} a
|
||||
* @param {String} b
|
||||
* @return {Number} -1, 1 or 0.
|
||||
*/
|
||||
const naturalCompare = (a, b) => {
|
||||
let i;
|
||||
let codeA;
|
||||
let codeB = 1;
|
||||
let posA = 0;
|
||||
let posB = 0;
|
||||
const alphabet = String.alphabet;
|
||||
|
||||
function getCode(str, pos, code) {
|
||||
if (code) {
|
||||
for (i = pos; code = getCode(str, i), code < 76 && code > 65;) ++i;
|
||||
return Number(str.slice(pos - 1, i));
|
||||
}
|
||||
code = alphabet && alphabet.indexOf(str.charAt(pos));
|
||||
return code > -1 ? code + 76 : ((code = str.charCodeAt(pos) || 0), code < 45 || code > 127) ? code
|
||||
: code < 46 ? 65 // -
|
||||
: code < 48 ? code - 1
|
||||
: code < 58 ? code + 18 // 0-9
|
||||
: code < 65 ? code - 11
|
||||
: code < 91 ? code + 11 // A-Z
|
||||
: code < 97 ? code - 37
|
||||
: code < 123 ? code + 5 // a-z
|
||||
: code - 63;
|
||||
}
|
||||
|
||||
if ((a += '') != (b += '')) for (;codeB;) {
|
||||
codeA = getCode(a, posA++);
|
||||
codeB = getCode(b, posB++);
|
||||
|
||||
if (codeA < 76 && codeB < 76 && codeA > 66 && codeB > 66) {
|
||||
codeA = getCode(a, posA, posA);
|
||||
codeB = getCode(b, posB, posA = i);
|
||||
posB = i;
|
||||
}
|
||||
|
||||
if (codeA != codeB) return (codeA < codeB) ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export { sortAsc, sortDesc, naturalCompare };
|
@ -8,7 +8,7 @@ export const upload = function(superClass) {
|
||||
* @param {Object} opts Upload options
|
||||
* @returns Promise
|
||||
*/
|
||||
uploadFiles(url, files, data = {}) {
|
||||
uploadFiles(url, files = [], data = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest();
|
||||
const formData = new FormData();
|
||||
|
52
visibility.js
Normal file
52
visibility.js
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
@license
|
||||
Copyright (c) 2025 trading_peter
|
||||
This program is available under Apache License Version 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Orchastrates a intersetion observer to execute code if the element becomes visible or hidden/disconnected.
|
||||
*/
|
||||
export const Visibility = function(superClass) {
|
||||
return class extends superClass {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Create intersection observer
|
||||
this._observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
this.visibilityChanged(true);
|
||||
} else {
|
||||
this.visibilityChanged(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the element becomes either visible / connected or hidden / disconnected.
|
||||
* The callback's logic must be able to handle multiple, successive calls with the same state.
|
||||
* @param {boolean} visible
|
||||
*/
|
||||
visibilityChanged(visible) {
|
||||
console.warn(this.tagName, 'should override onVisible');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Start observing this element
|
||||
this._observer.observe(this);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
// Clean up
|
||||
this._observer.unobserve(this);
|
||||
this._observer.disconnect();
|
||||
|
||||
this.visibilityChanged(false);
|
||||
}
|
||||
};
|
||||
}
|
28
wait-for.js
Normal file
28
wait-for.js
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Wait for some test to be true, returns a promise that resolves when condition is met.
|
||||
* Useful to wait for element render, for example. Uses requestAnimationFrame.
|
||||
*
|
||||
* @param {Function} testFn Test function to call. Should return true to resolve and false to continue testing.
|
||||
* @param {Number} maxTries Maximum number of rounds to test. If exceeded, the promise rejects. Defaults to 5000.
|
||||
* @return {Promise} Promise that resolves when test passes or rejects when maxTries is reached
|
||||
*/
|
||||
export const waitFor = (testFn, maxTries = 5000) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tries = 0;
|
||||
const boundTestFn = testFn.bind(this);
|
||||
|
||||
function waiter() {
|
||||
if (tries === maxTries) {
|
||||
return reject(new Error('waitFor: maximum tries exceeded'));
|
||||
}
|
||||
if (!boundTestFn()) {
|
||||
window.requestAnimationFrame(waiter);
|
||||
tries++;
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
waiter();
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user