@jsenv/navi 0.3.2 → 0.3.4
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/jsenv_navi.js
CHANGED
|
@@ -11806,7 +11806,24 @@ const openCallout = (
|
|
|
11806
11806
|
newMessage = error.message;
|
|
11807
11807
|
newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
|
|
11808
11808
|
}
|
|
11809
|
-
|
|
11809
|
+
|
|
11810
|
+
// Check if the message is a full HTML document (starts with DOCTYPE)
|
|
11811
|
+
if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
|
|
11812
|
+
// Create iframe to isolate the HTML document
|
|
11813
|
+
const iframe = document.createElement("iframe");
|
|
11814
|
+
iframe.style.border = "none";
|
|
11815
|
+
iframe.style.width = "100%";
|
|
11816
|
+
iframe.style.minHeight = "200px";
|
|
11817
|
+
iframe.style.maxHeight = "400px";
|
|
11818
|
+
iframe.style.backgroundColor = "white";
|
|
11819
|
+
iframe.srcdoc = newMessage;
|
|
11820
|
+
|
|
11821
|
+
// Clear existing content and add iframe
|
|
11822
|
+
calloutMessageElement.innerHTML = "";
|
|
11823
|
+
calloutMessageElement.appendChild(iframe);
|
|
11824
|
+
} else {
|
|
11825
|
+
calloutMessageElement.innerHTML = newMessage;
|
|
11826
|
+
}
|
|
11810
11827
|
};
|
|
11811
11828
|
{
|
|
11812
11829
|
const handleClickOutside = (event) => {
|
|
@@ -11918,6 +11935,9 @@ const openCallout = (
|
|
|
11918
11935
|
update(message, { level });
|
|
11919
11936
|
|
|
11920
11937
|
{
|
|
11938
|
+
const documentScrollLeftAtOpen = document.documentElement.scrollLeft;
|
|
11939
|
+
const documentScrollTopAtOpen = document.documentElement.scrollTop;
|
|
11940
|
+
|
|
11921
11941
|
let positioner;
|
|
11922
11942
|
let strategy;
|
|
11923
11943
|
const determine = () => {
|
|
@@ -11940,7 +11960,10 @@ const openCallout = (
|
|
|
11940
11960
|
}
|
|
11941
11961
|
positioner?.stop();
|
|
11942
11962
|
if (newStrategy === "centered") {
|
|
11943
|
-
positioner = centerCalloutInViewport(calloutElement
|
|
11963
|
+
positioner = centerCalloutInViewport(calloutElement, {
|
|
11964
|
+
documentScrollLeftAtOpen,
|
|
11965
|
+
documentScrollTopAtOpen,
|
|
11966
|
+
});
|
|
11944
11967
|
} else {
|
|
11945
11968
|
positioner = stickCalloutToAnchor(calloutElement, anchorElement);
|
|
11946
11969
|
}
|
|
@@ -12058,6 +12081,12 @@ import.meta.css = /* css */ `
|
|
|
12058
12081
|
word-break: break-word;
|
|
12059
12082
|
overflow-wrap: anywhere;
|
|
12060
12083
|
}
|
|
12084
|
+
.navi_callout_message iframe {
|
|
12085
|
+
display: block;
|
|
12086
|
+
margin: 0;
|
|
12087
|
+
border: 1px solid #ddd;
|
|
12088
|
+
border-radius: 4px;
|
|
12089
|
+
}
|
|
12061
12090
|
.navi_callout_close_button_column {
|
|
12062
12091
|
display: flex;
|
|
12063
12092
|
height: 22px;
|
|
@@ -12142,7 +12171,10 @@ const createCalloutElement = () => {
|
|
|
12142
12171
|
return calloutElement;
|
|
12143
12172
|
};
|
|
12144
12173
|
|
|
12145
|
-
const centerCalloutInViewport = (
|
|
12174
|
+
const centerCalloutInViewport = (
|
|
12175
|
+
calloutElement,
|
|
12176
|
+
{ documentScrollLeftAtOpen, documentScrollTopAtOpen },
|
|
12177
|
+
) => {
|
|
12146
12178
|
// Set up initial styles for centered positioning
|
|
12147
12179
|
const calloutBoxElement = calloutElement.querySelector(".navi_callout_box");
|
|
12148
12180
|
const calloutFrameElement = calloutElement.querySelector(
|
|
@@ -12195,10 +12227,10 @@ const centerCalloutInViewport = (calloutElement) => {
|
|
|
12195
12227
|
finalHeight,
|
|
12196
12228
|
);
|
|
12197
12229
|
|
|
12198
|
-
// Center in viewport
|
|
12230
|
+
// Center in viewport (accounting for document scroll)
|
|
12199
12231
|
const viewportWidth = window.innerWidth;
|
|
12200
|
-
const left = (viewportWidth - finalWidth) / 2;
|
|
12201
|
-
const top = (viewportHeight - finalHeight) / 2;
|
|
12232
|
+
const left = documentScrollLeftAtOpen + (viewportWidth - finalWidth) / 2;
|
|
12233
|
+
const top = documentScrollTopAtOpen + (viewportHeight - finalHeight) / 2;
|
|
12202
12234
|
|
|
12203
12235
|
calloutStyleController.set(calloutElement, {
|
|
12204
12236
|
opacity: 1,
|
|
@@ -12211,7 +12243,6 @@ const centerCalloutInViewport = (calloutElement) => {
|
|
|
12211
12243
|
|
|
12212
12244
|
// Initial positioning
|
|
12213
12245
|
updateCenteredPosition();
|
|
12214
|
-
|
|
12215
12246
|
window.addEventListener("resize", updateCenteredPosition);
|
|
12216
12247
|
|
|
12217
12248
|
// Return positioning function for dynamic updates
|
|
@@ -12424,6 +12455,20 @@ const escapeHtml = (string) => {
|
|
|
12424
12455
|
.replace(/'/g, "'");
|
|
12425
12456
|
};
|
|
12426
12457
|
|
|
12458
|
+
/**
|
|
12459
|
+
* Checks if a string is a full HTML document (starts with DOCTYPE)
|
|
12460
|
+
* @param {string} content - The content to check
|
|
12461
|
+
* @returns {boolean} - True if it looks like a complete HTML document
|
|
12462
|
+
*/
|
|
12463
|
+
const isHtmlDocument = (content) => {
|
|
12464
|
+
if (typeof content !== "string") {
|
|
12465
|
+
return false;
|
|
12466
|
+
}
|
|
12467
|
+
// Trim whitespace and check if it starts with DOCTYPE (case insensitive)
|
|
12468
|
+
const trimmed = content.trim();
|
|
12469
|
+
return /^<!doctype\s+html/i.test(trimmed);
|
|
12470
|
+
};
|
|
12471
|
+
|
|
12427
12472
|
// It's ok to do this because the element is absolutely positioned
|
|
12428
12473
|
const cloneCalloutToMeasureNaturalSize = (calloutElement) => {
|
|
12429
12474
|
// Create invisible clone to measure natural size
|
|
@@ -13320,11 +13365,12 @@ const installCustomConstraintValidation = (
|
|
|
13320
13365
|
closeElementValidationMessage("cleanup");
|
|
13321
13366
|
};
|
|
13322
13367
|
|
|
13323
|
-
|
|
13324
|
-
|
|
13368
|
+
const anchorElement =
|
|
13369
|
+
failedConstraintInfo.target || elementReceivingValidationMessage;
|
|
13325
13370
|
validationInterface.validationMessage = openCallout(
|
|
13326
13371
|
failedConstraintInfo.message,
|
|
13327
13372
|
{
|
|
13373
|
+
anchorElement,
|
|
13328
13374
|
level: failedConstraintInfo.level,
|
|
13329
13375
|
closeOnClickOutside: failedConstraintInfo.closeOnClickOutside,
|
|
13330
13376
|
onClose: () => {
|
package/package.json
CHANGED
|
@@ -117,7 +117,24 @@ export const openCallout = (
|
|
|
117
117
|
newMessage = error.message;
|
|
118
118
|
newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
|
|
121
|
+
// Check if the message is a full HTML document (starts with DOCTYPE)
|
|
122
|
+
if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
|
|
123
|
+
// Create iframe to isolate the HTML document
|
|
124
|
+
const iframe = document.createElement("iframe");
|
|
125
|
+
iframe.style.border = "none";
|
|
126
|
+
iframe.style.width = "100%";
|
|
127
|
+
iframe.style.minHeight = "200px";
|
|
128
|
+
iframe.style.maxHeight = "400px";
|
|
129
|
+
iframe.style.backgroundColor = "white";
|
|
130
|
+
iframe.srcdoc = newMessage;
|
|
131
|
+
|
|
132
|
+
// Clear existing content and add iframe
|
|
133
|
+
calloutMessageElement.innerHTML = "";
|
|
134
|
+
calloutMessageElement.appendChild(iframe);
|
|
135
|
+
} else {
|
|
136
|
+
calloutMessageElement.innerHTML = newMessage;
|
|
137
|
+
}
|
|
121
138
|
};
|
|
122
139
|
close_on_click_outside: {
|
|
123
140
|
const handleClickOutside = (event) => {
|
|
@@ -229,6 +246,9 @@ export const openCallout = (
|
|
|
229
246
|
update(message, { level });
|
|
230
247
|
|
|
231
248
|
positioning: {
|
|
249
|
+
const documentScrollLeftAtOpen = document.documentElement.scrollLeft;
|
|
250
|
+
const documentScrollTopAtOpen = document.documentElement.scrollTop;
|
|
251
|
+
|
|
232
252
|
let positioner;
|
|
233
253
|
let strategy;
|
|
234
254
|
const determine = () => {
|
|
@@ -251,7 +271,10 @@ export const openCallout = (
|
|
|
251
271
|
}
|
|
252
272
|
positioner?.stop();
|
|
253
273
|
if (newStrategy === "centered") {
|
|
254
|
-
positioner = centerCalloutInViewport(calloutElement
|
|
274
|
+
positioner = centerCalloutInViewport(calloutElement, {
|
|
275
|
+
documentScrollLeftAtOpen,
|
|
276
|
+
documentScrollTopAtOpen,
|
|
277
|
+
});
|
|
255
278
|
} else {
|
|
256
279
|
positioner = stickCalloutToAnchor(calloutElement, anchorElement);
|
|
257
280
|
}
|
|
@@ -369,6 +392,12 @@ import.meta.css = /* css */ `
|
|
|
369
392
|
word-break: break-word;
|
|
370
393
|
overflow-wrap: anywhere;
|
|
371
394
|
}
|
|
395
|
+
.navi_callout_message iframe {
|
|
396
|
+
display: block;
|
|
397
|
+
margin: 0;
|
|
398
|
+
border: 1px solid #ddd;
|
|
399
|
+
border-radius: 4px;
|
|
400
|
+
}
|
|
372
401
|
.navi_callout_close_button_column {
|
|
373
402
|
display: flex;
|
|
374
403
|
height: 22px;
|
|
@@ -453,7 +482,10 @@ const createCalloutElement = () => {
|
|
|
453
482
|
return calloutElement;
|
|
454
483
|
};
|
|
455
484
|
|
|
456
|
-
const centerCalloutInViewport = (
|
|
485
|
+
const centerCalloutInViewport = (
|
|
486
|
+
calloutElement,
|
|
487
|
+
{ documentScrollLeftAtOpen, documentScrollTopAtOpen },
|
|
488
|
+
) => {
|
|
457
489
|
// Set up initial styles for centered positioning
|
|
458
490
|
const calloutBoxElement = calloutElement.querySelector(".navi_callout_box");
|
|
459
491
|
const calloutFrameElement = calloutElement.querySelector(
|
|
@@ -506,10 +538,10 @@ const centerCalloutInViewport = (calloutElement) => {
|
|
|
506
538
|
finalHeight,
|
|
507
539
|
);
|
|
508
540
|
|
|
509
|
-
// Center in viewport
|
|
541
|
+
// Center in viewport (accounting for document scroll)
|
|
510
542
|
const viewportWidth = window.innerWidth;
|
|
511
|
-
const left = (viewportWidth - finalWidth) / 2;
|
|
512
|
-
const top = (viewportHeight - finalHeight) / 2;
|
|
543
|
+
const left = documentScrollLeftAtOpen + (viewportWidth - finalWidth) / 2;
|
|
544
|
+
const top = documentScrollTopAtOpen + (viewportHeight - finalHeight) / 2;
|
|
513
545
|
|
|
514
546
|
calloutStyleController.set(calloutElement, {
|
|
515
547
|
opacity: 1,
|
|
@@ -522,7 +554,6 @@ const centerCalloutInViewport = (calloutElement) => {
|
|
|
522
554
|
|
|
523
555
|
// Initial positioning
|
|
524
556
|
updateCenteredPosition();
|
|
525
|
-
|
|
526
557
|
window.addEventListener("resize", updateCenteredPosition);
|
|
527
558
|
|
|
528
559
|
// Return positioning function for dynamic updates
|
|
@@ -735,6 +766,20 @@ const escapeHtml = (string) => {
|
|
|
735
766
|
.replace(/'/g, "'");
|
|
736
767
|
};
|
|
737
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Checks if a string is a full HTML document (starts with DOCTYPE)
|
|
771
|
+
* @param {string} content - The content to check
|
|
772
|
+
* @returns {boolean} - True if it looks like a complete HTML document
|
|
773
|
+
*/
|
|
774
|
+
const isHtmlDocument = (content) => {
|
|
775
|
+
if (typeof content !== "string") {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
// Trim whitespace and check if it starts with DOCTYPE (case insensitive)
|
|
779
|
+
const trimmed = content.trim();
|
|
780
|
+
return /^<!doctype\s+html/i.test(trimmed);
|
|
781
|
+
};
|
|
782
|
+
|
|
738
783
|
// It's ok to do this because the element is absolutely positioned
|
|
739
784
|
const cloneCalloutToMeasureNaturalSize = (calloutElement) => {
|
|
740
785
|
// Create invisible clone to measure natural size
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Test HTML Document in Callout</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 20px;
|
|
10
|
+
font-family: Arial, sans-serif;
|
|
11
|
+
}
|
|
12
|
+
.test-button {
|
|
13
|
+
display: inline-block;
|
|
14
|
+
margin: 10px;
|
|
15
|
+
padding: 10px 20px;
|
|
16
|
+
color: white;
|
|
17
|
+
font-size: 14px;
|
|
18
|
+
background: #007acc;
|
|
19
|
+
border: none;
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
.test-section {
|
|
24
|
+
margin: 40px 0;
|
|
25
|
+
padding: 20px;
|
|
26
|
+
border: 1px solid #ddd;
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
}
|
|
29
|
+
pre {
|
|
30
|
+
padding: 10px;
|
|
31
|
+
font-size: 12px;
|
|
32
|
+
background: #f5f5f5;
|
|
33
|
+
border-radius: 4px;
|
|
34
|
+
overflow-x: auto;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<h1>HTML Document in Callout Test</h1>
|
|
40
|
+
|
|
41
|
+
<div class="test-section">
|
|
42
|
+
<h2>Test 1: Regular HTML content (no DOCTYPE)</h2>
|
|
43
|
+
<button class="test-button" onclick="showRegularHtml()">
|
|
44
|
+
Show Regular HTML
|
|
45
|
+
</button>
|
|
46
|
+
<p>This should display as normal innerHTML (no iframe).</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="test-section">
|
|
50
|
+
<h2>Test 2: Complete HTML document (with DOCTYPE)</h2>
|
|
51
|
+
<button class="test-button" onclick="showHtmlDocument()">
|
|
52
|
+
Show HTML Document
|
|
53
|
+
</button>
|
|
54
|
+
<p>This should display in an iframe to prevent style conflicts.</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="test-section">
|
|
58
|
+
<h2>Test 3: HTML document with conflicting styles</h2>
|
|
59
|
+
<button class="test-button" onclick="showConflictingStylesDocument()">
|
|
60
|
+
Show Document with Conflicting Styles
|
|
61
|
+
</button>
|
|
62
|
+
<p>This demonstrates how iframe isolation prevents style conflicts.</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<script type="module">
|
|
66
|
+
import { openCallout } from "@jsenv/navi";
|
|
67
|
+
|
|
68
|
+
let currentCallout = null;
|
|
69
|
+
|
|
70
|
+
function closeCurrentCallout() {
|
|
71
|
+
if (currentCallout) {
|
|
72
|
+
currentCallout.close();
|
|
73
|
+
currentCallout = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
window.showRegularHtml = () => {
|
|
78
|
+
closeCurrentCallout();
|
|
79
|
+
const regularHtml = `
|
|
80
|
+
<h3>Regular HTML Content</h3>
|
|
81
|
+
<p>This is just regular HTML content without DOCTYPE.</p>
|
|
82
|
+
<ul>
|
|
83
|
+
<li>Item 1</li>
|
|
84
|
+
<li>Item 2</li>
|
|
85
|
+
<li>Item 3</li>
|
|
86
|
+
</ul>
|
|
87
|
+
<p><strong>Note:</strong> This should display as innerHTML, not in an iframe.</p>
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
currentCallout = openCallout(regularHtml, {
|
|
91
|
+
level: "info",
|
|
92
|
+
debug: true,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
window.showHtmlDocument = () => {
|
|
97
|
+
closeCurrentCallout();
|
|
98
|
+
const htmlDocument = `<!DOCTYPE html>
|
|
99
|
+
<html>
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="utf-8">
|
|
102
|
+
<title>Embedded Document</title>
|
|
103
|
+
<style>
|
|
104
|
+
body {
|
|
105
|
+
font-family: Georgia, serif;
|
|
106
|
+
background: linear-gradient(45deg, #e3f2fd, #fff3e0);
|
|
107
|
+
margin: 0;
|
|
108
|
+
padding: 20px;
|
|
109
|
+
}
|
|
110
|
+
h1 { color: #1976d2; }
|
|
111
|
+
.highlight { background: yellow; padding: 2px 4px; }
|
|
112
|
+
</style>
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<h1>Complete HTML Document</h1>
|
|
116
|
+
<p>This is a <span class="highlight">complete HTML document</span> with its own styles.</p>
|
|
117
|
+
<p>It's displayed in an iframe to prevent any style conflicts with the parent page.</p>
|
|
118
|
+
<div style="border: 2px solid #4caf50; padding: 10px; margin: 10px 0; border-radius: 8px;">
|
|
119
|
+
<strong>Isolated Environment:</strong> This document has its own CSS and doesn't interfere with the callout styles.
|
|
120
|
+
</div>
|
|
121
|
+
</body>
|
|
122
|
+
</html>`;
|
|
123
|
+
|
|
124
|
+
currentCallout = openCallout(htmlDocument, {
|
|
125
|
+
level: "warning",
|
|
126
|
+
debug: true,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
window.showConflictingStylesDocument = () => {
|
|
131
|
+
closeCurrentCallout();
|
|
132
|
+
const conflictingDocument = `<!DOCTYPE html>
|
|
133
|
+
<html>
|
|
134
|
+
<head>
|
|
135
|
+
<meta charset="utf-8">
|
|
136
|
+
<title>Conflicting Styles</title>
|
|
137
|
+
<style>
|
|
138
|
+
/* These styles would conflict with the main page if not isolated */
|
|
139
|
+
body {
|
|
140
|
+
background: #ff5722;
|
|
141
|
+
color: white;
|
|
142
|
+
font-family: 'Comic Sans MS', cursive;
|
|
143
|
+
margin: 0;
|
|
144
|
+
padding: 15px;
|
|
145
|
+
}
|
|
146
|
+
button {
|
|
147
|
+
background: #4caf50;
|
|
148
|
+
color: white;
|
|
149
|
+
border: none;
|
|
150
|
+
padding: 8px 16px;
|
|
151
|
+
border-radius: 20px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
.navi_callout {
|
|
155
|
+
/* This would break the callout if not isolated */
|
|
156
|
+
display: none !important;
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<h1>🎨 Document with Conflicting Styles</h1>
|
|
162
|
+
<p>This document has styles that would completely break the callout if not isolated in an iframe!</p>
|
|
163
|
+
<button onclick="alert('This button works inside the iframe!')">Click me!</button>
|
|
164
|
+
<p><em>Notice how the styles don't affect the callout container.</em></p>
|
|
165
|
+
</body>
|
|
166
|
+
</html>`;
|
|
167
|
+
|
|
168
|
+
currentCallout = openCallout(conflictingDocument, {
|
|
169
|
+
level: "error",
|
|
170
|
+
debug: true,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Close callout on escape key
|
|
175
|
+
document.addEventListener("keydown", (e) => {
|
|
176
|
+
if (e.key === "Escape") {
|
|
177
|
+
closeCurrentCallout();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
</script>
|
|
181
|
+
</body>
|
|
182
|
+
</html>
|
|
@@ -370,13 +370,12 @@ export const installCustomConstraintValidation = (
|
|
|
370
370
|
closeElementValidationMessage("cleanup");
|
|
371
371
|
};
|
|
372
372
|
|
|
373
|
-
const
|
|
373
|
+
const anchorElement =
|
|
374
374
|
failedConstraintInfo.target || elementReceivingValidationMessage;
|
|
375
|
-
|
|
376
375
|
validationInterface.validationMessage = openCallout(
|
|
377
376
|
failedConstraintInfo.message,
|
|
378
377
|
{
|
|
379
|
-
|
|
378
|
+
anchorElement,
|
|
380
379
|
level: failedConstraintInfo.level,
|
|
381
380
|
closeOnClickOutside: failedConstraintInfo.closeOnClickOutside,
|
|
382
381
|
onClose: () => {
|