├── CNAME
├── resources
├── ski
│ ├── bg.png
│ ├── objects.png
│ └── player.png
├── surf
│ ├── bg.png
│ ├── objects.png
│ └── player.png
├── icons
│ ├── favicon.png
│ ├── apple-icon.png
│ ├── ms-icon-70x70.png
│ ├── apple-icon-57x57.png
│ ├── apple-icon-60x60.png
│ ├── apple-icon-72x72.png
│ ├── apple-icon-76x76.png
│ ├── ms-icon-144x144.png
│ ├── ms-icon-150x150.png
│ ├── ms-icon-310x310.png
│ ├── android-icon-36x36.png
│ ├── android-icon-48x48.png
│ ├── android-icon-72x72.png
│ ├── android-icon-96x96.png
│ ├── apple-icon-114x114.png
│ ├── apple-icon-120x120.png
│ ├── apple-icon-144x144.png
│ ├── apple-icon-152x152.png
│ ├── apple-icon-180x180.png
│ ├── android-icon-144x144.png
│ ├── android-icon-192x192.png
│ ├── apple-icon-precomposed.png
│ └── favicon.svg
├── js
│ ├── surf-error-reporting.js
│ ├── base-error-reporting.js
│ ├── promise_resolver.js
│ ├── assert.js
│ ├── strings.js
│ ├── load_time_data.js
│ ├── parse_html_subset.js
│ ├── util.js
│ └── cr.js
└── css
│ └── interface.css
├── browserconfig.xml
├── manifest.json
├── README.md
└── index.html
/CNAME:
--------------------------------------------------------------------------------
1 | surf.jackbuehner.com
--------------------------------------------------------------------------------
/resources/ski/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/ski/bg.png
--------------------------------------------------------------------------------
/resources/surf/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/surf/bg.png
--------------------------------------------------------------------------------
/resources/ski/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/ski/objects.png
--------------------------------------------------------------------------------
/resources/ski/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/ski/player.png
--------------------------------------------------------------------------------
/resources/surf/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/surf/objects.png
--------------------------------------------------------------------------------
/resources/surf/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/surf/player.png
--------------------------------------------------------------------------------
/resources/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/favicon.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon.png
--------------------------------------------------------------------------------
/resources/icons/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/ms-icon-70x70.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-57x57.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-60x60.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-72x72.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-76x76.png
--------------------------------------------------------------------------------
/resources/icons/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/ms-icon-144x144.png
--------------------------------------------------------------------------------
/resources/icons/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/ms-icon-150x150.png
--------------------------------------------------------------------------------
/resources/icons/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/ms-icon-310x310.png
--------------------------------------------------------------------------------
/resources/icons/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/android-icon-36x36.png
--------------------------------------------------------------------------------
/resources/icons/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/android-icon-48x48.png
--------------------------------------------------------------------------------
/resources/icons/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/android-icon-72x72.png
--------------------------------------------------------------------------------
/resources/icons/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/android-icon-96x96.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-114x114.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-144x144.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/resources/icons/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/android-icon-144x144.png
--------------------------------------------------------------------------------
/resources/icons/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/android-icon-192x192.png
--------------------------------------------------------------------------------
/resources/icons/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackbuehner/MicrosoftEdge-Surf/HEAD/resources/icons/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/resources/js/surf-error-reporting.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | hookErrorReporting('webui-surf-game');
--------------------------------------------------------------------------------
/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #243A5E
--------------------------------------------------------------------------------
/resources/js/base-error-reporting.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | function hookErrorReporting(component) {
5 | window.onerror = (message, source, lineno, columnNumber, error) => {
6 | const errorInfo = {
7 | column: columnNumber,
8 | component,
9 | line: lineno,
10 | message: error.message,
11 | name: error.name,
12 | source_url: source,
13 | stack: error.stack
14 | };
15 | chrome.errorReporting.reportError(errorInfo);
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "SURF",
3 | "name": "Microsoft Edge SURF",
4 | "icons": [
5 | {
6 | "src": "resources/icons/android-icon-36x36.png",
7 | "sizes": "36x36",
8 | "type": "image/png",
9 | "density": "0.75"
10 | },
11 | {
12 | "src": "resources/icons/android-icon-48x48.png",
13 | "sizes": "48x48",
14 | "type": "image/png",
15 | "density": "1.0"
16 | },
17 | {
18 | "src": "resources/icons/android-icon-72x72.png",
19 | "sizes": "72x72",
20 | "type": "image/png",
21 | "density": "1.5"
22 | },
23 | {
24 | "src": "resources/icons/android-icon-96x96.png",
25 | "sizes": "96x96",
26 | "type": "image/png",
27 | "density": "2.0"
28 | },
29 | {
30 | "src": "resources/icons/android-icon-144x144.png",
31 | "sizes": "144x144",
32 | "type": "image/png",
33 | "density": "3.0"
34 | },
35 | {
36 | "src": "resources/icons/android-icon-192x192.png",
37 | "sizes": "192x192",
38 | "type": "image/png",
39 | "density": "4.0"
40 | },
41 | {
42 | "src": "resources/icons/favicon.png",
43 | "type": "image/png",
44 | "sizes": "512x512"
45 | }
46 | ],
47 | "start_url": "/",
48 | "version": "1.1",
49 | "background_color": "#243A5E",
50 | "display": "fullscreen",
51 | "scope": "/",
52 | "theme_color": "#243A5E"
53 | }
54 |
--------------------------------------------------------------------------------
/resources/js/promise_resolver.js:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | // #import {assertNotReached} from './assert.m.js';
6 |
7 | /**
8 | * @fileoverview PromiseResolver is a helper class that allows creating a
9 | * Promise that will be fulfilled (resolved or rejected) some time later.
10 | *
11 | * Example:
12 | * var resolver = new PromiseResolver();
13 | * resolver.promise.then(function(result) {
14 | * console.log('resolved with', result);
15 | * });
16 | * ...
17 | * ...
18 | * resolver.resolve({hello: 'world'});
19 | */
20 |
21 | /** @template T */
22 | // eslint-disable-next-line no-var
23 | /* #export */ var PromiseResolver = class {
24 | constructor() {
25 | /** @private {function(T=): void} */
26 | this.resolve_;
27 |
28 | /** @private {function(*=): void} */
29 | this.reject_;
30 |
31 | /** @private {boolean} */
32 | this.isFulfilled_ = false;
33 |
34 | /** @private {!Promise} */
35 | this.promise_ = new Promise((resolve, reject) => {
36 | this.resolve_ = /** @param {T=} resolution */ (resolution) => {
37 | resolve(resolution);
38 | this.isFulfilled_ = true;
39 | };
40 | this.reject_ = /** @param {*=} reason */ (reason) => {
41 | reject(reason);
42 | this.isFulfilled_ = true;
43 | };
44 | });
45 | }
46 |
47 | /** @return {boolean} Whether this resolver has been resolved or rejected. */
48 | get isFulfilled() {
49 | return this.isFulfilled_;
50 | }
51 |
52 | set isFulfilled(i) {
53 | assertNotReached();
54 | }
55 |
56 | /** @return {!Promise} */
57 | get promise() {
58 | return this.promise_;
59 | }
60 |
61 | set promise(p) {
62 | assertNotReached();
63 | }
64 |
65 | /** @return {function(T=): void} */
66 | get resolve() {
67 | return this.resolve_;
68 | }
69 |
70 | set resolve(r) {
71 | assertNotReached();
72 | }
73 |
74 | /** @return {function(*=): void} */
75 | get reject() {
76 | return this.reject_;
77 | }
78 |
79 | set reject(s) {
80 | assertNotReached();
81 | }
82 | };
83 |
84 | /* #ignore */ console.warn('crbug/1173575, non-JS module files deprecated.');
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Microsoft Edge's *Let's Surf*
2 | The *Let's Surf* game from ``edge://surf``
3 |
4 |
5 |
6 |
7 |
8 | The game is created by Microsoft – please see the **Credits** in the game menu for more information. The files in this repo are from Microsoft Edge; however, some of them have been modified so the game can function independently from Edge.
9 |
10 | ## Play
11 | Play the game online at https://surf.jackbuehner.com
12 |
13 | ### Features
14 | - Endless mode: Surf as far as you can while avoiding obstacles and the kraken. You can switch modes via the game settings menu.
15 | - Time trials: Reach the end of the course as fast as you can! Collect coins to shorten your time. The course is always the same, so can you find the shortest route?
16 | - Zig zag mode: Surf through as many gates as you can in a row! Your streak will reset if you miss a gate, but you can keep playing until your lives run out.
17 | - High scores: Each game mode keeps a record of your high score, and we’ll let you know whenever you set a new record. Reset your stats any time for a fresh start.
18 | - Reduced speed mode: If you prefer a more relaxed pace or need extra time to pull off those surfing moves, enable reduced speed mode to slow down the game speed.
19 | - Themes: Choose between a summer surfing theme or a winter skiing theme. Choose your favorite theme in the game settings menu.
20 | - And surprises! With support for keyboard, mouse, touch, and controllers you can play the game your way... and you may find one of the many Easter Eggs and fun surprises!
21 |
22 | ## Changes
23 | - Added picker to choose between "Let's surf" and "Let's ski" in the game settings menu.
24 | - Changed file paths so the repo is more organized.
25 | - Added an SVG favicon.
26 | - Added the ``manifest.json`` file so the site can be installed as a PWA app.
27 | - Pretty-printed the ``surf.bundle.js`` file to ease the modification process.
28 | - Use ``localStorage`` function to save the stats instead of the original [WebUI](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/webui_explainer.md) ones (``chrome.send`` in particular).
29 | - Added mobile support. The game is responsive to most screen sizes.
30 |
31 | More information can be seen in past [pull requests](https://github.com/jackbuehner/MicrosoftEdge-S.U.R.F./pulls?q=is%3Apr+is%3Aclosed) and [releases](https://github.com/jackbuehner/MicrosoftEdge-S.U.R.F./releases).
32 |
33 | ## Contribute
34 | If you know how to fix anything that is not working, feel free to open a pull request. I'll merge it as soon as I see it and confirm it does not break anything.
35 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/resources/js/assert.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /**
6 | * @fileoverview Assertion support.
7 | */
8 |
9 | /**
10 | * Verify |condition| is truthy and return |condition| if so.
11 | * @template T
12 | * @param {T} condition A condition to check for truthiness. Note that this
13 | * may be used to test whether a value is defined or not, and we don't want
14 | * to force a cast to Boolean.
15 | * @param {string=} opt_message A message to show on failure.
16 | * @return {T} A non-null |condition|.
17 | * @closurePrimitive {asserts.truthy}
18 | * @suppress {reportUnknownTypes} because T is not sufficiently constrained.
19 | */
20 | /* #export */ function assert(condition, opt_message) {
21 | if (!condition) {
22 | let message = 'Assertion failed';
23 | if (opt_message) {
24 | message = message + ': ' + opt_message;
25 | }
26 | const error = new Error(message);
27 | const global = function() {
28 | const thisOrSelf = this || self;
29 | /** @type {boolean} */
30 | thisOrSelf.traceAssertionsForTesting;
31 | return thisOrSelf;
32 | }();
33 | if (global.traceAssertionsForTesting) {
34 | console.warn(error.stack);
35 | }
36 | throw error;
37 | }
38 | return condition;
39 | }
40 |
41 | /**
42 | * Call this from places in the code that should never be reached.
43 | *
44 | * For example, handling all the values of enum with a switch() like this:
45 | *
46 | * function getValueFromEnum(enum) {
47 | * switch (enum) {
48 | * case ENUM_FIRST_OF_TWO:
49 | * return first
50 | * case ENUM_LAST_OF_TWO:
51 | * return last;
52 | * }
53 | * assertNotReached();
54 | * return document;
55 | * }
56 | *
57 | * This code should only be hit in the case of serious programmer error or
58 | * unexpected input.
59 | *
60 | * @param {string=} opt_message A message to show when this is hit.
61 | * @closurePrimitive {asserts.fail}
62 | */
63 | /* #export */ function assertNotReached(opt_message) {
64 | assert(false, opt_message || 'Unreachable code hit');
65 | }
66 |
67 | /**
68 | * @param {*} value The value to check.
69 | * @param {function(new: T, ...)} type A user-defined constructor.
70 | * @param {string=} opt_message A message to show when this is hit.
71 | * @return {T}
72 | * @template T
73 | */
74 | /* #export */ function assertInstanceof(value, type, opt_message) {
75 | // We don't use assert immediately here so that we avoid constructing an error
76 | // message if we don't have to.
77 | if (!(value instanceof type)) {
78 | assertNotReached(
79 | opt_message ||
80 | 'Value ' + value + ' is not a[n] ' + (type.name || typeof type));
81 | }
82 | return value;
83 | }
84 |
85 | /* #ignore */ console.warn('crbug/1173575, non-JS module files deprecated.');
86 |
--------------------------------------------------------------------------------
/resources/js/strings.js:
--------------------------------------------------------------------------------
1 | loadTimeData.data = {
2 | bestScore: "Best $1 $2 $3",
3 | bestScoreFlyout: "Best score: $1",
4 | bestScoreMenuDisplay: "$1 $2",
5 | cancel: "Cancel",
6 | close: "Close",
7 | code: "Cheat code activated!",
8 | codeCheat: "Cheat code activated! $1",
9 | codeScoring: "Scoring turned off for this round.",
10 | endlessBestScore: -1,
11 | endlessDescriptor: "distance",
12 | endlessModeTitleInline: "Endless:",
13 | endlessTitle: "Endless",
14 | endlessUnit: "m",
15 | fontfamily: "system-ui, sans-serif",
16 | fontsize: "75%",
17 | gameCreditsButton: "Game credits",
18 | gameCreditsDialogTitle: "Game credits",
19 | gameMode: "Game mode changed to $1",
20 | gameModeSelectLabel: "Game mode",
21 | gameModeSelectLabelDisabled: "Game mode (start new game to change)",
22 | gameSettings: "Game settings",
23 | highVisibilityActive: false,
24 | highVisiblityModeToggleLabel: "High visiblity",
25 | howToPlayButton: "How to play",
26 | howToPlayControllerBoost: "Press RT to use your speed boost",
27 | howToPlayControllerMovement: "Use the thumbstick or D-pad to steer",
28 | howToPlayControllerRefresh: "Press LB+RB to restart the game",
29 | howToPlayDialogTitle: "How to play",
30 | howToPlayEndless:
31 | "Travel as far as you can and rescue a friend for help with enemies",
32 | howToPlayKeyboardBoost: "Press `F` to use your speed boost",
33 | howToPlayKeyboardMovement: "Use the arrow or `WASD` keys to steer",
34 | howToPlayKeyboardRefresh: "Refresh the page to restart the game",
35 | howToPlayMouseBoost: "Right-click to use your speed boost",
36 | howToPlayMouseMovement: "Move the mouse to steer",
37 | howToPlayMouseRefresh: "Refresh the page to restart the game",
38 | howToPlayTimeTrial:
39 | "Reach the end as fast as you can and collect coins to subtract time",
40 | howToPlayTouchBoost: "Swipe down twice to use your speed boost",
41 | howToPlayTouchMovement: "Swipe or tap to steer",
42 | howToPlayTouchRefresh: "Refresh the page to restart the game",
43 | howToPlayZigZag: "Pass through as many gates as you can in a row",
44 | is_windows_xbox_sku: "false",
45 | keyboardAction: "spacebar",
46 | language: "en",
47 | lastSelectedMode: "endless",
48 | lastSelectedPlayer: 4,
49 | lastSelectedTheme: "surf",
50 | menuInfo: "to start playing",
51 | mouseAction: "double click",
52 | newBestScore: "New best $1 $2 $3",
53 | newGame: "Start new game",
54 | off: "Off",
55 | on: "On",
56 | overInfo: "to play again",
57 | overTitle: "Nice try!",
58 | pauseInfo: "to resume playing",
59 | pauseTitle: "Game paused",
60 | reducedSpeedActive: false,
61 | reducedSpeedModeToggleLabel: "Reduced speed",
62 | resetAllStats: "Reset all stats",
63 | resetAllStatsDialogText:
64 | "Are you sure you want to reset all your game stats, including your high scores?",
65 | resetAllStatsDialogTitle: "Reset all stats",
66 | share: "Share with a friend",
67 | shareCopy: "Copy",
68 | shareLink: "edge://surf",
69 | shareLinkCopied: "Copied!",
70 | skiTheme: "Let's ski",
71 | specialThanks: "Special thanks:",
72 | surfTheme: "Let's surf",
73 | textdirection: "ltr",
74 | theme: "Theme changed to $1",
75 | themeSelectLabel: "Theme",
76 | timetrialBestScore: -1,
77 | timetrialDescriptor: "time",
78 | timetrialModeTitleInline: "Time trial:",
79 | timetrialTitle: "Time trial",
80 | timetrialUnit: "s",
81 | touchAction: "double tap",
82 | zigzagBestScore: -1,
83 | zigzagDescriptor: "streak",
84 | zigzagModeTitleInline: "Zig zag:",
85 | zigzagTitle: "Zig zag",
86 | zigzagUnit: "gates",
87 | };
88 |
--------------------------------------------------------------------------------
/resources/icons/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/resources/js/load_time_data.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /**
6 | * @fileoverview This file defines a singleton which provides access to all data
7 | * that is available as soon as the page's resources are loaded (before DOM
8 | * content has finished loading). This data includes both localized strings and
9 | * any data that is important to have ready from a very early stage (e.g. things
10 | * that must be displayed right away).
11 | *
12 | * Note that loadTimeData is not guaranteed to be consistent between page
13 | * refreshes (https://crbug.com/740629) and should not contain values that might
14 | * change if the page is re-opened later.
15 | */
16 |
17 | /** @type {!LoadTimeData} */
18 | // eslint-disable-next-line no-var
19 | var loadTimeData;
20 |
21 | class LoadTimeData {
22 | constructor() {
23 | /** @type {?Object} */
24 | this.data_ = null;
25 | }
26 |
27 | /**
28 | * Sets the backing object.
29 | *
30 | * Note that there is no getter for |data_| to discourage abuse of the form:
31 | *
32 | * var value = loadTimeData.data()['key'];
33 | *
34 | * @param {Object} value The de-serialized page data.
35 | */
36 | set data(value) {
37 | expect(!this.data_, 'Re-setting data.');
38 | this.data_ = value;
39 | }
40 |
41 | /**
42 | * @param {string} id An ID of a value that might exist.
43 | * @return {boolean} True if |id| is a key in the dictionary.
44 | */
45 | valueExists(id) {
46 | return id in this.data_;
47 | }
48 |
49 | /**
50 | * Fetches a value, expecting that it exists.
51 | * @param {string} id The key that identifies the desired value.
52 | * @return {*} The corresponding value.
53 | */
54 | getValue(id) {
55 | expect(this.data_, 'No data. Did you remember to include strings.js?');
56 | const value = this.data_[id];
57 | expect(typeof value !== 'undefined', 'Could not find value for ' + id);
58 | return value;
59 | }
60 |
61 | /**
62 | * As above, but also makes sure that the value is a string.
63 | * @param {string} id The key that identifies the desired string.
64 | * @return {string} The corresponding string value.
65 | */
66 | getString(id) {
67 | const value = this.getValue(id);
68 | expectIsType(id, value, 'string');
69 | return /** @type {string} */ (value);
70 | }
71 |
72 | /**
73 | * Returns a formatted localized string where $1 to $9 are replaced by the
74 | * second to the tenth argument.
75 | * @param {string} id The ID of the string we want.
76 | * @param {...(string|number)} var_args The extra values to include in the
77 | * formatted output.
78 | * @return {string} The formatted string.
79 | */
80 | getStringF(id, var_args) {
81 | const value = this.getString(id);
82 | if (!value) {
83 | return '';
84 | }
85 |
86 | const args = Array.prototype.slice.call(arguments);
87 | args[0] = value;
88 | return this.substituteString.apply(this, args);
89 | }
90 |
91 | /**
92 | * Returns a formatted localized string where $1 to $9 are replaced by the
93 | * second to the tenth argument. Any standalone $ signs must be escaped as
94 | * $$.
95 | * @param {string} label The label to substitute through.
96 | * This is not an resource ID.
97 | * @param {...(string|number)} var_args The extra values to include in the
98 | * formatted output.
99 | * @return {string} The formatted string.
100 | */
101 | substituteString(label, var_args) {
102 | const varArgs = arguments;
103 | return label.replace(/\$(.|$|\n)/g, function(m) {
104 | expect(m.match(/\$[$1-9]/), 'Unescaped $ found in localized string.');
105 | return m === '$$' ? '$' : varArgs[m[1]];
106 | });
107 | }
108 |
109 | /**
110 | * Returns a formatted string where $1 to $9 are replaced by the second to
111 | * tenth argument, split apart into a list of pieces describing how the
112 | * substitution was performed. Any standalone $ signs must be escaped as $$.
113 | * @param {string} label A localized string to substitute through.
114 | * This is not an resource ID.
115 | * @param {...(string|number)} var_args The extra values to include in the
116 | * formatted output.
117 | * @return {!Array} The formatted
118 | * string pieces.
119 | */
120 | getSubstitutedStringPieces(label, var_args) {
121 | const varArgs = arguments;
122 | // Split the string by separately matching all occurrences of $1-9 and of
123 | // non $1-9 pieces.
124 | const pieces = (label.match(/(\$[1-9])|(([^$]|\$([^1-9]|$))+)/g) ||
125 | []).map(function(p) {
126 | // Pieces that are not $1-9 should be returned after replacing $$
127 | // with $.
128 | if (!p.match(/^\$[1-9]$/)) {
129 | expect(
130 | (p.match(/\$/g) || []).length % 2 === 0,
131 | 'Unescaped $ found in localized string.');
132 | return {value: p.replace(/\$\$/g, '$'), arg: null};
133 | }
134 |
135 | // Otherwise, return the substitution value.
136 | return {value: varArgs[p[1]], arg: p};
137 | });
138 |
139 | return pieces;
140 | }
141 |
142 | /**
143 | * As above, but also makes sure that the value is a boolean.
144 | * @param {string} id The key that identifies the desired boolean.
145 | * @return {boolean} The corresponding boolean value.
146 | */
147 | getBoolean(id) {
148 | const value = this.getValue(id);
149 | expectIsType(id, value, 'boolean');
150 | return /** @type {boolean} */ (value);
151 | }
152 |
153 | /**
154 | * As above, but also makes sure that the value is an integer.
155 | * @param {string} id The key that identifies the desired number.
156 | * @return {number} The corresponding number value.
157 | */
158 | getInteger(id) {
159 | const value = this.getValue(id);
160 | expectIsType(id, value, 'number');
161 | expect(value === Math.floor(value), 'Number isn\'t integer: ' + value);
162 | return /** @type {number} */ (value);
163 | }
164 |
165 | /**
166 | * Override values in loadTimeData with the values found in |replacements|.
167 | * @param {Object} replacements The dictionary object of keys to replace.
168 | */
169 | overrideValues(replacements) {
170 | expect(
171 | typeof replacements === 'object',
172 | 'Replacements must be a dictionary object.');
173 | for (const key in replacements) {
174 | this.data_[key] = replacements[key];
175 | }
176 | }
177 |
178 | /**
179 | * Reset loadTimeData's data. Should only be used in tests.
180 | * @param {?Object} newData The data to restore to, when null restores to
181 | * unset state.
182 | */
183 | resetForTesting(newData = null) {
184 | this.data_ = newData;
185 | }
186 |
187 | /**
188 | * @return {boolean} Whether loadTimeData.data has been set.
189 | */
190 | isInitialized() {
191 | return this.data_ !== null;
192 | }
193 | }
194 |
195 | /**
196 | * Checks condition, throws error message if expectation fails.
197 | * @param {*} condition The condition to check for truthiness.
198 | * @param {string} message The message to display if the check fails.
199 | */
200 | function expect(condition, message) {
201 | if (!condition) {
202 | throw new Error(
203 | 'Unexpected condition on ' + document.location.href + ': ' + message);
204 | }
205 | }
206 |
207 | /**
208 | * Checks that the given value has the given type.
209 | * @param {string} id The id of the value (only used for error message).
210 | * @param {*} value The value to check the type on.
211 | * @param {string} type The type we expect |value| to be.
212 | */
213 | function expectIsType(id, value, type) {
214 | expect(
215 | typeof value === type, '[' + value + '] (' + id + ') is not a ' + type);
216 | }
217 |
218 | expect(!loadTimeData, 'should only include this file once');
219 | loadTimeData = new LoadTimeData;
220 |
221 | // Expose |loadTimeData| directly on |window|, since within a JS module the
222 | // scope is local and not all files have been updated to import the exported
223 | // |loadTimeData| explicitly.
224 | window.loadTimeData = loadTimeData;
225 |
226 | console.warn('crbug/1173575, non-JS module files deprecated.');
--------------------------------------------------------------------------------
/resources/js/parse_html_subset.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /**
6 | * This function maps a given string to a TrustedHTML object
7 | * without performing any validation. Callsites must ensure
8 | * that the resulting TrustedHTML will only be used in inert
9 | * documents or sanitize the input before passing to this function.
10 | * @param {string} s The string to convert.
11 | * @return {!TrustedHTML} A TrustedHTML containing input string.
12 | */
13 | const toTrustedHtml = (function() {
14 | /** @type {!TrustedTypePolicy} */
15 | let unsanitizedPolicy;
16 |
17 | return function(s) {
18 | if (!unsanitizedPolicy) {
19 | unsanitizedPolicy = trustedTypes.createPolicy(
20 | 'parse-html-subset',
21 | {createHTML: unsanitizedHtml => unsanitizedHtml});
22 | }
23 | return unsanitizedPolicy.createHTML(s);
24 | };
25 | })();
26 |
27 | /**
28 | * @typedef {{
29 | * substitutions: (!Array|undefined),
30 | * attrs: (!Array|undefined),
31 | * tags: (!Array|undefined),
32 | * }}
33 | */
34 | /* #export */ let SanitizeInnerHtmlOpts;
35 |
36 | /**
37 | * Make a string safe for Polymer bindings that are inner-h-t-m-l or other
38 | * innerHTML use.
39 | * @param {string} rawString The unsanitized string
40 | * @param {SanitizeInnerHtmlOpts=} opts Optional additional allowed tags and
41 | * attributes.
42 | * @return {string}
43 | */
44 | /* #export */ const sanitizeInnerHtml = function(rawString, opts) {
45 | opts = opts || {};
46 | return parseHtmlSubset('' + rawString + '', opts.tags, opts.attrs)
47 | .firstChild.innerHTML;
48 | };
49 |
50 |
51 | /**
52 | * Parses a very small subset of HTML. This ensures that insecure HTML /
53 | * javascript cannot be injected into WebUI.
54 | * @param {string} s The string to parse.
55 | * @param {!Array=} opt_extraTags Optional extra allowed tags.
56 | * @param {!Array=} opt_extraAttrs
57 | * Optional extra allowed attributes (all tags are run through these).
58 | * @throws {Error} In case of non supported markup.
59 | * @return {!DocumentFragment} A document fragment containing the DOM tree.
60 | */
61 | /* #export */ const parseHtmlSubset = (function() {
62 | 'use strict';
63 |
64 | /** @typedef {function(!Node, string):boolean} */
65 | let AllowFunction;
66 |
67 | /** @type {!AllowFunction} */
68 | const allowAttribute = (node, value) => true;
69 |
70 | /**
71 | * Allow-list of attributes in parseHtmlSubset.
72 | * @type {!Map}
73 | * @const
74 | */
75 | const allowedAttributes = new Map([
76 | [
77 | 'href',
78 | (node, value) => {
79 | // Only allow a[href] starting with edge:// or https:// or equaling
80 | // to #.
81 | return node.tagName === 'A' &&
82 | (value.startsWith('edge://') || value.startsWith('https://') ||
83 | value === '#');
84 | }
85 | ],
86 | [
87 | 'target',
88 | (node, value) => {
89 | // Only allow a[target='_blank'].
90 | // TODO(dbeam): are there valid use cases for target !== '_blank'?
91 | return node.tagName === 'A' && value === '_blank';
92 | }
93 | ],
94 | ]);
95 |
96 | /**
97 | * Allow-list of optional attributes in parseHtmlSubset.
98 | * @type {!Map}
99 | * @const
100 | */
101 | const allowedOptionalAttributes = new Map([
102 | ['class', allowAttribute],
103 | ['id', allowAttribute],
104 | ['is', (node, value) => value === 'action-link' || value === ''],
105 | ['role', (node, value) => value === 'link'],
106 | [
107 | 'src',
108 | (node, value) => {
109 | // Only allow img[src] starting with edge://
110 | return node.tagName === 'IMG' && value.startsWith('edge://');
111 | }
112 | ],
113 | ['tabindex', allowAttribute],
114 | ['aria-hidden', allowAttribute],
115 | ['aria-labelledby', allowAttribute],
116 | ]);
117 |
118 | /**
119 | * Allow-list of tag names in parseHtmlSubset.
120 | * @type {!Set}
121 | * @const
122 | */
123 | const allowedTags =
124 | new Set(['A', 'B', 'BR', 'DIV', 'KBD', 'P', 'PRE', 'SPAN', 'STRONG']);
125 |
126 | /**
127 | * Allow-list of optional tag names in parseHtmlSubset.
128 | * @type {!Set}
129 | * @const
130 | */
131 | const allowedOptionalTags = new Set(['IMG', 'LI', 'UL']);
132 |
133 | /**
134 | * @param {!Array} optTags an Array to merge.
135 | * @return {!Set} Set of allowed tags.
136 | */
137 | function mergeTags(optTags) {
138 | const clone = new Set(allowedTags);
139 | optTags.forEach(str => {
140 | const tag = str.toUpperCase();
141 | if (allowedOptionalTags.has(tag)) {
142 | clone.add(tag);
143 | }
144 | });
145 | return clone;
146 | }
147 |
148 | /**
149 | * @param {!Array} optAttrs an Array to merge.
150 | * @return {!Map} Map of allowed
151 | * attributes.
152 | */
153 | function mergeAttrs(optAttrs) {
154 | const clone = new Map([...allowedAttributes]);
155 | optAttrs.forEach(key => {
156 | if (allowedOptionalAttributes.has(key)) {
157 | clone.set(key, allowedOptionalAttributes.get(key));
158 | }
159 | });
160 | return clone;
161 | }
162 |
163 | function walk(n, f) {
164 | f(n);
165 | for (let i = 0; i < n.childNodes.length; i++) {
166 | walk(n.childNodes[i], f);
167 | }
168 | }
169 |
170 | function assertElement(tags, node) {
171 | if (!tags.has(node.tagName)) {
172 | throw Error(node.tagName + ' is not supported');
173 | }
174 | }
175 |
176 | function assertAttribute(attrs, attrNode, node) {
177 | const n = attrNode.nodeName;
178 | const v = attrNode.nodeValue;
179 | if (!attrs.has(n) || !attrs.get(n)(node, v)) {
180 | throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported');
181 | }
182 | }
183 |
184 | return function(s, opt_extraTags, opt_extraAttrs) {
185 | const tags = opt_extraTags ? mergeTags(opt_extraTags) : allowedTags;
186 | const attrs =
187 | opt_extraAttrs ? mergeAttrs(opt_extraAttrs) : allowedAttributes;
188 |
189 | const doc = document.implementation.createHTMLDocument('');
190 | const r = doc.createRange();
191 | r.selectNode(doc.body);
192 |
193 | if (window.trustedTypes) {
194 | s = toTrustedHtml(s);
195 | }
196 |
197 | // This does not execute any scripts because the document has no view.
198 | const df = r.createContextualFragment(s);
199 | walk(df, function(node) {
200 | switch (node.nodeType) {
201 | case Node.ELEMENT_NODE:
202 | assertElement(tags, node);
203 | const nodeAttrs = node.attributes;
204 | for (let i = 0; i < nodeAttrs.length; ++i) {
205 | assertAttribute(attrs, nodeAttrs[i], node);
206 | }
207 | break;
208 |
209 | case Node.COMMENT_NODE:
210 | case Node.DOCUMENT_FRAGMENT_NODE:
211 | case Node.TEXT_NODE:
212 | break;
213 |
214 | default:
215 | throw Error('Node type ' + node.nodeType + ' is not supported');
216 | }
217 | });
218 | return df;
219 | };
220 | })();
221 |
222 | /* #ignore */ console.warn('crbug/1173575, non-JS module files deprecated.');
223 |
224 | /**
225 | * Parses a very small subset of HTML. This ensures that insecure HTML /
226 | * javascript cannot be injected into WebUI. Returns HTML instead of
227 | * DocumentFragment.
228 | * @param {string} s The string to parse.
229 | * @param {!Array=} opt_extraTags Optional extra allowed tags.
230 | * @param {!Array=} opt_extraAttrs
231 | * Optional extra allowed attributes (all tags are run through these).
232 | * @throws {Error} In case of non supported markup.
233 | * @return {string|!TrustedHTML} a sanitized HTML.
234 | */
235 | /* #export */ const parseToSafeHtml = (function() {
236 | 'use strict';
237 |
238 | return function(s, opt_extraTags, opt_extraAttrs) {
239 | const div = document.createElement('div');
240 | div.appendChild(parseHtmlSubset(s, opt_extraTags, opt_extraAttrs));
241 | if (!window.trustedTypes) {
242 | return div.innerHTML;
243 | }
244 |
245 | return toTrustedHtml(div.innerHTML);
246 | };
247 | })();
248 |
--------------------------------------------------------------------------------
/resources/css/interface.css:
--------------------------------------------------------------------------------
1 | /** Copyright (C) Microsoft Corporation. All rights reserved.
2 | * Use of this source code is governed by a BSD-style license that can be
3 | * found in the LICENSE file.
4 | */
5 |
6 | /* minimal css reset */
7 | html, body, div, h1, h2, h3, h4, h5, h6, a, p, img, ul, li, button { padding: 0; margin: 0; border: 0; outline: 0; }
8 |
9 | html, body {
10 | width: 100vw; /* fullscreen */
11 | height: 100vh; /* fullscreen */
12 | overflow: hidden;
13 | user-select: none;
14 | font-family: system-ui, sans-serif;
15 | font-size: 16px;
16 | color: #000000;
17 | touch-action: pinch-zoom; /* prevents navigating back with one finger */
18 | }
19 |
20 | /* game layers fill entire page */
21 | #game-canvas, #game-bg, #game-gradient, #game-ui, #game-tint {
22 | overflow: hidden;
23 | position: absolute;
24 | user-select: none;
25 | width: 100vw;
26 | height: 100vh;
27 | top: 0;
28 | left: 0;
29 | }
30 |
31 | /* from edge */
32 | #hamburger-container {
33 | overflow: visible;
34 | position: absolute;
35 | top: 10px;
36 | right: 8px;
37 | }
38 |
39 | html[dir="rtl"] #hamburger-container {
40 | right: unset;
41 | left: 10px;
42 | }
43 |
44 | html[dir="rtl"].icon-arrow-right,
45 | html[dir="rtl"].icon-arrow-left {
46 | /* undo RTL */
47 | transform: scale(-1, 1);
48 | }
49 |
50 | #modal-root {
51 | position: fixed;
52 | }
53 |
54 |
55 |
56 | /* page structure */
57 |
58 | #game-gradient {
59 | z-index: 1;
60 | pointer-events: none;
61 | }
62 |
63 | @media (forced-colors:active) {
64 | /* CanvasText, LinkText, VisitedText, ActiveText, GrayText, Field, FieldText, HighlightText, Highlight (txt bg), ButtonText, ButtonFace (btn bg) */
65 | #game-bg { background: Canvas !important; }
66 | }
67 |
68 | #game-bg {
69 | background-repeat: repeat;
70 | image-rendering: -moz-crisp-edges;
71 | image-rendering: -webkit-crisp-edges;
72 | image-rendering: pixelated;
73 | image-rendering: crisp-edges; /* important */
74 | z-index: 2;
75 | pointer-events: none;
76 | }
77 |
78 | #game-canvas {
79 | image-rendering: -moz-crisp-edges;
80 | image-rendering: -webkit-crisp-edges;
81 | image-rendering: pixelated;
82 | image-rendering: crisp-edges; /* important */
83 | z-index: 3;
84 | }
85 |
86 | /* overlay */
87 |
88 | #game-tint {
89 | pointer-events: none;
90 | z-index: 4;
91 | background: linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0.1) 100%);
92 | transition: opacity 0.35s, visibility 0s 0.35s;
93 | transition-timing-function: cubic-bezier(0.30, 0.20, 0.20, 1.00);
94 | visibility: hidden;
95 | opacity: 0;
96 | backdrop-filter: blur(0px);
97 | -webkit-backdrop-filter: blur(0px);
98 | }
99 |
100 | #game-tint.visible {
101 | pointer-events: inherit;
102 | transition: opacity 0.35s; /* instant visibility */
103 | visibility: visible;
104 | opacity: 1;
105 | backdrop-filter: blur(4px);
106 | -webkit-backdrop-filter: blur(4px);
107 | }
108 |
109 | /* main game ui */
110 |
111 | #game-ui {
112 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
113 | height: calc(100vh - 32px); /* fullscreen minus 32 padding */
114 | padding: 16px;
115 | z-index: 5;
116 | }
117 |
118 | #ui-title,
119 | #ui-subtitle {
120 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
121 | font-weight: bold;
122 | text-align: center;
123 | text-transform: uppercase;
124 | }
125 |
126 | #ui-title {
127 | margin-top: calc(10vh - 40px);
128 | line-height: 48px;
129 | font-size: 48px;
130 | }
131 |
132 | #ui-subtitle {
133 | margin-top: 10px;
134 | line-height: 24px;
135 | font-size: 24px;
136 | }
137 |
138 | /* shrinks title for super small screens */
139 | @media screen and (max-height: 480px) {
140 | #ui-title { line-height: 36px; font-size: 36px; }
141 | #ui-subtitle { line-height: 20px; font-size: 20px; }
142 | }
143 |
144 | /* hides mode title on tiny screens */
145 | @media screen and (max-height: 360px) {
146 | #ui-subtitle { visibility: hidden; }
147 | }
148 |
149 | /* hides game title on tiny screens */
150 | @media screen and (max-height: 280px) {
151 | #ui-title { visibility: hidden; }
152 | }
153 |
154 | #ui-selector {
155 | position: absolute;
156 | top: calc(40vh - 64px);
157 | left: calc(50vw - 240px);
158 | width: 480px; /* fullscreen minus 32 padding */
159 | height: 128px;
160 | display: flex;
161 | justify-content: space-between;
162 | align-items: center;
163 | }
164 |
165 | #ui-selector span {
166 | border-radius: 4px;
167 | background-color: rgba(255,255,255,0);
168 | transition: background-color 0.1s;
169 | }
170 |
171 | #ui-selector span:hover {
172 | background: rgba(255,255,255,0.4);
173 | cursor: pointer;
174 | }
175 |
176 | /* shrink menu arrows on narrow screens */
177 | @media screen and (max-width: 520px) {
178 | #ui-selector { left: calc(50vw - 220px); width: 440px; }
179 | }
180 |
181 | /* hide menu arrows on narrow screens */
182 | @media screen and (max-width: 420px) {
183 | #ui-selector span { visibility: hidden; }
184 | }
185 |
186 | #ui-instruct {
187 | position: absolute;
188 | top: calc(100vh * 0.6 - 32px); /* minus half height */
189 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
190 | text-align: center;
191 | }
192 |
193 | #instruct-content {
194 | display: inline-block;
195 | padding: 16px 12px;
196 | }
197 |
198 | #instruct-content.tinted {
199 | background-color: rgba(255,255,255,0.6);
200 | box-shadow: 0px 16px 24px rgba(0,0,0,0.16);
201 | border-radius: 4px;
202 | }
203 |
204 | @supports not (-moz-appearance:none) {
205 | #instruct-content.tinted {
206 | backdrop-filter: blur(32px);
207 | -webkit-backdrop-filter: blur(32px);
208 | }
209 | } /* Backdrop filter is broken on Firefox */
210 |
211 | #instruct-icon { /* xbox + ps */
212 | display: inline-block;
213 | vertical-align: middle;
214 | width: 32px;
215 | height: 32px;
216 | }
217 |
218 | #instruct-action { /* spacebar, double tap, double click */
219 | display: inline-block;
220 | margin: 0 8px;
221 | vertical-align: middle;
222 | }
223 |
224 | #instruct-action.outline { /* spacebar, double tap, double click */
225 | padding: 4px 12px 5px;
226 | border: 2px solid #000000;
227 | border-radius: 4px;
228 | font-size: 20px;
229 | font-weight: bold;
230 | text-transform: uppercase;
231 | }
232 |
233 | #instruct-text {
234 | display: inline-block;
235 | margin: 0 8px;
236 | vertical-align: middle;
237 | font-size: 24px;
238 | font-weight: 600;
239 | }
240 |
241 | #ui-notify {
242 | position: absolute;
243 | top: calc(100vh * 0.75 - 24px);
244 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
245 | text-align: center;
246 | transition: opacity 0.5s, visibility 0s 0.5s;
247 | transition-timing-function: cubic-bezier(0.30, 0.20, 0.20, 1.00);
248 | visibility: hidden;
249 | opacity: 0;
250 | }
251 |
252 | #ui-notify.visible {
253 | transition: opacity 0.35s; /* instant visibility */
254 | visibility: visible;
255 | opacity: 1;
256 | }
257 |
258 | #notify-content {
259 | display: inline-block;
260 | padding: 12px 20px;
261 | font-weight: 600;
262 | border-radius: 4px;
263 | color: #FFFFFF;
264 | background-color: rgba(0,0,0,0.7);
265 | backdrop-filter: blur(32px);
266 | -webkit-backdrop-filter: blur(32px);
267 | }
268 |
269 | #notify-shareLink {
270 | display: inline-flex;
271 | vertical-align: -33%;
272 | align-items: center;
273 | font-weight: normal;
274 | background-color: rgba(255,255,255,0.2);
275 | padding: 4px 8px 2px;
276 | margin-inline-start: 12px;
277 | border-radius: 4px;
278 | cursor: pointer;
279 | color: currentColor;
280 | font-family: inherit;
281 | }
282 |
283 | #notify-shareLink:hover {
284 | background-color: rgba(255,255,255,0.3);
285 | }
286 |
287 | #notify-shareText {
288 | }
289 |
290 | /* game stats and ui */
291 |
292 | #ui-dash {
293 | position: relative;
294 | width: 100%;
295 | margin: 0 auto;
296 | }
297 |
298 | #dash-stats {
299 | display: flex;
300 | width: 280px;
301 | margin: 0 auto;
302 | }
303 |
304 | #stats-score,
305 | .stats-icons {
306 | display: flex;
307 | justify-content: center;
308 | height: 22px;
309 | flex-basis: 136px;
310 | }
311 |
312 | .stats-icons {
313 | flex-basis: 72px;
314 | image-rendering: -moz-crisp-edges;
315 | image-rendering: -webkit-crisp-edges;
316 | image-rendering: pixelated;
317 | image-rendering: crisp-edges; /* important */
318 | }
319 |
320 | .stats-icons div {
321 | width: 22px;
322 | height: 22px;
323 | margin-inline-end: 2px;
324 | background-size: 1920px 512px;
325 | }
326 |
327 | .life-full { background-position: -1px -1px; }
328 | .life-empty { background-position: -1px -25px; }
329 | .boost-full { background-position: -25px -1px; }
330 | .boost-empty { background-position: -25px -25px; }
331 | .shield { background-position: -49px -1px; }
332 | .infinite { background-position: -49px -25px; }
333 |
334 | #score-icon {
335 | display: inline-block;
336 | vertical-align: middle;
337 | height: 22px;
338 | margin-inline-end: 4px;
339 | }
340 |
341 | #score-text {
342 | vertical-align: middle;
343 | font-size: 16px;
344 | font-weight: bold;
345 | line-height: 22px;
346 | }
347 |
348 | .icon-star {
349 | vertical-align: top;
350 | }
351 |
352 | .icon-fill {
353 | fill: #000000;
354 | }
355 |
356 | #ui-notify .icon-fill {
357 | fill: #FFFFFF;
358 | }
359 |
360 | @media (forced-colors:active) {
361 | /* CanvasText, LinkText, VisitedText, ActiveText, GrayText, Field, FieldText, HighlightText, Highlight (txt bg), ButtonText, ButtonFace (btn bg) */
362 | .icon-fill { fill: CanvasText !important; }
363 | }
--------------------------------------------------------------------------------
/resources/js/util.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | // #import {assertInstanceof} from './assert.m.js';
6 | // #import {dispatchSimpleEvent} from './cr.m.js';
7 |
8 | /**
9 | * Alias for document.getElementById. Found elements must be HTMLElements.
10 | * @param {string} id The ID of the element to find.
11 | * @return {HTMLElement} The found element or null if not found.
12 | */
13 | /* #export */ function $(id) {
14 | // Disable getElementById restriction here, since we are instructing other
15 | // places to re-use the $() that is defined here.
16 | // eslint-disable-next-line no-restricted-properties
17 | const el = document.getElementById(id);
18 | return el ? assertInstanceof(el, HTMLElement) : null;
19 | }
20 |
21 | // TODO(devlin): This should return SVGElement, but closure compiler is missing
22 | // those externs.
23 | /**
24 | * Alias for document.getElementById. Found elements must be SVGElements.
25 | * @param {string} id The ID of the element to find.
26 | * @return {Element} The found element or null if not found.
27 | */
28 | /* #export */ function getSVGElement(id) {
29 | // Disable getElementById restriction here, since it is not suitable for SVG
30 | // elements.
31 | // eslint-disable-next-line no-restricted-properties
32 | const el = document.getElementById(id);
33 | return el ? assertInstanceof(el, Element) : null;
34 | }
35 |
36 | /**
37 | * @return {?Element} The currently focused element (including elements that are
38 | * behind a shadow root), or null if nothing is focused.
39 | */
40 | /* #export */ function getDeepActiveElement() {
41 | let a = document.activeElement;
42 | while (a && a.shadowRoot && a.shadowRoot.activeElement) {
43 | a = a.shadowRoot.activeElement;
44 | }
45 | return a;
46 | }
47 |
48 | //
49 |
50 | /**
51 | * @param {Node} el A node to search for ancestors with |className|.
52 | * @param {string} className A class to search for.
53 | * @return {Element} A node with class of |className| or null if none is found.
54 | */
55 | /* #export */ function findAncestorByClass(el, className) {
56 | return /** @type {Element} */ (findAncestor(el, function(el) {
57 | return el.classList && el.classList.contains(className);
58 | }));
59 | }
60 |
61 | /**
62 | * Return the first ancestor for which the {@code predicate} returns true.
63 | * @param {Node} node The node to check.
64 | * @param {function(Node):boolean} predicate The function that tests the
65 | * nodes.
66 | * @param {boolean=} includeShadowHosts
67 | * @return {Node} The found ancestor or null if not found.
68 | */
69 | /* #export */ function findAncestor(node, predicate, includeShadowHosts) {
70 | while (node !== null) {
71 | if (predicate(node)) {
72 | break;
73 | }
74 | node = includeShadowHosts && node instanceof ShadowRoot ? node.host :
75 | node.parentNode;
76 | }
77 | return node;
78 | }
79 |
80 | /**
81 | * Disables text selection and dragging, with optional callbacks to specify
82 | * overrides.
83 | * @param {function(Event):boolean=} opt_allowSelectStart Unless this function
84 | * is defined and returns true, the onselectionstart event will be
85 | * suppressed.
86 | * @param {function(Event):boolean=} opt_allowDragStart Unless this function
87 | * is defined and returns true, the ondragstart event will be suppressed.
88 | */
89 | /* #export */ function disableTextSelectAndDrag(
90 | opt_allowSelectStart, opt_allowDragStart) {
91 | // Disable text selection.
92 | document.onselectstart = function(e) {
93 | if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) {
94 | e.preventDefault();
95 | }
96 | };
97 |
98 | // Disable dragging.
99 | document.ondragstart = function(e) {
100 | if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) {
101 | e.preventDefault();
102 | }
103 | };
104 | }
105 |
106 | /**
107 | * Check the directionality of the page.
108 | * @return {boolean} True if Chrome is running an RTL UI.
109 | */
110 | /* #export */ function isRTL() {
111 | return document.documentElement.dir === 'rtl';
112 | }
113 |
114 | /**
115 | * Get an element that's known to exist by its ID. We use this instead of just
116 | * calling getElementById and not checking the result because this lets us
117 | * satisfy the JSCompiler type system.
118 | * @param {string} id The identifier name.
119 | * @return {!HTMLElement} the Element.
120 | */
121 | /* #export */ function getRequiredElement(id) {
122 | return assertInstanceof(
123 | $(id), HTMLElement, 'Missing required element: ' + id);
124 | }
125 |
126 | /**
127 | * Query an element that's known to exist by a selector. We use this instead of
128 | * just calling querySelector and not checking the result because this lets us
129 | * satisfy the JSCompiler type system.
130 | * @param {string} selectors CSS selectors to query the element.
131 | * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional
132 | * context object for querySelector.
133 | * @return {!HTMLElement} the Element.
134 | */
135 | /* #export */ function queryRequiredElement(selectors, opt_context) {
136 | const element = (opt_context || document).querySelector(selectors);
137 | return assertInstanceof(
138 | element, HTMLElement, 'Missing required element: ' + selectors);
139 | }
140 |
141 | /**
142 | * Creates a new URL which is the old URL with a GET param of key=value.
143 | * @param {string} url The base URL. There is not sanity checking on the URL so
144 | * it must be passed in a proper format.
145 | * @param {string} key The key of the param.
146 | * @param {string} value The value of the param.
147 | * @return {string} The new URL.
148 | */
149 | /* #export */ function appendParam(url, key, value) {
150 | const param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
151 |
152 | if (url.indexOf('?') === -1) {
153 | return url + '?' + param;
154 | }
155 | return url + '&' + param;
156 | }
157 |
158 | /**
159 | * Creates an element of a specified type with a specified class name.
160 | * @param {string} type The node type.
161 | * @param {string} className The class name to use.
162 | * @return {Element} The created element.
163 | */
164 | /* #export */ function createElementWithClassName(type, className) {
165 | const elm = document.createElement(type);
166 | elm.className = className;
167 | return elm;
168 | }
169 |
170 | /**
171 | * transitionend does not always fire (e.g. when animation is aborted
172 | * or when no paint happens during the animation). This function sets up
173 | * a timer and emulate the event if it is not fired when the timer expires.
174 | * @param {!HTMLElement} el The element to watch for transitionend.
175 | * @param {number=} opt_timeOut The maximum wait time in milliseconds for the
176 | * transitionend to happen. If not specified, it is fetched from |el|
177 | * using the transitionDuration style value.
178 | */
179 | /* #export */ function ensureTransitionEndEvent(el, opt_timeOut) {
180 | if (opt_timeOut === undefined) {
181 | const style = getComputedStyle(el);
182 | opt_timeOut = parseFloat(style.transitionDuration) * 1000;
183 |
184 | // Give an additional 50ms buffer for the animation to complete.
185 | opt_timeOut += 50;
186 | }
187 |
188 | let fired = false;
189 | el.addEventListener('transitionend', function f(e) {
190 | el.removeEventListener('transitionend', f);
191 | fired = true;
192 | });
193 | window.setTimeout(function() {
194 | if (!fired) {
195 | cr.dispatchSimpleEvent(el, 'transitionend', true);
196 | }
197 | }, opt_timeOut);
198 | }
199 |
200 | /**
201 | * Alias for document.scrollTop getter.
202 | * @param {!HTMLDocument} doc The document node where information will be
203 | * queried from.
204 | * @return {number} The Y document scroll offset.
205 | */
206 | /* #export */ function scrollTopForDocument(doc) {
207 | return doc.documentElement.scrollTop || doc.body.scrollTop;
208 | }
209 |
210 | /**
211 | * Alias for document.scrollTop setter.
212 | * @param {!HTMLDocument} doc The document node where information will be
213 | * queried from.
214 | * @param {number} value The target Y scroll offset.
215 | */
216 | /* #export */ function setScrollTopForDocument(doc, value) {
217 | doc.documentElement.scrollTop = doc.body.scrollTop = value;
218 | }
219 |
220 | /**
221 | * Alias for document.scrollLeft getter.
222 | * @param {!HTMLDocument} doc The document node where information will be
223 | * queried from.
224 | * @return {number} The X document scroll offset.
225 | */
226 | /* #export */ function scrollLeftForDocument(doc) {
227 | return doc.documentElement.scrollLeft || doc.body.scrollLeft;
228 | }
229 |
230 | /**
231 | * Alias for document.scrollLeft setter.
232 | * @param {!HTMLDocument} doc The document node where information will be
233 | * queried from.
234 | * @param {number} value The target X scroll offset.
235 | */
236 | /* #export */ function setScrollLeftForDocument(doc, value) {
237 | doc.documentElement.scrollLeft = doc.body.scrollLeft = value;
238 | }
239 |
240 | /**
241 | * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding.
242 | * @param {string} original The original string.
243 | * @return {string} The string with all the characters mentioned above replaced.
244 | */
245 | /* #export */ function HTMLEscape(original) {
246 | return original.replace(/&/g, '&')
247 | .replace(//g, '>')
249 | .replace(/"/g, '"')
250 | .replace(/'/g, ''');
251 | }
252 |
253 | /**
254 | * Shortens the provided string (if necessary) to a string of length at most
255 | * |maxLength|.
256 | * @param {string} original The original string.
257 | * @param {number} maxLength The maximum length allowed for the string.
258 | * @return {string} The original string if its length does not exceed
259 | * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...'
260 | * appended.
261 | */
262 | /* #export */ function elide(original, maxLength) {
263 | if (original.length <= maxLength) {
264 | return original;
265 | }
266 | return original.substring(0, maxLength - 1) + '\u2026';
267 | }
268 |
269 | /**
270 | * Quote a string so it can be used in a regular expression.
271 | * @param {string} str The source string.
272 | * @return {string} The escaped string.
273 | */
274 | /* #export */ function quoteString(str) {
275 | return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
276 | }
277 |
278 | /**
279 | * Calls |callback| and stops listening the first time any event in |eventNames|
280 | * is triggered on |target|.
281 | * @param {!EventTarget} target
282 | * @param {!Array|string} eventNames Array or space-delimited string of
283 | * event names to listen to (e.g. 'click mousedown').
284 | * @param {function(!Event)} callback Called at most once. The
285 | * optional return value is passed on by the listener.
286 | */
287 | /* #export */ function listenOnce(target, eventNames, callback) {
288 | if (!Array.isArray(eventNames)) {
289 | eventNames = eventNames.split(/ +/);
290 | }
291 |
292 | const removeAllAndCallCallback = function(event) {
293 | eventNames.forEach(function(eventName) {
294 | target.removeEventListener(eventName, removeAllAndCallCallback, false);
295 | });
296 | return callback(event);
297 | };
298 |
299 | eventNames.forEach(function(eventName) {
300 | target.addEventListener(eventName, removeAllAndCallCallback, false);
301 | });
302 | }
303 |
304 | /**
305 | * @param {!Event} e
306 | * @return {boolean} Whether a modifier key was down when processing |e|.
307 | */
308 | /* #export */ function hasKeyModifiers(e) {
309 | return !!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey);
310 | }
311 |
312 | /**
313 | * @param {!Element} el
314 | * @return {boolean} Whether the element is interactive via text input.
315 | */
316 | /* #export */ function isTextInputElement(el) {
317 | return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
318 | }
319 |
320 | /* #ignore */ console.warn('crbug/1173575, non-JS module files deprecated.');
321 |
--------------------------------------------------------------------------------
/resources/js/cr.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /** @typedef {{eventName: string, uid: number}} */
6 | // eslint-disable-next-line no-var
7 | var WebUIListener;
8 |
9 | /** Platform, package, object property, and Event support. **/
10 | // eslint-disable-next-line no-var
11 | var cr = cr || function(global) {
12 | 'use strict';
13 |
14 | /**
15 | * Builds an object structure for the provided namespace path,
16 | * ensuring that names that already exist are not overwritten. For
17 | * example:
18 | * "a.b.c" -> a = {};a.b={};a.b.c={};
19 | * @param {string} name Name of the object that this file defines.
20 | * @return {!Object} The last object exported (i.e. exportPath('cr.ui')
21 | * returns a reference to the ui property of window.cr).
22 | * @private
23 | */
24 | function exportPath(name) {
25 | const parts = name.split('.');
26 | let cur = global;
27 |
28 | for (let part; parts.length && (part = parts.shift());) {
29 | if (part in cur) {
30 | cur = cur[part];
31 | } else {
32 | cur = cur[part] = {};
33 | }
34 | }
35 | return cur;
36 | }
37 |
38 | /**
39 | * Fires a property change event on the target.
40 | * @param {EventTarget} target The target to dispatch the event on.
41 | * @param {string} propertyName The name of the property that changed.
42 | * @param {*} newValue The new value for the property.
43 | * @param {*} oldValue The old value for the property.
44 | */
45 | function dispatchPropertyChange(target, propertyName, newValue, oldValue) {
46 | const e = new Event(propertyName + 'Change');
47 | e.propertyName = propertyName;
48 | e.newValue = newValue;
49 | e.oldValue = oldValue;
50 | target.dispatchEvent(e);
51 | }
52 |
53 | /**
54 | * Converts a camelCase javascript property name to a hyphenated-lower-case
55 | * attribute name.
56 | * @param {string} jsName The javascript camelCase property name.
57 | * @return {string} The equivalent hyphenated-lower-case attribute name.
58 | */
59 | function getAttributeName(jsName) {
60 | return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
61 | }
62 |
63 | /**
64 | * The kind of property to define in {@code defineProperty}.
65 | * @enum {string}
66 | * @const
67 | */
68 | const PropertyKind = {
69 | /**
70 | * Plain old JS property where the backing data is stored as a "private"
71 | * field on the object.
72 | * Use for properties of any type. Type will not be checked.
73 | */
74 | JS: 'js',
75 |
76 | /**
77 | * The property backing data is stored as an attribute on an element.
78 | * Use only for properties of type {string}.
79 | */
80 | ATTR: 'attr',
81 |
82 | /**
83 | * The property backing data is stored as an attribute on an element. If the
84 | * element has the attribute then the value is true.
85 | * Use only for properties of type {boolean}.
86 | */
87 | BOOL_ATTR: 'boolAttr'
88 | };
89 |
90 | /**
91 | * Helper function for defineProperty that returns the getter to use for the
92 | * property.
93 | * @param {string} name The name of the property.
94 | * @param {PropertyKind} kind The kind of the property.
95 | * @return {function():*} The getter for the property.
96 | */
97 | function getGetter(name, kind) {
98 | let attributeName;
99 | switch (kind) {
100 | case PropertyKind.JS:
101 | const privateName = name + '_';
102 | return function() {
103 | return this[privateName];
104 | };
105 | case PropertyKind.ATTR:
106 | attributeName = getAttributeName(name);
107 | return function() {
108 | return this.getAttribute(attributeName);
109 | };
110 | case PropertyKind.BOOL_ATTR:
111 | attributeName = getAttributeName(name);
112 | return function() {
113 | return this.hasAttribute(attributeName);
114 | };
115 | }
116 |
117 | // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
118 | // the browser/unit tests to preprocess this file through grit.
119 | throw 'not reached';
120 | }
121 |
122 | /**
123 | * Helper function for defineProperty that returns the setter of the right
124 | * kind.
125 | * @param {string} name The name of the property we are defining the setter
126 | * for.
127 | * @param {PropertyKind} kind The kind of property we are getting the
128 | * setter for.
129 | * @param {function(*, *):void=} opt_setHook A function to run after the
130 | * property is set, but before the propertyChange event is fired.
131 | * @return {function(*):void} The function to use as a setter.
132 | */
133 | function getSetter(name, kind, opt_setHook) {
134 | let attributeName;
135 | switch (kind) {
136 | case PropertyKind.JS:
137 | const privateName = name + '_';
138 | return function(value) {
139 | const oldValue = this[name];
140 | if (value !== oldValue) {
141 | this[privateName] = value;
142 | if (opt_setHook) {
143 | opt_setHook.call(this, value, oldValue);
144 | }
145 | dispatchPropertyChange(this, name, value, oldValue);
146 | }
147 | };
148 |
149 | case PropertyKind.ATTR:
150 | attributeName = getAttributeName(name);
151 | return function(value) {
152 | const oldValue = this[name];
153 | if (value !== oldValue) {
154 | if (value === undefined) {
155 | this.removeAttribute(attributeName);
156 | } else {
157 | this.setAttribute(attributeName, value);
158 | }
159 | if (opt_setHook) {
160 | opt_setHook.call(this, value, oldValue);
161 | }
162 | dispatchPropertyChange(this, name, value, oldValue);
163 | }
164 | };
165 |
166 | case PropertyKind.BOOL_ATTR:
167 | attributeName = getAttributeName(name);
168 | return function(value) {
169 | const oldValue = this[name];
170 | if (value !== oldValue) {
171 | if (value) {
172 | this.setAttribute(attributeName, name);
173 | } else {
174 | this.removeAttribute(attributeName);
175 | }
176 | if (opt_setHook) {
177 | opt_setHook.call(this, value, oldValue);
178 | }
179 | dispatchPropertyChange(this, name, value, oldValue);
180 | }
181 | };
182 | }
183 |
184 | // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
185 | // the browser/unit tests to preprocess this file through grit.
186 | throw 'not reached';
187 | }
188 |
189 | /**
190 | * Defines a property on an object. When the setter changes the value a
191 | * property change event with the type {@code name + 'Change'} is fired.
192 | * @param {!Object} obj The object to define the property for.
193 | * @param {string} name The name of the property.
194 | * @param {PropertyKind=} opt_kind What kind of underlying storage to use.
195 | * @param {function(*, *):void=} opt_setHook A function to run after the
196 | * property is set, but before the propertyChange event is fired.
197 | *
198 | * TODO(crbug.com/425829): This function makes use of deprecated getter or
199 | * setter functions.
200 | * @suppress {deprecated}
201 | */
202 | function defineProperty(obj, name, opt_kind, opt_setHook) {
203 | if (typeof obj === 'function') {
204 | obj = obj.prototype;
205 | }
206 |
207 | const kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS);
208 |
209 | // TODO(crbug.com/425829): Remove above suppression once we no longer use
210 | // deprecated functions lookupGetter, defineGetter, lookupSetter, and
211 | // defineSetter.
212 | // eslint-disable-next-line no-restricted-properties
213 | if (!obj.__lookupGetter__(name)) {
214 | // eslint-disable-next-line no-restricted-properties
215 | obj.__defineGetter__(name, getGetter(name, kind));
216 | }
217 |
218 | // eslint-disable-next-line no-restricted-properties
219 | if (!obj.__lookupSetter__(name)) {
220 | // eslint-disable-next-line no-restricted-properties
221 | obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
222 | }
223 | }
224 |
225 | /**
226 | * Returns a getter and setter to be used as property descriptor in
227 | * Object.defineProperty(). When the setter changes the value a property
228 | * change event with the type {@code name + 'Change'} is fired.
229 | * @param {string} name The name of the property.
230 | * @param {PropertyKind=} opt_kind What kind of underlying storage to use.
231 | * @param {function(*, *):void=} opt_setHook A function to run after the
232 | * property is set, but before the propertyChange event is fired.
233 | */
234 | function getPropertyDescriptor(name, opt_kind, opt_setHook) {
235 | const kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS);
236 |
237 | const desc = {
238 | get: getGetter(name, kind),
239 | set: getSetter(name, kind, opt_setHook),
240 | };
241 | return desc;
242 | }
243 |
244 | /**
245 | * Counter for use with createUid
246 | */
247 | let uidCounter = 1;
248 |
249 | /**
250 | * @return {number} A new unique ID.
251 | */
252 | function createUid() {
253 | return uidCounter++;
254 | }
255 |
256 | /**
257 | * Dispatches a simple event on an event target.
258 | * @param {!EventTarget} target The event target to dispatch the event on.
259 | * @param {string} type The type of the event.
260 | * @param {boolean=} opt_bubbles Whether the event bubbles or not.
261 | * @param {boolean=} opt_cancelable Whether the default action of the event
262 | * can be prevented. Default is true.
263 | * @return {boolean} If any of the listeners called {@code preventDefault}
264 | * during the dispatch this will return false.
265 | */
266 | function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
267 | const e = new Event(type, {
268 | bubbles: opt_bubbles,
269 | cancelable: opt_cancelable === undefined || opt_cancelable
270 | });
271 | return target.dispatchEvent(e);
272 | }
273 |
274 | /**
275 | * Calls |fun| and adds all the fields of the returned object to the object
276 | * named by |name|. For example, cr.define('cr.ui', function() {
277 | * function List() {
278 | * ...
279 | * }
280 | * function ListItem() {
281 | * ...
282 | * }
283 | * return {
284 | * List: List,
285 | * ListItem: ListItem,
286 | * };
287 | * });
288 | * defines the functions cr.ui.List and cr.ui.ListItem.
289 | * @param {string} name The name of the object that we are adding fields to.
290 | * @param {!Function} fun The function that will return an object containing
291 | * the names and values of the new fields.
292 | */
293 | function define(name, fun) {
294 | const obj = exportPath(name);
295 | const exports = fun();
296 | for (const propertyName in exports) {
297 | // Maybe we should check the prototype chain here? The current usage
298 | // pattern is always using an object literal so we only care about own
299 | // properties.
300 | const propertyDescriptor =
301 | Object.getOwnPropertyDescriptor(exports, propertyName);
302 | if (propertyDescriptor) {
303 | Object.defineProperty(obj, propertyName, propertyDescriptor);
304 | }
305 | }
306 | }
307 |
308 | /**
309 | * Adds a {@code getInstance} static method that always return the same
310 | * instance object.
311 | * @param {!Function} ctor The constructor for the class to add the static
312 | * method to.
313 | */
314 | function addSingletonGetter(ctor) {
315 | ctor.getInstance = function() {
316 | return ctor.instance_ || (ctor.instance_ = new ctor());
317 | };
318 | }
319 |
320 | /**
321 | * The mapping used by the sendWithPromise mechanism to tie the Promise
322 | * returned to callers with the corresponding WebUI response. The mapping is
323 | * from ID to the PromiseResolver helper; the ID is generated by
324 | * sendWithPromise and is unique across all invocations of said method.
325 | * @type {!Object}
326 | */
327 | const chromeSendResolverMap = {};
328 |
329 | /**
330 | * The named method the WebUI handler calls directly in response to a
331 | * chrome.send call that expects a response. The handler requires no knowledge
332 | * of the specific name of this method, as the name is passed to the handler
333 | * as the first argument in the arguments list of chrome.send. The handler
334 | * must pass the ID, also sent via the chrome.send arguments list, as the
335 | * first argument of the JS invocation; additionally, the handler may
336 | * supply any number of other arguments that will be included in the response.
337 | * @param {string} id The unique ID identifying the Promise this response is
338 | * tied to.
339 | * @param {boolean} isSuccess Whether the request was successful.
340 | * @param {*} response The response as sent from C++.
341 | */
342 | function webUIResponse(id, isSuccess, response) {
343 | const resolver = chromeSendResolverMap[id];
344 | delete chromeSendResolverMap[id];
345 |
346 | if (isSuccess) {
347 | resolver.resolve(response);
348 | } else {
349 | resolver.reject(response);
350 | }
351 | }
352 |
353 | /**
354 | * A variation of chrome.send, suitable for messages that expect a single
355 | * response from C++.
356 | * @param {string} methodName The name of the WebUI handler API.
357 | * @param {...*} var_args Variable number of arguments to be forwarded to the
358 | * C++ call.
359 | * @return {!Promise}
360 | */
361 | function sendWithPromise(methodName, var_args) {
362 | const args = Array.prototype.slice.call(arguments, 1);
363 | const promiseResolver = new PromiseResolver();
364 | const id = methodName + '_' + createUid();
365 | chromeSendResolverMap[id] = promiseResolver;
366 | chrome.send(methodName, [id].concat(args));
367 | return promiseResolver.promise;
368 | }
369 |
370 | /**
371 | * A map of maps associating event names with listeners. The 2nd level map
372 | * associates a listener ID with the callback function, such that individual
373 | * listeners can be removed from an event without affecting other listeners of
374 | * the same event.
375 | * @type {!Object>}
376 | */
377 | const webUIListenerMap = {};
378 |
379 | /**
380 | * The named method the WebUI handler calls directly when an event occurs.
381 | * The WebUI handler must supply the name of the event as the first argument
382 | * of the JS invocation; additionally, the handler may supply any number of
383 | * other arguments that will be forwarded to the listener callbacks.
384 | * @param {string} event The name of the event that has occurred.
385 | * @param {...*} var_args Additional arguments passed from C++.
386 | */
387 | function webUIListenerCallback(event, var_args) {
388 | const eventListenersMap = webUIListenerMap[event];
389 | if (!eventListenersMap) {
390 | // C++ event sent for an event that has no listeners.
391 | // TODO(dpapad): Should a warning be displayed here?
392 | return;
393 | }
394 |
395 | const args = Array.prototype.slice.call(arguments, 1);
396 | for (const listenerId in eventListenersMap) {
397 | eventListenersMap[listenerId].apply(null, args);
398 | }
399 | }
400 |
401 | /**
402 | * Registers a listener for an event fired from WebUI handlers. Any number of
403 | * listeners may register for a single event.
404 | * @param {string} eventName The event to listen to.
405 | * @param {!Function} callback The callback run when the event is fired.
406 | * @return {!WebUIListener} An object to be used for removing a listener via
407 | * cr.removeWebUIListener. Should be treated as read-only.
408 | */
409 | function addWebUIListener(eventName, callback) {
410 | webUIListenerMap[eventName] = webUIListenerMap[eventName] || {};
411 | const uid = createUid();
412 | webUIListenerMap[eventName][uid] = callback;
413 | return {eventName: eventName, uid: uid};
414 | }
415 |
416 | /**
417 | * Removes a listener. Does nothing if the specified listener is not found.
418 | * @param {!WebUIListener} listener The listener to be removed (as returned by
419 | * addWebUIListener).
420 | * @return {boolean} Whether the given listener was found and actually
421 | * removed.
422 | */
423 | function removeWebUIListener(listener) {
424 | const listenerExists = webUIListenerMap[listener.eventName] &&
425 | webUIListenerMap[listener.eventName][listener.uid];
426 | if (listenerExists) {
427 | delete webUIListenerMap[listener.eventName][listener.uid];
428 | return true;
429 | }
430 | return false;
431 | }
432 |
433 | return {
434 | addSingletonGetter: addSingletonGetter,
435 | define: define,
436 | defineProperty: defineProperty,
437 | getPropertyDescriptor: getPropertyDescriptor,
438 | dispatchPropertyChange: dispatchPropertyChange,
439 | dispatchSimpleEvent: dispatchSimpleEvent,
440 | PropertyKind: PropertyKind,
441 |
442 | // C++ <-> JS communication related methods.
443 | addWebUIListener: addWebUIListener,
444 | removeWebUIListener: removeWebUIListener,
445 | sendWithPromise: sendWithPromise,
446 | webUIListenerCallback: webUIListenerCallback,
447 | webUIResponse: webUIResponse,
448 |
449 | /** Whether we are using a Mac or not. */
450 | get isMac() {
451 | return /Mac/.test(navigator.platform);
452 | },
453 |
454 | /** Whether this is on the Windows platform or not. */
455 | get isWindows() {
456 | return /Win/.test(navigator.platform);
457 | },
458 |
459 | /** Whether this is the ChromeOS/ash web browser. */
460 | get isChromeOS() {
461 | let returnValue = false;
462 | // TODO(https://crbug.com/1118190): grit conditionals do not work in many
463 | // WebUI tests.
464 | //
465 | return returnValue;
466 | },
467 |
468 | /** Whether this is the ChromeOS/Lacros web browser. */
469 | get isLacros() {
470 | let returnValue = false;
471 | // TODO(https://crbug.com/1118190): grit conditionals do not work in many
472 | // WebUI tests.
473 | //
474 | return returnValue;
475 | },
476 |
477 | /** Whether this is on vanilla Linux (not chromeOS). */
478 | get isLinux() {
479 | return /Linux/.test(navigator.userAgent);
480 | },
481 |
482 | /** Whether this is on Android. */
483 | get isAndroid() {
484 | return /Android/.test(navigator.userAgent);
485 | },
486 |
487 | /** Whether this is on iOS. */
488 | get isIOS() {
489 | return /EdgiOS/.test(navigator.userAgent);
490 | }
491 | };
492 | }(this);
493 |
--------------------------------------------------------------------------------