1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
content / test / data / cross_site_scroll_into_view_factory.html [blame]
<!DOCTYPE html>
<html class="rootScroller">
<!-- This file is based on cross_site_iframe_factory.html. See comments in that
file as well as tree_parser_util.js for more details and syntax information.
This page creates a nested, non-branching, frame tree with each child frame being
fully outside of its embedder's initial viewport. The inner-most frame will
create a <input> box, also outside the frame's initial viewport.
See window.Attributes map for supported attributes.
Usage Example:
cross_site_scroll_into_view_factory
.html?siteA{MobileViewport,RTL}(siteB(siteC{TouchActionNone}))
-->
<head>
<title>Cross-site scroll-into-view frame tree factory</title>
<script>
window.Attributes = {
// Make's a frame's document "dir=rtl" to support testing right-to-left
// writing modes.
RTL: 0,
// Adds a viewport meta tag to a frame, making it "mobile friendly". This
// occurs for subframes as well but only has an effect in the root frame.
MobileViewport: 1,
// Adds a viewport meta tag to a frame, making it
// "mobile friendly" and also limiting minimum-zoom. This occurs for
// subframes as well but only has an effect in the root frame.
MobileViewportNoZoom: 2,
// Adds a `touch-action: none` style to the inner-most
// frame's <input> box. No-op in other frames.
TouchActionNone: 3,
// Uses a <fencedframe> element rather than an <iframe> for the child
// frame.
FencedFrame: 4,
// Puts the <input> box into a root scroller element.
RootScroller: 5,
// Keep this up to date with last enum.
MAX_VALUE: 5
}
</script>
<style>
html,body {
width: 100%;
height: 100%;
}
html {
overflow: hidden;
}
.rootScroller {
overflow: auto;
}
iframe, fencedframe {
position: absolute;
/* 4 screens worth of inset, to make sure this is off screen on Android
* where minimum scale on load is 0.25 */
inset-inline-start: 400%;
inset-block-start: 400%;
width: 60%;
height: 60%;
}
input {
position: absolute;
inset-inline-start: 400%;
inset-block-start: 400%;
width: 50%;
}
div.rootScroller {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: coral;
}
.spacer {
/* make the document scrollable */
position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
width: 1200%;
height: 1200%;
}
.touchActionNone {
touch-action: none;
}
.layoutViewport {
/* Used to let the test measure the size of the layout viewport; needed when
* testing desktop page on mobile where ICB size doesn't match layout
* viewport size (i.e. the "shrinks viewport contents to fit" setting) */
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
visibility: hidden;
}
</style>
</head>
<body>
<div class="layoutViewport"></div>
<h2 id='siteNameHeading'></h2>
<div class="spacer"></div>
<script src='tree_parser_util.js'></script>
<script>
function backgroundColorForSite(site) {
var lightness = 0.75;
// The site names will be of the form siteA, siteB, etc so map the fifth
// character to an index. This could be negative, we don't really care.
var index = site.charCodeAt(4) - 'a'.charCodeAt(0);
// If the first character is 'a', this will the the starting color.
var hueOfA = 200; // Spoiler alert: it's blue.
// Color palette generation articles suggest that spinning the hue wheel by
// the golden ratio yields a magically nice color distribution. Something
// about sunflower seeds. I am skeptical of the rigor of that claim (probably
// any irrational number at a slight offset from 2/3 would do) but it does
// look pretty.
var phi = 2 / (1 + Math.pow(5, .5));
var hue = Math.round((360 * index * phi + hueOfA) % 360);
return 'hsl(' + hue + ', 60%, ' + Math.round(100 * lightness) + '%)';
}
function isUrl(siteString) {
try {
var url = new URL(siteString);
} catch (e) {
if (e instanceof TypeError)
return false;
}
return true;
}
/**
* Extract the specified port number, if any. Returns empty string if not
* specified.
*/
function sitePortNumber(siteString) {
let index = siteString.indexOf(':');
if (index == -1)
return ""
return siteString.substring(index + 1);
}
/**
* Adds ".test" to an argument if it doesn't already have a top level domain.
* Converts "siteA" to "a.test" to match test host names defined in test SSL
* certificate. Adds the specified port number, if any, or the default port
* otherwise.
*/
function canonicalizeSiteAndPort(siteString, defaultPort) {
var portNumber = sitePortNumber(siteString) || defaultPort;
var hostName = siteString.split(':')[0];
if (hostName !== "localhost" && hostName.indexOf('.') == -1)
hostName = hostName + '.test';
if (hostName.startsWith('site'))
hostName = hostName.substring('site'.length).toLowerCase();
return hostName + (portNumber ? ':' + portNumber : "");
}
function hasAttribute(attribute) {
if (!Number.isInteger(attribute) || attribute < 0
|| attribute > Attributes.MAX_VALUE) {
throw new Error("hasAttribute parameter invalid: " + attribute);
}
for (var frame_attribute of frame_attributes) {
if (frame_attribute == attribute) {
return true;
}
}
return false;
}
function processFrameAttributes(frameTree) {
window.frame_attributes = [];
for (var attribute of frameTree.attributes) {
if (!Attributes.hasOwnProperty(attribute))
throw new Error("Invalid specified attribute: " + attribute);
frame_attributes.push(Attributes[attribute]);
}
}
function main() {
var goCrossSite = !window.location.protocol.startsWith('file');
var queryString = decodeURIComponent(window.location.search.substring(1));
var frameTree = TreeParserUtil.parse(queryString);
var currentSite = isUrl(frameTree.value)
? frameTree.value
: canonicalizeSiteAndPort(frameTree.value, "");
processFrameAttributes(frameTree);
if (hasAttribute(Attributes.RTL)) {
document.documentElement.setAttribute('dir', 'rtl')
}
if (hasAttribute(Attributes.MobileViewport)) {
const meta = document.createElement('meta');
meta.setAttribute('name', 'viewport');
meta.setAttribute('content', 'width=device-width');
document.head.appendChild(meta);
}
if (hasAttribute(Attributes.MobileViewportNoZoom)) {
const meta = document.createElement('meta');
meta.setAttribute('name', 'viewport');
meta.setAttribute('content', 'width=device-width,minimum-scale=1');
document.head.appendChild(meta);
}
if (hasAttribute(Attributes.RootScroller)) {
const html = document.documentElement;
const kMaxOffset = 1000000;
html.scrollLeft = kMaxOffset;
html.scrollTop = kMaxOffset;
html.classList.remove('rootScroller');
const rootScroller = document.createElement('div');
rootScroller.classList.add('rootScroller');
const spacer = document.createElement('div');
spacer.classList.add('spacer');
spacer.innerText = "Root Scroller";
rootScroller.appendChild(spacer);
document.body.appendChild(rootScroller);
}
document.getElementById('siteNameHeading').appendChild(
document.createTextNode(currentSite));
// Apply style to the current document.
document.body.style.backgroundColor = backgroundColorForSite(currentSite);
if (frameTree.children.length == 0) {
let input = document.createElement('input');
if (hasAttribute(Attributes.TouchActionNone))
input.classList.add('touchActionNone');
if (document.documentElement.classList.contains('rootScroller'))
document.body.appendChild(input);
else
document.querySelector('.rootScroller').appendChild(input);
return;
}
// ScrollIntoView tests don't need multiple children and we don't support it.
console.assert(frameTree.children.length == 1);
// Compute the URL for this child frame.
let url = frameTree.children[0].value;
let siteAndPort = url;
if (!isUrl(url)) {
siteAndPort = canonicalizeSiteAndPort(url, window.location.port);
const subtreeString = TreeParserUtil.flatten(frameTree.children[0]);
url = '';
url += window.location.protocol + '//'; // scheme (preserved)
url += goCrossSite ? siteAndPort : window.location.host; // host and port
url += window.location.pathname; // path (preserved)
url += '?' + encodeURIComponent(subtreeString); // query
}
// Construct the child frame.
var elementType = hasAttribute(Attributes.FencedFrame) ? 'fencedframe' : 'iframe';
var frame = document.createElement(elementType);
if (elementType == "fencedframe") {
frame.config = new FencedFrameConfig(url);
} else {
frame.src = url;
}
frame.id = "childframe";
// Add the child frame to the document.
document.body.appendChild(frame);
}
main();
</script>
</body></html>