@marianmeres/stuic 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/autoscroll.d.ts +21 -0
- package/dist/actions/autoscroll.js +60 -0
- package/dist/actions/on-outside.d.ts +9 -0
- package/dist/actions/on-outside.js +27 -0
- package/dist/actions/tooltip/_make-visible.d.ts +3 -0
- package/dist/actions/tooltip/_make-visible.js +25 -0
- package/dist/actions/tooltip/_maybe-pick-safe-placement.d.ts +4 -0
- package/dist/actions/tooltip/_maybe-pick-safe-placement.js +86 -0
- package/dist/actions/tooltip/_set-position.d.ts +2 -0
- package/dist/actions/tooltip/_set-position.js +125 -0
- package/dist/actions/tooltip/tooltip.d.ts +41 -0
- package/dist/actions/tooltip/tooltip.js +296 -0
- package/dist/components/Drawer/Drawer.svelte +2 -2
- package/dist/components/HoverExpandableWidth/HoverExpandableWidth.svelte +1 -3
- package/dist/components/Switch/Switch.svelte +4 -2
- package/dist/components/Switch/Switch.svelte.d.ts +1 -1
- package/dist/components/popover/Popover.svelte +24 -0
- package/dist/components/popover/Popover.svelte.d.ts +20 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/utils/calculate-alignment.d.ts +68 -0
- package/dist/utils/calculate-alignment.js +183 -0
- package/dist/utils/get-id.d.ts +1 -0
- package/dist/utils/get-id.js +2 -0
- package/package.json +9 -3
- package/dist/actions/click-outside.d.ts +0 -3
- package/dist/actions/click-outside.js +0 -15
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare type Subscribe<T> = (value: T) => void;
|
|
2
|
+
declare type Unsubscribe = () => void;
|
|
3
|
+
declare type Update<T> = (value: T) => T;
|
|
4
|
+
interface StoreReadable<T> {
|
|
5
|
+
subscribe(cb: Subscribe<T>): Unsubscribe;
|
|
6
|
+
}
|
|
7
|
+
interface StoreLike<T> extends StoreReadable<T> {
|
|
8
|
+
set(value: T): void;
|
|
9
|
+
update(cb: Update<T>): void;
|
|
10
|
+
}
|
|
11
|
+
export type AutoscrollOptions = ScrollOptions & {
|
|
12
|
+
dependencies?: StoreReadable<any>[];
|
|
13
|
+
logger?: (...args: any[]) => void;
|
|
14
|
+
newScrollableContentSignal?: StoreLike<boolean>;
|
|
15
|
+
shouldScrollThresholdPx?: number;
|
|
16
|
+
startScrollTimeout?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function autoscroll(node: HTMLElement, options?: AutoscrollOptions): {
|
|
19
|
+
destroy(): void;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
shouldScrollThresholdPx: 100,
|
|
3
|
+
startScrollTimeout: 300,
|
|
4
|
+
};
|
|
5
|
+
export function autoscroll(node, options = {
|
|
6
|
+
shouldScrollThresholdPx: DEFAULTS.shouldScrollThresholdPx,
|
|
7
|
+
startScrollTimeout: DEFAULTS.startScrollTimeout,
|
|
8
|
+
}) {
|
|
9
|
+
// use "smooth" by default
|
|
10
|
+
options.behavior ??= 'smooth';
|
|
11
|
+
options.shouldScrollThresholdPx ??= DEFAULTS.shouldScrollThresholdPx;
|
|
12
|
+
options.startScrollTimeout ??= DEFAULTS.startScrollTimeout;
|
|
13
|
+
const { behavior, shouldScrollThresholdPx, dependencies, logger, newScrollableContentSignal, startScrollTimeout, } = options || {};
|
|
14
|
+
let origScrollHeight = 0;
|
|
15
|
+
const log = (...args) => typeof logger === 'function' && logger.apply(null, [...args]);
|
|
16
|
+
const shouldScroll = () => {
|
|
17
|
+
const { scrollTop, clientHeight } = node;
|
|
18
|
+
const result = origScrollHeight - scrollTop - clientHeight < shouldScrollThresholdPx;
|
|
19
|
+
log('shouldScroll?', result, { scrollTop, origScrollHeight, clientHeight });
|
|
20
|
+
return result;
|
|
21
|
+
};
|
|
22
|
+
const scroll = () => {
|
|
23
|
+
const opts = { top: node.scrollHeight, left: node.scrollWidth, behavior };
|
|
24
|
+
log(`scrollTo(${JSON.stringify(opts)})`);
|
|
25
|
+
node.scrollTo(opts);
|
|
26
|
+
};
|
|
27
|
+
// for when children change sizes
|
|
28
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
29
|
+
log('observed resize...');
|
|
30
|
+
shouldScroll() && scroll();
|
|
31
|
+
});
|
|
32
|
+
// for when children
|
|
33
|
+
const mutationObserver = new MutationObserver(() => {
|
|
34
|
+
log('observed mutation...');
|
|
35
|
+
shouldScroll() ? scroll() : newScrollableContentSignal?.set(true);
|
|
36
|
+
origScrollHeight = node.scrollHeight;
|
|
37
|
+
});
|
|
38
|
+
const unsubs = dependencies?.map((dep) => dep.subscribe((v) => {
|
|
39
|
+
log('dependency update...', v);
|
|
40
|
+
setTimeout(scroll, startScrollTimeout);
|
|
41
|
+
})) ?? [];
|
|
42
|
+
// observe size of all children
|
|
43
|
+
for (const child of node.children) {
|
|
44
|
+
resizeObserver.observe(child);
|
|
45
|
+
}
|
|
46
|
+
mutationObserver.observe(node, { childList: true, subtree: true });
|
|
47
|
+
return {
|
|
48
|
+
destroy() {
|
|
49
|
+
if (mutationObserver) {
|
|
50
|
+
mutationObserver.disconnect();
|
|
51
|
+
}
|
|
52
|
+
if (resizeObserver) {
|
|
53
|
+
resizeObserver.disconnect();
|
|
54
|
+
}
|
|
55
|
+
for (const unsubscribe of unsubs) {
|
|
56
|
+
unsubscribe();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// will notify and (optionally) execute on outside click/focusin/...
|
|
2
|
+
export const onOutside = (node, options) => {
|
|
3
|
+
const DEFAULT_OPTIONS = {
|
|
4
|
+
handler: null,
|
|
5
|
+
events: ['click', 'focusin'],
|
|
6
|
+
};
|
|
7
|
+
const { handler, events } = { ...DEFAULT_OPTIONS, ...(options || {}) };
|
|
8
|
+
const listener = (event) => {
|
|
9
|
+
if (!event?.target)
|
|
10
|
+
return;
|
|
11
|
+
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
|
12
|
+
node.dispatchEvent(new CustomEvent('outside', { detail: event.target }));
|
|
13
|
+
if (typeof handler === 'function')
|
|
14
|
+
handler(event.target);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
events?.forEach((eventName) => {
|
|
18
|
+
document.addEventListener(eventName, listener, true);
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
destroy() {
|
|
22
|
+
events?.forEach((eventName) => {
|
|
23
|
+
document.removeEventListener(eventName, listener, true);
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type TooltipLogger } from './tooltip.js';
|
|
2
|
+
export declare const _makeInVisible: (div: HTMLElement | null, arrow: HTMLElement | null, log: TooltipLogger) => void;
|
|
3
|
+
export declare const _makeVisible: (div: HTMLElement | null, arrow: HTMLElement | null, log: TooltipLogger) => void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { _TRANSITION_OPACITY_DUR } from './tooltip.js';
|
|
2
|
+
export const _makeInVisible = (div, arrow, log) => {
|
|
3
|
+
(div || arrow) && log('_makeInVisible');
|
|
4
|
+
div && (div.style.opacity = '0');
|
|
5
|
+
arrow && (arrow.style.opacity = '0');
|
|
6
|
+
setTimeout(() => {
|
|
7
|
+
arrow?.classList.add('hidden');
|
|
8
|
+
div?.classList.add('hidden');
|
|
9
|
+
if (div) {
|
|
10
|
+
div.style.left = `auto`;
|
|
11
|
+
div.style.top = `auto`;
|
|
12
|
+
}
|
|
13
|
+
}, _TRANSITION_OPACITY_DUR);
|
|
14
|
+
};
|
|
15
|
+
export const _makeVisible = (div, arrow, log) => {
|
|
16
|
+
(div || arrow) && log('_makeVisible');
|
|
17
|
+
if (div) {
|
|
18
|
+
div.classList.remove('hidden');
|
|
19
|
+
div.style.opacity = '1';
|
|
20
|
+
}
|
|
21
|
+
if (arrow) {
|
|
22
|
+
arrow.classList.remove('hidden');
|
|
23
|
+
arrow.style.opacity = '1';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { calculateAlignment } from '../../index.js';
|
|
2
|
+
import type { Alignment } from '../../utils/calculate-alignment.js';
|
|
3
|
+
import type { TooltipLogger, TooltipOptions } from './tooltip.js';
|
|
4
|
+
export declare const _maybePickSafePlacement: (calc: ReturnType<typeof calculateAlignment>, opts: TooltipOptions, log: TooltipLogger) => Alignment | false;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const _maybePickSafePlacement = (calc, opts, log) => {
|
|
2
|
+
let preferred = opts.alignment;
|
|
3
|
+
let picked = preferred;
|
|
4
|
+
const sides = {
|
|
5
|
+
top: ['top', 'topLeft', 'topRight'],
|
|
6
|
+
bottom: ['bottom', 'bottomLeft', 'bottomRight'],
|
|
7
|
+
left: ['left', 'leftTop', 'leftBottom'],
|
|
8
|
+
right: ['right', 'rightTop', 'rightBottom'],
|
|
9
|
+
};
|
|
10
|
+
// try alternatives within same side
|
|
11
|
+
const _trySideVariant = (val) => {
|
|
12
|
+
for (let side of Object.keys(sides)) {
|
|
13
|
+
if (val.startsWith(side)) {
|
|
14
|
+
for (let pos of sides[side]) {
|
|
15
|
+
if (calc.position[pos].safe) {
|
|
16
|
+
log(`_trySideVariant for '${val}', found '${pos}'`);
|
|
17
|
+
return pos;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
log(`_trySideVariant for '${val}', found none (returning orig '${val}')`);
|
|
23
|
+
return val;
|
|
24
|
+
};
|
|
25
|
+
// DRY
|
|
26
|
+
const _trySideOpposite = (val) => {
|
|
27
|
+
const opposites = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' };
|
|
28
|
+
for (let [k, v] of Object.entries(opposites)) {
|
|
29
|
+
if (val.startsWith(k)) {
|
|
30
|
+
const r = val.replace(k, v);
|
|
31
|
+
log(`_trySideOpposite for '${val}', found '${r}'`);
|
|
32
|
+
return r;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
log(`_trySideOpposite for '${val}', found none (returning orig '${val}')`);
|
|
36
|
+
return val;
|
|
37
|
+
};
|
|
38
|
+
const _switchAxis = (val) => {
|
|
39
|
+
const opposites = { top: 'right', bottom: 'right', left: 'top', right: 'top' };
|
|
40
|
+
for (let [k, v] of Object.entries(opposites)) {
|
|
41
|
+
if (val.startsWith(k)) {
|
|
42
|
+
log(`_switchAxis for '${val}', found '${v}'`);
|
|
43
|
+
return v;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
log(`_switchAxis for '${val}', found none (returning orig '${val}')`);
|
|
47
|
+
return val;
|
|
48
|
+
};
|
|
49
|
+
// kind of stupid, brute force approach...
|
|
50
|
+
// same side variant
|
|
51
|
+
if (!calc.position[picked].safe) {
|
|
52
|
+
picked = _trySideVariant(picked);
|
|
53
|
+
}
|
|
54
|
+
// oposite side
|
|
55
|
+
if (!calc.position[picked].safe) {
|
|
56
|
+
picked = _trySideOpposite(picked);
|
|
57
|
+
// oposite side variant
|
|
58
|
+
if (!calc.position[picked].safe) {
|
|
59
|
+
picked = _trySideVariant(picked);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// switch axis
|
|
63
|
+
if (!calc.position[picked].safe) {
|
|
64
|
+
picked = _switchAxis(picked);
|
|
65
|
+
// now second round
|
|
66
|
+
// same side variant
|
|
67
|
+
if (!calc.position[picked].safe) {
|
|
68
|
+
picked = _trySideVariant(picked);
|
|
69
|
+
}
|
|
70
|
+
// oposite side
|
|
71
|
+
if (!calc.position[picked].safe) {
|
|
72
|
+
picked = _trySideOpposite(picked);
|
|
73
|
+
// oposite side variant
|
|
74
|
+
if (!calc.position[picked].safe) {
|
|
75
|
+
picked = _trySideVariant(picked);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// finally, if still no luck, revert to
|
|
80
|
+
// a) either unsafe original (to avoid noise) and do not dance anymore, or
|
|
81
|
+
// b) hide (if configured so)
|
|
82
|
+
if (!calc.position[picked].safe) {
|
|
83
|
+
picked = opts.hideOnInsufficientSpace ? false : preferred;
|
|
84
|
+
}
|
|
85
|
+
return picked;
|
|
86
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { TooltipLogger, TooltipOptions } from './tooltip.js';
|
|
2
|
+
export declare const _setPosition: (boundaryRoot: HTMLElement | undefined, parent: HTMLElement, div: HTMLElement | null, arrow: HTMLElement | null, opts: TooltipOptions, log: TooltipLogger) => Promise<unknown>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { get as storeGet } from 'svelte/store';
|
|
2
|
+
import { calculateAlignment, windowSize } from '../../index.js';
|
|
3
|
+
import { _maybePickSafePlacement } from './_maybe-pick-safe-placement.js';
|
|
4
|
+
export const _setPosition = async (boundaryRoot, // will default to window dimensions
|
|
5
|
+
parent, div, arrow, opts, log) => {
|
|
6
|
+
// if (!div || !arrow) return log('_setPosition noop');
|
|
7
|
+
if (!div || !arrow)
|
|
8
|
+
return;
|
|
9
|
+
log('_setPosition');
|
|
10
|
+
const rootRect = boundaryRoot?.getBoundingClientRect() || {
|
|
11
|
+
...storeGet(windowSize),
|
|
12
|
+
x: 0,
|
|
13
|
+
y: 0,
|
|
14
|
+
};
|
|
15
|
+
const boundaryRootRect = {
|
|
16
|
+
x: rootRect.x,
|
|
17
|
+
y: rootRect.y,
|
|
18
|
+
width: rootRect.width,
|
|
19
|
+
height: rootRect.height,
|
|
20
|
+
};
|
|
21
|
+
const parentRect = parent.getBoundingClientRect();
|
|
22
|
+
// IMPORTANT!
|
|
23
|
+
// make sure the div is not hidden, so the rect dimensions will work fine
|
|
24
|
+
div.classList.remove('hidden');
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
// must wait for the next repaint to have correct dimensions...
|
|
27
|
+
requestAnimationFrame(() => {
|
|
28
|
+
const divRect = div.getBoundingClientRect();
|
|
29
|
+
const r = calculateAlignment(boundaryRootRect, parentRect, divRect, opts.offset);
|
|
30
|
+
log('tooltip rect', divRect);
|
|
31
|
+
log('placements', r);
|
|
32
|
+
// try to pick safe
|
|
33
|
+
const safe = _maybePickSafePlacement(r, opts, log);
|
|
34
|
+
// maybe quit...
|
|
35
|
+
if (safe === false) {
|
|
36
|
+
log('No safe position found...');
|
|
37
|
+
return resolve(false);
|
|
38
|
+
}
|
|
39
|
+
log(`Preferred: '${opts.alignment}', safe: '${safe}'.`);
|
|
40
|
+
// position the actual tooltip/popover
|
|
41
|
+
div.style.left = `${r.position[safe].x}px`;
|
|
42
|
+
div.style.top = `${r.position[safe].y}px`;
|
|
43
|
+
// div.style.left = `${0}px`;
|
|
44
|
+
// div.style.top = `${0}px`;
|
|
45
|
+
// now dance with the arrow...
|
|
46
|
+
let arrowStyles = {
|
|
47
|
+
borderStyle: 'solid',
|
|
48
|
+
left: 'auto',
|
|
49
|
+
top: 'auto',
|
|
50
|
+
};
|
|
51
|
+
// need to reset all on reposition
|
|
52
|
+
['borderTop', 'borderBottom', 'borderLeft', 'borderRight'].forEach((k) => {
|
|
53
|
+
arrowStyles[`${k}Width`] = '0';
|
|
54
|
+
arrowStyles[`${k}Color`] = null;
|
|
55
|
+
});
|
|
56
|
+
//
|
|
57
|
+
const arrowSize = opts.arrowSize;
|
|
58
|
+
let xOffset = 0;
|
|
59
|
+
let yOffset = 0;
|
|
60
|
+
//
|
|
61
|
+
if (safe.startsWith('top')) {
|
|
62
|
+
arrowStyles = {
|
|
63
|
+
...arrowStyles,
|
|
64
|
+
borderLeftColor: `transparent`,
|
|
65
|
+
borderRightColor: `transparent`,
|
|
66
|
+
borderLeftWidth: `${arrowSize * 0.75}px`,
|
|
67
|
+
borderRightWidth: `${arrowSize * 0.75}px`,
|
|
68
|
+
borderTopWidth: `${arrowSize}px`,
|
|
69
|
+
};
|
|
70
|
+
xOffset -= arrowSize / 2;
|
|
71
|
+
yOffset -= arrowSize * 0.1;
|
|
72
|
+
}
|
|
73
|
+
//
|
|
74
|
+
else if (safe.startsWith('bottom')) {
|
|
75
|
+
arrowStyles = {
|
|
76
|
+
...arrowStyles,
|
|
77
|
+
borderLeftColor: `transparent`,
|
|
78
|
+
borderRightColor: `transparent`,
|
|
79
|
+
borderLeftWidth: `${arrowSize * 0.75}px`,
|
|
80
|
+
borderRightWidth: `${arrowSize * 0.75}px`,
|
|
81
|
+
borderBottomWidth: `${arrowSize}px`,
|
|
82
|
+
};
|
|
83
|
+
xOffset -= arrowSize / 2;
|
|
84
|
+
yOffset -= arrowSize * 0.9;
|
|
85
|
+
}
|
|
86
|
+
//
|
|
87
|
+
else if (safe.startsWith('right')) {
|
|
88
|
+
arrowStyles = {
|
|
89
|
+
...arrowStyles,
|
|
90
|
+
borderTopColor: `transparent`,
|
|
91
|
+
borderBottomColor: `transparent`,
|
|
92
|
+
borderTopWidth: `${arrowSize * 0.75}px`,
|
|
93
|
+
borderBottomWidth: `${arrowSize * 0.75}px`,
|
|
94
|
+
borderRightWidth: `${arrowSize}px`,
|
|
95
|
+
};
|
|
96
|
+
xOffset -= arrowSize * 0.9;
|
|
97
|
+
yOffset -= arrowSize / 2;
|
|
98
|
+
}
|
|
99
|
+
//
|
|
100
|
+
else if (safe.startsWith('left')) {
|
|
101
|
+
arrowStyles = {
|
|
102
|
+
...arrowStyles,
|
|
103
|
+
borderTopColor: `transparent`,
|
|
104
|
+
borderBottomColor: `transparent`,
|
|
105
|
+
borderTopWidth: `${arrowSize * 0.75}px`,
|
|
106
|
+
borderBottomWidth: `${arrowSize * 0.75}px`,
|
|
107
|
+
borderLeftWidth: `${arrowSize}px`,
|
|
108
|
+
};
|
|
109
|
+
xOffset -= arrowSize * 0.1;
|
|
110
|
+
yOffset -= arrowSize / 2;
|
|
111
|
+
}
|
|
112
|
+
arrowStyles = {
|
|
113
|
+
...arrowStyles,
|
|
114
|
+
left: `${r.origin[safe].x + xOffset}px`,
|
|
115
|
+
top: `${r.origin[safe].y + yOffset}px`,
|
|
116
|
+
};
|
|
117
|
+
// log('applying arrowStyles', arrowStyles);
|
|
118
|
+
Object.entries(arrowStyles).forEach(([k, v]) => {
|
|
119
|
+
arrow.style[k] = v;
|
|
120
|
+
});
|
|
121
|
+
//
|
|
122
|
+
resolve(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
import { type Readable, type Writable } from 'svelte/store';
|
|
3
|
+
import type { Alignment } from '../../utils/calculate-alignment.js';
|
|
4
|
+
export declare class TooltipConfig {
|
|
5
|
+
static presetBase: string;
|
|
6
|
+
static class: string;
|
|
7
|
+
static arrowPresetBase: string;
|
|
8
|
+
static arrowClass: string;
|
|
9
|
+
static defaultOptions: Partial<TooltipOptions>;
|
|
10
|
+
}
|
|
11
|
+
export declare class PopoverConfig {
|
|
12
|
+
static presetBase: string;
|
|
13
|
+
static class: string;
|
|
14
|
+
static arrowPresetBase: string;
|
|
15
|
+
static arrowClass: string;
|
|
16
|
+
static defaultOptions: Partial<TooltipOptions>;
|
|
17
|
+
}
|
|
18
|
+
export type TooltipLogger = (...args: any[]) => void;
|
|
19
|
+
export interface TooltipOptions {
|
|
20
|
+
content: string;
|
|
21
|
+
popover: HTMLElement | null;
|
|
22
|
+
alignment: Alignment;
|
|
23
|
+
allowHtml: boolean;
|
|
24
|
+
delay: number;
|
|
25
|
+
class: string;
|
|
26
|
+
arrowClass: string;
|
|
27
|
+
triggers: string[];
|
|
28
|
+
logger?: TooltipLogger;
|
|
29
|
+
boundaryRoot?: HTMLElement;
|
|
30
|
+
arrowSize: number;
|
|
31
|
+
offset: number;
|
|
32
|
+
hideOnInsufficientSpace: boolean;
|
|
33
|
+
touch?: Readable<number>;
|
|
34
|
+
trigger?: Readable<boolean>;
|
|
35
|
+
notifier?: Writable<boolean>;
|
|
36
|
+
}
|
|
37
|
+
export declare const _TRANSITION_OPACITY_DUR = 150;
|
|
38
|
+
export declare function tooltip(node: HTMLElement, initialOptions?: string | Partial<TooltipOptions>): {
|
|
39
|
+
update(newOptions: string | Partial<TooltipOptions>): void;
|
|
40
|
+
destroy(): void;
|
|
41
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { createStore } from '@marianmeres/store';
|
|
2
|
+
import { derived, get as storeGet, writable, } from 'svelte/store';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
import { windowSize } from '../../index.js';
|
|
5
|
+
import { _makeInVisible, _makeVisible } from './_make-visible.js';
|
|
6
|
+
import { _setPosition } from './_set-position.js';
|
|
7
|
+
export class TooltipConfig {
|
|
8
|
+
static presetBase = `
|
|
9
|
+
text-sm
|
|
10
|
+
bg-gray-950 dark:bg-gray-950
|
|
11
|
+
text-gray-50 dark:text-gray-50
|
|
12
|
+
px-3 py-2
|
|
13
|
+
rounded-md
|
|
14
|
+
shadow-lg
|
|
15
|
+
z-50
|
|
16
|
+
max-w-96 w-max
|
|
17
|
+
`;
|
|
18
|
+
static class = '';
|
|
19
|
+
static arrowPresetBase = `border-gray-950 dark:border-gray-950 z-50`;
|
|
20
|
+
static arrowClass = ``;
|
|
21
|
+
static defaultOptions = {};
|
|
22
|
+
}
|
|
23
|
+
export class PopoverConfig {
|
|
24
|
+
static presetBase = `
|
|
25
|
+
text-sm
|
|
26
|
+
bg-gray-950 dark:bg-gray-950
|
|
27
|
+
text-gray-50 dark:text-gray-50
|
|
28
|
+
px-3 py-2
|
|
29
|
+
rounded-md
|
|
30
|
+
shadow-lg
|
|
31
|
+
z-50
|
|
32
|
+
`;
|
|
33
|
+
// max-w-96 w-max
|
|
34
|
+
static class = '';
|
|
35
|
+
static arrowPresetBase = `border-gray-950 dark:border-gray-950 z-50`;
|
|
36
|
+
static arrowClass = ``;
|
|
37
|
+
static defaultOptions = {};
|
|
38
|
+
}
|
|
39
|
+
const DEFAULTS = {
|
|
40
|
+
content: '',
|
|
41
|
+
popover: null,
|
|
42
|
+
alignment: 'top',
|
|
43
|
+
allowHtml: true,
|
|
44
|
+
delay: 300,
|
|
45
|
+
triggers: ['hover'], // 'focus', 'click'
|
|
46
|
+
class: '',
|
|
47
|
+
arrowClass: '',
|
|
48
|
+
arrowSize: 8,
|
|
49
|
+
offset: 10,
|
|
50
|
+
hideOnInsufficientSpace: false,
|
|
51
|
+
};
|
|
52
|
+
const TRIGGERS = {
|
|
53
|
+
hover: { show: ['mouseover'], hide: ['mouseout'] },
|
|
54
|
+
focus: { show: ['focusin'], hide: ['blur'] },
|
|
55
|
+
click: { show: ['click'], hide: [] }, // no hide for click, will toggle instead
|
|
56
|
+
};
|
|
57
|
+
// default TW value
|
|
58
|
+
export const _TRANSITION_OPACITY_DUR = 150;
|
|
59
|
+
const _ensureDiv = (div, opts, log) => {
|
|
60
|
+
log('_ensureDiv');
|
|
61
|
+
if (!div) {
|
|
62
|
+
log('creating tooltip div...');
|
|
63
|
+
div = document.createElement('div');
|
|
64
|
+
document.body.appendChild(div);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
log('div exists... going to apply classes');
|
|
68
|
+
}
|
|
69
|
+
let classes = '';
|
|
70
|
+
if (opts.popover) {
|
|
71
|
+
classes = [PopoverConfig.presetBase, PopoverConfig.class, opts.class].join(' ');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
classes = [TooltipConfig.presetBase, TooltipConfig.class, opts.class].join(' ');
|
|
75
|
+
}
|
|
76
|
+
// make sure these are never overwritten (must come last)
|
|
77
|
+
classes = twMerge(classes, 'fixed block transition-opacity')
|
|
78
|
+
.split(/\s/)
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
div.classList.add(...classes);
|
|
81
|
+
div.style.opacity = '0';
|
|
82
|
+
// log(`Div classes applied (+ opacity 0)`, classes);
|
|
83
|
+
return div;
|
|
84
|
+
};
|
|
85
|
+
const _ensureArrow = (arrow, opts, log) => {
|
|
86
|
+
log('_ensureArrow');
|
|
87
|
+
if (!arrow) {
|
|
88
|
+
log('creating tooltip arrow...');
|
|
89
|
+
arrow = document.createElement('div');
|
|
90
|
+
document.body.appendChild(arrow);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
log('arrow exists... going to apply classes');
|
|
94
|
+
}
|
|
95
|
+
let classes = '';
|
|
96
|
+
// prettier-ignore
|
|
97
|
+
if (opts.popover) {
|
|
98
|
+
classes = [PopoverConfig.arrowPresetBase, PopoverConfig.arrowClass, opts.arrowClass].join(' ');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
classes = [TooltipConfig.arrowPresetBase, TooltipConfig.arrowClass, opts.arrowClass].join(' ');
|
|
102
|
+
}
|
|
103
|
+
// make sure these are never overwritten (must come last)
|
|
104
|
+
classes = twMerge(classes, 'fixed block size-0 transition-opacity')
|
|
105
|
+
.split(/\s/)
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
arrow.classList.add(...classes);
|
|
108
|
+
arrow.style.opacity = '0';
|
|
109
|
+
// log(`Arrow classes applied (+ opacity 0)`, classes);
|
|
110
|
+
return arrow;
|
|
111
|
+
};
|
|
112
|
+
// the action api
|
|
113
|
+
export function tooltip(node, initialOptions = {}) {
|
|
114
|
+
if (typeof initialOptions === 'string')
|
|
115
|
+
initialOptions = { content: initialOptions };
|
|
116
|
+
//
|
|
117
|
+
initialOptions ??= {};
|
|
118
|
+
let defaults = DEFAULTS;
|
|
119
|
+
if (initialOptions.popover) {
|
|
120
|
+
defaults = { ...defaults, ...PopoverConfig.defaultOptions };
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
defaults = { ...defaults, ...TooltipConfig.defaultOptions };
|
|
124
|
+
}
|
|
125
|
+
let opts = { ...defaults, ...initialOptions };
|
|
126
|
+
const content = createStore(opts.content || '');
|
|
127
|
+
// maybe use aria-label (if present and content is not set)
|
|
128
|
+
const ariaLabel = node.getAttribute('aria-label');
|
|
129
|
+
if (ariaLabel && !content.get())
|
|
130
|
+
content.set(ariaLabel);
|
|
131
|
+
const _log = (...args) => typeof opts.logger === 'function' &&
|
|
132
|
+
opts.logger.apply(null, [`[tooltip/${node.id || '?'}]`, ...args]);
|
|
133
|
+
//
|
|
134
|
+
let _delayTimer;
|
|
135
|
+
const _resetDelayTimer = () => {
|
|
136
|
+
if (_delayTimer)
|
|
137
|
+
clearTimeout(_delayTimer);
|
|
138
|
+
_delayTimer = null;
|
|
139
|
+
};
|
|
140
|
+
const _planDelayedExec = (_fn, _delay) => {
|
|
141
|
+
_resetDelayTimer();
|
|
142
|
+
_delayTimer = setTimeout(() => {
|
|
143
|
+
_fn();
|
|
144
|
+
_resetDelayTimer();
|
|
145
|
+
}, _delay);
|
|
146
|
+
};
|
|
147
|
+
// use popover if provided, otherwise new div will be createed
|
|
148
|
+
let div = opts.popover;
|
|
149
|
+
let arrow;
|
|
150
|
+
let _isOn = writable(false); // internal state store
|
|
151
|
+
//
|
|
152
|
+
const _show = async () => {
|
|
153
|
+
_log('_show');
|
|
154
|
+
// return early on no content
|
|
155
|
+
if (!opts.popover && !content.get()) {
|
|
156
|
+
_log('Nothing to show (neither popover, nor content provided)');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// create, apply core styles and append to DOM
|
|
160
|
+
div = _ensureDiv(div, opts, _log);
|
|
161
|
+
arrow = _ensureArrow(arrow, opts, _log);
|
|
162
|
+
// set content
|
|
163
|
+
if (!opts.popover) {
|
|
164
|
+
if (opts.allowHtml)
|
|
165
|
+
div.innerHTML = content.get();
|
|
166
|
+
else
|
|
167
|
+
div.textContent = content.get();
|
|
168
|
+
}
|
|
169
|
+
// measure stuff and set position (provided opts.alignment is considered just as
|
|
170
|
+
// "preferred", which means it may be overwritten if there is no available space)
|
|
171
|
+
if (!(await _setPosition(opts.boundaryRoot, node, div, arrow, opts, _log))) {
|
|
172
|
+
_makeInVisible(div, arrow, _log);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// finally, fade in
|
|
176
|
+
_makeVisible(div, arrow, _log);
|
|
177
|
+
setTimeout(() => _isOn.set(true), _TRANSITION_OPACITY_DUR);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
let show = () => _planDelayedExec(_show, opts.delay);
|
|
181
|
+
//
|
|
182
|
+
const _hide = async () => {
|
|
183
|
+
_log('_hide');
|
|
184
|
+
_makeInVisible(div, arrow, _log);
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
destroy();
|
|
187
|
+
_isOn.set(false);
|
|
188
|
+
}, _TRANSITION_OPACITY_DUR);
|
|
189
|
+
};
|
|
190
|
+
const hide = () => _planDelayedExec(_hide, opts.delay);
|
|
191
|
+
// no delay for toggle
|
|
192
|
+
const toggle = async () => {
|
|
193
|
+
_log('toggle');
|
|
194
|
+
storeGet(_isOn) ? await _hide() : await _show();
|
|
195
|
+
};
|
|
196
|
+
//
|
|
197
|
+
const destroy = () => {
|
|
198
|
+
if (!div && !arrow && !opts.popover && !storeGet(_isOn))
|
|
199
|
+
return;
|
|
200
|
+
_log('destroy');
|
|
201
|
+
if (!opts.popover) {
|
|
202
|
+
div?.remove();
|
|
203
|
+
div = null;
|
|
204
|
+
}
|
|
205
|
+
arrow?.remove();
|
|
206
|
+
arrow = null;
|
|
207
|
+
_isOn.set(false);
|
|
208
|
+
};
|
|
209
|
+
//
|
|
210
|
+
let unsubs = [_isOn.subscribe((v) => opts?.notifier?.set(v))];
|
|
211
|
+
// by default, listen to windowSize change, as well as window and boundaryRoot scroll
|
|
212
|
+
const _boundaryRootScroll = writable(0);
|
|
213
|
+
const onScroll = () => _boundaryRootScroll.set(Date.now());
|
|
214
|
+
if (opts.boundaryRoot) {
|
|
215
|
+
opts.boundaryRoot.addEventListener('scroll', onScroll);
|
|
216
|
+
unsubs.push(() => opts.boundaryRoot?.removeEventListener('scroll', onScroll));
|
|
217
|
+
}
|
|
218
|
+
// also listen to window scroll
|
|
219
|
+
window.addEventListener('scroll', onScroll);
|
|
220
|
+
unsubs.push(() => window.removeEventListener('scroll', onScroll));
|
|
221
|
+
const _positionTriggers = [_boundaryRootScroll, windowSize];
|
|
222
|
+
if (opts.touch?.subscribe)
|
|
223
|
+
_positionTriggers.push(opts.touch);
|
|
224
|
+
const touch = derived(_positionTriggers, ([_]) => Date.now());
|
|
225
|
+
// final, derived, notifier
|
|
226
|
+
let _touchCount = 0;
|
|
227
|
+
unsubs.push(touch.subscribe(async () => {
|
|
228
|
+
// ignore first
|
|
229
|
+
if (_touchCount++) {
|
|
230
|
+
_log('touch...');
|
|
231
|
+
if (!(await _setPosition(opts.boundaryRoot, node, div, arrow, opts, _log))) {
|
|
232
|
+
_makeInVisible(div, arrow, _log);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
_makeVisible(div, arrow, _log);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}));
|
|
239
|
+
// important to normalize triggers - if does contain 'click' remove all others
|
|
240
|
+
if (opts.triggers.includes('click')) {
|
|
241
|
+
_log('Click trigger recognized...');
|
|
242
|
+
opts.triggers = ['click'];
|
|
243
|
+
_log('Patching show as toggle');
|
|
244
|
+
show = toggle; // also patch show/hide logic, since click does not have 'out' event...
|
|
245
|
+
}
|
|
246
|
+
// if manual trigger exists... (use raw show/hide, not the delayed one)
|
|
247
|
+
if (opts.trigger?.subscribe) {
|
|
248
|
+
unsubs.push(opts.trigger.subscribe((v) => (v ? _show() : _hide())));
|
|
249
|
+
}
|
|
250
|
+
// add show
|
|
251
|
+
opts.triggers?.forEach((trigger) => {
|
|
252
|
+
TRIGGERS[trigger]?.show.forEach((eventName) => {
|
|
253
|
+
_log('addEventListener', eventName, 'show');
|
|
254
|
+
node.addEventListener(eventName, show);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
// add hide
|
|
258
|
+
opts.triggers?.forEach((trigger) => {
|
|
259
|
+
TRIGGERS[trigger]?.hide.forEach((eventName) => {
|
|
260
|
+
_log('addEventListener', eventName, 'hide');
|
|
261
|
+
node.addEventListener(eventName, hide);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
//
|
|
266
|
+
update(newOptions) {
|
|
267
|
+
if (typeof newOptions === 'string')
|
|
268
|
+
newOptions = { content: newOptions };
|
|
269
|
+
destroy();
|
|
270
|
+
// these are not allowed to update on existing instance
|
|
271
|
+
delete newOptions.triggers;
|
|
272
|
+
delete newOptions.boundaryRoot;
|
|
273
|
+
delete newOptions.touch;
|
|
274
|
+
//
|
|
275
|
+
opts = { ...opts, ...newOptions };
|
|
276
|
+
content.set(opts.content);
|
|
277
|
+
},
|
|
278
|
+
//
|
|
279
|
+
destroy() {
|
|
280
|
+
unsubs.forEach((unsub) => typeof unsub === 'function' && unsub());
|
|
281
|
+
destroy();
|
|
282
|
+
// remove show
|
|
283
|
+
opts.triggers?.forEach((trigger) => {
|
|
284
|
+
TRIGGERS[trigger]?.show.forEach((eventName) => {
|
|
285
|
+
node.removeEventListener(eventName, show);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
// remove hide
|
|
289
|
+
opts.triggers?.forEach((trigger) => {
|
|
290
|
+
TRIGGERS[trigger]?.hide.forEach((eventName) => {
|
|
291
|
+
node.removeEventListener(eventName, hide);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -3,7 +3,7 @@ import { createSwitchStore } from "@marianmeres/switch-store";
|
|
|
3
3
|
import { createEventDispatcher } from "svelte";
|
|
4
4
|
import { fly } from "svelte/transition";
|
|
5
5
|
import { twMerge } from "tailwind-merge";
|
|
6
|
-
import {
|
|
6
|
+
import { onOutside } from "../../actions/on-outside.js";
|
|
7
7
|
import { prefersReducedMotionStore } from "../../utils/prefers-reduced-motion.js";
|
|
8
8
|
import Backdrop from "../Backdrop/Backdrop.svelte";
|
|
9
9
|
export const createDrawerStore = (open = false) => createSwitchStore(open);
|
|
@@ -78,7 +78,7 @@ $:
|
|
|
78
78
|
...(_presetsAnim[position] || {}),
|
|
79
79
|
}}
|
|
80
80
|
class={twMerge(`overflow-y-auto ${_presetsCls[position] || ''} ${_class}`)}
|
|
81
|
-
use:
|
|
81
|
+
use:onOutside
|
|
82
82
|
>
|
|
83
83
|
<slot />
|
|
84
84
|
</div>
|
|
@@ -11,7 +11,8 @@ export class SwitchConfig {
|
|
|
11
11
|
hover:brightness-[1.05] active:brightness-[0.95]
|
|
12
12
|
disabled:!cursor-not-allowed disabled:!opacity-50 disabled:hover:brightness-100
|
|
13
13
|
|
|
14
|
-
bg-zinc-300
|
|
14
|
+
bg-zinc-300 dark:bg-zinc-700
|
|
15
|
+
data-[checked=true]:bg-zinc-700 dark:data-[checked=true]:bg-zinc-300
|
|
15
16
|
`.trim();
|
|
16
17
|
static presetsSize = {
|
|
17
18
|
xs: "h-4 w-7",
|
|
@@ -29,7 +30,8 @@ export class SwitchConfig {
|
|
|
29
30
|
translate-x-1 rounded-full
|
|
30
31
|
transition-all duration-100
|
|
31
32
|
shadow
|
|
32
|
-
bg-white
|
|
33
|
+
bg-white dark:bg-black
|
|
34
|
+
text-black dark:text-white
|
|
33
35
|
`.trim();
|
|
34
36
|
static presetsSizeDot = {
|
|
35
37
|
// size + translate-x = width
|
|
@@ -34,7 +34,7 @@ declare const __propDef: {
|
|
|
34
34
|
variant?: string | undefined;
|
|
35
35
|
stopPropagation?: boolean | undefined;
|
|
36
36
|
preventDefault?: boolean | undefined;
|
|
37
|
-
preHook?: ((
|
|
37
|
+
preHook?: ((previosValue: boolean) => Promise<void | boolean>) | undefined;
|
|
38
38
|
size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
|
|
39
39
|
};
|
|
40
40
|
events: {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script>import { tooltip } from "../..";
|
|
2
|
+
export let options = {};
|
|
3
|
+
const POPOVER_DEFAULTS = {
|
|
4
|
+
hideOnInsufficientSpace: true,
|
|
5
|
+
triggers: ["click"]
|
|
6
|
+
};
|
|
7
|
+
let popover;
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#if popover}
|
|
11
|
+
<span
|
|
12
|
+
use:tooltip={{
|
|
13
|
+
...POPOVER_DEFAULTS,
|
|
14
|
+
...(options || {}),
|
|
15
|
+
popover,
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
<slot />
|
|
19
|
+
</span>
|
|
20
|
+
{/if}
|
|
21
|
+
|
|
22
|
+
<div bind:this={popover} class="hidden">
|
|
23
|
+
<slot name="popover" />
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
import type { TooltipOptions } from '../../actions/tooltip/tooltip.js';
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: {
|
|
5
|
+
options?: Partial<TooltipOptions> | undefined;
|
|
6
|
+
};
|
|
7
|
+
events: {
|
|
8
|
+
[evt: string]: CustomEvent<any>;
|
|
9
|
+
};
|
|
10
|
+
slots: {
|
|
11
|
+
default: {};
|
|
12
|
+
popover: {};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type PopoverProps = typeof __propDef.props;
|
|
16
|
+
export type PopoverEvents = typeof __propDef.events;
|
|
17
|
+
export type PopoverSlots = typeof __propDef.slots;
|
|
18
|
+
export default class Popover extends SvelteComponent<PopoverProps, PopoverEvents, PopoverSlots> {
|
|
19
|
+
}
|
|
20
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ export { default as Drawer, createDrawerStore } from './components/Drawer/Drawer
|
|
|
8
8
|
export { default as HoverExpandableWidth } from './components/HoverExpandableWidth/HoverExpandableWidth.svelte';
|
|
9
9
|
export { default as Switch } from './components/Switch/Switch.svelte';
|
|
10
10
|
export { default as X } from './components/X/X.svelte';
|
|
11
|
-
export { clickOutside } from './actions/click-outside.js';
|
|
12
11
|
export { focusTrap } from './actions/focus-trap.js';
|
|
13
|
-
export {
|
|
12
|
+
export { onOutside } from './actions/on-outside.js';
|
|
13
|
+
export { tooltip, TooltipConfig } from './actions/tooltip/tooltip.js';
|
|
14
|
+
export { calculateAlignment } from './utils/calculate-alignment.js';
|
|
14
15
|
export { DevicePointer } from './utils/device-pointer.js';
|
|
16
|
+
export { getId } from './utils/get-id.js';
|
|
17
|
+
export { windowSize, breakpoint } from './utils/window-size.js';
|
package/dist/index.js
CHANGED
|
@@ -18,8 +18,11 @@ export { default as Switch } from './components/Switch/Switch.svelte';
|
|
|
18
18
|
//
|
|
19
19
|
export { default as X } from './components/X/X.svelte';
|
|
20
20
|
// actions
|
|
21
|
-
export { clickOutside } from './actions/click-outside.js';
|
|
22
21
|
export { focusTrap } from './actions/focus-trap.js';
|
|
22
|
+
export { onOutside } from './actions/on-outside.js';
|
|
23
|
+
export { tooltip, TooltipConfig } from './actions/tooltip/tooltip.js';
|
|
23
24
|
// utils
|
|
24
|
-
export {
|
|
25
|
+
export { calculateAlignment } from './utils/calculate-alignment.js';
|
|
25
26
|
export { DevicePointer } from './utils/device-pointer.js';
|
|
27
|
+
export { getId } from './utils/get-id.js';
|
|
28
|
+
export { windowSize, breakpoint } from './utils/window-size.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface AreaLike {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
export interface PositionLike {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RectLike extends AreaLike, PositionLike {
|
|
10
|
+
}
|
|
11
|
+
export type Alignment = 'top' | 'topRight' | 'topLeft' | 'right' | 'rightTop' | 'rightBottom' | 'bottom' | 'bottomRight' | 'bottomLeft' | 'left' | 'leftTop' | 'leftBottom';
|
|
12
|
+
export declare const calculateAlignment: (boundaryRoot: RectLike, anchor: RectLike, el: AreaLike, offset?: number) => {
|
|
13
|
+
origin: {
|
|
14
|
+
top: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
topRight: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
topLeft: {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
};
|
|
26
|
+
bottom: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
30
|
+
bottomRight: {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
};
|
|
34
|
+
bottomLeft: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
};
|
|
38
|
+
left: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
};
|
|
42
|
+
leftTop: {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
};
|
|
46
|
+
leftBottom: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
};
|
|
50
|
+
right: {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
};
|
|
54
|
+
rightTop: {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
};
|
|
58
|
+
rightBottom: {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
position: Record<Alignment, PositionLike & {
|
|
64
|
+
safeY: boolean;
|
|
65
|
+
safeX: boolean;
|
|
66
|
+
safe: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const clog = console.log;
|
|
2
|
+
// just basic plus/minus stuff
|
|
3
|
+
export const calculateAlignment = (boundaryRoot,
|
|
4
|
+
// anchor box
|
|
5
|
+
anchor,
|
|
6
|
+
// element to be placed (the one we're calculating position for)
|
|
7
|
+
el,
|
|
8
|
+
// in px
|
|
9
|
+
offset = 5) => {
|
|
10
|
+
const anchorHalfWidth = anchor.width / 2;
|
|
11
|
+
const anchorHalfHeight = anchor.height / 2;
|
|
12
|
+
// x/y of the origin (center of the anchor with applied offset) position
|
|
13
|
+
const originTop = { x: anchor.x + anchorHalfWidth, y: anchor.y - offset };
|
|
14
|
+
const originBottom = {
|
|
15
|
+
x: anchor.x + anchorHalfWidth,
|
|
16
|
+
y: anchor.y + anchor.height + offset,
|
|
17
|
+
};
|
|
18
|
+
const originLeft = { x: anchor.x - offset, y: anchor.y + anchorHalfHeight };
|
|
19
|
+
const originRight = {
|
|
20
|
+
x: anchor.x + anchor.width + offset,
|
|
21
|
+
y: anchor.y + anchorHalfHeight,
|
|
22
|
+
};
|
|
23
|
+
const origin = {
|
|
24
|
+
top: originTop,
|
|
25
|
+
topRight: originTop,
|
|
26
|
+
topLeft: originTop,
|
|
27
|
+
//
|
|
28
|
+
bottom: originBottom,
|
|
29
|
+
bottomRight: originBottom,
|
|
30
|
+
bottomLeft: originBottom,
|
|
31
|
+
//
|
|
32
|
+
left: originLeft,
|
|
33
|
+
leftTop: originLeft,
|
|
34
|
+
leftBottom: originLeft,
|
|
35
|
+
//
|
|
36
|
+
right: originRight,
|
|
37
|
+
rightTop: originRight,
|
|
38
|
+
rightBottom: originRight,
|
|
39
|
+
};
|
|
40
|
+
const elHalfWidth = el.width / 2;
|
|
41
|
+
const elHalfHeigh = el.height / 2;
|
|
42
|
+
// position is named from the "anchor alignment" relative perspective
|
|
43
|
+
const position = {
|
|
44
|
+
//
|
|
45
|
+
top: {
|
|
46
|
+
x: origin.top.x - elHalfWidth,
|
|
47
|
+
y: origin.top.y - el.height,
|
|
48
|
+
safeX: true,
|
|
49
|
+
safeY: true,
|
|
50
|
+
safe: true,
|
|
51
|
+
},
|
|
52
|
+
topRight: {
|
|
53
|
+
x: origin.top.x + anchorHalfWidth - el.width,
|
|
54
|
+
y: origin.top.y - el.height,
|
|
55
|
+
safeX: true,
|
|
56
|
+
safeY: true,
|
|
57
|
+
safe: true,
|
|
58
|
+
},
|
|
59
|
+
topLeft: {
|
|
60
|
+
x: origin.top.x - anchorHalfWidth,
|
|
61
|
+
y: origin.top.y - el.height,
|
|
62
|
+
safeX: true,
|
|
63
|
+
safeY: true,
|
|
64
|
+
safe: true,
|
|
65
|
+
},
|
|
66
|
+
right: {
|
|
67
|
+
x: origin.right.x,
|
|
68
|
+
y: origin.right.y - elHalfHeigh,
|
|
69
|
+
safeX: true,
|
|
70
|
+
safeY: true,
|
|
71
|
+
safe: true,
|
|
72
|
+
},
|
|
73
|
+
rightTop: {
|
|
74
|
+
x: origin.right.x,
|
|
75
|
+
y: origin.right.y - anchorHalfHeight,
|
|
76
|
+
safeX: true,
|
|
77
|
+
safeY: true,
|
|
78
|
+
safe: true,
|
|
79
|
+
},
|
|
80
|
+
rightBottom: {
|
|
81
|
+
x: origin.right.x,
|
|
82
|
+
y: origin.right.y + anchorHalfHeight - el.height,
|
|
83
|
+
safeX: true,
|
|
84
|
+
safeY: true,
|
|
85
|
+
safe: true,
|
|
86
|
+
},
|
|
87
|
+
//
|
|
88
|
+
bottom: {
|
|
89
|
+
x: origin.bottom.x - elHalfWidth,
|
|
90
|
+
y: origin.bottom.y,
|
|
91
|
+
safeX: true,
|
|
92
|
+
safeY: true,
|
|
93
|
+
safe: true,
|
|
94
|
+
},
|
|
95
|
+
bottomLeft: {
|
|
96
|
+
x: origin.bottom.x - anchorHalfWidth,
|
|
97
|
+
y: origin.bottom.y,
|
|
98
|
+
safeX: true,
|
|
99
|
+
safeY: true,
|
|
100
|
+
safe: true,
|
|
101
|
+
},
|
|
102
|
+
bottomRight: {
|
|
103
|
+
x: origin.bottom.x + anchorHalfWidth - el.width,
|
|
104
|
+
y: origin.bottom.y,
|
|
105
|
+
safeX: true,
|
|
106
|
+
safeY: true,
|
|
107
|
+
safe: true,
|
|
108
|
+
},
|
|
109
|
+
//
|
|
110
|
+
left: {
|
|
111
|
+
x: origin.left.x - el.width,
|
|
112
|
+
y: origin.left.y - elHalfHeigh,
|
|
113
|
+
safeX: true,
|
|
114
|
+
safeY: true,
|
|
115
|
+
safe: true,
|
|
116
|
+
},
|
|
117
|
+
leftTop: {
|
|
118
|
+
x: origin.left.x - el.width,
|
|
119
|
+
y: origin.left.y - anchorHalfHeight,
|
|
120
|
+
safeX: true,
|
|
121
|
+
safeY: true,
|
|
122
|
+
safe: true,
|
|
123
|
+
},
|
|
124
|
+
leftBottom: {
|
|
125
|
+
x: origin.left.x - el.width,
|
|
126
|
+
y: origin.left.y + anchorHalfHeight - el.height,
|
|
127
|
+
safeX: true,
|
|
128
|
+
safeY: true,
|
|
129
|
+
safe: true,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const isSafeX = (x) => x >= boundaryRoot.x && x + el.width <= boundaryRoot.x + boundaryRoot.width;
|
|
133
|
+
const isSafeY = (y) => y >= boundaryRoot.y && y + el.height <= boundaryRoot.y + boundaryRoot.height;
|
|
134
|
+
//
|
|
135
|
+
Object.entries(position).forEach((entry) => {
|
|
136
|
+
const [plc, { x, y }] = entry;
|
|
137
|
+
position[plc].safeX = isSafeX(x);
|
|
138
|
+
position[plc].safeY = isSafeY(y);
|
|
139
|
+
position[plc].safe = position[plc].safeX && position[plc].safeY;
|
|
140
|
+
});
|
|
141
|
+
// now, the positions were calculated, but so far, we're too strict, so there will
|
|
142
|
+
// be unnecessary false negatives, which can be relaxed using "fluid" approach
|
|
143
|
+
// must come below safety check above
|
|
144
|
+
const fluid = {
|
|
145
|
+
x: {
|
|
146
|
+
top: Math.max(0, position.top.x),
|
|
147
|
+
topLeft: position.topLeft.x - (position.topLeft.x + el.width - boundaryRoot.width),
|
|
148
|
+
topRight: Math.max(0, position.topRight.x),
|
|
149
|
+
//
|
|
150
|
+
bottom: Math.max(0, position.bottom.x),
|
|
151
|
+
bottomLeft: position.bottomLeft.x - (position.bottomLeft.x + el.width - boundaryRoot.width),
|
|
152
|
+
bottomRight: Math.max(0, position.bottomRight.x),
|
|
153
|
+
},
|
|
154
|
+
y: {
|
|
155
|
+
// right: position.top.y - (position.top.y + el.height - boundaryRoot.height),
|
|
156
|
+
right: Math.max(0, position.right.y),
|
|
157
|
+
rightTop: Math.max(0, position.rightTop.y),
|
|
158
|
+
rightBottom: Math.max(0, position.rightBottom.y),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
// x axis
|
|
162
|
+
if (el.width < boundaryRoot.width && anchor.x > 0) {
|
|
163
|
+
['top', 'topLeft', 'topRight', 'bottom', 'bottomLeft', 'bottomRight'].forEach((k) => {
|
|
164
|
+
if (!position[k].safeX && fluid.x?.[k] !== undefined) {
|
|
165
|
+
position[k].x = fluid.x[k];
|
|
166
|
+
position[k].safeX = isSafeX(position[k].x);
|
|
167
|
+
position[k].safe = position[k].safeX && position[k].safeY;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// y
|
|
172
|
+
if (el.height < boundaryRoot.height && anchor.y > 0) {
|
|
173
|
+
['right', 'rightTop', 'rightBottom', 'left', 'leftTop', 'leftBottom'].forEach((k) => {
|
|
174
|
+
if (!position[k].safeY && fluid.y?.[k] !== undefined) {
|
|
175
|
+
position[k].y = fluid.y[k];
|
|
176
|
+
position[k].safeY = isSafeY(position[k].y);
|
|
177
|
+
position[k].safe = position[k].safeX && position[k].safeY;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// todo y fluids
|
|
182
|
+
return { origin, position };
|
|
183
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getId: (prefix?: string) => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run package && node ./scripts/date.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"format": "prettier --write .",
|
|
15
15
|
"prettier": "npm run format",
|
|
16
16
|
"release:patch": "release -v patch",
|
|
17
|
-
"release": "release -v minor"
|
|
17
|
+
"release": "release -v minor",
|
|
18
|
+
"test": "vitest"
|
|
18
19
|
},
|
|
19
20
|
"exports": {
|
|
20
21
|
".": {
|
|
@@ -38,9 +39,11 @@
|
|
|
38
39
|
"@sveltejs/adapter-auto": "^2.0.0",
|
|
39
40
|
"@sveltejs/kit": "^1.27.4",
|
|
40
41
|
"@sveltejs/package": "^2.0.0",
|
|
42
|
+
"@types/node": "^20.11.0",
|
|
41
43
|
"autoprefixer": "^10.4.16",
|
|
42
44
|
"clsx": "^2.0.0",
|
|
43
45
|
"esm-env": "^1.0.0",
|
|
46
|
+
"nodemon": "^3.0.2",
|
|
44
47
|
"postcss": "^8.4.32",
|
|
45
48
|
"prettier": "^3.0.0",
|
|
46
49
|
"prettier-plugin-svelte": "^3.0.0",
|
|
@@ -49,15 +52,18 @@
|
|
|
49
52
|
"svelte": "^4.2.7",
|
|
50
53
|
"svelte-check": "^3.6.0",
|
|
51
54
|
"tailwindcss": "^3.3.6",
|
|
55
|
+
"ts-node": "^10.9.2",
|
|
52
56
|
"tslib": "^2.4.1",
|
|
53
57
|
"typescript": "^5.0.0",
|
|
54
|
-
"vite": "^4.4.2"
|
|
58
|
+
"vite": "^4.4.2",
|
|
59
|
+
"vitest": "^1.1.3"
|
|
55
60
|
},
|
|
56
61
|
"svelte": "./dist/index.js",
|
|
57
62
|
"types": "./dist/index.d.ts",
|
|
58
63
|
"type": "module",
|
|
59
64
|
"dependencies": {
|
|
60
65
|
"@marianmeres/clog": "^1.0.1",
|
|
66
|
+
"@marianmeres/store": "^1.5.0",
|
|
61
67
|
"@marianmeres/switch-store": "^1.3.1",
|
|
62
68
|
"tailwind-merge": "^2.1.0"
|
|
63
69
|
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export const clickOutside = (node, callback) => {
|
|
2
|
-
const handleClick = (event) => {
|
|
3
|
-
if (!event?.target)
|
|
4
|
-
return;
|
|
5
|
-
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
|
6
|
-
callback();
|
|
7
|
-
}
|
|
8
|
-
};
|
|
9
|
-
document.addEventListener('click', handleClick, true);
|
|
10
|
-
return {
|
|
11
|
-
destroy() {
|
|
12
|
-
document.removeEventListener('click', handleClick, true);
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
};
|