autumnnote 2.1.0 → 2.3.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/README.md +74 -6
- package/dist/autumnnote.cjs +27 -22
- package/dist/autumnnote.es.js +474 -591
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +27 -22
- package/dist/autumnnote.umd.js +27 -22
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +12 -3
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -38
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -304
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -322
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1392
- package/src/js/module/Toolbar.js +0 -855
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/module/table-grid.js +0 -102
- package/src/js/module/table-icons.js +0 -33
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2877
package/src/js/index.umd.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* index.umd.js — UMD entry point for AutumnNote
|
|
3
|
-
*
|
|
4
|
-
* Re-exports only the default export so the UMD global is the factory object
|
|
5
|
-
* directly, enabling the script-tag usage documented in the README:
|
|
6
|
-
*
|
|
7
|
-
* <script src="dist/autumnnote.umd.js"></script>
|
|
8
|
-
* <script>
|
|
9
|
-
* const editor = AutumnNote.create('#my-editor');
|
|
10
|
-
* </script>
|
|
11
|
-
*/
|
|
12
|
-
// Script-tag consumers have no bundler to tree-shake with, so every locale is
|
|
13
|
-
// pre-registered here and `lang: 'vi'` works without extra imports. ESM
|
|
14
|
-
// consumers opt in per locale via `autumnnote/i18n/<code>` instead.
|
|
15
|
-
import './i18n/all.js';
|
|
16
|
-
|
|
17
|
-
export { default } from './index.js';
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AutoSaveRestore.js — Detects a previously auto-saved draft and offers the
|
|
3
|
-
* user a banner to restore or discard it.
|
|
4
|
-
*
|
|
5
|
-
* Activated when both `autoSave` and `autoSaveRestore` options are true.
|
|
6
|
-
* On initialize it checks localStorage for a draft that is within the
|
|
7
|
-
* `autoSaveRestoreTimeout` day globalThis. If one is found a dismissible banner
|
|
8
|
-
* is prepended to the editor container.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export class AutoSaveRestore {
|
|
12
|
-
/** @param {import('../Context.js').Context} context */
|
|
13
|
-
constructor(context) {
|
|
14
|
-
this.context = context;
|
|
15
|
-
this.options = context.options;
|
|
16
|
-
|
|
17
|
-
/** @type {HTMLElement|null} */
|
|
18
|
-
this._banner = null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
initialize() {
|
|
22
|
-
if (!this.options.autoSave || !this.options.autoSaveRestore) return this;
|
|
23
|
-
|
|
24
|
-
const key = this.options.autoSaveKey;
|
|
25
|
-
const metaKey = key + ':asrmeta';
|
|
26
|
-
|
|
27
|
-
let saved;
|
|
28
|
-
let meta;
|
|
29
|
-
try {
|
|
30
|
-
saved = localStorage.getItem(key);
|
|
31
|
-
meta = JSON.parse(localStorage.getItem(metaKey) || '{}');
|
|
32
|
-
} catch (_) {
|
|
33
|
-
void _;
|
|
34
|
-
return this;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!saved) return this;
|
|
38
|
-
|
|
39
|
-
const timeout = this.options.autoSaveRestoreTimeout;
|
|
40
|
-
if (timeout > 0) {
|
|
41
|
-
const ageMs = Date.now() - (meta.savedAt || 0);
|
|
42
|
-
if (ageMs > timeout * 86400000) {
|
|
43
|
-
try { localStorage.removeItem(key); localStorage.removeItem(metaKey); } catch (_) { void _; }
|
|
44
|
-
return this;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this._showBanner(saved, meta);
|
|
49
|
-
return this;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
destroy() {
|
|
53
|
-
this._removeBanner();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Internal
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
_showBanner(draftHtml, meta) {
|
|
61
|
-
const locale = this.context.locale;
|
|
62
|
-
const t = locale.autoSaveRestore || {};
|
|
63
|
-
const label = t.found || 'Draft found. Restore?';
|
|
64
|
-
const restoreLabel = t.restore || 'Restore';
|
|
65
|
-
const discardLabel = t.discard || 'Discard';
|
|
66
|
-
|
|
67
|
-
const banner = document.createElement('div');
|
|
68
|
-
banner.className = 'an-asr-banner';
|
|
69
|
-
banner.setAttribute('role', 'alert');
|
|
70
|
-
|
|
71
|
-
const msg = document.createElement('span');
|
|
72
|
-
msg.className = 'an-asr-msg';
|
|
73
|
-
if (meta.savedAt) {
|
|
74
|
-
const d = new Date(meta.savedAt);
|
|
75
|
-
const formatted = d.toLocaleString();
|
|
76
|
-
msg.textContent = (t.foundAt || 'Draft from {date}. Restore?').replace('{date}', formatted);
|
|
77
|
-
} else {
|
|
78
|
-
msg.textContent = label;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const restoreBtn = document.createElement('button');
|
|
82
|
-
restoreBtn.type = 'button';
|
|
83
|
-
restoreBtn.className = 'an-btn an-asr-btn-restore';
|
|
84
|
-
restoreBtn.textContent = restoreLabel;
|
|
85
|
-
restoreBtn.addEventListener('click', () => this._restore(draftHtml));
|
|
86
|
-
|
|
87
|
-
const discardBtn = document.createElement('button');
|
|
88
|
-
discardBtn.type = 'button';
|
|
89
|
-
discardBtn.className = 'an-btn an-asr-btn-discard';
|
|
90
|
-
discardBtn.textContent = discardLabel;
|
|
91
|
-
discardBtn.addEventListener('click', () => this._discard());
|
|
92
|
-
|
|
93
|
-
banner.appendChild(msg);
|
|
94
|
-
banner.appendChild(restoreBtn);
|
|
95
|
-
banner.appendChild(discardBtn);
|
|
96
|
-
|
|
97
|
-
this._banner = banner;
|
|
98
|
-
const container = this.context.layoutInfo.container;
|
|
99
|
-
container.insertBefore(banner, container.firstChild);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
_restore(draftHtml) {
|
|
103
|
-
this.context.setHTML(draftHtml);
|
|
104
|
-
this.context.clearHistory();
|
|
105
|
-
this._removeBanner();
|
|
106
|
-
|
|
107
|
-
if (typeof this.options.onAutoSaveRestore === 'function') {
|
|
108
|
-
this.options.onAutoSaveRestore(draftHtml, this.context);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
_discard() {
|
|
113
|
-
const key = this.options.autoSaveKey;
|
|
114
|
-
try {
|
|
115
|
-
localStorage.removeItem(key);
|
|
116
|
-
localStorage.removeItem(key + ':asrmeta');
|
|
117
|
-
} catch (_) { void _; }
|
|
118
|
-
this._removeBanner();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
_removeBanner() {
|
|
122
|
-
this._banner?.remove();
|
|
123
|
-
this._banner = null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
2
|
-
import { withSavedRange } from '../core/range.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Shared lifecycle and shell-building logic for all modal dialogs.
|
|
6
|
-
* Subclasses implement _buildDialog() for their specific form fields.
|
|
7
|
-
*/
|
|
8
|
-
export class BaseDialog {
|
|
9
|
-
/** @param {import('../Context.js').Context} context */
|
|
10
|
-
constructor(context) {
|
|
11
|
-
this.context = context;
|
|
12
|
-
this.options = context.options;
|
|
13
|
-
/** @type {HTMLElement|null} */
|
|
14
|
-
this._dialog = null;
|
|
15
|
-
this._disposers = [];
|
|
16
|
-
this._savedRange = null;
|
|
17
|
-
/** @type {HTMLElement|null} First focusable input; set by subclass in _buildDialog(). */
|
|
18
|
-
this._firstInput = null;
|
|
19
|
-
this._removeTrap = null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Abstract — subclasses must override
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
/** @returns {HTMLElement} */
|
|
27
|
-
_buildDialog() { throw new Error('_buildDialog() must be implemented by subclass'); }
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Lifecycle (shared)
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
initialize() {
|
|
34
|
-
this._dialog = this._buildDialog();
|
|
35
|
-
document.body.appendChild(this._dialog);
|
|
36
|
-
return this;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
destroy() {
|
|
40
|
-
this._disposers.forEach((d) => d());
|
|
41
|
-
this._disposers = [];
|
|
42
|
-
if (this._dialog?.parentNode) {
|
|
43
|
-
this._dialog.remove();
|
|
44
|
-
}
|
|
45
|
-
this._dialog = null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Shared show helpers
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
/** Saves the current selection range before opening the dialog. */
|
|
53
|
-
_saveRange() {
|
|
54
|
-
withSavedRange((range) => { this._savedRange = range; });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
_open() {
|
|
58
|
-
if (this._dialog) {
|
|
59
|
-
this._dialog.style.display = 'flex';
|
|
60
|
-
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
61
|
-
setTimeout(() => this._firstInput?.focus(), 50);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
_close() {
|
|
66
|
-
if (this._dialog) this._dialog.style.display = 'none';
|
|
67
|
-
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
68
|
-
this._savedRange = null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Shell builders (used by subclass _buildDialog())
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Builds the overlay + box + header shell common to all dialogs.
|
|
77
|
-
* Also wires up draggable and overlay-click-to-close.
|
|
78
|
-
* @param {string} ariaLabel
|
|
79
|
-
* @param {string} iconHtml Raw SVG string for the dialog icon
|
|
80
|
-
* @param {string} titleText
|
|
81
|
-
* @returns {{ overlay: HTMLElement, box: HTMLElement }}
|
|
82
|
-
*/
|
|
83
|
-
_buildDialogShell(ariaLabel, iconHtml, titleText) {
|
|
84
|
-
const overlay = createElement('div', {
|
|
85
|
-
class: 'an-dialog-overlay',
|
|
86
|
-
role: 'dialog',
|
|
87
|
-
'aria-modal': 'true',
|
|
88
|
-
'aria-label': ariaLabel,
|
|
89
|
-
});
|
|
90
|
-
const box = createElement('div', { class: 'an-dialog-box' });
|
|
91
|
-
|
|
92
|
-
const header = createElement('div', { class: 'an-dialog-header' });
|
|
93
|
-
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
94
|
-
iconEl.innerHTML = iconHtml;
|
|
95
|
-
const titleEl = createElement('h3', { class: 'an-dialog-title' });
|
|
96
|
-
titleEl.textContent = titleText;
|
|
97
|
-
header.appendChild(iconEl);
|
|
98
|
-
header.appendChild(titleEl);
|
|
99
|
-
|
|
100
|
-
box.appendChild(header);
|
|
101
|
-
overlay.appendChild(box);
|
|
102
|
-
makeDraggable(header, box);
|
|
103
|
-
|
|
104
|
-
const d = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
105
|
-
this._disposers.push(d);
|
|
106
|
-
|
|
107
|
-
return { overlay, box };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Builds an action button row with a primary insert button and a cancel button.
|
|
112
|
-
* Disposers are registered automatically.
|
|
113
|
-
* @param {string} insertLabel
|
|
114
|
-
* @param {string} cancelLabel
|
|
115
|
-
* @param {() => void} onInsert
|
|
116
|
-
* @returns {HTMLElement}
|
|
117
|
-
*/
|
|
118
|
-
_buildButtonRow(insertLabel, cancelLabel, onInsert) {
|
|
119
|
-
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
120
|
-
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
121
|
-
insertBtn.textContent = insertLabel;
|
|
122
|
-
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
123
|
-
cancelBtn.textContent = cancelLabel;
|
|
124
|
-
btnRow.appendChild(insertBtn);
|
|
125
|
-
btnRow.appendChild(cancelBtn);
|
|
126
|
-
|
|
127
|
-
const d1 = on(insertBtn, 'click', onInsert);
|
|
128
|
-
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
129
|
-
this._disposers.push(d1, d2);
|
|
130
|
-
|
|
131
|
-
return btnRow;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BaseMediaTooltip.js — Shared hover-tooltip behaviour for media embeds.
|
|
3
|
-
*
|
|
4
|
-
* ImageTooltip and VideoTooltip duplicated their button factory, show/hide
|
|
5
|
-
* timers and viewport-aware positioning (~22% of both files). Those pieces live
|
|
6
|
-
* here; each subclass keeps only what genuinely differs — which element counts
|
|
7
|
-
* as the target, which buttons the bar contains, and the actions behind them.
|
|
8
|
-
*
|
|
9
|
-
* Mirrors the existing BaseDialog / BaseResizer pattern.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { createElement, on } from '../core/dom.js';
|
|
13
|
-
|
|
14
|
-
/** Delay before a hovered target shows its tooltip. */
|
|
15
|
-
export const SHOW_DELAY = 100;
|
|
16
|
-
/** Grace period before a tooltip hides after the pointer leaves. */
|
|
17
|
-
export const HIDE_DELAY = 180;
|
|
18
|
-
|
|
19
|
-
export class BaseMediaTooltip {
|
|
20
|
-
/** @param {import('../Context.js').Context} context */
|
|
21
|
-
constructor(context) {
|
|
22
|
-
this.context = context;
|
|
23
|
-
this.options = context.options;
|
|
24
|
-
/** @type {HTMLElement|null} The tooltip bar */
|
|
25
|
-
this._el = null;
|
|
26
|
-
/** @type {HTMLElement|null} Currently hovered target */
|
|
27
|
-
this._active = null;
|
|
28
|
-
this._showTimer = null;
|
|
29
|
-
this._hideTimer = null;
|
|
30
|
-
this._disposers = [];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Subclass hooks
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Whether `_scheduleHide` is allowed to start the hide countdown. Subclasses
|
|
39
|
-
* override to pin the tooltip open (e.g. while a video preview is playing).
|
|
40
|
-
* @returns {boolean}
|
|
41
|
-
*/
|
|
42
|
-
_canHide() { return true; }
|
|
43
|
-
|
|
44
|
-
/** Runs just before the tooltip is hidden, for subclass-specific teardown. */
|
|
45
|
-
_beforeHide() {}
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Buttons
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Builds a tooltip button and registers its click disposer.
|
|
53
|
-
* @param {string} icon - inline SVG markup
|
|
54
|
-
* @param {string} title
|
|
55
|
-
* @param {() => void} handler
|
|
56
|
-
* @param {boolean} [isDanger]
|
|
57
|
-
* @returns {HTMLElement}
|
|
58
|
-
*/
|
|
59
|
-
_makeBtn(icon, title, handler, isDanger = false) {
|
|
60
|
-
const btn = createElement('button', {
|
|
61
|
-
type: 'button',
|
|
62
|
-
class: isDanger ? 'an-link-tooltip-btn an-link-tooltip-btn--danger' : 'an-link-tooltip-btn',
|
|
63
|
-
title,
|
|
64
|
-
});
|
|
65
|
-
btn.innerHTML = icon;
|
|
66
|
-
this._disposers.push(on(btn, 'click', (e) => {
|
|
67
|
-
e.preventDefault();
|
|
68
|
-
e.stopPropagation();
|
|
69
|
-
handler();
|
|
70
|
-
}));
|
|
71
|
-
return btn;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Show / Hide
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
_scheduleShow(target) {
|
|
79
|
-
if (this._active === target && this._el.style.display !== 'none') return;
|
|
80
|
-
clearTimeout(this._hideTimer);
|
|
81
|
-
this._hideTimer = null;
|
|
82
|
-
clearTimeout(this._showTimer);
|
|
83
|
-
this._showTimer = setTimeout(() => {
|
|
84
|
-
this._active = target;
|
|
85
|
-
this._show(target);
|
|
86
|
-
}, SHOW_DELAY);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
_scheduleHide() {
|
|
90
|
-
if (!this._canHide()) return;
|
|
91
|
-
clearTimeout(this._showTimer);
|
|
92
|
-
this._showTimer = null;
|
|
93
|
-
// Always reset the hide timer so rapid mouseout→mouseover sequences
|
|
94
|
-
// don't leave a stale timer that hides the tooltip prematurely.
|
|
95
|
-
clearTimeout(this._hideTimer);
|
|
96
|
-
this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
_show(_target) {
|
|
100
|
-
this._el.style.display = 'flex';
|
|
101
|
-
// Defer positioning: offsetWidth on a newly-visible element forces layout
|
|
102
|
-
requestAnimationFrame(() => {
|
|
103
|
-
if (this._active) this._positionNear(this._active);
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
_hide() {
|
|
108
|
-
this._beforeHide();
|
|
109
|
-
this._el.style.display = 'none';
|
|
110
|
-
this._active = null;
|
|
111
|
-
this._clearTimers();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
_clearTimers() {
|
|
115
|
-
clearTimeout(this._showTimer);
|
|
116
|
-
clearTimeout(this._hideTimer);
|
|
117
|
-
this._showTimer = null;
|
|
118
|
-
this._hideTimer = null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Places the tooltip under the target, flipping above and clamping
|
|
123
|
-
* horizontally when it would leave the viewport.
|
|
124
|
-
* @param {HTMLElement} target
|
|
125
|
-
*/
|
|
126
|
-
_positionNear(target) {
|
|
127
|
-
const rect = target.getBoundingClientRect();
|
|
128
|
-
const tipW = this._el.offsetWidth || 220;
|
|
129
|
-
const tipH = this._el.offsetHeight || 32;
|
|
130
|
-
const margin = 6;
|
|
131
|
-
|
|
132
|
-
let top = rect.bottom + margin;
|
|
133
|
-
let left = rect.left + (rect.width - tipW) / 2;
|
|
134
|
-
|
|
135
|
-
if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
|
|
136
|
-
if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
|
|
137
|
-
if (left < margin) left = margin;
|
|
138
|
-
|
|
139
|
-
this._el.style.top = `${top}px`;
|
|
140
|
-
this._el.style.left = `${left}px`;
|
|
141
|
-
}
|
|
142
|
-
}
|