├── .gitignore ├── BUILD.bazel ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── WORKSPACE ├── demo ├── BUILD.bazel ├── codelab.html ├── embed.html ├── hello.html ├── hello.js ├── hello_test.js └── img │ ├── 156b5e3cc8373d55.png │ ├── 166c3b4982e4a0ad.png │ ├── 1f454b6807700695.png │ ├── 39b4e0371e9703e6.png │ ├── 433870360ad308d4.png │ ├── 7656372ff6c6a0f7.png │ ├── 81347b12f83e4291.png │ ├── 8a959b48e233bc93.png │ ├── 9efdf0d1258b78e4.png │ ├── aa64e93e8151b642.png │ ├── ab9c361527825fac.png │ ├── b1728ef310c444f5.png │ ├── bf15c2f18d7f945c.png │ ├── cbfdd0302b611ab0.png │ ├── cf095c2153306fa7.png │ ├── daefd30e8a290df5.png │ ├── dc07bbc9fcfe7c5b.png │ └── ed4633f91ec1389f.png ├── google-codelab-analytics ├── BUILD.bazel ├── google_codelab_analytics.js ├── google_codelab_analytics_def.js └── google_codelab_analytics_test.js ├── google-codelab-index ├── BUILD.bazel ├── _cards.scss ├── _categories.scss ├── google_codelab_index.js ├── google_codelab_index.scss ├── google_codelab_index.soy ├── google_codelab_index_cards.js ├── google_codelab_index_cards_def.js ├── google_codelab_index_def.js └── index.html ├── google-codelab-step ├── BUILD.bazel ├── _syntax.scss ├── google-codelab-step.html ├── google_codelab_step.js ├── google_codelab_step.scss ├── google_codelab_step.soy ├── google_codelab_step_def.js ├── google_codelab_step_test.js ├── img-1.png ├── img-2.png ├── img-3.png ├── img-4.png ├── img-5.png ├── img-6.png ├── img-7.png └── img-8.png ├── google-codelab-survey ├── BUILD.bazel ├── google-codelab-survey.html ├── google_codelab_survey.js ├── google_codelab_survey.scss ├── google_codelab_survey.soy ├── google_codelab_survey_def.js └── google_codelab_survey_test.js ├── google-codelab ├── BUILD.bazel ├── _drawer.scss ├── _steps.scss ├── google_codelab.js ├── google_codelab.scss ├── google_codelab.soy ├── google_codelab_def.js ├── img │ ├── 25c5ac88e3641e75.png │ ├── 350dceb89c6e3968.png │ ├── 3f1ab21e1e5c772b.png │ ├── 53b42d1efc0e0295.png │ ├── 5c79e3f467c21ce6.png │ ├── 7c7f4389428d02f9.png │ ├── 9dec2e61f3d3b641.png │ ├── a21ac67adf427ddc.png │ ├── a322aaec88da31f0.png │ ├── afb844ab04c5e37a.png │ ├── b79cf053ec60b7a4.png │ ├── dd9ae517d0d8e68f.png │ ├── f43aa9981defd294.png │ └── fb8ec99e99f182ac.png └── index.html ├── third_party ├── BUILD.es6shim ├── BUILD.polyfill └── BUILD.prettify └── tools ├── BUILD.bazel ├── bazel.rc ├── ci-continuous.sh ├── ci-presubmit.sh ├── defs.bzl ├── gen_test_html.template ├── server.go └── webtest.go /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | load("//tools:defs.bzl", "concat") 4 | 5 | licenses(["notice"]) 6 | 7 | exports_files(["LICENSE", "README.md"]) 8 | 9 | filegroup( 10 | name = "all_files", 11 | srcs = [ 12 | ":codelab_elements_js", 13 | ":codelab_elements_css", 14 | ":codelab_index_js", 15 | ":codelab_index_css", 16 | ], 17 | ) 18 | 19 | genrule( 20 | name = "bundle", 21 | outs = ["bundle.zip"], 22 | srcs = [ 23 | "LICENSE", 24 | "README.md", 25 | ":all_files", 26 | "@prettify//:prettify", 27 | "@polyfill//:custom_elements", 28 | "@polyfill//:native_shim", 29 | ], 30 | cmd = "zip -j $@ $(SRCS)", 31 | ) 32 | 33 | genrule( 34 | name = "codelab_elements_js", 35 | outs = ["codelab-elements.js"], 36 | srcs = [ 37 | "//google-codelab-analytics:google_codelab_analytics_bin", 38 | "//google-codelab:google_codelab_bin", 39 | "//google-codelab-step:google_codelab_step_bin", 40 | "//google-codelab-survey:google_codelab_survey_bin", 41 | ], 42 | cmd = concat("js"), 43 | ) 44 | 45 | genrule( 46 | name = "codelab_elements_css", 47 | outs = ["codelab-elements.css"], 48 | srcs = [ 49 | "//google-codelab:google_codelab_scss_bin", 50 | "//google-codelab-step:google_codelab_step_scss_bin", 51 | "//google-codelab-survey:google_codelab_survey_scss_bin", 52 | ], 53 | cmd = concat("css"), 54 | ) 55 | 56 | genrule( 57 | name = "codelab_index_js", 58 | outs = ["codelab-index.js"], 59 | srcs = [ 60 | "//google-codelab-index:google_codelab_index_bin", 61 | ], 62 | cmd = concat("js"), 63 | ) 64 | 65 | genrule( 66 | name = "codelab_index_css", 67 | outs = ["codelab-index.css"], 68 | srcs = [ 69 | "//google-codelab-index:google_codelab_index_scss_bin", 70 | ], 71 | cmd = concat("css"), 72 | ) 73 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | This repository is deprecated - make needed changes to codelab-elements code in 3 | [googlecodelabs/tools/codelab-elements](https://github.com/googlecodelabs/tools/tree/master/codelab-elements). 4 | 5 | # Codelab Custom Elements 6 | 7 | The next generation of the codelab elements without any framework or library 8 | dependencies, only the [Custom Elements](https://html.spec.whatwg.org/multipage/custom-elements.html) 9 | standard spec. 10 | 11 | If this is a release bundle, produced with a `bazel build :bundle` command, 12 | you should see `codelab-elements.js`, `codelab-elements.css` and other files, 13 | ready to be added to an HTML page like the following. Only relevant parts are shown: 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | 21 | A codelab demo 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Contents of the first step. 31 | 32 | 33 | Contents of the second step. 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | You can download the latest version 45 | from https://github.com/googlecodelabs/codelab-elements. 46 | 47 | ## Dev environment 48 | 49 | All you need is [bazel](https://docs.bazel.build/versions/master/install.html). 50 | 51 | After bazel is installed, try executing the following: 52 | 53 | bazel test --test_output=all //demo:hello_test 54 | 55 | It will take some time at the first run because bazel will download and compile 56 | all dependencies needed to work with the code and run tests. This includes 57 | Google Closure library and compiler, Go language and browsers to run local JS 58 | tests on. 59 | 60 | ### Building 61 | 62 | Check out a demo HelloElement target. To build the element, execute the following: 63 | 64 | bazel build //demo:hello_bin 65 | 66 | It should output something like this: 67 | 68 | INFO: Analysed target //demo:hello_bin (0 packages loaded). 69 | INFO: Found 1 target... 70 | Target //demo:hello_bin up-to-date: 71 | bazel-bin/demo/hello_bin.js 72 | bazel-bin/demo/hello_bin.js.map 73 | INFO: Elapsed time: 0.716s, Critical Path: 0.03s 74 | INFO: Build completed successfully, 1 total action 75 | 76 | ### Testing 77 | 78 | All elements should have their test targets. 79 | As a starting point, check out HelloElement tests: 80 | 81 | bazel test --test_output=errors //demo:hello_test 82 | 83 | You should see something like this: 84 | 85 | INFO: Elapsed time: 5.394s, Critical Path: 4.60s 86 | INFO: Build completed successfully, 2 total actions 87 | //demo:hello_test_chromium-local PASSED in 4.6s 88 | 89 | When things go wrong, it is usually easier to inspect and analyze output 90 | with debug enabled: 91 | 92 | bazel test -s --verbose_failures --test_output=all --test_arg=-debug demo/hello_test 93 | 94 | ### Manual inspection from a browser 95 | 96 | To browse things around manually with a real browser, execute the following: 97 | 98 | bazel run //tools:server 99 | 100 | and navigate to http://localhost:8080. 101 | 102 | ## Notes 103 | 104 | This is not an official Google product. 105 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "googlecodelabs_custom_elements") 2 | 3 | # Required by io_bazel_rules_webtesting. 4 | skylib_ver = "f9b0ff1dd3d119d19b9cacbbc425a9e61759f1f5" 5 | http_archive( 6 | name = "bazel_skylib", 7 | sha256 = "ce27a2007deda8a1de65df9de3d4cd93a5360ead43c5ff3017ae6b3a2abe485e", 8 | strip_prefix = "bazel-skylib-{v}".format(v=skylib_ver), 9 | urls = [ 10 | "https://github.com/bazelbuild/bazel-skylib/archive/{v}.tar.gz".format(v=skylib_ver), 11 | ], 12 | ) 13 | 14 | rules_closure_ver = "0.9.0" 15 | http_archive( 16 | name = "io_bazel_rules_closure", 17 | sha256 = "054717a2e6a415001bc4c608b208723526bdf6cace3592ca6efb3749ba18ce21", 18 | strip_prefix = "rules_closure-{v}".format(v=rules_closure_ver), 19 | url = "https://github.com/shawnbuso/rules_closure/archive/{v}.zip".format(v=rules_closure_ver), 20 | ) 21 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories") 22 | closure_repositories() 23 | 24 | http_archive( 25 | name = "io_bazel_rules_go", 26 | sha256 = "53c8222c6eab05dd49c40184c361493705d4234e60c42c4cd13ab4898da4c6be", 27 | url = "https://github.com/bazelbuild/rules_go/releases/download/0.10.0/rules_go-0.10.0.tar.gz", 28 | ) 29 | load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains") 30 | go_rules_dependencies() 31 | go_register_toolchains() 32 | 33 | rules_webtesting_ver = "936c760cff973a63031be0d0518b40a228e224e3" 34 | http_archive( 35 | name = "io_bazel_rules_webtesting", 36 | sha256 = "797b75e792a34728a6a3846c7c3d3ad669f12cd8490b888cc969bad93d236b1b", 37 | strip_prefix = "rules_webtesting-{v}".format(v=rules_webtesting_ver), 38 | url = "https://github.com/bazelbuild/rules_webtesting/archive/{v}.zip".format(v=rules_webtesting_ver), 39 | ) 40 | load( 41 | "@io_bazel_rules_webtesting//web:repositories.bzl", 42 | "browser_repositories", 43 | "web_test_repositories", 44 | ) 45 | web_test_repositories() 46 | browser_repositories(chromium = True) 47 | 48 | prettify_ver = "2013-03-04" 49 | new_http_archive( 50 | name = "prettify", 51 | build_file = "third_party/BUILD.prettify", 52 | strip_prefix = "code-prettify-{v}".format(v=prettify_ver), 53 | url = "https://github.com/google/code-prettify/archive/{v}.zip".format(v=prettify_ver), 54 | ) 55 | 56 | new_http_archive( 57 | name = "polyfill", 58 | build_file = "third_party/BUILD.polyfill", 59 | sha256 = "9606cdeacbb67f21fb495a4b0a0e5ea6a137fc453945907822e1b930e77124d4", 60 | strip_prefix = "custom-elements-1.0.8", 61 | url = "https://github.com/webcomponents/custom-elements/archive/v1.0.8.zip", 62 | ) 63 | 64 | es6shim_ver = "8d7aec1403751686dbbd3c4fa13a7bb584a75bf3" 65 | new_http_archive( 66 | name = "es6shim", 67 | build_file = "third_party/BUILD.es6shim", 68 | sha256 = "108e7de0edc041a36561e1518fc5d87569f5cfd5449977658cc9b1e2c84743b3", 69 | strip_prefix = "es6-shim-{v}".format(v=es6shim_ver), 70 | url = "https://github.com/es-shims/es6-shim/archive/{v}.zip".format(v=es6shim_ver), 71 | ) 72 | 73 | git_repository( 74 | name = "io_bazel_rules_sass", 75 | remote = "https://github.com/bazelbuild/rules_sass.git", 76 | tag = "0.0.3", 77 | ) 78 | 79 | load("@io_bazel_rules_sass//sass:sass.bzl", "sass_repositories") 80 | 81 | sass_repositories() 82 | -------------------------------------------------------------------------------- /demo/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) 4 | 5 | exports_files(["LICENSE"]) 6 | 7 | load("//tools:defs.bzl", 8 | "closure_js_library", "closure_js_binary", "closure_js_test") 9 | 10 | # All static artifacts needed for various demo files. 11 | # Used by //tools:server target. 12 | filegroup( 13 | name = "demo_files", 14 | srcs = glob([ 15 | "*.html", 16 | "img/*.png", 17 | ]), 18 | ) 19 | 20 | # A playground HelloElement. 21 | closure_js_library( 22 | name = "hello_lib", 23 | srcs = ["hello.js"], 24 | convention = "GOOGLE", 25 | ) 26 | 27 | # Compiled version of HelloElement, suitable for distribution. 28 | closure_js_binary( 29 | name = "hello_bin", 30 | entry_points = ["googlecodelabs.HelloElement"], 31 | deps = [":hello_lib"], 32 | ) 33 | 34 | # A few tests for HelloElement. 35 | # The following closure_js_test expands into something like this: 36 | # 37 | # js_test( 38 | # name = "hello_test", 39 | # srcs = ["hello_test.js"], 40 | # browsers = [ 41 | # # For experimental purposes only. Eventually you should 42 | # # create your own browser definitions. 43 | # "@io_bazel_rules_webtesting//browsers:chromium-local", 44 | # ], 45 | # data = ["@polyfill//:custom_elements"], 46 | # entry_points = ["googlecodelabs.hello_test"], 47 | # suppress = ["JSC_EXTRA_REQUIRE_WARNING"], 48 | # deps = [ 49 | # ":hello_lib", 50 | # "@io_bazel_rules_closure//closure/library:testing", 51 | # ], 52 | # ) 53 | # 54 | # Additionally, a gen_hello_test.html file is generated, 55 | # declaring the hello_test_bin.js script source. 56 | closure_js_test( 57 | name = "hello_test", 58 | srcs = ["hello_test.js"], 59 | entry_points = ["googlecodelabs.hello_test"], 60 | deps = [":hello_lib"], 61 | ) 62 | -------------------------------------------------------------------------------- /demo/embed.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Embeddable codelab demo 23 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 54 | 55 | 56 | 57 |
58 | 59 |

Codelabs > Build Google Maps Using Web Components & No Code!

60 | 61 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur quis dolor vel arcu blandit tristique. Proin vestibulum nec felis non fringilla. Pellentesque vulputate dui ut risus bibendum, sed egestas arcu ullamcorper. Quisque eget eros pellentesque, aliquet tortor placerat, vehicula lectus. Fusce sit amet mattis turpis, et tempus orci. Vestibulum mauris velit, vulputate a risus quis, imperdiet hendrerit ante. Nunc sollicitudin risus tortor, ac venenatis sem volutpat malesuada. Mauris neque metus, ornare eget porta id, tincidunt vitae magna. In scelerisque quam auctor maximus pellentesque. Sed laoreet ex mi, vel lacinia urna consectetur id. Sed est quam, finibus eget orci in, vulputate tempus diam

62 | 63 | 71 | 72 | 73 | 74 |

In this codelab, You'll create a fully working Google Maps app using elements in Polymer's Google Web Components collection. The app will be responsive, include driving directions, and transit a mode. Along the way, you'll also learn about Polymer's data-binding features and iron element set.

75 |

What you'll learn

76 |
    77 |
  • How to start a new Polymer-based project using Chrome Dev Editor
  • 78 |
  • How to use elements in Polymer's iron, paper, and Google Web Component sets.
  • 79 |
  • How to use Polymer's data binding features to reduce boilerplate code.
  • 80 |
81 |

What you'll need

82 |
    83 |
  • Basic understanding of HTML, CSS, and web development.
  • 84 |
  • Install Chrome Dev Editor or use your own editor of choice.
  • 85 |
86 |

How would rate your experience with Polymer?

87 | NoviceIntermediateAdvanced
88 | 89 |
90 | 91 | 92 | 93 |

Create a new project

94 | 95 | 97 |

The first time you run Chrome Dev Editor it will ask you to setup your workspace environment.

Fire up Chrome Dev Editor and start a new project:

    98 |
  1. Click to start a new project.
  2. 99 |
  3. Enter "PolymerMapsCodelab" as the Project name.
  4. 100 |
  5. In the Project type dropdown, select "JavaScript web app (using Polymer paper elements)".
  6. 101 |
  7. Click the Create button.
  8. 102 |

Chrome Dev Editor creates a basic scaffold for your Polymer app. In the background, it also uses Bower to download and install a list of dependencies (including the Polymer core library) into the bower_components/ folder. Fetching the components make take some time if your internet connection is slow. You'll learn more about using Bower in the next step.

103 |

bower.json

104 | 105 |
PolymerMapsCodelab/
106 |   bower_components/ <!-- installed dependencies from Bower -->
107 |   bower.json  <!-- Bower metadata files used for managing deps -->
108 |   index.html  <!-- your app -->
109 |   main.js
110 |   styles.css
111 | 112 |

Preview the app

113 |

At any point, select the index.html file and hit the  button in the top toolbar to run the app. Chrome Dev Editor fires up a web server and navigates to the index.html page. This is great way to preview changes as you make them.

114 |

Next up

115 |

At this point the app doesn't do much. Let's add a map!

116 | 117 |
118 | 119 | 120 | 121 |

The Google Web Components provide the <google-map> element for declaratively rendering a Google Map. To use it, you first need to install it using Bower.

122 | 124 | 125 |

Install the element

126 |

Normally, you'd run bower install GoogleWebComponents/google-map --save on the command line to install <google-map> and save it as a dependency. However, Chrome Dev Editor does not have a command line for running Bower commands. Instead, you need to manually edit bower.json to include google-map, then run Chrome Dev Editor's Bower Update feature. Bower Update checks the dependencies in bower.json and installs any missing ones.

  1. Edit bower.json and add google-map to the dependencies object:
127 |
"dependencies": {
128 |   "iron-elements": "PolymerElements/iron-elements#^1.0.0",
129 |   "paper-elements": "PolymerElements/paper-elements#^1.0.1",
130 |   "google-map": "GoogleWebComponents/google-map#^1.0.3"
131 | }
132 |
    133 |
  1. Right-click the bower.json filename in the editor.
  2. 134 |
  3. Select Bower Update from the context menu.
  4. 135 |

The download may take few seconds. You can verify that <google-map> (and any dependencies) were installed by checking that bower_components/google-map/ was created and populated.

136 |

Use the element

137 |

To employ <google-map>, you need to:

    138 |
  1. Use an HTML Import to load it in index.html.
  2. 139 |
  3. Declare an instance of the element on the page.
  4. 140 |

In index.html, remove all other HTML imports in the <head> and replace them with a single import that loads google-map.html:

141 |

index.html

142 | 143 |
<head>
144 |   ....
145 |   <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
146 |   <link rel="import" href="bower_components/google-map/google-map.html">
147 | </head>
148 | 149 | 150 |

Next, replace the contents of <body> with an instance of <google-map>:

151 |

index.html

152 | 153 |
<body unresolved>
154 |   <google-map latitude="37.779" longitude="-122.3892" zoom="13"></google-map>
155 | </body>
156 |

As you can see, using <google-map> is completely declarative! The map is centered using the latitude and longitude attributes and its zoom level is set by the zoom attribute.

157 |

Style the map

158 |

If you run the app right now, nothing will display. In order for the map to properly display itself, you need to set its container (in this case, <body>) to have a fixed height.

Open styles.css and replace its contents with default styling:

159 |

styles.css

160 | 161 |
body, html {
162 |   font-family: 'Roboto', Arial, sans-serif;
163 |   height: 100%;
164 |   margin: 0;
165 | }
166 | 167 |

Add a marker

168 |

<google-map> supports adding map markers to the map by declaring <google-map-marker> elements as children. The marker locations are also set using latitude and longitude attributes.

Back in index.html, add a draggable <google-map-marker> to the map:

169 |

index.html

170 | 171 |
<google-map latitude="37.779" longitude="-122.3892" zoom="13" disable-default-ui>
172 |   <google-map-marker latitude="37.779" longitude="-122.3892"
173 |       title="Go Giants!" draggable="true"></google-map-marker>
174 | </google-map>
175 |

Notice that we've also disabled the map's controls by setting disableDefaultUi to true. Since it's a boolean property, its presence as an HTML attribute makes it truthy.

176 |

Run the app

177 |

If you haven't already done so, hit the  button. At this point, you should see a map that takes up the entire viewport and has a single marker pin.

178 |

Frequently Asked Questions

179 | 181 | 182 |
183 | 184 |
185 | 186 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur quis dolor vel arcu blandit tristique. Proin vestibulum nec felis non fringilla. Pellentesque vulputate dui ut risus bibendum, sed egestas arcu ullamcorper. Quisque eget eros pellentesque, aliquet tortor placerat, vehicula lectus. Fusce sit amet mattis turpis, et tempus orci. Vestibulum mauris velit, vulputate a risus quis, imperdiet hendrerit ante. Nunc sollicitudin risus tortor, ac venenatis sem volutpat malesuada. Mauris neque metus, ornare eget porta id, tincidunt vitae magna. In scelerisque quam auctor maximus pellentesque. Sed laoreet ex mi, vel lacinia urna consectetur id. Sed est quam, finibus eget orci in, vulputate tempus diam

187 | 188 |
189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /demo/hello.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | Hello Custom Elements 21 | 22 | 23 | 24 | Hello, I'm a custom element. 25 | If you see this static text, it means I haven't been upgraded. :( 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /demo/hello.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.HelloElement'); 19 | 20 | /** 21 | * @extends {HTMLElement} 22 | */ 23 | class HelloElement extends HTMLElement { 24 | /** 25 | * @export 26 | * @override 27 | */ 28 | connectedCallback() { 29 | console.log("HelloElement: connected"); 30 | this.innerHTML = "Hello. I have just been upgraded!"; 31 | } 32 | } 33 | 34 | window.customElements.define('hello-element', HelloElement); 35 | 36 | exports = HelloElement; 37 | -------------------------------------------------------------------------------- /demo/hello_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.hello_test'); 19 | goog.setTestOnly(); 20 | 21 | const HelloElement = goog.require('googlecodelabs.HelloElement'); 22 | const testSuite = goog.require('goog.testing.testSuite'); 23 | goog.require('goog.testing.asserts'); 24 | goog.require('goog.testing.jsunit'); 25 | 26 | testSuite({ 27 | testHelloEquals() { 28 | const x = 6; 29 | assertEquals(6, x); 30 | }, 31 | 32 | testHelloUpgraded() { 33 | const div = document.createElement('div'); 34 | div.innerHTML = "static"; 35 | document.body.appendChild(div); 36 | let text = div.textContent; 37 | assert(`"${text}" does not end with 'upgraded!'`, text.endsWith("upgraded!")); 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /demo/img/156b5e3cc8373d55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/156b5e3cc8373d55.png -------------------------------------------------------------------------------- /demo/img/166c3b4982e4a0ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/166c3b4982e4a0ad.png -------------------------------------------------------------------------------- /demo/img/1f454b6807700695.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/1f454b6807700695.png -------------------------------------------------------------------------------- /demo/img/39b4e0371e9703e6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/39b4e0371e9703e6.png -------------------------------------------------------------------------------- /demo/img/433870360ad308d4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/433870360ad308d4.png -------------------------------------------------------------------------------- /demo/img/7656372ff6c6a0f7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/7656372ff6c6a0f7.png -------------------------------------------------------------------------------- /demo/img/81347b12f83e4291.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/81347b12f83e4291.png -------------------------------------------------------------------------------- /demo/img/8a959b48e233bc93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/8a959b48e233bc93.png -------------------------------------------------------------------------------- /demo/img/9efdf0d1258b78e4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/9efdf0d1258b78e4.png -------------------------------------------------------------------------------- /demo/img/aa64e93e8151b642.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/aa64e93e8151b642.png -------------------------------------------------------------------------------- /demo/img/ab9c361527825fac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/ab9c361527825fac.png -------------------------------------------------------------------------------- /demo/img/b1728ef310c444f5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/b1728ef310c444f5.png -------------------------------------------------------------------------------- /demo/img/bf15c2f18d7f945c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/bf15c2f18d7f945c.png -------------------------------------------------------------------------------- /demo/img/cbfdd0302b611ab0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/cbfdd0302b611ab0.png -------------------------------------------------------------------------------- /demo/img/cf095c2153306fa7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/cf095c2153306fa7.png -------------------------------------------------------------------------------- /demo/img/daefd30e8a290df5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/daefd30e8a290df5.png -------------------------------------------------------------------------------- /demo/img/dc07bbc9fcfe7c5b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/dc07bbc9fcfe7c5b.png -------------------------------------------------------------------------------- /demo/img/ed4633f91ec1389f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/demo/img/ed4633f91ec1389f.png -------------------------------------------------------------------------------- /google-codelab-analytics/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) 4 | 5 | exports_files(["LICENSE"]) 6 | 7 | load("//tools:defs.bzl", 8 | "closure_js_library", "closure_js_binary", "closure_js_test") 9 | 10 | 11 | filegroup( 12 | name = "google_codelab_analytics_files", 13 | srcs = [ 14 | ":google_codelab_analytics_bin", 15 | ], 16 | ) 17 | 18 | # Codelab Analytics. 19 | closure_js_library( 20 | name = "google_codelab_analytics", 21 | srcs = [ 22 | "google_codelab_analytics.js", 23 | "google_codelab_analytics_def.js" 24 | ], 25 | deps = [ 26 | "@io_bazel_rules_closure//closure/library", 27 | ], 28 | ) 29 | 30 | # Codelab Analytics Tests 31 | closure_js_test( 32 | name = "google_codelab_analytics_test", 33 | srcs = ["google_codelab_analytics_test.js"], 34 | entry_points = ["googlecodelabs.CodelabAnalyticsTest"], 35 | deps = [ 36 | "@io_bazel_rules_closure//closure/library", 37 | ":google_codelab_analytics" 38 | ], 39 | ) 40 | 41 | # Compiled version of GoogleCodelabAnalytics element, suitable for distribution. 42 | closure_js_binary( 43 | name = "google_codelab_analytics_bin", 44 | entry_points = ["googlecodelabs.CodelabAnalyticsDef"], 45 | deps = [":google_codelab_analytics"], 46 | ) 47 | -------------------------------------------------------------------------------- /google-codelab-analytics/google_codelab_analytics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabAnalytics'); 19 | 20 | const EventHandler = goog.require('goog.events.EventHandler'); 21 | 22 | /** 23 | * The general codelab action event fired for trackable interactions. 24 | * @const string 25 | */ 26 | const ACTION_EVENT = 'google-codelab-action'; 27 | 28 | /** 29 | * The general codelab pageview event fired for trackable pageviews. 30 | * @const string 31 | */ 32 | const PAGEVIEW_EVENT = 'google-codelab-pageview'; 33 | 34 | /** 35 | * The Google Analytics ID. Analytics CE will not complete initialization 36 | * without a valid Analytics ID value set for this. 37 | * @const {string} 38 | */ 39 | const GAID_ATTR = 'gaid'; 40 | 41 | /** 42 | * The GAID defined by the current codelab. 43 | * @const {string} 44 | */ 45 | const CODELAB_GAID_ATTR = 'codelab-gaid'; 46 | 47 | /** @const {string} */ 48 | const CODELAB_ENV_ATTR = 'environment'; 49 | 50 | /** @const {string} */ 51 | const CODELAB_CATEGORY_ATTR = 'category'; 52 | 53 | /** @const {string} */ 54 | const ANALYTICS_READY_ATTR = 'anayltics-ready'; 55 | 56 | /** 57 | * A list of selectors whose elements are waiting for this to be set up. 58 | * @const Array 59 | */ 60 | const DEPENDENT_SELECTORS = ['google-codelab']; 61 | 62 | 63 | /** 64 | * Event detail passed when firing ACTION_EVENT. 65 | * 66 | * @typedef {{ 67 | * category: string, 68 | * action: string, 69 | * label: (?string|undefined), 70 | * value: (?number|undefined) 71 | * }} 72 | */ 73 | let AnalyticsTrackingEvent; 74 | 75 | /** 76 | * Event detail passed when firing ACTION_EVENT. 77 | * 78 | * @typedef {{ 79 | * page: string, 80 | * title: string, 81 | * }} 82 | */ 83 | let AnalyticsPageview; 84 | 85 | 86 | /** 87 | * @extends {HTMLElement} 88 | * @suppress {reportUnknownTypes} 89 | */ 90 | class CodelabAnalytics extends HTMLElement { 91 | /** @return {string} */ 92 | static getTagName() { return 'google-codelab-analytics'; } 93 | 94 | constructor() { 95 | super(); 96 | 97 | /** @private {boolean} */ 98 | this.hasSetup_ = false; 99 | 100 | /** @private {?string} */ 101 | this.gaid_; 102 | 103 | /** 104 | * @private {!EventHandler} 105 | * @const 106 | */ 107 | this.eventHandler_ = new EventHandler(); 108 | 109 | /** 110 | * @private {!EventHandler} 111 | * @const 112 | */ 113 | this.pageviewEventHandler_ = new EventHandler(); 114 | 115 | /** @private {?string} */ 116 | this.codelabCategory_ = this.getAttribute(CODELAB_CATEGORY_ATTR) || ''; 117 | 118 | /** @private {?string} */ 119 | this.codelabEnv_ = this.getAttribute(CODELAB_ENV_ATTR) || ''; 120 | } 121 | 122 | /** 123 | * @export 124 | * @override 125 | */ 126 | connectedCallback() { 127 | this.gaid_ = this.getAttribute(GAID_ATTR) || ''; 128 | 129 | if (this.hasSetup_ || !this.gaid_) { 130 | return; 131 | } 132 | 133 | if (!goog.isDef(window['ga'])) { 134 | this.initGAScript_().then((response) => { 135 | if (response) { 136 | this.init_(); 137 | } 138 | }); 139 | } else { 140 | this.init_(); 141 | } 142 | } 143 | 144 | /** @private */ 145 | init_() { 146 | this.createTrackers_(); 147 | this.addEventListeners_(); 148 | this.setAnalyticsReadyAttrs_(); 149 | this.hasSetup_ = true; 150 | } 151 | 152 | /** @private */ 153 | addEventListeners_() { 154 | this.eventHandler_.listen(document.body, ACTION_EVENT, 155 | (e) => { 156 | const detail = /** @type {AnalyticsTrackingEvent} */ ( 157 | e.getBrowserEvent().detail); 158 | // Add tracking... 159 | this.trackEvent_( 160 | detail['category'], detail['action'], detail['label'], 161 | detail['value']); 162 | }); 163 | 164 | this.pageviewEventHandler_.listen(document.body, PAGEVIEW_EVENT, 165 | (e) => { 166 | const detail = /** @type {AnalyticsPageview} */ ( 167 | e.getBrowserEvent().detail); 168 | this.trackPageview_(detail['page'], detail['title']); 169 | }); 170 | } 171 | 172 | /** 173 | * @return {!Array} 174 | * @export 175 | */ 176 | static get observedAttributes() { 177 | return [CODELAB_GAID_ATTR, CODELAB_ENV_ATTR, CODELAB_CATEGORY_ATTR]; 178 | } 179 | 180 | /** 181 | * @param {string} attr 182 | * @param {?string} oldValue 183 | * @param {?string} newValue 184 | * @param {?string} namespace 185 | * @export 186 | * @override 187 | */ 188 | attributeChangedCallback(attr, oldValue, newValue, namespace) { 189 | switch (attr) { 190 | case GAID_ATTR: 191 | this.gaid_ = newValue; 192 | break; 193 | case CODELAB_GAID_ATTR: 194 | if (newValue && this.hasSetup_) { 195 | this.createCodelabGATracker_(); 196 | } 197 | break; 198 | case CODELAB_ENV_ATTR: 199 | this.codelabEnv_ = newValue; 200 | break; 201 | case CODELAB_CATEGORY_ATTR: 202 | this.codelabCategory_ = newValue; 203 | break; 204 | } 205 | } 206 | 207 | /** 208 | * Fires an analytics tracking event to all configured trackers. 209 | * @param {string} category The event category. 210 | * @param {string=} opt_action The event action. 211 | * @param {?string=} opt_label The event label. 212 | * @param {?number=} opt_value The event value. 213 | * @private 214 | */ 215 | trackEvent_(category, opt_action, opt_label, opt_value) { 216 | const params = { 217 | // Always event for trackEvent_ method 218 | 'hitType': 'event', 219 | 'dimension1': this.codelabEnv_, 220 | 'dimension2': this.codelabCategory_, 221 | 'eventCategory': category, 222 | 'eventAction': opt_action || '', 223 | 'eventLabel': opt_label || '', 224 | 'eventValue': opt_value || '', 225 | }; 226 | this.gaSend_(params); 227 | } 228 | 229 | /** 230 | * @param {?string=} opt_page The page to track. 231 | * @param {?string=} opt_title The codelabs title. 232 | * @private 233 | */ 234 | trackPageview_(opt_page, opt_title) { 235 | const params = { 236 | 'hitType': 'pageview', 237 | 'dimension1': this.codelabEnv_, 238 | 'dimension2': this.codelabCategory_, 239 | 'page': opt_page || '', 240 | 'title': opt_title || '' 241 | }; 242 | this.gaSend_(params); 243 | } 244 | 245 | /** 246 | * Sets analytics ready attributes on dependent elements. 247 | */ 248 | setAnalyticsReadyAttrs_() { 249 | DEPENDENT_SELECTORS.forEach((selector) => { 250 | document.querySelectorAll(selector).forEach((element) => { 251 | element.setAttribute(ANALYTICS_READY_ATTR, ANALYTICS_READY_ATTR); 252 | }); 253 | }); 254 | } 255 | 256 | /** @private */ 257 | gaSend_(params) { 258 | window['ga'](function() { 259 | const trackers = window['ga'].getAll(); 260 | trackers.forEach((tracker) => { 261 | tracker.send(params); 262 | }); 263 | }); 264 | } 265 | 266 | /** 267 | * @export 268 | * @override 269 | */ 270 | disconnectedCallback() { 271 | this.eventHandler_.removeAll(); 272 | } 273 | 274 | /** 275 | * @return {string} 276 | * @private 277 | */ 278 | getGAView_() { 279 | let parts = location.search.substring(1).split('&'); 280 | for (let i = 0; i < parts.length; i++) { 281 | let param = parts[i].split('='); 282 | if (param[0] === 'viewga') { 283 | return param[1]; 284 | } 285 | } 286 | return ''; 287 | } 288 | 289 | /** 290 | * @return {!Promise} 291 | * @export 292 | */ 293 | static injectGAScript() { 294 | /** @type {!HTMLScriptElement} */ 295 | const resource = /** @type {!HTMLScriptElement} */ ( 296 | document.createElement('script')); 297 | resource.src = '//www.google-analytics.com/analytics.js'; 298 | resource.async = false; 299 | return new Promise((resolve, reject) => { 300 | resource.onload = () => resolve(resource); 301 | resource.onerror = (event) => { 302 | // remove on error 303 | if (resource.parentNode) { 304 | resource.parentNode.removeChild(resource); 305 | } 306 | reject(); 307 | }; 308 | if (document.head) { 309 | document.head.appendChild(resource); 310 | } 311 | }); 312 | } 313 | 314 | /** 315 | * @return {!Promise} 316 | * @private 317 | */ 318 | async initGAScript_() { 319 | // This is a pretty-printed version of the function(i,s,o,g,r,a,m) script 320 | // provided by Google Analytics. 321 | window['GoogleAnalyticsObject'] = 'ga'; 322 | window['ga'] = window['ga'] || function() { 323 | (window['ga']['q'] = window['ga']['q'] || []).push(arguments); 324 | }; 325 | window['ga']['l'] = (new Date()).valueOf(); 326 | 327 | try { 328 | return await CodelabAnalytics.injectGAScript(); 329 | } catch(e) { 330 | return; 331 | } 332 | } 333 | 334 | /** @private */ 335 | createTrackers_() { 336 | // The default tracker is given name 't0' per analytics.js dev docs. 337 | if (this.gaid_ && !this.isTrackerCreated_(this.gaid_)) { 338 | window['ga']('create', this.gaid_, 'auto'); 339 | } 340 | 341 | const gaView = this.getGAView_(); 342 | if (gaView && !this.isTrackerCreated_(gaView)) { 343 | window['ga']('create', gaView, 'auto', 'view'); 344 | window['ga']('view.send', 'pageview'); 345 | } 346 | 347 | this.createCodelabGATracker_(); 348 | } 349 | 350 | /** 351 | * Creates a GA tracker specific to the codelab. 352 | * @private 353 | */ 354 | createCodelabGATracker_() { 355 | const codelabGAId = this.getAttribute(CODELAB_GAID_ATTR); 356 | if (codelabGAId && !this.isTrackerCreated_(codelabGAId)) { 357 | window['ga']('create', codelabGAId, 'auto', 'codelabAccount'); 358 | } 359 | } 360 | 361 | /** 362 | * @param {string} trackerId The tracker ID to check for. 363 | * @return {boolean} 364 | * @private 365 | */ 366 | isTrackerCreated_(trackerId) { 367 | const allTrackers = window['ga'].getAll(); 368 | let isCreated = false; 369 | allTrackers.forEach((tracker) => { 370 | if (tracker.get('trackingId') == trackerId) { 371 | isCreated = true; 372 | } 373 | }); 374 | return isCreated; 375 | } 376 | } 377 | 378 | exports = CodelabAnalytics; 379 | -------------------------------------------------------------------------------- /google-codelab-analytics/google_codelab_analytics_def.js: -------------------------------------------------------------------------------- 1 | goog.module('googlecodelabs.CodelabAnalyticsDef'); 2 | const CodelabAnalytics = goog.require('googlecodelabs.CodelabAnalytics'); 3 | 4 | try { 5 | window.customElements.define(CodelabAnalytics.getTagName(), CodelabAnalytics); 6 | } catch (e) { 7 | console.warn('googlecodelabs.CodelabAnalytics', e); 8 | } -------------------------------------------------------------------------------- /google-codelab-analytics/google_codelab_analytics_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabAnalyticsTest'); 19 | goog.setTestOnly(); 20 | 21 | const CodelabAnalytics = goog.require('googlecodelabs.CodelabAnalytics'); 22 | window.customElements.define(CodelabAnalytics.getTagName(), CodelabAnalytics); 23 | const MockControl = goog.require('goog.testing.MockControl'); 24 | const dom = goog.require('goog.dom'); 25 | const testSuite = goog.require('goog.testing.testSuite'); 26 | goog.require('goog.testing.asserts'); 27 | goog.require('goog.testing.jsunit'); 28 | 29 | let mockControl; 30 | /** 31 | * Noop the inject function, we don't need to be going to the actual network 32 | * in tests 33 | */ 34 | CodelabAnalytics.injectGAscript = () => {}; 35 | 36 | testSuite({ 37 | 38 | setUp() { 39 | mockControl = new MockControl(); 40 | }, 41 | 42 | tearDown() { 43 | dom.removeNode(document.body.querySelector('google-codelab-analytics')); 44 | 45 | mockControl.$resetAll(); 46 | mockControl.$tearDown(); 47 | }, 48 | 49 | testGAIDAttr_InitsTracker() { 50 | const analytics = new CodelabAnalytics(); 51 | 52 | // Need to mock as we don't have window.ga.getAll() 53 | const mockGetAll = mockControl.createFunctionMock('getAll'); 54 | const mockCreate = mockControl.createFunctionMock('create'); 55 | mockGetAll().$returns([]).$anyTimes(); 56 | mockCreate().$once(); 57 | 58 | analytics.setAttribute('gaid', 'UA-123'); 59 | 60 | window['ga'] = (...args) => { 61 | if (['create', 'getAll'].indexOf(args[0]) !== -1) { 62 | window['ga'][args[0]](); 63 | } 64 | }; 65 | window['ga']['getAll'] = mockGetAll; 66 | window['ga']['create'] = mockCreate; 67 | 68 | mockControl.$replayAll(); 69 | document.body.appendChild(analytics); 70 | mockControl.$verifyAll(); 71 | }, 72 | 73 | testViewParam_InitsViewTracker() { 74 | const analytics = new CodelabAnalytics(); 75 | analytics.setAttribute('gaid', 'UA-123'); 76 | 77 | const loc = window.location; 78 | var newurl = loc.protocol + '//' + loc.host + loc.pathname + 79 | '?viewga=testView¶m2=hi'; 80 | window.history.pushState({ path: newurl }, '', newurl); 81 | 82 | // Need to mock as we don't have window.ga.getAll() 83 | const mockGetAll = mockControl.createFunctionMock('getAll'); 84 | const mockCreate = mockControl.createFunctionMock('create'); 85 | mockGetAll().$returns([]).$anyTimes(); 86 | // Creates 2 trackers (because of view param). 87 | mockCreate().$times(2); 88 | 89 | window['ga'] = (...args) => { 90 | if (['create', 'getAll'].indexOf(args[0]) !== -1) { 91 | window['ga'][args[0]](); 92 | } 93 | }; 94 | window['ga']['getAll'] = mockGetAll; 95 | window['ga']['create'] = mockCreate; 96 | 97 | mockControl.$replayAll(); 98 | 99 | document.body.appendChild(analytics); 100 | 101 | mockControl.$verifyAll(); 102 | window.history.back(); 103 | }, 104 | 105 | testCodelabGAIDAttr_InitsCodelabTracker() { 106 | const analytics = new CodelabAnalytics(); 107 | analytics.setAttribute('gaid', 'UA-123'); 108 | // Need to mock as we don't have window.ga.getAll() 109 | const mockGetAll = mockControl.createFunctionMock('getAll'); 110 | const mockCreate = mockControl.createFunctionMock('create'); 111 | mockGetAll().$returns([]).$anyTimes(); 112 | // Creates 2 trackers (because of codelab gaid attribute). 113 | mockCreate().$times(2); 114 | 115 | window['ga'] = (...args) => { 116 | if (['create', 'getAll'].indexOf(args[0]) !== -1) { 117 | window['ga'][args[0]](); 118 | } 119 | }; 120 | window['ga']['getAll'] = mockGetAll; 121 | window['ga']['create'] = mockCreate; 122 | 123 | mockControl.$replayAll(); 124 | 125 | document.body.appendChild(analytics); 126 | analytics.setAttribute('codelab-gaid', 'UA-456'); 127 | mockControl.$verifyAll(); 128 | }, 129 | 130 | async testSetAnalyticsReadyAttrs() { 131 | const analytics = new CodelabAnalytics(); 132 | analytics.setAttribute('gaid', 'UA-123'); 133 | // Need to mock as we don't have window.ga.getAll() 134 | const mockGetAll = mockControl.createFunctionMock('getAll'); 135 | const mockCreate = mockControl.createFunctionMock('create'); 136 | mockGetAll().$returns([]).$anyTimes(); 137 | // Creates 2 trackers (because of codelab gaid attribute). 138 | mockCreate().$times(2); 139 | 140 | window['ga'] = (...args) => { 141 | if (['create', 'getAll'].indexOf(args[0]) !== -1) { 142 | window['ga'][args[0]](); 143 | } 144 | }; 145 | window['ga']['getAll'] = mockGetAll; 146 | window['ga']['create'] = mockCreate; 147 | 148 | mockControl.$replayAll(); 149 | 150 | const codelabElement = document.createElement('google-codelab'); 151 | document.body.appendChild(codelabElement); 152 | document.body.appendChild(analytics); 153 | 154 | // This is obviously awful, but for some reason 155 | // mockControl.$waitAndVerifyAll() isn't working with closure_js_test. See 156 | // https://github.com/bazelbuild/rules_closure/issues/316. Once that's 157 | // resolved we can use it in place of the timeout. 158 | setTimeout(() => { 159 | assertEquals('', codelabElement.getAttribute('analytics-ready')); 160 | }, 5000); 161 | 162 | }, 163 | 164 | testPageviewEventDispatch_SendsPageViewTracking() { 165 | 166 | }, 167 | 168 | testEventDispatch_SendsEventTracking() { 169 | 170 | }, 171 | 172 | testCodelabAttributes_UpdatesTrackingParams() { 173 | 174 | } 175 | }); 176 | -------------------------------------------------------------------------------- /google-codelab-index/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) 4 | 5 | exports_files(["LICENSE"]) 6 | 7 | load("//tools:defs.bzl", 8 | "closure_js_library", "closure_js_binary", "closure_js_test") 9 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library") 10 | load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library") 11 | 12 | filegroup( 13 | name = "google_codelab_index_files", 14 | srcs = glob([ 15 | "*.html", 16 | ]) + [ 17 | ":google_codelab_index_scss_bin", 18 | ":google_codelab_index_bin", 19 | ], 20 | ) 21 | 22 | # Codelabs 23 | closure_js_library( 24 | name = "google_codelab_index", 25 | srcs = [ 26 | "google_codelab_index.js", 27 | "google_codelab_index_def.js" 28 | ], 29 | deps = [ 30 | "@io_bazel_rules_closure//closure/library", 31 | ":google_codelab_index_soy", 32 | ":google_codelab_index_cards", 33 | ] 34 | ) 35 | 36 | closure_js_library( 37 | name = "google_codelab_index_cards", 38 | srcs = [ 39 | "google_codelab_index_cards.js", 40 | "google_codelab_index_cards_def.js" 41 | ], 42 | deps = [ 43 | "@io_bazel_rules_closure//closure/library", 44 | ":google_codelab_index_soy", 45 | ] 46 | ) 47 | 48 | # Compiled version of GoogleCodelabStep element, suitable for distribution. 49 | closure_js_binary( 50 | name = "google_codelab_index_bin", 51 | entry_points = [ 52 | "googlecodelabs.CodelabIndex.CardsDef", 53 | "googlecodelabs.CodelabIndexDef", 54 | ], 55 | deps = [":google_codelab_index"], 56 | ) 57 | 58 | sass_library( 59 | name = "categories_scss", 60 | srcs = ["_categories.scss"], 61 | ) 62 | 63 | sass_library( 64 | name = "cards_scss", 65 | srcs = ["_cards.scss"], 66 | ) 67 | 68 | sass_library( 69 | name = "google_codelab_scss", 70 | srcs = ["google_codelab.scss"], 71 | ) 72 | 73 | sass_binary( 74 | name = "google_codelab_index_scss_bin", 75 | src = "google_codelab_index.scss", 76 | deps = [ 77 | ":cards_scss", 78 | ":categories_scss", 79 | ] 80 | ) 81 | 82 | closure_js_template_library( 83 | name = "google_codelab_index_soy", 84 | srcs = ["google_codelab_index.soy"] 85 | ) 86 | -------------------------------------------------------------------------------- /google-codelab-index/_cards.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | google-codelab-index-cards { 19 | display: flex; 20 | flex-wrap: wrap; 21 | justify-content: space-between; 22 | margin: 0 0 24px 0; 23 | } 24 | 25 | google-codelab-index-cards[num="1"], 26 | google-codelab-index-cards[num="2"] { 27 | justify-content: flex-start; 28 | } 29 | 30 | google-codelab-index-cards .card { 31 | position: relative; 32 | display: flex; 33 | flex-direction: column; 34 | flex: 1 0 330px; 35 | margin: 0 0 16px 0; 36 | background-color: #fff; 37 | max-width: 330px; 38 | flex-basis: 330px; 39 | flex-grow: 0; 40 | flex-shrink: 0; 41 | } 42 | 43 | google-codelab-index-cards[num="1"] .card, 44 | google-codelab-index-cards[num="2"] .card { 45 | margin-right: 8px; 46 | } 47 | 48 | google-codelab-index-cards .card .card-header { 49 | display: flex; 50 | justify-content: space-between; 51 | align-items: center; 52 | padding: 16px; 53 | } 54 | 55 | google-codelab-index-cards .card-header .category-icon { 56 | width: 40px; 57 | height: 40px; 58 | border-radius: 50%; 59 | background-color: white; 60 | background-size: 70%; 61 | background-position: 50% 50%; 62 | background-repeat: no-repeat; 63 | } 64 | 65 | google-codelab-index-cards .card-header .card-duration { 66 | display: flex; 67 | align-items: center; 68 | color: #FFFFFF; 69 | } 70 | 71 | google-codelab-index-cards .card-header .card-duration img { 72 | margin-right: 4px; 73 | } 74 | 75 | google-codelab-index-cards .card .card-description { 76 | padding: 16px 32px; 77 | flex: 1 0 auto; 78 | color: #3C4043 79 | } 80 | 81 | google-codelab-index-cards .card .card-footer { 82 | display: flex; 83 | align-items: center; 84 | justify-content: space-between; 85 | border-top: 1px solid #DADCE0; 86 | padding: 8px 16px; 87 | } 88 | 89 | google-codelab-index-cards .card .card-start { 90 | color: #5c5c5c; 91 | line-height: 1; 92 | letter-spacing: 0.01em; 93 | min-width: 5.14em; 94 | margin: 0 0.29em; 95 | font: inherit; 96 | text-transform: uppercase; 97 | outline-width: 0; 98 | border-radius: 3px; 99 | user-select: none; 100 | cursor: pointer; 101 | z-index: 0; 102 | padding: 0.7em 0.57em; 103 | font-family: 'Roboto', 'Noto', sans-serif; 104 | -webkit-font-smoothing: antialiased; 105 | font-size: 14px; 106 | font-weight: 500; 107 | display: inline-flex; 108 | align-items: center; 109 | justify-content: center; 110 | } 111 | 112 | google-codelab-index-cards .card .card-updated { 113 | font-size: 12px; 114 | color: #9AA0A6; 115 | line-height: 16px; 116 | text-align: right; 117 | } 118 | 119 | @import "categories"; 120 | 121 | @media (min-width: 768px) { 122 | google-codelab-index-cards .card { 123 | min-height: 250px; 124 | } 125 | } 126 | 127 | @media (max-width: 1136px) and (min-width: 767px) { 128 | google-codelab-index-cards .card { 129 | flex: 0 0 33%; 130 | margin: 0 0% 2% 0; 131 | min-height: 200px; 132 | } 133 | } 134 | 135 | @media (max-width: 767px) { 136 | google-codelab-index-cards { 137 | flex-direction: column; 138 | align-items: center; 139 | margin: 0; 140 | } 141 | 142 | google-codelab-index-cards .card { 143 | margin: 0 0 16px; 144 | min-width: 330px; 145 | max-width: 100%; 146 | width: 100%; 147 | flex: 1 0 200px; 148 | } 149 | google-codelab-index-cards[num="1"] .card, 150 | google-codelab-index-cards[num="2"] .card { 151 | margin-right: 0; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /google-codelab-index/_categories.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /* Common colors used in more than one category. */ 19 | $appsBackgroundColor: #D93025; 20 | $unityBackgroundColor: #5F6368; 21 | $webBackgroundColor: #1A73E8; 22 | /* Common icons used in more than one category. */ 23 | $webIcon: 'web.svg'; 24 | 25 | /* Mixins. */ 26 | @mixin colorize-card($name, $color, $icon:false) { 27 | .#{$name}-bg { 28 | background-color: $color; 29 | } 30 | 31 | .#{$name}-icon { 32 | @if $icon { 33 | background-image: url(//codelabs.developers.google.com/images/icons/#{$icon}); 34 | } @else { 35 | opacity: 0; 36 | } 37 | } 38 | 39 | google-codelab-index-cards .card .#{$name}-start { 40 | color: $color; 41 | &:hover, 42 | &:active { 43 | background: rgba($color, 0.1); 44 | } 45 | } 46 | } 47 | 48 | @include colorize-card('default', #5F6368); 49 | @include colorize-card('about', #9AA0A6, 'google_g.svg'); 50 | @include colorize-card('ads', #188038, 'ads.png'); 51 | @include colorize-card('analytics', #E37400, 'analytics.png'); 52 | @include colorize-card('android', #71B958, 'android.svg'); 53 | @include colorize-card('android-auto', #71B958, 'android.svg'); 54 | @include colorize-card('android-tv', #71B958, 'android.svg'); 55 | @include colorize-card('android-wear', #71B958, 'android.svg'); 56 | @include colorize-card('android-things', #4B830D, 'android-things.svg'); 57 | @include colorize-card('apps', $appsBackgroundColor, 'apps.png'); 58 | @include colorize-card('assistant', $webBackgroundColor, 'assistant.png'); 59 | @include colorize-card('blockly', #A142FA, 'blockly.png'); 60 | @include colorize-card('brillo', $webBackgroundColor, 'brillo.png'); 61 | @include colorize-card('cast', #5F6368, 'cast.png'); 62 | @include colorize-card('chrome', $webBackgroundColor, 'chrome.svg'); 63 | @include colorize-card('cloud', $webBackgroundColor, 'cloud.png'); 64 | @include colorize-card('cloud-about', $webBackgroundColor, 'cloud.png'); 65 | @include colorize-card('cloud-app-engine', $webBackgroundColor, 'appengine.png'); 66 | @include colorize-card('cloud-appengine', $webBackgroundColor, 'appengine.png'); 67 | @include colorize-card('cloud-cloud-tools', $webBackgroundColor, 'cloud.png'); 68 | @include colorize-card('cloud-compute', $webBackgroundColor, 'cloud-compute.png'); 69 | @include colorize-card('cloud-data', $webBackgroundColor, 'cloud-data.png'); 70 | @include colorize-card('cloud-general', $webBackgroundColor, 'cloud.png'); 71 | @include colorize-card('cloud-machine-learning', $webBackgroundColor, 'cloud-ml.png'); 72 | @include colorize-card('cloud-mobile', $webBackgroundColor, 'cloud-mobile.png'); 73 | @include colorize-card('cloud-monitoring', $webBackgroundColor, 'cloud-monitoring.png'); 74 | @include colorize-card('cloud-networking', $webBackgroundColor, 'cloud-networking.png'); 75 | @include colorize-card('cloud-others', $webBackgroundColor, 'cloud.png'); 76 | @include colorize-card('cloud-security', $webBackgroundColor, 'cloud-security.png'); 77 | @include colorize-card('cloud-web', $webBackgroundColor, $webIcon); 78 | @include colorize-card('design', #212121, 'design.svg'); 79 | @include colorize-card('firebase', #E37400, 'firebase.png'); 80 | @include colorize-card('firebase-web', $webBackgroundColor, $webIcon); 81 | @include colorize-card('flutter', #02569B, 'flutter.png'); 82 | @include colorize-card('games', #A142FA, 'cardboard.png'); 83 | @include colorize-card('iot', $unityBackgroundColor, 'iot.svg'); 84 | @include colorize-card('nest', #5F6368, 'nest.png'); 85 | @include colorize-card('openthread', #174EA6, 'openthread.png'); 86 | @include colorize-card('play-games', #A142FA, 'cardboard.png'); 87 | @include colorize-card('geo', $webBackgroundColor, 'geo.png'); 88 | @include colorize-card('search', $webBackgroundColor, 'google_g.svg'); 89 | @include colorize-card('sheets', $appsBackgroundColor, 'sheets.png'); 90 | @include colorize-card('tensorflow', #E37400, 'tensorflow.png'); 91 | @include colorize-card('unity', $unityBackgroundColor, 'unity.png'); 92 | @include colorize-card('vr', #A142FA, 'cardboard.png'); 93 | @include colorize-card('virtual-reality', #A142FA, 'vr.svg'); 94 | @include colorize-card('weave', #188038, 'weave.svg'); 95 | @include colorize-card('web', $webBackgroundColor, $webIcon); 96 | -------------------------------------------------------------------------------- /google-codelab-index/google_codelab_index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabIndex'); 19 | 20 | const Cards = goog.require('googlecodelabs.CodelabIndex.Cards'); 21 | const Debouncer = goog.require('goog.async.Debouncer'); 22 | const EventHandler = goog.require('goog.events.EventHandler'); 23 | const Templates = goog.require('googlecodelabs.CodelabIndex.Templates'); 24 | const dom = goog.require('goog.dom'); 25 | const events = goog.require('goog.events'); 26 | const soy = goog.require('goog.soy'); 27 | 28 | /** @const {string} */ 29 | const CATEGORY_ATTR = 'category'; 30 | 31 | /** @const {string} */ 32 | const CATEGORY_PARAM = 'cat'; 33 | 34 | /** @const {string} */ 35 | const SORT_ATTR = 'sort'; 36 | 37 | /** @const {string} */ 38 | const FILTER_ATTR = 'filter'; 39 | 40 | /** @const {string} */ 41 | const TAGS_ATTR = 'tags'; 42 | 43 | /** 44 | * Interval over which to debounce in ms. 45 | * @const {number} 46 | */ 47 | const SEARCH_DEBOUNCE_INTERVAL = 20; 48 | 49 | /** 50 | * @extends {HTMLElement} 51 | */ 52 | class CodelabIndex extends HTMLElement { 53 | /** @return {string} */ 54 | static getTagName() { return 'google-codelab-index'; } 55 | 56 | constructor() { 57 | super(); 58 | 59 | /** 60 | * @private {!EventHandler} 61 | * @const 62 | */ 63 | this.eventHandler_ = new EventHandler(); 64 | 65 | /** @private {boolean} */ 66 | this.hasSetup_ = false; 67 | 68 | /** @private {?Cards} */ 69 | this.cards_ = null; 70 | 71 | /** @private {?Element} */ 72 | this.sortBy_ = null; 73 | 74 | /** @private {?HTMLInputElement} */ 75 | this.search_ = null; 76 | 77 | /** @private {?Element} */ 78 | this.clearSearchBtn_ = null; 79 | 80 | /** @private {?HTMLSelectElement} */ 81 | this.categoriesSelect_ = null; 82 | 83 | /** 84 | * @private {!Debouncer} 85 | * @const 86 | */ 87 | this.searchDebouncer_ = new Debouncer( 88 | () => this.handleSearchDebounced_(), SEARCH_DEBOUNCE_INTERVAL); 89 | 90 | } 91 | 92 | /** 93 | * @export 94 | * @override 95 | */ 96 | connectedCallback() { 97 | if (!this.hasSetup_) { 98 | this.setupDom_(); 99 | } 100 | 101 | this.addEvents_(); 102 | 103 | window.requestAnimationFrame(() => { 104 | document.body.removeAttribute('unresolved'); 105 | }); 106 | } 107 | 108 | /** 109 | * @export 110 | * @override 111 | */ 112 | disconnectedCallback() { 113 | this.eventHandler_.removeAll(); 114 | } 115 | 116 | /** 117 | * @private 118 | */ 119 | addEvents_() { 120 | if (this.sortBy_) { 121 | const tabs = this.sortBy_.querySelector('#sort-by-tabs'); 122 | if (tabs) { 123 | this.eventHandler_.listen(tabs, events.EventType.CLICK, 124 | (e) => { 125 | e.preventDefault(); 126 | this.handleSortByClick_(e); 127 | }); 128 | } 129 | } 130 | 131 | if (this.search_) { 132 | this.eventHandler_.listen(this.search_, events.EventType.KEYUP, 133 | () => this.handleSearch_()); 134 | } 135 | 136 | if (this.clearSearchBtn_) { 137 | this.eventHandler_.listen(this.clearSearchBtn_, events.EventType.CLICK, 138 | () => this.clearSearch_()); 139 | } 140 | 141 | if (this.categoriesSelect_) { 142 | this.eventHandler_.listen(this.categoriesSelect_, events.EventType.CHANGE, 143 | () => this.selectCategories_()); 144 | } 145 | } 146 | 147 | /** 148 | * @private 149 | */ 150 | selectCategories_() { 151 | if (this.cards_ && this.categoriesSelect_) { 152 | this.cards_.setAttribute(CATEGORY_ATTR, this.categoriesSelect_.value); 153 | } 154 | } 155 | 156 | /** 157 | * @private 158 | */ 159 | clearSearch_() { 160 | if (this.search_) { 161 | this.search_.value = ''; 162 | } 163 | this.handleSearch_(); 164 | } 165 | 166 | /** 167 | * @private 168 | */ 169 | handleSearch_() { 170 | window.requestAnimationFrame(() => this.searchDebouncer_.fire()); 171 | } 172 | 173 | /** 174 | * @private 175 | */ 176 | handleSearchDebounced_() { 177 | const search = /** @type {!HTMLInputElement} */ (this.search_); 178 | const val = search.value.trim(); 179 | if (this.clearSearchBtn_) { 180 | if (val === '') { 181 | this.clearSearchBtn_.setAttribute('hide', ''); 182 | } else { 183 | this.clearSearchBtn_.removeAttribute('hide'); 184 | } 185 | } 186 | 187 | if (this.cards_) { 188 | this.cards_.setAttribute(FILTER_ATTR, val); 189 | } 190 | } 191 | 192 | /** 193 | * @param {!Event} e 194 | * @private 195 | */ 196 | handleSortByClick_(e) { 197 | const target = /** @type {!Element} */ (e.target); 198 | const sort = target.getAttribute(SORT_ATTR); 199 | if (this.cards_) { 200 | this.cards_.setAttribute(SORT_ATTR, sort); 201 | } 202 | const selected = this.querySelector('[selected]'); 203 | if (selected) { 204 | selected.removeAttribute('selected'); 205 | } 206 | target.setAttribute('selected', ''); 207 | } 208 | 209 | /** 210 | * @private 211 | */ 212 | setupDom_() { 213 | const mainInner = this.querySelector('main .main-inner'); 214 | if (!mainInner) { 215 | return; 216 | } 217 | 218 | this.search_ = /** @type {?HTMLInputElement} */ ( 219 | document.querySelector('#search-field')); 220 | this.clearSearchBtn_ = document.querySelector('#clear-icon'); 221 | 222 | const list = this.querySelector('main ul'); 223 | let cards = /** @type {!Cards} */ ( 224 | document.createElement('google-codelab-index-cards')); 225 | 226 | const url = new URL(document.location.toString()); 227 | if (url.searchParams.has(TAGS_ATTR)) { 228 | cards.setAttribute(TAGS_ATTR, 229 | url.searchParams.getAll(TAGS_ATTR).join(',')); 230 | } 231 | 232 | let selectedCategory = ''; 233 | if (url.searchParams.has(CATEGORY_PARAM)) { 234 | const categories = url.searchParams.getAll(CATEGORY_PARAM); 235 | selectedCategory = categories[0].trim().toLowerCase(); 236 | cards.setAttribute(CATEGORY_ATTR, categories.join(',')); 237 | } 238 | 239 | let sort = 'alpha'; 240 | if (url.searchParams.has(SORT_ATTR)) { 241 | sort = /** @type {string} */ (url.searchParams.get(SORT_ATTR)); 242 | cards.setAttribute(SORT_ATTR, sort); 243 | } 244 | 245 | if (list) { 246 | [...list.querySelectorAll('a')].forEach((link) => { 247 | cards['addCard'](link); 248 | }); 249 | dom.removeNode(list); 250 | dom.appendChild(mainInner, cards); 251 | } else { 252 | cards = mainInner.querySelector('google-codelab-index-cards'); 253 | } 254 | 255 | if (cards) { 256 | const categories = new Set(); 257 | [...cards.querySelectorAll('.card')].forEach((card) => { 258 | const category = card.getAttribute(CATEGORY_ATTR); 259 | if (category) { 260 | category.split(',').forEach((c) => { 261 | categories.add(c.trim()); 262 | }); 263 | } 264 | }); 265 | 266 | const sortBy = soy.renderAsElement(Templates.sortby, { 267 | categories: Array.from(categories).sort(), 268 | selectedCategory: selectedCategory, 269 | sort: sort 270 | }); 271 | sortBy.setAttribute('id', 'sort-by'); 272 | dom.insertSiblingBefore(sortBy, cards); 273 | 274 | this.sortBy_ = sortBy; 275 | this.cards_ = /** @type {!Cards} */ (cards); 276 | this.categoriesSelect_ = /** @type {?HTMLSelectElement} */ ( 277 | this.sortBy_.querySelector('#codelab-categories')); 278 | 279 | if (selectedCategory && this.categoriesSelect_) { 280 | [...this.categoriesSelect_.options].forEach((option) => { 281 | if (option.value.toLowerCase() === selectedCategory) { 282 | option.selected = true; 283 | } 284 | }); 285 | } 286 | } 287 | 288 | if (url.searchParams.has(FILTER_ATTR)) { 289 | const filter = /** @type {string} */ (url.searchParams.get(FILTER_ATTR)); 290 | if (this.search_) { 291 | this.search_.value = filter; 292 | this.handleSearch_(); 293 | } 294 | } 295 | 296 | this.hasSetup_ = true; 297 | } 298 | } 299 | 300 | exports = CodelabIndex; 301 | -------------------------------------------------------------------------------- /google-codelab-index/google_codelab_index.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | html, body { 19 | height: 100%; 20 | width: 100%; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | body { 26 | transition: opacity ease-in 0.2s; 27 | padding-top: 64px; 28 | background: #ECEFF1; 29 | font-family: 'Roboto', 'Noto', sans-serif; 30 | -webkit-font-smoothing: antialiased; 31 | font-size: 16px; 32 | font-weight: 400; 33 | line-height: 24px; 34 | color: #5c5c5c; 35 | } 36 | 37 | * { 38 | box-sizing: border-box; 39 | margin: 0; 40 | } 41 | 42 | a { 43 | text-decoration: none; 44 | } 45 | 46 | [hidden] { 47 | display: none !important; 48 | } 49 | 50 | #toolbar { 51 | display: flex; 52 | align-items: center; 53 | height: 64px; 54 | background-color: #455a64; 55 | color: #fff; 56 | padding: 16px; 57 | position: fixed; 58 | left: 0; 59 | right: 0; 60 | top: 0; 61 | z-index: 10000; 62 | } 63 | 64 | #toolbar .site-width { 65 | display: flex; 66 | align-items: center; 67 | } 68 | 69 | #toolbar .logo { 70 | display: flex; 71 | align-items: center; 72 | } 73 | 74 | #toolbar .logo-icon { 75 | margin-right: 16px; 76 | width: 30px; 77 | height: 24px; 78 | } 79 | 80 | #toolbar .logo-devs { 81 | width: 216px; 82 | height: 36px; 83 | margin-top: 5px; 84 | } 85 | 86 | #searchbar { 87 | background-color: #546E7A; 88 | transition: background-color 400ms cubic-bezier(0, 0, 0.2, 1); 89 | flex-grow: 1; 90 | display: flex; 91 | align-items: center; 92 | } 93 | 94 | #searchbar:hover { 95 | background-color: #78909C; 96 | } 97 | 98 | #searchbar input { 99 | border: 0; 100 | background: none; 101 | outline: 0; 102 | width: 100%; 103 | font-size: 16px; 104 | font-weight: 400; 105 | color: #fff; 106 | line-height: 40px; 107 | height: 40px; 108 | flex-grow: 1; 109 | z-index: 1; 110 | } 111 | 112 | #searchbar input::-webkit-input-placeholder { 113 | color: rgba(255,255,255,.5); 114 | } 115 | #searchbar input:-ms-input-placeholder { 116 | color: rgba(255,255,255,.5); 117 | } 118 | 119 | #search-icon, 120 | #clear-icon { 121 | width: 40px; 122 | height: 40px; 123 | display: flex; 124 | align-items: center; 125 | justify-content: center; 126 | flex-shrink: 0; 127 | color: #fff; 128 | } 129 | 130 | #clear-icon[hide] { 131 | visibility: hidden; 132 | pointer-events: none; 133 | } 134 | 135 | .site-width { 136 | margin: 0 auto; 137 | width: 90vw; 138 | max-width: 1024px; 139 | } 140 | 141 | #banner { 142 | background-color: #fff; 143 | padding: 0 40px 0 40px; 144 | padding: 40px 0 48px 0; 145 | box-shadow: 0px 3px 6px -3px #BDBDBD; 146 | } 147 | 148 | #banner p { 149 | line-height: 32px; 150 | } 151 | 152 | h2 { 153 | margin: 0; 154 | font-family: 'Roboto', 'Noto', sans-serif; 155 | -webkit-font-smoothing: antialiased; 156 | font-size: 45px; 157 | font-weight: 400; 158 | letter-spacing: -.018em; 159 | line-height: 48px; 160 | } 161 | 162 | #banner h2, #banner h3 { 163 | font-weight: 300; 164 | } 165 | 166 | #sort-by { 167 | padding: 24px 0; 168 | display: flex; 169 | justify-content: space-between; 170 | align-items: center; 171 | } 172 | 173 | #sort-by .sort-by-inner { 174 | display: flex; 175 | align-items: center; 176 | flex-shrink: 0; 177 | } 178 | 179 | #sort-by-tabs a { 180 | color: #98999a; 181 | text-transform: uppercase; 182 | line-height: 48px; 183 | font-weight: 500; 184 | font-size: 14px; 185 | padding: 0 12px; 186 | display: flex; 187 | border-bottom: 2px solid transparent; 188 | } 189 | 190 | #sort-by-tabs a:hover { 191 | background-color: #e0e0e0; 192 | } 193 | 194 | #sort-by-tabs a[selected] { 195 | color: #616161; 196 | border-bottom: 2px solid #616161; 197 | } 198 | 199 | #codelab-categories { 200 | border: none; 201 | background: #fff; 202 | padding: 8px; 203 | appearance: none; 204 | -webkit-appearance:none; 205 | font-size: 16px; 206 | line-height: 24px; 207 | color: #212121; 208 | border-radius: 0; 209 | position: relative; 210 | } 211 | 212 | #codelab-categories:focus { 213 | outline: 0; 214 | } 215 | 216 | footer { 217 | background-color: #424242; 218 | font-size: 13px; 219 | } 220 | 221 | footer .footer-wrapper { 222 | display: flex; 223 | padding: 40px 0; 224 | } 225 | 226 | footer .link-list { 227 | border-top: 1px solid #616161; 228 | flex: 1; 229 | margin-right: 32px; 230 | } 231 | 232 | footer .link-list label { 233 | display: block; 234 | color: #fff; 235 | margin: 8px 0 16px; 236 | } 237 | 238 | footer ul, footer li { 239 | list-style: none; 240 | margin: 0; 241 | padding: 0; 242 | } 243 | 244 | footer a { 245 | color: #9e9e9e; 246 | } 247 | 248 | footer .footerbar { 249 | background-color: #616161; 250 | color: #fff; 251 | font-size: 13px; 252 | padding: 10px 0; 253 | } 254 | 255 | footer .footerbar a { 256 | color: currentcolor; 257 | } 258 | 259 | @import "cards"; 260 | 261 | @media (min-width: 768px) { 262 | #toolbar .logo-devs { 263 | margin: 5px 32px 0 0; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /google-codelab-index/google_codelab_index.soy: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | {namespace googlecodelabs.CodelabIndex.Templates} 19 | 20 | /** 21 | * Renders the card 22 | */ 23 | {template .card} 24 | {@param title: string} 25 | {@param category: string} 26 | {@param duration: number} 27 | {@param updated: string} 28 | {@param author: string} 29 |
30 | 31 | 32 | {if $duration} 33 | 34 | {$duration} min 35 | {/if} 36 | 37 |
38 |
39 | {$title} 40 |
41 | 48 | {/template} 49 | 50 | 51 | {template .sortby} 52 | {@param sort: string} 53 | {@param categories: list} 54 |
55 | A-Z 56 | Recent 57 | Duration 58 |
59 |
60 | 66 |
67 | {/template} 68 | -------------------------------------------------------------------------------- /google-codelab-index/google_codelab_index_cards.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabIndex.Cards'); 19 | 20 | const HTML5LocalStorage = goog.require('goog.storage.mechanism.HTML5LocalStorage'); 21 | const Templates = goog.require('googlecodelabs.CodelabIndex.Templates'); 22 | const dom = goog.require('goog.dom'); 23 | const soy = goog.require('goog.soy'); 24 | 25 | /** @const {string} */ 26 | const AUTHOR_ATTR = 'author'; 27 | 28 | /** @const {string} */ 29 | const CATEGORY_ATTR = 'category'; 30 | 31 | /** @const {string} */ 32 | const CATEGORY_PARAM = 'cat'; 33 | 34 | /** @const {string} */ 35 | const TITLE_ATTR = 'title'; 36 | 37 | /** @const {string} */ 38 | const DURATION_ATTR = 'duration'; 39 | 40 | /** @const {string} */ 41 | const UPDATED_ATTR = 'updated'; 42 | 43 | /** @const {string} */ 44 | const TAGS_ATTR = 'tags'; 45 | 46 | /** @const {string} */ 47 | const SORT_ATTR = 'sort'; 48 | 49 | /** @const {string} */ 50 | const FILTER_ATTR = 'filter'; 51 | 52 | /** @const {string} */ 53 | const SORT_ALPHA = 'alpha'; 54 | 55 | /** @const {string} */ 56 | const SORT_RECENT = 'recent'; 57 | 58 | /** @const {string} */ 59 | const SORT_DURATION = 'duration'; 60 | 61 | /** @const {string} */ 62 | const HIDDEN_ATTR = 'hidden'; 63 | 64 | /** @const {string} */ 65 | const PROGRESS_ATTR = 'progress'; 66 | 67 | /** @const {string} */ 68 | const STEPS_ATTR = 'steps'; 69 | 70 | /** @const {string} */ 71 | const NUM_ATTR = 'num'; 72 | 73 | /** 74 | * @extends {HTMLElement} 75 | */ 76 | class Cards extends HTMLElement { 77 | /** @return {string} */ 78 | static getTagName() { return 'google-codelab-index-cards'; } 79 | 80 | constructor() { 81 | super(); 82 | 83 | /** 84 | * @private {!HTML5LocalStorage} 85 | * @const 86 | */ 87 | this.storage_ = new HTML5LocalStorage(); 88 | } 89 | 90 | /** 91 | * @export 92 | * @override 93 | */ 94 | connectedCallback() { 95 | if (!this.hasAttribute(SORT_ATTR)) { 96 | this.setAttribute(SORT_ATTR, SORT_ALPHA); 97 | } else { 98 | this.sort_(); 99 | } 100 | 101 | if (this.hasAttribute(FILTER_ATTR) || 102 | this.hasAttribute(CATEGORY_ATTR) || 103 | this.hasAttribute(TAGS_ATTR)) { 104 | this.filter_(); 105 | } 106 | } 107 | 108 | /** 109 | * @return {!Array} 110 | * @export 111 | */ 112 | static get observedAttributes() { 113 | return [SORT_ATTR, FILTER_ATTR, CATEGORY_ATTR, TAGS_ATTR]; 114 | } 115 | 116 | /** 117 | * @param {string} attr 118 | * @param {?string} oldValue 119 | * @param {?string} newValue 120 | * @param {?string} namespace 121 | * @export 122 | * @override 123 | */ 124 | attributeChangedCallback(attr, oldValue, newValue, namespace) { 125 | switch (attr) { 126 | case SORT_ATTR: 127 | this.sort_(); 128 | break; 129 | case FILTER_ATTR: 130 | case CATEGORY_ATTR: 131 | case TAGS_ATTR: 132 | this.filter_(); 133 | break; 134 | } 135 | } 136 | 137 | 138 | /** 139 | * @private 140 | */ 141 | sort_() { 142 | let sort = this.getAttribute(SORT_ATTR) || SORT_ALPHA; 143 | const cards = [...this.querySelectorAll('.card')]; 144 | if (cards.length < 2) { 145 | // No point sorting 0 or 1 items. 146 | return; 147 | } 148 | 149 | switch (sort) { 150 | case SORT_DURATION: 151 | cards.sort(this.sortDuration_.bind(this)); 152 | break; 153 | case SORT_RECENT: 154 | cards.sort(this.sortRecent_.bind(this)); 155 | break; 156 | case SORT_ALPHA: 157 | default: 158 | sort = SORT_ALPHA; 159 | cards.sort(this.sortAlpha_.bind(this)); 160 | break; 161 | } 162 | 163 | cards.forEach((card) => this.appendChild(card)); 164 | 165 | const url = new URL(document.location.toString()); 166 | if (!sort || sort === SORT_ALPHA) { 167 | url.searchParams.delete(SORT_ATTR); 168 | } else { 169 | url.searchParams.set(SORT_ATTR, sort); 170 | } 171 | 172 | this.setNum_(); 173 | 174 | const path = `${url.pathname}${url.search}`; 175 | window.history.replaceState({path}, document.title, path); 176 | } 177 | 178 | /** 179 | * @private 180 | */ 181 | setNum_() { 182 | const num = this.querySelectorAll(`.card:not([${HIDDEN_ATTR}])`).length; 183 | this.setAttribute(NUM_ATTR, num); 184 | } 185 | 186 | /** 187 | * @param {!Element} a 188 | * @param {!Element} b 189 | * @return {number} 190 | * @private 191 | */ 192 | sortDuration_(a, b) { 193 | if (!a || !b) { 194 | return 0; 195 | } 196 | const aDuration = parseFloat(a.getAttribute(DURATION_ATTR)) || 0; 197 | const bDuration = parseFloat(b.getAttribute(DURATION_ATTR)) || 0; 198 | const diff = aDuration - bDuration; 199 | if (diff === 0) { 200 | return this.sortRecent_(a, b); 201 | } else { 202 | return diff; 203 | } 204 | } 205 | 206 | /** 207 | * @param {!Element} a 208 | * @param {!Element} b 209 | * @return {number} 210 | * @private 211 | */ 212 | sortRecent_(a, b) { 213 | if (!a || !b) { 214 | return 0; 215 | } 216 | const aUpdated = new Date(a.getAttribute(UPDATED_ATTR) || 0); 217 | const bUpdated = new Date(b.getAttribute(UPDATED_ATTR) || 0); 218 | const diff = bUpdated.getTime() - aUpdated.getTime(); 219 | if (diff === 0) { 220 | return this.sortAlpha_(a, b); 221 | } else { 222 | return diff; 223 | } 224 | } 225 | 226 | /** 227 | * @param {!Element} a 228 | * @param {!Element} b 229 | * @return {number} 230 | * @private 231 | */ 232 | sortAlpha_(a, b) { 233 | if (!a || !b) { 234 | return 0; 235 | } 236 | const aTitle = a.getAttribute(TITLE_ATTR); 237 | const bTitle = b.getAttribute(TITLE_ATTR); 238 | if (aTitle < bTitle) { 239 | return -1; 240 | } else if (aTitle > bTitle) { 241 | return 1; 242 | } else { 243 | return 0; 244 | } 245 | } 246 | 247 | /** 248 | * @private 249 | */ 250 | filter_() { 251 | const filter = this.normalizeValue_(this.getAttribute(FILTER_ATTR)); 252 | const tags = this.cleanStrings_( 253 | (this.getAttribute(TAGS_ATTR) || '').split(',')); 254 | const categories = this.cleanStrings_( 255 | (this.getAttribute(CATEGORY_ATTR) || '').split(',')); 256 | 257 | const cards = [...this.querySelectorAll('.card')]; 258 | cards.forEach((card) => { 259 | const title = this.normalizeValue_(card.getAttribute(TITLE_ATTR)); 260 | const cardCategories = this.cleanStrings_( 261 | (card.getAttribute(CATEGORY_ATTR) || '').split(',')); 262 | const cardTags = this.cleanStrings_( 263 | (card.getAttribute(TAGS_ATTR) || '').split(',')); 264 | 265 | let matchesFilter = true; 266 | let matchesTags = true; 267 | let matchesCategory = true; 268 | 269 | if (filter) { 270 | matchesFilter = title.indexOf(filter) !== -1; 271 | } 272 | 273 | if (tags.length) { 274 | matchesTags = this.arrayContains_(cardTags, tags); 275 | } 276 | 277 | if (categories.length) { 278 | matchesCategory = this.arrayContains_(cardCategories, categories); 279 | } 280 | 281 | if (matchesFilter && matchesTags && matchesCategory) { 282 | card.removeAttribute(HIDDEN_ATTR); 283 | } else { 284 | card.setAttribute(HIDDEN_ATTR, ''); 285 | } 286 | }); 287 | 288 | const url = new URL(document.location.toString()); 289 | if (tags.length) { 290 | url.searchParams.set(TAGS_ATTR, tags.join(',')); 291 | } else { 292 | url.searchParams.delete(TAGS_ATTR); 293 | } 294 | 295 | if (categories.length) { 296 | url.searchParams.set(CATEGORY_PARAM, categories.join(',')); 297 | } else { 298 | url.searchParams.delete(CATEGORY_PARAM); 299 | } 300 | 301 | if (filter) { 302 | url.searchParams.set(FILTER_ATTR, filter); 303 | } else { 304 | url.searchParams.delete(FILTER_ATTR); 305 | } 306 | 307 | this.setNum_(); 308 | 309 | const path = `${url.pathname}${url.search}`; 310 | window.history.replaceState({path}, document.title, path); 311 | } 312 | 313 | /** 314 | * Returns true if any of the items in A are in B. 315 | * @param {!Array} a 316 | * @param {!Array} b 317 | * @return {boolean} 318 | * @private 319 | */ 320 | arrayContains_(a, b) { 321 | for (let i = 0; i < a.length; i++) { 322 | if (b.includes(a[i])) { 323 | return true; 324 | } 325 | } 326 | return false; 327 | } 328 | 329 | /** 330 | * @param {string} category 331 | * @return {string} 332 | * @private 333 | */ 334 | normalizeCategory_(category) { 335 | return category.toLowerCase() 336 | .replace(/\s+/g, '-') // Replace spaces with - 337 | .replace(/--+/g, '-') // Replace multiple - with single - 338 | .trim().split(',').shift(); 339 | } 340 | 341 | /** 342 | * Trims whitespace and converts to lower case. 343 | * @param {string|undefined} v 344 | * @return {string} 345 | * @private 346 | */ 347 | normalizeValue_(v) { 348 | return (v || '').trim().toLowerCase() 349 | .replace('\n', '') 350 | .replace(/\s+/g, ' '); 351 | } 352 | 353 | /** 354 | * @param {!Array} strings 355 | * @return {!Array} 356 | * @private 357 | */ 358 | cleanStrings_(strings) { 359 | strings = strings || []; 360 | const a = []; 361 | strings.forEach((s) => { 362 | const v = this.normalizeValue_(s); 363 | if (v) { 364 | a.push(v); 365 | } 366 | }); 367 | return a.sort(); 368 | } 369 | 370 | /** 371 | * @param {!HTMLAnchorElement} link 372 | * @export 373 | */ 374 | addCard(link) { 375 | const info = { 376 | category: this.normalizeCategory_(link.getAttribute(CATEGORY_ATTR) || ''), 377 | title: link.getAttribute(TITLE_ATTR) || '', 378 | duration: parseInt(link.getAttribute(DURATION_ATTR), 10) || 0, 379 | updated: this.prettyDate_(link.getAttribute(UPDATED_ATTR)) || '', 380 | tags: link.getAttribute(TAGS_ATTR) || '', 381 | author: link.getAttribute(AUTHOR_ATTR) || '' 382 | }; 383 | soy.renderElement(link, Templates.card, info); 384 | link.classList.add('card'); 385 | this.addHomeLinkForCard_(link); 386 | this.showProgressForCard_(link); 387 | this.appendChild(link); 388 | } 389 | 390 | /** 391 | * @param {!HTMLAnchorElement} link 392 | * @private 393 | */ 394 | addHomeLinkForCard_(link) { 395 | const url = new URL(link.href, document.location.origin); 396 | if (!url.searchParams.has('index')) { 397 | url.searchParams.set('index', document.location.pathname); 398 | } 399 | dom.safe.setAnchorHref(link, url.href); 400 | } 401 | 402 | /** 403 | * @param {!Element} link 404 | * @private 405 | */ 406 | showProgressForCard_(link) { 407 | const id = link.getAttribute('id'); 408 | if (id) { 409 | const progress = this.storage_.get(`progress_${id}`); 410 | const steps = link.getAttribute(STEPS_ATTR); 411 | if (progress && steps) { 412 | link.setAttribute(PROGRESS_ATTR, 413 | (parseFloat(progress) / parseFloat(steps) - 1).toFixed(2)); 414 | } 415 | } 416 | } 417 | 418 | /** 419 | * @param {string} updated 420 | * @returns {string} 421 | * @private 422 | */ 423 | prettyDate_(updated) { 424 | if (!updated) { 425 | return ''; 426 | } 427 | const mNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 428 | 'Sep', 'Oct', 'Nov', 'Dec']; 429 | const d = new Date(updated); 430 | return mNames[d.getMonth()] + ' ' + d.getUTCDate() + ', ' + d.getFullYear(); 431 | }; 432 | } 433 | 434 | exports = Cards; 435 | -------------------------------------------------------------------------------- /google-codelab-index/google_codelab_index_cards_def.js: -------------------------------------------------------------------------------- 1 | goog.module('googlecodelabs.CodelabIndex.CardsDef'); 2 | const CodelabIndexCards = goog.require('googlecodelabs.CodelabIndex.Cards'); 3 | 4 | try { 5 | window.customElements.define(CodelabIndexCards.getTagName(), CodelabIndexCards); 6 | } catch (e) { 7 | console.warn('googlecodelabs.CodelabIndex.Cards', e); 8 | } -------------------------------------------------------------------------------- /google-codelab-index/google_codelab_index_def.js: -------------------------------------------------------------------------------- 1 | goog.module('googlecodelabs.CodelabIndexDef'); 2 | const CodelabIndex = goog.require('googlecodelabs.CodelabIndex'); 3 | 4 | try { 5 | window.customElements.define(CodelabIndex.getTagName(), CodelabIndex); 6 | } catch (e) { 7 | console.warn('googlecodelabs.CodelabIndex', e); 8 | } -------------------------------------------------------------------------------- /google-codelab-step/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) 4 | 5 | exports_files(["LICENSE"]) 6 | 7 | load("//tools:defs.bzl", 8 | "closure_js_library", "closure_js_binary", "closure_js_test") 9 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library") 10 | load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library") 11 | 12 | 13 | filegroup( 14 | name = "google_codelab_step_files", 15 | srcs = glob([ 16 | "*.html", 17 | "*.png", 18 | ]) + [ 19 | ":google_codelab_step_scss_bin", 20 | ":google_codelab_step_bin", 21 | ], 22 | ) 23 | 24 | # Codelab step. 25 | closure_js_library( 26 | name = "google_codelab_step", 27 | srcs = [ 28 | "google_codelab_step.js", 29 | "google_codelab_step_def.js" 30 | ], 31 | deps = [ 32 | "@io_bazel_rules_closure//closure/library", 33 | ":google_codelab_step_soy", 34 | ], 35 | ) 36 | 37 | # Compiled version of GoogleCodelabStep element, suitable for distribution. 38 | closure_js_binary( 39 | name = "google_codelab_step_bin", 40 | entry_points = ["googlecodelabs.CodelabStepDef"], 41 | deps = [":google_codelab_step"], 42 | ) 43 | 44 | sass_library( 45 | name = "syntax", 46 | srcs = ["_syntax.scss"], 47 | ) 48 | 49 | sass_binary( 50 | name = "google_codelab_step_scss_bin", 51 | src = "google_codelab_step.scss", 52 | deps = [ 53 | ":syntax", 54 | ] 55 | ) 56 | 57 | closure_js_template_library( 58 | name = "google_codelab_step_soy", 59 | srcs = ["google_codelab_step.soy"] 60 | ) 61 | 62 | closure_js_test( 63 | name = "google_codelab_step_test", 64 | srcs = ["google_codelab_step_test.js"], 65 | entry_points = ["googlecodelabs.CodelabStepTest"], 66 | deps = [":google_codelab_step"], 67 | ) 68 | -------------------------------------------------------------------------------- /google-codelab-step/_syntax.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | pre, code { 19 | font-family: 'Source Code Pro', Helvetica, Arial; 20 | font-size: inherit; 21 | border-radius: 4px; 22 | overflow-x: auto; 23 | overflow-y: visible; 24 | } 25 | code { 26 | background-color: #E8EAEd; 27 | padding: 0.1em 0.3em; 28 | } 29 | pre { 30 | display: block; 31 | color: white; 32 | background-color: #28323f; 33 | padding: 14px; 34 | -webkit-text-size-adjust: none; 35 | line-height: 1.4; 36 | } 37 | pre > code { 38 | padding: 0; 39 | background-color: transparent; 40 | } 41 | code em { 42 | color: #97C8F2; 43 | } 44 | pre .str, code .str { color: #34A853; } /* string - green */ 45 | pre .kwd, code .kwd { color: #F538A0; } /* keyword - dark pink */ 46 | pre .com, code .com { color: #BDC1C6; font-style: italic; } /* comment - gray */ 47 | pre .typ, code .typ { color: #24C1E0; } /* type - light blue */ 48 | pre .lit, code .lit { color: #4285F4; } /* literal - blue */ 49 | pre .pun, code .pun { color: #F8F9FA; } /* punctuation - white */ 50 | pre .pln, code .pln { color: #F8F9FA; } /* plaintext - white */ 51 | pre .tag, code .tag { color: #24C1E0; } /* html/xml tag - light blue */ 52 | pre .atn, code .atn { color: #EDA912; } /* html/xml attribute name - khaki */ 53 | pre .atv, code .atv { color: #34A853; } /* html/xml attribute value - green */ 54 | pre .dec, code .dec { color: #5195EA; } /* decimal - blue */ 55 | 56 | paper-button { 57 | display: inline-flex; 58 | align-items: center; 59 | justify-content: center; 60 | position: relative; 61 | box-sizing: border-box; 62 | min-width: 5.14em; 63 | margin: 0 0.29em; 64 | background: transparent; 65 | -webkit-tap-highlight-color: transparent; 66 | font: inherit; 67 | text-transform: uppercase; 68 | outline-width: 0; 69 | border-radius: 3px; 70 | user-select: none; 71 | cursor: pointer; 72 | z-index: 0; 73 | padding: 0.7em 0.57em; 74 | font-family: 'Roboto', 'Noto', sans-serif; 75 | -webkit-font-smoothing: antialiased; 76 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 77 | } 78 | -------------------------------------------------------------------------------- /google-codelab-step/google-codelab-step.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Google Codelab Step 22 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

What you'll learn

37 |
    38 |
  • How to start a new Polymer-based project using Chrome Dev Editor
  • 39 |
  • How to use elements in Polymer's iron, paper, and Google Web Component sets.
  • 40 |
  • How to use Polymer's data binding features to reduce boilerplate code.
  • 41 |
42 | 43 |

The 44 | Google Web Components provide the 45 | <google-map> element for declaratively rendering a Google Map. To use it, you first need to install it using Bower.

46 | 56 | 57 |

Create a new project

58 | 59 | 69 | 70 |

The first time you run Chrome Dev Editor it will ask you to setup your workspace environment.

71 |

Fire up Chrome Dev Editor and start a new project:

72 |
    73 |
  1. Click 74 | to start a new project.
  2. 75 |
  3. Enter 76 | "PolymerMapsCodelab" as the 77 | Project name.
  4. 78 |
  5. In the 79 | Project type dropdown, select "JavaScript web app (using Polymer paper elements)".
  6. 80 |
  7. Click the 81 | Create button.
  8. 82 |
83 |

84 | 85 |

86 |

Chrome Dev Editor creates a basic scaffold for your Polymer app. In the background, it also uses 87 | Bower to download and install a list of dependencies (including the Polymer core library) into the 88 | bower_components/ folder. 89 | Fetching the components make take some time if your internet connection is slow. You'll learn more about using 90 | Bower in the next step.

91 |

bower.json

92 | 93 |
PolymerMapsCodelab/
 94 |         bower_components/ <!-- installed dependencies from Bower -->
 95 |         bower.json  <!-- Bower metadata files used for managing deps -->
 96 |         index.html  <!-- your app -->
 97 |         main.js
 98 |         styles.css
99 | 100 |

Install the 101 | element

102 |

Normally, you'd run 103 | bower install GoogleWebComponents/google-map --save on the command line to install 104 | <google-map> and save it as a dependency. However, Chrome Dev Editor does not have a command line for running Bower commands. Instead, 105 | you need to manually edit 106 | bower.json to include 107 | google-map, then run Chrome Dev Editor's 108 | Bower Update feature. 109 | Bower Update checks the dependencies in 110 | bower.json and installs any missing ones.

111 |
    112 |
  1. Edit 113 | bower.json and add 114 | google-map to the 115 | dependencies object:
  2. 116 |
117 | 118 |
"dependencies": {
119 | "iron-elements": "PolymerElements/iron-elements#^1.0.0",
120 | "paper-elements": "PolymerElements/paper-elements#^1.0.1",
121 | "google-map": "GoogleWebComponents/google-map#^1.0.3"
122 | }
123 | 124 |
    125 |
  1. Right-click the 126 | bower.json filename in the editor.
  2. 127 |
  3. Select 128 | Bower Update from the context menu.
  4. 129 |
130 |

131 | 132 |

133 |

The download may take few seconds. You can verify that 134 | <google-map> (and any dependencies) were installed by checking that 135 | bower_components/google-map/ was created and populated.

136 |

Use the 137 | element

138 |

To employ 139 | <google-map>, you need to:

140 |
    141 |
  1. Use an 142 | HTML Import to load it in 143 | index.html.
  2. 144 |
  3. Declare an instance of the element on the page.
  4. 145 |
146 |

In 147 | index.html, 148 | remove all other HTML imports 149 | in the 150 | <head> and replace them with a single import that loads 151 | google-map.html:

152 |

153 | index.html 154 |

155 | 156 |
<head>
157 | ....
158 | <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
159 | <link rel="import" href="bower_components/google-map/google-map.html">
160 | </head>
161 | 162 | 168 |

Next, replace the contents of 169 | <body> with an instance of 170 | <google-map>:

171 |

index.html

172 | 173 |
<body unresolved>
174 | <google-map latitude="37.779" longitude="-122.3892" zoom="13"></google-map>
175 | </body>
176 | 177 |

Here's a pre block without code:

178 |
$ node client.js list wdfasdf asdf asdf asdf asdf asdf asdf asdf asdf asdfa sdf asdfa sdf asdfa sdfasdfasdfasdf
179 | { books:
180 |  [ { id: 123,
181 |      title: 'A Tale of Two Cities',
182 |      author: 'Charles Dickens' } ] }
183 | 184 |

As you can see, using 185 | <google-map> is completely declarative! The map is centered using the 186 | latitude and 187 | longitude attributes and its zoom level is set by the 188 | zoom attribute.

189 |

Style the map

190 |

If you run the app right now, nothing will display. In order for the map to properly display itself, you need to set 191 | its container (in this case, 192 | <body>) to have a fixed height.

193 |

Open 194 | styles.css and replace its contents with default styling:

195 |

styles.css

196 | 197 |
body, html {
198 | font-family: 'Roboto', Arial, sans-serif;
199 | height: 100%;
200 | margin: 0;
201 | }
202 | 203 |

Add a marker

204 |

205 | <google-map> supports adding map markers to the map by declaring 206 | <google-map-marker> elements as children. The marker locations are also set using 207 | latitude and 208 | longitude attributes.

209 |

Back in 210 | index.html, add a draggable 211 | <google-map-marker> to the map:

212 |

index.html

213 | 214 |
<google-map latitude="37.779" longitude="-122.3892" zoom="13" disable-default-ui>
215 | <google-map-marker latitude="37.779" longitude="-122.3892"
216 |     title="Go Giants!" draggable="true"></google-map-marker>
217 | </google-map>
218 | 219 |

Notice that we've also disabled the map's controls by setting 220 | disableDefaultUi to true. Since it's a boolean property, its presence as an HTML attribute makes it truthy.

221 |

Run the app

222 |

If you haven't already done so, hit the 223 | button. At this point, you should see a map that takes up the entire viewport and has a single marker pin.

224 |

225 | 226 |

227 |

Frequently Asked Questions

228 | 240 | 241 | 242 |

How would rate your experience with Polymer?

243 | 244 | Novice 245 | Intermediate 246 | Advanced 247 | 248 |
249 | 250 |
251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /google-codelab-step/google_codelab_step.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabStep'); 19 | 20 | const EventHandler = goog.require('goog.events.EventHandler'); 21 | const Templates = goog.require('googlecodelabs.CodelabStep.Templates'); 22 | const dom = goog.require('goog.dom'); 23 | const soy = goog.require('goog.soy'); 24 | 25 | /** @const {string} */ 26 | const LABEL_ATTR = 'label'; 27 | 28 | /** @const {string} */ 29 | const STEP_ATTR = 'step'; 30 | 31 | /** 32 | * The general codelab action event fired for trackable interactions. 33 | * @const {string} 34 | */ 35 | const CODELAB_ACTION_EVENT = 'google-codelab-action'; 36 | 37 | /** 38 | * @extends {HTMLElement} 39 | * @suppress {reportUnknownTypes} 40 | */ 41 | class CodelabStep extends HTMLElement { 42 | /** @return {string} */ 43 | static getTagName() { return 'google-codelab-step'; } 44 | 45 | constructor() { 46 | super(); 47 | 48 | /** 49 | * @private {?Element} 50 | */ 51 | this.instructions_ = null; 52 | 53 | /** 54 | * @private {?Element} 55 | */ 56 | this.inner_ = null; 57 | 58 | /** @private {boolean} */ 59 | this.hasSetup_ = false; 60 | 61 | /** 62 | * @private {string} 63 | */ 64 | this.step_ = '0'; 65 | 66 | /** 67 | * @private {string} 68 | */ 69 | this.label_ = ''; 70 | 71 | /** 72 | * @private {?Element} 73 | */ 74 | this.title_ = null; 75 | 76 | /** 77 | * @private {!EventHandler} 78 | * @const 79 | */ 80 | this.eventHandler_ = new EventHandler(); 81 | } 82 | 83 | /** 84 | * @export 85 | * @override 86 | */ 87 | connectedCallback() { 88 | this.setupDom_(); 89 | } 90 | 91 | /** 92 | * @return {!Array} 93 | * @export 94 | */ 95 | static get observedAttributes() { 96 | return [LABEL_ATTR, STEP_ATTR]; 97 | } 98 | 99 | /** 100 | * @param {string} attr 101 | * @param {?string} oldValue 102 | * @param {?string} newValue 103 | * @param {?string} namespace 104 | * @export 105 | * @override 106 | */ 107 | attributeChangedCallback(attr, oldValue, newValue, namespace) { 108 | if (attr === LABEL_ATTR || attr === STEP_ATTR) { 109 | this.updateTitle_(); 110 | } 111 | } 112 | 113 | /** 114 | * @private 115 | */ 116 | updateTitle_() { 117 | if (this.hasAttribute(LABEL_ATTR)) { 118 | this.label_ = this.getAttribute(LABEL_ATTR); 119 | } 120 | 121 | if (this.hasAttribute(STEP_ATTR)) { 122 | this.step_ = this.getAttribute(STEP_ATTR); 123 | } 124 | 125 | if (!this.title_) { 126 | return; 127 | } 128 | 129 | const title = soy.renderAsElement(Templates.title, { 130 | step: this.step_, 131 | label: this.label_ 132 | }); 133 | 134 | dom.replaceNode(title, this.title_); 135 | this.title_ = title; 136 | } 137 | 138 | /** 139 | * @private 140 | */ 141 | setupDom_() { 142 | if (this.hasSetup_) { 143 | return; 144 | } 145 | 146 | this.instructions_ = dom.createElement('div'); 147 | this.instructions_.classList.add('instructions'); 148 | this.inner_ = dom.createElement('div'); 149 | this.inner_.classList.add('inner'); 150 | this.inner_.innerHTML = this.innerHTML; 151 | dom.removeChildren(this); 152 | 153 | const title = soy.renderAsElement(Templates.title, { 154 | step: this.step_, 155 | label: this.label_, 156 | }); 157 | this.title_ = title; 158 | 159 | dom.insertChildAt(this.inner_, title, 0); 160 | 161 | const codeElements = this.inner_.querySelectorAll('pre code'); 162 | codeElements.forEach((el) => { 163 | const code = window['prettyPrintOne'](el.innerHTML); 164 | el.innerHTML = code; 165 | this.eventHandler_.listen( 166 | el, 'copy', () => this.handleSnippetCopy_(el)); 167 | }); 168 | 169 | dom.appendChild(this.instructions_, this.inner_); 170 | dom.appendChild(this, this.instructions_); 171 | 172 | this.hasSetup_ = true; 173 | } 174 | 175 | /** 176 | * @param {!Element} el The element on which we added this event listener. 177 | * This is not the same as the target of the event, because the event 178 | * target can be a child of this element. 179 | * @private 180 | */ 181 | handleSnippetCopy_(el) { 182 | const event = new CustomEvent(CODELAB_ACTION_EVENT, { 183 | detail: { 184 | 'category': 'codelab', 185 | 'action': 'copy', 186 | 'label': el.textContent.substring(0, 500) 187 | } 188 | }); 189 | document.body.dispatchEvent(event); 190 | } 191 | } 192 | 193 | exports = CodelabStep; 194 | -------------------------------------------------------------------------------- /google-codelab-step/google_codelab_step.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | $google-material-blue-600: #1A73E8; 19 | $google-material-blue-800: #185ABC; 20 | $google-material-green-600: #1E8E3E; 21 | $google-material-grey-900: #212124; 22 | $google-material-grey-800: #3C4043; 23 | $google-material-grey-700: #5f6368; 24 | $google-material-grey-600: #80868B; 25 | $google-material-grey-300: #DADCE0; 26 | $google-material-grey-100: #F1F3F4; 27 | 28 | $google-codelab-step-background: #fff; 29 | $google-codelab-step-link-color: $google-material-blue-600; 30 | 31 | 32 | google-codelab-step { 33 | line-height: 24px; 34 | display: block; 35 | } 36 | 37 | google-codelab-step { 38 | @import "syntax"; 39 | } 40 | 41 | google-codelab-step h2.step-title { 42 | font-family: 'Google Sans', Arial, sans-serif !important; 43 | font-size: 28px !important; 44 | font-weight: 400 !important; 45 | line-height: 1em !important; 46 | margin: 0 0 30px !important; 47 | } 48 | 49 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions { 50 | box-shadow: 0px 1px 2px 0px rgba(60, 64, 67, 0.3), 0px 2px 6px 2px rgba(60, 64, 67, 0.15); 51 | background: $google-codelab-step-background; 52 | max-width: 800px; 53 | font-size: 14px; 54 | margin: 0 auto; 55 | margin-bottom: 32px; 56 | border-radius: 4px; 57 | } 58 | 59 | google-codelab-step .instructions .inner { 60 | padding: 24px; 61 | } 62 | 63 | google-codelab[theme="minimal"] google-codelab-step .instructions .inner { 64 | padding: 0 24px; 65 | } 66 | 67 | @media (max-width: 800px) { 68 | google-codelab .instructions { 69 | margin: 0 0 16px 0; 70 | } 71 | } 72 | 73 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions a, 74 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions a:visited { 75 | color: $google-codelab-step-link-color; 76 | } 77 | 78 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions h2, 79 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions h3, 80 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions h4 { 81 | font-weight: 400; 82 | margin: 0; 83 | } 84 | 85 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions h2 { 86 | font-weight: 300; 87 | line-height: 1em; 88 | font-size: 22px; 89 | } 90 | 91 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions { 92 | line-height: 24px; 93 | } 94 | 95 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions li { 96 | margin: 0.5em 0; 97 | } 98 | 99 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions h2 { 100 | font-weight: 500; 101 | margin: 1.5em 0 0 0; 102 | font-size: 20px; 103 | } 104 | 105 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions h3 { 106 | font-weight: 500; 107 | margin: 20px 0 0 0; 108 | } 109 | 110 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions aside { 111 | padding: 0.5em 1em; 112 | margin: 2em 0; 113 | border-left: 4px solid; 114 | border-radius: 4px; 115 | } 116 | 117 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions aside p { 118 | margin: 0.5em 0; 119 | } 120 | 121 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions aside.note, 122 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions aside.notice { 123 | border-color: #EA8600; 124 | background: #FEF7E0; 125 | color: $google-material-grey-900; 126 | } 127 | 128 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions aside.tip, 129 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions aside.special { 130 | border-color: #137333; 131 | background: #E6F4EA; 132 | color: $google-material-grey-900; 133 | } 134 | 135 | google-codelab:not([theme="minimal"]) google-codelab-step .instructions aside.warning { 136 | border-color: #EA8600; 137 | background: #FEF7E0; 138 | color: $google-material-grey-900; 139 | } 140 | 141 | google-codelab-step .instructions aside.callout { 142 | background-color: #E8F0FE; 143 | margin: 20px 0; 144 | padding: 15px; 145 | border-left: 3px solid $google-material-blue-800; 146 | border-radius: 4px; 147 | color: $google-material-grey-900; 148 | font-size: 14px; 149 | line-height: 1.5; 150 | } 151 | 152 | google-codelab-step aside.callout b { 153 | color: $google-material-blue-800; 154 | } 155 | 156 | google-codelab-step .instructions ul.checklist { 157 | list-style: none; 158 | padding: 0 0 0 1em; 159 | } 160 | 161 | google-codelab-step .instructions ul.checklist li, 162 | google-codelab-step .instructions ::content ul.checklist li { 163 | padding-left: 24px; 164 | background-size: 20px; 165 | background-repeat: no-repeat; 166 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAWlBMVEUAAAAxokwwoks1pFAxokwxokwxokwxokwxokwnnkQnnkQnnkRou3y84cTS69cxokwonkQxokwnnkRqvH1VsmtluXlVsmsnnkRdtnLw+PIxokwqn0YinEAfmj3goh/UAAAAGnRSTlMA2CcEo+6AQT7+2IOBJxPl27alhoBnX15SCCe258UAAAB+SURBVEjH7dA5EoAgEERR3BcQ923Q+1/T0SqKlNbMouP3gxkRFvZpyQb64VSQT4mOcYc8mU5DnqIG8zXoozj4d34tML+YrET8XBFx4e2F4oAL4N7J3EUB/EfSUwD/zG3hvFdROu9XtL31vgXguQA9F6DnAvM8WbOHpkXYD3cBBCcPjtASYjwAAAAASUVORK5CYII='); 167 | } 168 | 169 | google-codelab-step .instructions table code, 170 | google-codelab-step .instructions h2 code { 171 | background: white; 172 | } 173 | 174 | google-codelab-step .instructions .indented { 175 | margin-left: 40px; 176 | } 177 | 178 | google-codelab-step .instructions strong { 179 | font-weight: 600; 180 | } 181 | 182 | google-codelab-step .instructions paper-button { 183 | border-radius: 4px; 184 | color: #FFFFFF; 185 | font-family: 'Google Sans', Arial, sans-serif; 186 | font-size: 14px; 187 | font-weight: 600; 188 | letter-spacing: .6px; 189 | padding-bottom: 6px; 190 | padding-left: 12px; 191 | padding-right: 16px; 192 | padding-top: 6px; 193 | text-transform: none; 194 | } 195 | 196 | google-codelab-step .instructions paper-button a { 197 | text-decoration: none; 198 | color: inherit !important; 199 | } 200 | 201 | google-codelab-step a paper-button { 202 | /* The text-decoration styling doesn't apply to inline blocks. 203 | We use this trick to remove underline when paper-button 204 | is wrapped in the anchor element. */ 205 | display: inline-block; 206 | } 207 | 208 | google-codelab-step .instructions paper-button.colored { 209 | background-color: $google-material-green-600; 210 | } 211 | 212 | google-codelab-step .instructions paper-button.red { 213 | background-color: #D93025; 214 | } 215 | 216 | google-codelab-step .instructions iron-icon { 217 | padding-right: 8px; 218 | max-height: 16px; 219 | max-width: 16px; 220 | } 221 | 222 | google-codelab-step .instructions img { 223 | max-width: 100%; 224 | vertical-align: bottom; 225 | } 226 | 227 | google-codelab-step .instructions table { 228 | border-spacing: 0; 229 | } 230 | 231 | google-codelab-step .instructions td { 232 | vertical-align: top; 233 | border-bottom: 1px solid #CCC; 234 | padding: 8px; 235 | } 236 | 237 | google-codelab-step .instructions table p { 238 | margin: 0; 239 | } 240 | 241 | google-codelab:not([theme="minimal"]) .instructions h3.faq { 242 | border-bottom: 1px solid #ddd; 243 | } 244 | 245 | google-codelab:not([theme="minimal"]) .instructions ul.faq { 246 | list-style: none; 247 | padding-left: 1em; 248 | } 249 | 250 | google-codelab:not([theme="minimal"]) .instructions .faq li { 251 | font-size: 1.1em; 252 | margin-bottom: 0.8em; 253 | } 254 | 255 | google-codelab:not([theme="minimal"]) .instructions .faq a { 256 | color: inherit; 257 | text-decoration: none; 258 | } 259 | 260 | google-codelab:not([theme="minimal"]) .instructions .faq a:hover { 261 | text-decoration: underline; 262 | } 263 | 264 | google-codelab-step .instructions .faq a[href*="cloud.google.com"] { 265 | padding-left: 22px; 266 | background-size: 20px; 267 | background-repeat: no-repeat; 268 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC9FBMVEX////u7u7v7+/ZRDf/zEH/zkPbRTlJifT/z0XYRDhRj/XWRDdPjvVOjfX/zkH/z0fXQzdGiPXUQzdMi/VKi/Xr6+3t7e1SkPXbRDdLjPW70fPaQjVOjPXx8PD9/f1nnff19fXRQzdHifVJhOxEh/Tu7/JLifH9y0DUQjVtofhOi/NKh+9HgelGf+b+7saaWXX/zDz+yTrZRzrRQjT7+/vxx8PeqkTsuj32uzrOQzdqn/j39/fr6Oj9yT/3vjvaQDPUPTBJivVKifPq7O/z0Mz968T4wj7zwD3NPTHGPDD0+P9Bifrv8/Xx8vJJhu5EfOPo3Nv/0Ez8y0rxvT3ZPjBDi/tBhPT55ePkysj+3Hr/0VLHVErcpkPcTUH7xD35wTzztznpsTjKRTjo8P3U4/z6+vr//PmQt/mBrff//fayy/ZUkfZjmfSXufP78/L18+//+Oj66Of+9eBCdt3t59f/89TvxcPsurXasKztqKL+4Y7jjIT0033ShH3hc2ngaV7/1F3/0ljjv1fbWU37xkDaoEDCSj/xujvptjvutjrxtjn8xDflrDb5vjXJQjX2tzP4+v+bwf3I3Pyiw/t0pvhel/VZlPW90fP/+/DV3/Cdu+/e4uv33Nk+btP+8Mz+8Mniwb7+7L3u371Vdrv+6bDusq3+56j+5aTx2Z+Gjpj+4pbolI3RjYehnIT+2XPedW370mbHaGDMZl3bY1jQVUvbU0f/00PYTkLKSj/tsjjwpjfeXjfnfjbEQjbXPjTSPS/w9vu50vs7hfr5+flypPf89vb89fTF1fHM2vBsnfB3ou/67+6swOpekert6uNdi+M5deLq4OA/dt83bt80a9xmjdby19VKc8jx5cf12cP/7r/gwr+pnr/v4b7xwLtffbf+6rHx3KnorKjYqKTXpqHXo57aop3ZkovTwIboh3+jX3f30XDgenCWUm3Kc2zIsGnMcGj4y1zgYFXgX1TfrEXWlz7mhjfsrTbkljbabDbcVTbsmTBDSJ02AAAFFklEQVRYw+2WV1jTUABGkyZpi6W0tKUWC0hBsAporbMWrCgqKA6WCooK7r333nvvvffee++99957b33x3uTetIEGnnzx83y89Tunf25SWuI/f4Oqk6cU4ijLkr9sfp5Ckyfk6h8o5JkH4umpVPr5+fnqdLro6GiZTBYkk3l71z5SNbdA8zyt8wLYhKefUunr66vV6WQsMDExF3/StLytka/0VPopfbUgoEWBoCBv76lNcj6AFnnQ+6MB0BcsKNA8x2M4iPbjAThQgVsAA545TWg8RQlc5MMBflrg8wHgg3NsIX6OAa10Lr6SDQgHgAXetSeJD5iqRTrytfAKdHwgiAu0EZ3QvEBhYLLAu6+LDoIOtCApKSnVUqoBDondyiahoTggazt9ej4hd94Wxbya4z5wskZoYVgA0/NXlGSlVr3iJpOpOMsbt36r4BphsACQHSezBfSP4zQQs8Zs3n7P3QmeDQ8OAwVwdkEt0ADhhI3jFCxms7lfu+yBthHhwcFhoaCgnXZC4obqPYbZPABqgHVpNj/yak1QABdRQatriwcIiXy312alrBRFqff175jFb9lrZecIVGgz09+NTurjbw/LNFmtUqnVw8PRM0AY6JowaHFN7iIKz2rvtCIjfbBPkvF992aapABKakzvKByQymxbO5e7iDb8BfjoA5uWsPv7kJxPSi4Oy7RpgA/+kl8KArdolUq1uDMshLXCA8jD6NXqpEQPAmDCszibTSFlccS4+DMGMCrVtvdzO0eEh59CPulfgsDYoQ/Rb4q1mbiCsX8zZ2AVrQLIV16uGRGMTxD6PIEkh2TBOPA4og09nSeYJoe+fND1SxdmSRCBgg+6PypE9o01mTRq6Bs+1yEQqcCHJK69cn62xDnA3YS6W4qD51kBA9LV6MX5aXKuIB90/wyJnztCQFMJKuif2zQANfANn9CnchlYzyWYF7OFV8ATgAPV624BAzQKCgTSSxIsDxIYOQR00m7kFgjpsQP6CgUlNWxFd7LDAEaOoPvoc74ESa1NsUBXK9QelLHfafTqclZmKwXvsjpJihyi/mkc9j3Su/P7BngxeMJHPXoru9vbCP4vYZ9aT/DMpxkAW7CsCCFZ3D5I5BowAPnp1wgnqRaGQ84kduEKemchwC5BJ1jX6Tt6Ey50SvBiMH1AgCvYA1i9BP8UkRtjsW8c3E5wjcstfCBxUQgW/APt9sBAfxIR/2ScBvnS5O6EgA4baL7w+hzpRELyhNSqZ1Jgf/0xQsgSuXPCQzxBSPwaG/YpawyRhRnrvBh0DvSGLkITn2A9M/Kljt5HiawsTGAwxVbohS7+JGPfOBgNEJDK3wg60d2EBTvM2KdWE27opPLCWPqEZP9W+BCLfIoa3JFwxyoLX4haVNFHSMUecQrsGx8RbmmZxgfoIV9KIeoDhg4d+jXTzPubCREWWmhOB0SV5hgxYkRGxpgxY36i+2cEgRixQLN1FpqnYJWkKklJScNLjxy5MyPjhwf0Kegn9yZEmZdAuxQqVwEkDR8+auTO77uhD3XKsbWOeKBZL4ugULlyuXJlyjQaNeqbFfhG6BuSwdeqOF0H0q6FBmyhUaNdu6FPQd/RHw4Qpxfj4heMatCwIZywh1JTLAYD+mkhSoeBLoGoqCENYGDXL943oB834iyhLdjHgfF79mHfsb0bkRvLBhZDjB49euzYsePH/96/vzyHeXN3Inc6zSuCqcRys1tJjm4xdYj//JP8AXE5S/JuAn7MAAAAAElFTkSuQmCC'); 269 | } 270 | 271 | google-codelab-step .instructions .faq a[href*="stackoverflow.com"] { 272 | padding-left: 22px; 273 | background-size: 24px; 274 | background-repeat: no-repeat; 275 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAATwAAAE8CAMAAABq2/00AAAA4VBMVEUAAACCg4aCg4aCg4bIj0P1fx/2hh+ojHCojHCojHD2ih/UjCn0eiD0eiD2hB/2hR/RjjDUjCnKkkPUjCnUjCn2ih/UjCnCllPCllP2ih/CllPCllPCllP2ih/CllPCllPCllP2ih/2ih/CllPCllP2ih/2ih/CllPUjCnCllPUjCn0eiD0eiD2ih+ojHD0eiDUjCn0eiD0eiCojHDUjCn0eiDUjCn0eiDUjCnUjCn0eiCojHD0eiCojHD2ih+ojHD2ih+ojHCojHCdiXaViHuCg4aojHD0eiDCllPUjCn2ih8XYwy7AAAARXRSTlMAv0CAEEAQv4BA7++/gCBQQN8wIIDPv2Dfv5+PUDDPryCAcO+/r49wn4Bg79/frJ9QcGDPr6+Pj3DPz2AwIJ9wYOaPVDAbIL/gAAAGhElEQVR42uzbsYrCQBDG8WmyxaJJlcIixQmxMSIRgiBB7Bbm/R/oPAnH3e0SxwSvyf/3DLt8zDeMAAAAAAAAAAAAAAAAAAAAAAAAAAAA4F9k7Xm1Xwlek53WqyY87AVWfbHumvCTE5hkIZYJTFyIrQU2+xA5CmxWIULcWq1DTGBzCrFcMDluC4ENcTtDEyKdwKYLkYuAuH27ggFtuj7ETgLiNo0B7Z3c1YnNMUQaWbTC6/W1uKUPHbha7wr60GnP7ot3YpGHWCsL5Wod1JP70LMsU+H12424nfDsBj4XgzMD2kN20F82YtDSh965nf61JW7Nzy7iKwY067OLlfJcw/qx1KQPQ9zSh1aaVtGHGnxoUummrB97WZhSk3byRM/6UST3mpQRtwZbTTo4GXdhQLvbaNJVxnX0oSMftyBuDQpN8o4BzaDWpFrG5KwfH5zXpJuMCfShox83pw812GnSxrx+XPI5hjvowF7ttawfB5km+Yq4nf5xS84x5lR7nGPMqfaI2znVHgPajGqPc4w51R7nGJ/sm29P2zAQh49h1kJgJHGWlrbQItAIebNO67owadpgwOzv/4Wm8S8NLsXxGSTu/HyEk/O78+McRu0FH4pQe2EdA6P2wjoGRu2FdotQe2EdA6P2wjoGRu2FdotQe+GChlF7DNcxKulN7bFbx4h1lPlSe+x8aKK1Tqae1B6zdYxM35BLP2qP1TqGjPQdmfCi9ji125l+ICp9qD1G6xixXiSPPag9PusYiW5SCLza49JuK/2YKEOrPSYXNBFpkyTGqj0e6xiFXkoukWqPwzpGrJ8iEyi1x8GHnuknSUqU2qO/jlHpVeQpwhB8oL6OISK9mkK4qz3q7bbQzxFV7mqP9jpGqi1IYme1R3sdI060BTPpaAiot9ss0hZkwk3tUb+gyUJbkEzd1B55HxqfaQvy1Ent0V/HKCNtwVy0NwSfGPhQMdcWRJWD2uOwjiFzbcFZ3F7tsVjHmDqMLTZqj3S7bTm2RJloZwh2maxjyJnd2NJS7XFZx7AcW2QrtUe93dZUDmOLjdrbaTTa7e9fNgj1i7ZjS9lK7dU+dPvzzgaRZ8elpPZji73a2/j749vOLuWytRtbCmmv9jghLMcWuw936zfwwnZssTAEexw+VidTmstn1N4Wr2/2gcrBlO6HY3eHKNqPLYfh2FmPLeZ/aXvh2D1Q2o0tYkHt8ZxQliIybTm2NNXex30IWI8tcePDPYSAywPv+61w7NwfeHfJHrvJS44tSQmUGan+oCfAgTi3fOCly1D952A4eqkH3gTocqDuOT5NX2JsmQJZhFrkaHwy8fzAmwNdeuoRRgQiTakEugyUSR2B+LFlDoTpK4M6AvH/pUUC6JIqAyMCMQ+8pMe8E2XgGIFlxKxbAIyVgUUEWj/wUh6QAZQBIgJlzqlbQKwMUBE4Tdh0C4ChMsBFoMgiJt2ivpvVoCNQznh0CxDKAB2B9dgSA2l6ygAbgfUDbwG0GSgDPxEo5tS7xYq7GT4CU8Im6pbRsNkxMBFIex5ezqQ3qM8fPgL5kZ6Mj1QNLgKpR90y4tNjpbxFID+ExwjsAUN8ReAYmOIjAlmePF8RyLHv+orAPgScI3AIb531tQZ/XjECR3DL5VoTeDOsXTVYf8UIFHDLu6sm8GZAFA8ZgccQimcgLCPwNBRvOROLCExD8Z4mXR2BRxCK5+wCx6F47hHYC8Vzj8BJKJ5zBPYhFM85Agf0i3dx3ulcdLubm5e+I3BEv3id65r7Ov70EoGCV/EW+NrpdLu/cHWkX7zz65pn6hiK9689O8hJKIaiMCxK0nYzTBgSYxDs/pekiTohEq/v0eRKv28J/+zkXMbrF2IdxQvEC3ScOF7r6+3LrtZDa1vxVinfHaeId+qjTBCv9kGO4i1XJoi365/EWxCv9EF24i1XJ4jXv4iXKd7p/uNt+yjt/uO1PspZvOVmmGcPrZ1q3ZUi3qoDaNva4WYdj5PFu2nHMm+8nzqKF4y3vuOreL923PcrqngRz6291FrKXrxbdWzifXB6iydehHh5iDco3maQpxnivQ2yEU888cQLEi8R8cSLES8R8cSLES8R8cSLES8R8cSLES8R8QbFexzkPEO86xxA4on3F+LlIZ54QeLlIZ54QeLlIZ54QeLlId6qeOk8/BviiRciXibiiRciXibiiRciXibiiRciXibiiQcAAAAAAAAAAAAAAAAAwHTeAe21evvWi2VXAAAAAElFTkSuQmCC'); 276 | } 277 | 278 | google-codelab-step .instructions .faq a[href*="support.google.com/webmasters/"] { 279 | padding-left: 24px; 280 | background-size: 24px; 281 | background-repeat: no-repeat; 282 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAolBMVEUAAADW1tbW1tZ6enrQ0dJ6enrMzMzq6+zq6+x6enru7+/m5+ju7+/m5+j////S09REi/XW1tbm5+ju7+9PT0/Q0dK8vLxGjfVZWVlNTU16enrMzMzIyMj09PS/0uuSkpJpaWmbm5tim/e0tLSEhIStx+3e3+GsrKxwcHBZk/dPk/R1p/KPtu/U3eqmpqZlZWVfmO5woeqHreWvwN6nu9qLi4vZSE73AAAADnRSTlMAEcxmzO7MzDMz7u6IiHn/rpYAAALMSURBVHja7dXpjtowFIZhUkqZlSV1ncQdEsK+zD7T+7+1xjTiwxVOAmeORyP5+2UspPchEtDy8/Pz8/Pz8/M7cZeduHKdyxbL0FfVAMUraHdUHUB12i22tZWqBRSDgKFfC4CApV8PgIClXw/gE6hyTd/lAR7gAV8fEHwbWtc/ul/WfQ8IfQKAIECfCoCA3sdq+nQB+nQABIQ+CQABtY9Z+lQB+gwACBj6AFAE6DM9AL0fAalPB/yGgNAfntwHAAJKnwCAwNLneQAAVAvM/ttzVLlB4z2/A6AFVoDRf43IfewVgGKNAG/Uvrl39BsCXqh9cy8nA/51tmk63+jDZp6m9wTA4DzANiyW6lOqTxAMHAD22XASReuwpKDvELCNookBGJwx9E8B3OvsQp8e9GmOviNANA/Dh4k+TApBukHfFSBa7/I7whqf3xWA/gOAoU8DDJwBCHk+wIAw9CsAPw/W++CFB/MAD/hCgL59PXPS8toRYKHEndkTauEQIIUQ0hToG2eAXOicKdBXmSPAo9CTpkDo5U4Ad6IEGILy0gFgFJctCACIh/yATABwIChvVyE3IBR7AAQAiOmIGZADAMEBQMkRL+AJAAgOALGUI07AQgBgCPb3uRbwAaYAGAIAZlIL2AA5AIYAgJXUAjaAAsAQADCWO4EjAAQGQAvYAUsDsDQAeuwAMZvu89PZ/jZhBmQCi2dl60+MPiMAf8UgZEUpK/LoJ4oX0C9LIIyF0U8yXkBPVCzRWzIDZjX9sWQG9Kv7ScYN6KnKfizZAf2qfpLzA3p5RV9JboDeytp/kk4Ao5WtP+UFQKCO9sdTSQQ03+OR/kxKdgAWZv/11VI6AWBmP5HSNQD5zwQknwxIqIBu468hBgDqNYCuFXBDASSNAbdWQHB1PiBpDLgOWtZdXPEDri/QswjYAOjbF9x0OQHd26Dl5+fn5+fn5+dn7i/3LEaKJNV/0wAAAABJRU5ErkJggg=='); 283 | } 284 | 285 | google-codelab-step .instructions .faq a[href*="developer.android.com"], 286 | google-codelab-step .instructions .faq a[href*="android-developer"] { 287 | padding-left: 20px; 288 | background-repeat: no-repeat; 289 | background-size: 20px; 290 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAq1BMVEX///+lyjmfxyGhyCqkyjakyTP+/vuiyS7z+OWlyjKgxyTX57ChyCfa6bTx9uLy9+Sz0l6szk/D24Tt9NrN4ZnK4JSiyCyexh34+/D6/PTp8dPh7cjf7L7k78vc6rnW5qzU5ajS5KPP4p+21GWnzD3F3Yi813Kx0ViqzUWexhr8/vjv9d7Z6LKy0lyZxADh7cLA2n2rzkr1+erJ35C61m6uz1KcxRHn8M6/2XgEePWtAAACr0lEQVRo3u3Z2XKiQBiG4Y9ebNO2aCCyzOC+7zH7/V/ZRJZQZaJpWnKS4TlJUItXbX4pFJXK71DTusvccI6z5oMSAutdhDP2uzpK4CxwxsRBGYJdA19qLAOU4s4LkyUN7G4Ude2glmzuRihHKDqAPZhNuJRKSckms37rvctrKMnG728l5cxKMU7JuCF7KEvf87h1gnu0iXK4B2p9Sc3dUgZNMusMLvu42r20LiBXT8JMWRepN1xlGu+fK8/6xFPxwsvH697/eP/j7uDTOvB+dxwX/AaM2SQ+6icrYHDyGmgfWE3irNeCqTaPAwcArydroboAbuMAb8NQRNJn20SYtHK8HaKZzoffhZl5tlNxsD5PsnUQ2b9jwwnO35WvRo3lN/o2TDwKS5M3gokF0w2wZxgIlpY2FpgsgdQPkLXJQar0A/LVIDB6Idp2GxQQrJI/rbq2Voijlc5S9LZs8uYCqBVyXLTZhLW/fSn3hDMmZAQ05I020gT2PmeM+w4uGpLksFs+oEEtbaqJVnpUkz4uyebf6xQK0CY6XjoSC1xQ9/PPr4KB52zuvTrOa5EssC0auM0C0tYKtI0DqgpUgSpQBapAFfifAj90Tn5YZoGnooH8eutB56KDDosGhlTrYiTyk0epVdFAKJLn9rLHRSNfcE4tF0UDcAXlXPhDfGPtjGejEMUDCEdPY8eFHoOADvPAxiDQLBBQkUHAJfoB34UBS/9CfAETe6lZYKZft/QIFe94vicRy7tcvKOkB0PhYOo4zlNWYNvjpjOdZwU+O24OQlynK62EGCF2J9IbpIsy/P0I3CHW+Qj8qQJVoAr8VOCx3EB+YvDSwIDmXzaW4oZZseU6Lfr5aaAUPT8uyPuT33VeIpRkI5RHyRQfHEKFUj2UJuh1GjaQsxudKECl8jv8A6GtQkKSkMLrAAAAAElFTkSuQmCC'); 291 | } 292 | 293 | google-codelab-step .instructions h3 > a[href*="github"], 294 | google-codelab-step .instructions h3 > a[href*="github"]:visited { 295 | color: black; 296 | text-decoration: none; 297 | padding-left: 24px; 298 | background-repeat: no-repeat; 299 | background-size: 18px; 300 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAMAAAAOusbgAAAAflBMVEUAAACXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZbf2s+YAAAAKXRSTlMA+SAC7QrIBPXROWMX17rCSmusXEaAD9zNeXFWJqOLMx3mtJJAK5xP4Jw4LyAAAAT6SURBVGje1NbdcqJAEAXg0zJAQEFA0QhojJHE8/4vuBfrJlvQGHSGbO13CzVT09M/g//HIssPRZAYIUkxSVAc8myBSfnrMjVUmbRc+5jEsUqFN0lazeDYogo4SlC5jPq6EI4mxRpOhKuId4pWof228y0fsJ1bbp0lfFCS4XFNSgtpg8eEO6EV2YV4QBvRWtTiXl4udEByD3fxCzpS+LhDk9CZpMFosaFDJsZItdApqTHKnM7NMULOCeQW5534zDUnUuOmWDgRiXFDYzgZ02CQn3BCiY8BXsFJFd7YQjLPkeGDTPRsRhZVK+zIABxX+yfe6Wm/OgL4YIe0UIQRO8x1jm/yiHeI8s11RdP7FKJvx649PmUBRwoyfNqza6dUktzsNt7HliNsa+9m95UGXSl7XvQn2OV0qOpzO1tsNotZe66rw+lyXbb08bcX9qToyNgjYTcoS3PaxT4UflydzLJ7nFDYk3V+SdhzQZfnQTP89VW5jPDbmRTAWvDdnAq1zFnC2pLfHHnFn9uYK3yJ+HOhZoRPa2peYe2VmjX+KKjawNKGqgJXC6HqDEtnqmSB3yrqSlgqqau+UkAVwVLEKz1tZxxQw1LNATObSNvHOqUqCGEtDKhKAcAXqlo40Ag14g92D77BiTcO9pByqNbsDfeIcvCK3+HI++AlG2piOBJTY4AjNVs4s6XmiMwitSzSK0Nu0bQs2leOAzUzODOj5qDPYvHgTCj6TA703HLoog+oxGIgWgzHBMbigWnx1DSQyTcOqBDw34Sav9q1sx1HYSAKoNcYE4cl0AlhIB22rF3//4PTI43ULbrMYjzKy5zHROiGyNhlyqAXDC5z8Acc6mcFu3+OpeCD+Y87ONMRR1isik6qet+wah3gzMEwfIPXLIsB9sQp4UxJnL2pDFRwRBHrauoEZHDkaeoV5PSKKpNydMTSCk4oTawO8O07Nva9JN+8Z0skHNgk5l3bkf7hLUcjW+CceH2B1YqeeDmzTXU5sB/EE4rZmDucsA9ksBtvJuoQq4SaDE5sqc8n2+eaNyqBOTmHtcycG8xo3B4lrMgjmZ0GrwtYaQsLbUBmImZeojKuHRbyHjRmz5ZFQRUdByfGxCPEfPJW07gbVyVoiU9xIwYz97GVs1LD5oMmbPknvcKntLmXNNDvT2GBEV1WXXyadjA0Q64bIMygLsTo00aCsbmWfCbfDOEnt6QF8EzjlBi6BasVdscU5HYwbdz9LO65y9ae4tjKwTzDTJUZfxlPJjRLhoELfdPHgNrl9ZIFK6I5LhjyxOB75QdFsqAEVIKmCW+qcZ0B9xvOPft7eReaVnFPRPmj1lPvbcJdx6toUrkB46zpmyeAnFLViMGDZvSkKfo8Y3ikAORjB8RVqonIT4+hxJjW/qRRzezNvd1VoegKoG1uGOPRhBomaktfAok/PJ9a3PykQEk1xsQ0bqtg5Pk/+04qBtryotDlG4wpaJTvzS7Q3sCwDdbhgu1sw9yi7V/9XFSEb3MwbAbXYWn5H7zF3+5J2gZHNhuPX5frsaqaOvWpsAw+rC3FY6tg/cRMYU88zya4DzGbV7oL3npYQNWugmuFZSLtIlhHWOwcrA8OzrAgT3pdsD5J2PH2a4L3HuzdStvgMscq8j2hL/e5wcm7BLA2OuBbFebVKWBirYR/D6OWc16JizqEO0WUCrHzMMrbCZFGBf5j/QYa/td1VlNoIAAAAABJRU5ErkJggg=='); 301 | } 302 | -------------------------------------------------------------------------------- /google-codelab-step/google_codelab_step.soy: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | {namespace googlecodelabs.CodelabStep.Templates} 19 | 20 | /** 21 | * Renders the step title 22 | */ 23 | {template .title} 24 | {@param step: string} 25 | {@param label: string} 26 |

{$step}. {$label}

27 | {/template} 28 | -------------------------------------------------------------------------------- /google-codelab-step/google_codelab_step_def.js: -------------------------------------------------------------------------------- 1 | goog.module('googlecodelabs.CodelabStepDef'); 2 | const CodelabStep = goog.require('googlecodelabs.CodelabStep'); 3 | 4 | try { 5 | window.customElements.define(CodelabStep.getTagName(), CodelabStep); 6 | } catch (e) { 7 | console.warn('googlecodelabs.CodelabStep', e); 8 | } -------------------------------------------------------------------------------- /google-codelab-step/google_codelab_step_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabStepTest'); 19 | goog.setTestOnly(); 20 | 21 | const CodelabStep = goog.require('googlecodelabs.CodelabStep'); 22 | window.customElements.define(CodelabStep.getTagName(), CodelabStep); 23 | const MockControl = goog.require('goog.testing.MockControl'); 24 | const testSuite = goog.require('goog.testing.testSuite'); 25 | goog.require('goog.testing.asserts'); 26 | goog.require('goog.testing.jsunit'); 27 | 28 | let mockControl; 29 | 30 | /** 31 | * @param {string} s 32 | * @return {string} 33 | */ 34 | window['prettyPrintOne'] = (s) => {return s;}; 35 | 36 | testSuite({ 37 | setUp() { 38 | mockControl = new MockControl(); 39 | }, 40 | 41 | tearDown() { 42 | mockControl.$resetAll(); 43 | mockControl.$tearDown(); 44 | }, 45 | 46 | testDomIsSetUpCorrectly() { 47 | const codelabStep = new CodelabStep(); 48 | codelabStep.innerHTML = '

Test

'; 49 | 50 | document.body.appendChild(codelabStep); 51 | 52 | assertNotUndefined(codelabStep.querySelector('.instructions')); 53 | assertNotUndefined(codelabStep.querySelector('.inner')); 54 | assertNotUndefined(codelabStep.querySelector('h2.step-title')); 55 | assertEquals('Test', codelabStep.querySelector('h1').innerHTML); 56 | 57 | document.body.removeChild(codelabStep); 58 | }, 59 | 60 | testCodePrettyprint() { 61 | const mockPrettyPrint = mockControl.createMethodMock(window, 'prettyPrintOne'); 62 | mockPrettyPrint('Code').$returns('MockCodeTest').$once(); 63 | 64 | mockControl.$replayAll(); 65 | 66 | const codelabStep = new CodelabStep(); 67 | codelabStep.innerHTML = '

Testing

Code
'; 68 | document.body.appendChild(codelabStep); 69 | 70 | mockControl.$verifyAll(); 71 | 72 | assertNotEquals(-1, codelabStep.innerHTML.indexOf('MockCodeTest')); 73 | 74 | document.body.removeChild(codelabStep); 75 | }, 76 | 77 | testSnippetCopy() { 78 | const codelabStep = new CodelabStep(); 79 | codelabStep.innerHTML = '

Testing

Code
'; 80 | document.body.appendChild(codelabStep); 81 | 82 | document.body.addEventListener('google-codelab-action', (e) => { 83 | const detail = e.detail; 84 | assertEquals('codelab', detail['category']); 85 | assertEquals('copy', detail['action']); 86 | assertEquals('Code', detail['label']); 87 | }); 88 | 89 | const copyEvent = new ClipboardEvent('copy', { 90 | view: window, 91 | bubbles: true, 92 | cancelable: true 93 | }); 94 | document.body.querySelector('.test-code').dispatchEvent(copyEvent); 95 | 96 | document.body.removeChild(codelabStep); 97 | }, 98 | 99 | testUpdateTitle() { 100 | const codelabStep = new CodelabStep(); 101 | 102 | document.body.appendChild(codelabStep); 103 | 104 | let title = codelabStep.querySelector('h2.step-title'); 105 | assertEquals('0. ', title.textContent); 106 | 107 | codelabStep.setAttribute('step', '3'); 108 | title = codelabStep.querySelector('h2.step-title'); 109 | assertEquals('3. ', title.textContent); 110 | 111 | codelabStep.setAttribute('label', 'test label'); 112 | title = codelabStep.querySelector('h2.step-title'); 113 | assertEquals('3. test label', title.textContent); 114 | 115 | document.body.removeChild(codelabStep); 116 | } 117 | }); 118 | -------------------------------------------------------------------------------- /google-codelab-step/img-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-1.png -------------------------------------------------------------------------------- /google-codelab-step/img-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-2.png -------------------------------------------------------------------------------- /google-codelab-step/img-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-3.png -------------------------------------------------------------------------------- /google-codelab-step/img-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-4.png -------------------------------------------------------------------------------- /google-codelab-step/img-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-5.png -------------------------------------------------------------------------------- /google-codelab-step/img-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-6.png -------------------------------------------------------------------------------- /google-codelab-step/img-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-7.png -------------------------------------------------------------------------------- /google-codelab-step/img-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab-step/img-8.png -------------------------------------------------------------------------------- /google-codelab-survey/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) 4 | 5 | exports_files(["LICENSE"]) 6 | 7 | load("//tools:defs.bzl", 8 | "closure_js_library", "closure_js_binary", "closure_js_test") 9 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library") 10 | load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library") 11 | 12 | filegroup( 13 | name = "google_codelab_survey_files", 14 | srcs = glob([ 15 | "*.html", 16 | "*.png", 17 | ]) + [ 18 | ":google_codelab_survey_scss_bin", 19 | ":google_codelab_survey_bin", 20 | ], 21 | ) 22 | 23 | # Codelab survey. 24 | closure_js_library( 25 | name = "google_codelab_survey", 26 | srcs = [ 27 | "google_codelab_survey.js", 28 | "google_codelab_survey_def.js" 29 | ], 30 | deps = [ 31 | "@io_bazel_rules_closure//closure/library", 32 | ":google_codelab_survey_soy", 33 | ], 34 | ) 35 | 36 | # Compiled version of CodelabSurvey element, suitable for distribution. 37 | closure_js_binary( 38 | name = "google_codelab_survey_bin", 39 | entry_points = ["googlecodelabs.CodelabSurveyDef"], 40 | deps = [":google_codelab_survey"], 41 | ) 42 | 43 | closure_js_test( 44 | name = "google_codelab_survey_test", 45 | srcs = ["google_codelab_survey_test.js"], 46 | entry_points = ["googlecodelabs.CodelabSurveyTest"], 47 | deps = [ 48 | "@io_bazel_rules_closure//closure/library", 49 | ":google_codelab_survey" 50 | ], 51 | ) 52 | 53 | closure_js_template_library( 54 | name = "google_codelab_survey_soy", 55 | srcs = ["google_codelab_survey.soy"] 56 | ) 57 | 58 | sass_binary( 59 | name = "google_codelab_survey_scss_bin", 60 | src = "google_codelab_survey.scss", 61 | ) 62 | -------------------------------------------------------------------------------- /google-codelab-survey/google-codelab-survey.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Google Codelab Survey 22 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

How would rate your experience with Polymer?

36 | 37 | Novice 38 | Intermediate 39 | Advanced 40 | 41 | 42 |

How will you use use this tutorial?

43 | 44 | I won't 45 | Complete it 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /google-codelab-survey/google_codelab_survey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabSurvey'); 19 | 20 | const EventHandler = goog.require('goog.events.EventHandler'); 21 | const HTML5LocalStorage = 22 | goog.require('goog.storage.mechanism.HTML5LocalStorage'); 23 | const Templates = goog.require('googlecodelabs.CodelabSurvey.Templates'); 24 | const dom = goog.require('goog.dom'); 25 | const events = goog.require('goog.events'); 26 | const soy = goog.require('goog.soy'); 27 | 28 | 29 | /** 30 | * The prefix for all survey keys in local storage. 31 | * @const {string} 32 | */ 33 | const STORAGE_KEY_PREFIX = 'codelab-survey-'; 34 | 35 | /** 36 | * The id for the current survey. 37 | * @const {string} 38 | */ 39 | const SURVEY_ID_ATTR = 'survey-id'; 40 | 41 | 42 | /** 43 | * The upgraded id (to prevent FUOC). 44 | * @const {string} 45 | */ 46 | const SURVEY_UPGRADED_ATTR = 'upgraded'; 47 | 48 | 49 | /** @const {string} */ 50 | const DEFAULT_SURVEY_NAME = 'default-codelabs-survey'; 51 | 52 | 53 | /** @enum {string} */ 54 | const CssClass = { 55 | 'OPTIONS_WRAPPER': 'survey-question-options', 56 | 'RADIO_WRAPPER': 'survey-option-wrapper', 57 | 'RADIO_TEXT': 'option-text' 58 | }; 59 | 60 | 61 | /** 62 | * @extends {HTMLElement} 63 | */ 64 | class CodelabSurvey extends HTMLElement { 65 | /** @return {string} */ 66 | static getTagName() { return 'google-codelab-survey'; } 67 | 68 | constructor() { 69 | super(); 70 | 71 | /** 72 | * The name of the survey 73 | * @private {string} 74 | * @const 75 | */ 76 | this.surveyName_ = this.getAttribute(SURVEY_ID_ATTR) || DEFAULT_SURVEY_NAME; 77 | 78 | /** 79 | * @private {!HTML5LocalStorage} 80 | * @const 81 | */ 82 | this.storage_ = new HTML5LocalStorage(); 83 | 84 | /** 85 | * @private {string} 86 | * @const 87 | */ 88 | this.storageKey_ = STORAGE_KEY_PREFIX + this.surveyName_; 89 | 90 | /** 91 | * @private {!Object} 92 | * @const 93 | */ 94 | this.storedData_ = {}; 95 | 96 | /** 97 | * @private {!EventHandler} 98 | * @const 99 | */ 100 | this.eventHandler_ = new EventHandler(); 101 | } 102 | 103 | /** 104 | * @export 105 | * @override 106 | */ 107 | connectedCallback() { 108 | this.checkStoredData_(); 109 | this.updateDom_(); 110 | this.bindEvents_(); 111 | } 112 | 113 | /** @private */ 114 | bindEvents_() { 115 | this.eventHandler_.listen(document.body, events.EventType.CLICK, 116 | (e) => this.handleClick_(e.target)); 117 | } 118 | 119 | /** 120 | * @param {!Element} el 121 | * @private 122 | */ 123 | handleClick_(el) { 124 | const isOptionWrapper = el.classList.contains( 125 | CssClass.RADIO_WRAPPER); 126 | const elParent = el.parentElement; 127 | let isOptionChild = false; 128 | if (elParent) { 129 | isOptionChild = elParent.classList.contains(CssClass.RADIO_WRAPPER); 130 | } 131 | 132 | if (isOptionWrapper || isOptionChild) { 133 | let optionEl = el; 134 | if (isOptionChild) { 135 | optionEl = /** @type {!Element} */ (elParent); 136 | } 137 | if (optionEl) { 138 | this.handleOptionSelected_(optionEl); 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * @param {!Element} optionEl 145 | * @private 146 | */ 147 | handleOptionSelected_(optionEl) { 148 | const optionTextEl = optionEl.querySelector(`.${CssClass.RADIO_TEXT}`); 149 | let answer = ''; 150 | if (optionTextEl) { 151 | answer = optionTextEl.textContent; 152 | } 153 | /** @type {?HTMLInputElement} */ 154 | const inputEl = /** @type {?HTMLInputElement} */ ( 155 | optionEl.querySelector('input')); 156 | if (inputEl) { 157 | inputEl.checked = true; 158 | const question = inputEl.name; 159 | this.storedData_[this.surveyName_][question] = answer; 160 | this.storage_.set( 161 | this.storageKey_, JSON.stringify(this.storedData_[this.surveyName_])); 162 | const event = new CustomEvent('google-codelab-action', { 163 | detail: { 164 | 'category': 'survey', 165 | 'action': question.substring(0, 500), 166 | 'label': answer.substring(0, 500) 167 | } 168 | }); 169 | document.body.dispatchEvent(event); 170 | } 171 | } 172 | 173 | /** @private */ 174 | checkStoredData_() { 175 | const storedData = this.storage_.get(this.storageKey_); 176 | if (storedData) { 177 | this.storedData_[this.surveyName_] = /** @type {!Object} */ ( 178 | JSON.parse(storedData)); 179 | } else { 180 | this.storedData_[this.surveyName_] = {}; 181 | } 182 | } 183 | 184 | /** @private */ 185 | updateDom_() { 186 | const radioGroupEls = this.querySelectorAll('paper-radio-group'); 187 | const questionEls = this.querySelectorAll('h4'); 188 | const surveyQuestions = []; 189 | if (radioGroupEls.length && (questionEls.length == radioGroupEls.length)) { 190 | radioGroupEls.forEach((radioGroupEl, index) => { 191 | const surveyOptions = []; 192 | const polymerRadioEls = radioGroupEl.querySelectorAll( 193 | 'paper-radio-button'); 194 | dom.removeNode(radioGroupEl); 195 | polymerRadioEls.forEach(radioEl => { 196 | const title = radioEl.textContent; 197 | surveyOptions.push({ 198 | radioId: this.normalizeIdAttr_(title), 199 | radioTitle: title 200 | }); 201 | }); 202 | surveyQuestions.push({ 203 | question: questionEls[index].textContent, 204 | options: surveyOptions 205 | }); 206 | dom.removeNode(questionEls[index]); 207 | }); 208 | const updatedDom = soy.renderAsElement(Templates.survey, { 209 | surveyName: this.surveyName_, 210 | surveyQuestions: surveyQuestions 211 | }); 212 | this.appendChild(updatedDom); 213 | } 214 | this.setAnsweredQuestions_(); 215 | this.setAttribute(SURVEY_UPGRADED_ATTR, ''); 216 | } 217 | 218 | /** @private */ 219 | setAnsweredQuestions_() { 220 | const surveyData = this.storedData_[this.surveyName_]; 221 | if (surveyData) { 222 | Object.keys(surveyData).forEach(key => { 223 | const id = this.normalizeIdAttr_(surveyData[key]); 224 | /** @type {?HTMLInputElement} */ 225 | const inp = /** @type {?HTMLInputElement} */ ( 226 | this.querySelector(`#${id}`)); 227 | if (inp) { 228 | inp.checked = true; 229 | } 230 | }); 231 | } 232 | } 233 | 234 | /** 235 | * @param {string} id 236 | * @return {string} 237 | * @private 238 | */ 239 | normalizeIdAttr_(id) { 240 | return id.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9 \-]/g, '').toLowerCase(); 241 | } 242 | 243 | /** 244 | * @export 245 | * @override 246 | */ 247 | disconnectedCallback() { 248 | this.eventHandler_.removeAll(); 249 | } 250 | } 251 | 252 | exports = CodelabSurvey; 253 | -------------------------------------------------------------------------------- /google-codelab-survey/google_codelab_survey.scss: -------------------------------------------------------------------------------- 1 | google-codelab-survey { 2 | visibility: hidden; 3 | } 4 | 5 | google-codelab-survey[upgraded] { 6 | visibility: visible; 7 | } 8 | 9 | google-codelab-survey { 10 | display: block; 11 | margin: 2em 0; 12 | padding: 0; 13 | background: #e8f0fe; 14 | border-left: 4px solid #185abc; 15 | border-radius: 4px; 16 | color: #3c4043; 17 | } 18 | 19 | google-codelab-survey h4 { 20 | font-size: 16px; 21 | font-weight: 400; 22 | padding: .8em 0 0; 23 | margin: 0; 24 | } 25 | 26 | google-codelab-survey .survey-question-wrapper { 27 | padding: .4em 0 1.1em 30px; 28 | } 29 | 30 | google-codelab-survey .survey-question-options { 31 | padding: .8em 0 0; 32 | } 33 | 34 | .survey-option-wrapper { 35 | cursor: pointer; 36 | display: block; 37 | padding: 0 0 4px; 38 | position: relative; 39 | -webkit-user-select: none; 40 | -moz-user-select: none; 41 | -ms-user-select: none; 42 | user-select: none; 43 | vertical-align: middle; 44 | } 45 | 46 | google-codelab-survey .option-text { 47 | color: #212121; 48 | font-size: 16px; 49 | padding-left: 24px; 50 | } 51 | 52 | .survey-option-wrapper input { 53 | position: absolute; 54 | opacity: 0; 55 | } 56 | 57 | .custom-radio-button { 58 | position: absolute; 59 | top: 5px; 60 | left: 0; 61 | height: 13px; 62 | width: 13px; 63 | background-color: #fff; 64 | border: 2px solid #3f51b5; 65 | border-radius: 50%; 66 | } 67 | 68 | .custom-radio-button:after { 69 | content: ""; 70 | position: absolute; 71 | display: none; 72 | } 73 | 74 | .survey-option-wrapper input:checked ~ .custom-radio-button:after { 75 | display: block; 76 | } 77 | 78 | .survey-option-wrapper .custom-radio-button:after { 79 | top: 1px; 80 | left: 1px; 81 | width: 7px; 82 | height: 7px; 83 | border-radius: 50%; 84 | background: #3f51b5; 85 | } 86 | -------------------------------------------------------------------------------- /google-codelab-survey/google_codelab_survey.soy: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | {namespace googlecodelabs.CodelabSurvey.Templates} 19 | 20 | /** 21 | * Renders questions with mdc radio groups for a codelabs survey. 22 | */ 23 | {template .survey} 24 | {@param surveyName: string } 25 | {@param surveyQuestions: list<[question:string, options:list<[radioId:string, radioTitle:string]>]>} 26 |
27 | {for $surveyQuestion in $surveyQuestions} 28 |
29 |

{$surveyQuestion.question}

30 | {if length($surveyQuestion.options)} 31 |
32 | {for $option in $surveyQuestion.options} 33 | 44 | {/for} 45 |
46 | {/if} 47 |
48 | {/for} 49 |
50 | {/template} 51 | -------------------------------------------------------------------------------- /google-codelab-survey/google_codelab_survey_def.js: -------------------------------------------------------------------------------- 1 | goog.module('googlecodelabs.CodelabSurveyDef'); 2 | const CodelabSurvey = goog.require('googlecodelabs.CodelabSurvey'); 3 | 4 | try { 5 | window.customElements.define(CodelabSurvey.getTagName(), CodelabSurvey); 6 | } catch (e) { 7 | console.warn('googlecodelabs.CodelabSurvey', e); 8 | } -------------------------------------------------------------------------------- /google-codelab-survey/google_codelab_survey_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | goog.module('googlecodelabs.CodelabSurveyTest'); 19 | goog.setTestOnly(); 20 | 21 | const CodelabSurvey = goog.require('googlecodelabs.CodelabSurvey'); 22 | window.customElements.define(CodelabSurvey.getTagName(), CodelabSurvey); 23 | const HTML5LocalStorage = 24 | goog.require('goog.storage.mechanism.HTML5LocalStorage'); 25 | const testSuite = goog.require('goog.testing.testSuite'); 26 | goog.require('goog.testing.asserts'); 27 | goog.require('goog.testing.jsunit'); 28 | 29 | let div; 30 | 31 | /** @const {!HTML5LocalStorage} */ 32 | const localStorage = new HTML5LocalStorage(); 33 | 34 | const polymerHtml = '' + 35 | '

Question?

' + 36 | 'Title Text' + 37 | 'Second Option' + 38 | '
'; 39 | 40 | const polymerHtmlInvalid = '' + 41 | 'Title Text' + 42 | ''; 43 | 44 | testSuite({ 45 | 46 | setUp() { 47 | if (localStorage.isAvailable()) { 48 | localStorage.clear(); 49 | } 50 | div = document.createElement('div'); 51 | div.innerHTML = polymerHtml; 52 | }, 53 | 54 | tearDown() { 55 | if (localStorage.isAvailable()) { 56 | localStorage.clear(); 57 | } 58 | document.body.innerHTML = ''; 59 | div = null; 60 | }, 61 | 62 | testCodelabSurveyUpgraded() { 63 | document.body.appendChild(div); 64 | const surveyCE = div.querySelector('google-codelab-survey'); 65 | const radioInputEl = surveyCE.querySelector('input#title-text'); 66 | const radioLabelEl = surveyCE.querySelector('label#title-text-label'); 67 | const radioTextEl = surveyCE.querySelector('.option-text'); 68 | const surveyWrapperEl = surveyCE.querySelector('.survey-questions'); 69 | assertNotNull(radioInputEl); 70 | assertEquals('Question?', radioInputEl.name); 71 | assertNotNull(radioLabelEl); 72 | assertEquals('test', surveyWrapperEl.getAttribute('survey-name', '')); 73 | assertEquals('Title Text', radioTextEl.textContent); 74 | assertEquals('title-text', radioLabelEl.getAttribute('for', '')); 75 | assertTrue(surveyCE.hasAttribute('upgraded')); 76 | }, 77 | 78 | testCodelabSurveyIncorrectFormatNotUpgraded() { 79 | div.innerHTML = polymerHtmlInvalid; 80 | document.body.appendChild(div); 81 | const radioInputEl = div.querySelector('input#title-text'); 82 | const radioLabelEl = div.querySelector('label#title-text-label'); 83 | assertNull(radioInputEl); 84 | assertNull(radioLabelEl); 85 | }, 86 | 87 | testCodelabSurveyOptionClick() { 88 | document.body.appendChild(div); 89 | const optionEls = div.querySelectorAll('.survey-option-wrapper'); 90 | // If nothing is in local storage no options should be set. 91 | assertFalse(optionEls[0].querySelector('input').checked); 92 | assertFalse(optionEls[1].querySelector('input').checked); 93 | 94 | optionEls[0].click(); 95 | assertEquals('{"Question?":"Title Text"}', localStorage.get('codelab-survey-test')); 96 | optionEls[1].click(); 97 | assertEquals('{"Question?":"Second Option"}', localStorage.get('codelab-survey-test')); 98 | }, 99 | 100 | testCodelabSurveyLoadsStoredAnswers() { 101 | localStorage.set('codelab-survey-test', '{"Question?":"Second Option"}'); 102 | document.body.appendChild(div); 103 | const optionEls = div.querySelectorAll('.survey-option-wrapper'); 104 | 105 | // Second option should be selected (answer loaded from local storage) 106 | assertFalse(optionEls[0].querySelector('input').checked); 107 | assertTrue(optionEls[1].querySelector('input').checked); 108 | }, 109 | }); 110 | -------------------------------------------------------------------------------- /google-codelab/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) 4 | 5 | exports_files(["LICENSE"]) 6 | 7 | load("//tools:defs.bzl", 8 | "closure_js_library", "closure_js_binary", "closure_js_test") 9 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library") 10 | load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library") 11 | 12 | filegroup( 13 | name = "google_codelab_files", 14 | srcs = glob([ 15 | "*.html", 16 | "img/**", 17 | ]) + [ 18 | ":google_codelab_scss_bin", 19 | ":google_codelab_bin", 20 | ], 21 | ) 22 | 23 | # Codelab step. 24 | closure_js_library( 25 | name = "google_codelab", 26 | srcs = [ 27 | "google_codelab.js", 28 | "google_codelab_def.js" 29 | ], 30 | deps = [ 31 | "@io_bazel_rules_closure//closure/library", 32 | ":google_codelab_soy", 33 | ] 34 | ) 35 | 36 | # Compiled version of GoogleCodelabStep element, suitable for distribution. 37 | closure_js_binary( 38 | name = "google_codelab_bin", 39 | entry_points = ["googlecodelabs.CodelabDef"], 40 | deps = [":google_codelab"], 41 | ) 42 | 43 | sass_library( 44 | name = "google_codelab_scss", 45 | srcs = ["google_codelab.scss"], 46 | ) 47 | 48 | sass_library( 49 | name = "google_codelab_drawer_scss", 50 | srcs = ["_drawer.scss"], 51 | ) 52 | 53 | sass_library( 54 | name = "google_codelab_steps_scss", 55 | srcs = ["_steps.scss"], 56 | ) 57 | 58 | sass_binary( 59 | name = "google_codelab_scss_bin", 60 | src = "google_codelab.scss", 61 | deps = [ 62 | ":google_codelab_drawer_scss", 63 | ":google_codelab_steps_scss", 64 | ] 65 | ) 66 | 67 | closure_js_template_library( 68 | name = "google_codelab_soy", 69 | srcs = ["google_codelab.soy"] 70 | ) 71 | -------------------------------------------------------------------------------- /google-codelab/_drawer.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | google-codelab #drawer { 19 | background: #fff; 20 | width: 256px; 21 | flex-shrink: 0; 22 | position: relative; 23 | border-right: 1px solid #DADCE0; 24 | z-index: 100; 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | google-codelab #drawer .steps { 30 | flex-shrink: 1; 31 | flex-grow: 1; 32 | overflow-x: visible; 33 | display: flex; 34 | } 35 | 36 | google-codelab #drawer ol { 37 | margin: 0; 38 | padding: 16px 12px; 39 | counter-reset: li-count; 40 | list-style: none; 41 | overflow-x: visible; 42 | overflow-y: auto; 43 | flex-grow: 1; 44 | } 45 | 46 | google-codelab #drawer ol li { 47 | display: block; 48 | counter-increment: li-count; 49 | } 50 | 51 | google-codelab #drawer ol li a { 52 | text-decoration: none; 53 | display: flex; 54 | align-items: center; 55 | font-size: 14px; 56 | color: #80868B; 57 | border-radius: 4px; 58 | padding: 6px 16px; 59 | min-height: 48px; 60 | font-weight: 400; 61 | line-height: 20px; 62 | box-sizing: content-box; 63 | position: relative; 64 | font-family: 'Roboto', 'Noto', sans-serif; 65 | -webkit-font-smoothing: antialiased; 66 | transition: all 300ms ease-in-out; 67 | } 68 | 69 | google-codelab #drawer ol li a:active, 70 | google-codelab #drawer ol li a:focus { 71 | background: #c6c6c6; 72 | -webkit-tap-highlight-color: transparent; 73 | outline: 0; 74 | } 75 | 76 | google-codelab #drawer ol li a .step { 77 | display: flex; 78 | align-items: center; 79 | } 80 | 81 | google-codelab #drawer ol li .step:before { 82 | content: counter(li-count); 83 | display: inline-block; 84 | font-style: normal; 85 | width: 26px; 86 | min-width: 26px; 87 | color: #fff; 88 | background: #80868B; 89 | border-radius: 50%; 90 | text-align: center; 91 | height: 26px; 92 | vertical-align: middle; 93 | line-height: 26px; 94 | margin-right: 8px; 95 | font-weight: 400; 96 | position: relative; 97 | z-index: 2; 98 | transition: all 300ms ease-in-out; 99 | } 100 | 101 | google-codelab #drawer ol li a:before, 102 | google-codelab #drawer ol li a:after { 103 | content: ''; 104 | display: block; 105 | background-color: #80868B; 106 | width: 2px; 107 | height: 50%; 108 | z-index: 1; 109 | position: absolute; 110 | margin-left: 12px; 111 | } 112 | 113 | google-codelab #drawer ol li a:before { 114 | top: 0; 115 | } 116 | 117 | google-codelab #drawer ol li a:after { 118 | bottom: 0; 119 | } 120 | 121 | google-codelab #drawer ol li:first-child a:before, 122 | google-codelab #drawer ol li:last-child a:after { 123 | display: none; 124 | } 125 | 126 | google-codelab #drawer ol li[selected] a, 127 | google-codelab #drawer ol li a:focus { 128 | color: #212121; 129 | font-weight: 600; 130 | } 131 | 132 | google-codelab #drawer ol li[selected] a { 133 | background-color: #e0e0e0; 134 | } 135 | 136 | google-codelab #drawer ol li[selected] .step:before { 137 | font-weight: 600; 138 | } 139 | 140 | google-codelab #drawer ol li[completed] a { 141 | color: #212121; 142 | } 143 | 144 | google-codelab #drawer ol li[completed] a:before, 145 | google-codelab #drawer ol li[completed] a:after, 146 | google-codelab #drawer ol li[completed] .step:before { 147 | background-color: #1A73E8; 148 | color: #fff; 149 | } 150 | 151 | google-codelab #drawer ol li[selected] a:after { 152 | background-color: #80868B; 153 | } 154 | 155 | 156 | google-codelab #drawer .metadata { 157 | color: #777; 158 | font-size: 0.7em; 159 | padding: 16px; 160 | flex-shrink: 0; 161 | } 162 | 163 | google-codelab #drawer .metadata a { 164 | color: currentcolor; 165 | margin-left: 4px; 166 | } 167 | 168 | google-codelab #codelab-nav-buttons #menu { 169 | display: none; 170 | } 171 | 172 | google-codelab #drawer { 173 | ::-webkit-scrollbar { 174 | -webkit-appearance: none; 175 | width: 7px; 176 | } 177 | ::-webkit-scrollbar-thumb { 178 | border-radius: 4px; 179 | background-color: rgba(0,0,0,.5); 180 | -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5); 181 | } 182 | } 183 | 184 | 185 | @media (max-width: 768px) { 186 | google-codelab { 187 | display: block; 188 | position: relative; 189 | } 190 | 191 | google-codelab #main { 192 | height: 100%; 193 | } 194 | 195 | google-codelab #codelab-nav-buttons #arrow-back { 196 | display: none; 197 | } 198 | 199 | google-codelab #codelab-nav-buttons #menu { 200 | display: flex; 201 | } 202 | 203 | google-codelab #drawer { 204 | width: 256px; 205 | position: absolute; 206 | left: 0; 207 | top: 0; 208 | bottom: 0; 209 | z-index: 1000; 210 | will-change: transform; 211 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0); 212 | pointer-events: none; 213 | transform: translate3d(-100%, 0, 0); 214 | transition: transform ease-in-out 0.3s, box-shadow 0.3s; 215 | } 216 | 217 | google-codelab[drawer--open] #drawer { 218 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15); 219 | transform: translate3d(0, 0, 0); 220 | pointer-events: all; 221 | } 222 | 223 | google-codelab #main::before { 224 | content: ''; 225 | top: 0; 226 | left: 0; 227 | right: 0; 228 | bottom: 0; 229 | position: absolute; 230 | transition: opacity ease-in-out 0.38s; 231 | background-color: rgba(0, 0, 0, 0.3); 232 | z-index: 10; 233 | pointer-events: none; 234 | opacity: 0; 235 | } 236 | 237 | google-codelab[drawer--open] #main::before { 238 | opacity: 1; 239 | pointer-events: all; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /google-codelab/_steps.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | google-codelab #steps { 19 | overflow: hidden; 20 | flex-direction: column; 21 | position: relative; 22 | flex-grow: 1; 23 | } 24 | 25 | google-codelab google-codelab-step { 26 | display: none; 27 | width: 100%; 28 | transform: translate3d(0, 0, 0); 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | right: 0; 33 | bottom: 0; 34 | padding-top: 32px; 35 | overflow-y: auto; 36 | overflow-x: hidden; 37 | } 38 | 39 | google-codelab google-codelab-step[animating], 40 | google-codelab google-codelab-step[selected] { 41 | display: block; 42 | transform-origin: 0 50% 0; 43 | animation-fill-mode: both; 44 | } 45 | 46 | google-codelab google-codelab-step[animating] { 47 | pointer-events: none; 48 | position: absolute; 49 | overflow: hidden; 50 | } 51 | -------------------------------------------------------------------------------- /google-codelab/google_codelab.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | html, body { 19 | height: 100%; 20 | width: 100%; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | body { 25 | font-family: "Roboto",sans-serif; 26 | transition: opacity ease-in 0.2s; 27 | } 28 | 29 | * { 30 | box-sizing: border-box; 31 | } 32 | 33 | [hidden] { 34 | display: none !important; 35 | } 36 | 37 | google-codelab { 38 | display: flex; 39 | width: 100%; 40 | height: 100%; 41 | } 42 | 43 | google-codelab #main { 44 | display: flex; 45 | flex-direction: column; 46 | flex-grow: 1; 47 | position: relative; 48 | background: #F8F9FA; 49 | } 50 | 51 | google-codelab #codelab-title { 52 | background: #FFFFFF; 53 | box-shadow: 0px 1px 2px 0px rgba(60, 64, 67, 0.3), 0px 2px 6px 2px rgba(60, 64, 67, 0.15); 54 | color: #3C4043; 55 | display: flex; 56 | align-items: center; 57 | justify-content: space-between; 58 | height: 64px; 59 | padding: 0 16px; 60 | -webkit-font-smoothing: antialiased; 61 | z-index: 99; 62 | } 63 | 64 | google-codelab #codelab-title h1 { 65 | font-size: 20px; 66 | font-weight: 400; 67 | margin: 0 8px; 68 | font-family: 'Roboto', 'Noto', sans-serif; 69 | flex-grow: 1; 70 | flex-shrink: 1; 71 | white-space: nowrap; 72 | text-overflow: ellipsis; 73 | overflow: hidden; 74 | width: 0; 75 | } 76 | 77 | google-codelab #codelab-title #time-remaining { 78 | flex-shrink: 0; 79 | flex-grow: 0; 80 | display: flex; 81 | align-items: center; 82 | font-size: 16px; 83 | font-weight: 400; 84 | } 85 | 86 | google-codelab #codelab-title #time-remaining i { 87 | margin-right: 3px; 88 | } 89 | 90 | google-codelab #codelab-nav-buttons { 91 | display: flex; 92 | align-items: center; 93 | flex-grow: 0; 94 | flex-shrink: 0; 95 | } 96 | 97 | google-codelab #codelab-nav-buttons #arrow-back, 98 | google-codelab #codelab-nav-buttons #menu { 99 | display: flex; 100 | text-decoration: none; 101 | color: #3C4043; 102 | width: 40px; 103 | height: 40px; 104 | justify-content: center; 105 | align-items: center; 106 | } 107 | 108 | google-codelab #controls { 109 | position: absolute; 110 | bottom: 32px; 111 | left: 0; 112 | right: 0; 113 | display: flex; 114 | justify-content: center; 115 | z-index: 10; 116 | padding: 0 32px; 117 | } 118 | 119 | google-codelab #fabs { 120 | display: flex; 121 | flex-grow: 1; 122 | max-width: 1025px; 123 | } 124 | 125 | google-codelab #fabs .spacer { 126 | flex-grow: 1; 127 | } 128 | 129 | #previous-step, #next-step, #done { 130 | border-radius: 4px; 131 | font-family: 'Google Sans', Arial, sans-serif; 132 | font-size: 14px; 133 | font-weight: 600; 134 | letter-spacing: .6px; 135 | line-height: 24px; 136 | padding-bottom: 6px; 137 | padding-left: 24px; 138 | padding-right: 24px; 139 | padding-top: 6px; 140 | pointer-events: initial; 141 | text-transform: none; 142 | background: #FFFFFF; 143 | color: #1A73E8; 144 | transform: scale(1, 1); 145 | transition: transform 300ms ease-in-out; 146 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 147 | text-decoration: none; 148 | -webkit-font-smoothing: antialiased; 149 | } 150 | 151 | #next-step { 152 | color: #fff; 153 | background: #1A73E8; 154 | } 155 | 156 | #done { 157 | background: #1E8E3E; 158 | color: #fff; 159 | } 160 | 161 | google-codelab #fabs a[disappear] { 162 | transform: scale(0, 0); 163 | } 164 | 165 | #done { 166 | background: #0f9d58; 167 | } 168 | 169 | @import "drawer"; 170 | @import "steps"; 171 | -------------------------------------------------------------------------------- /google-codelab/google_codelab.soy: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | {namespace googlecodelabs.Codelab.Templates} 19 | 20 | /** 21 | * Renders the main structure 22 | */ 23 | {template .structure} 24 | {@param homeUrl: string} 25 |
Drawer
26 |
27 |
28 |
29 | arrow_back 30 | menu 31 |
32 |
33 |
34 |
35 |
36 | Back 37 |
38 | Next 39 | 40 |
41 |
42 |
43 | {/template} 44 | 45 | 46 | /** 47 | * Renders the title 48 | */ 49 | {template .title} 50 | {@param title: string} 51 |

{$title}

52 | {/template} 53 | 54 | 55 | /** 56 | * Renders the time remaining 57 | */ 58 | {template .timeRemaining} 59 | {@param time: number} 60 |
61 | access_time 62 | {if $time == 1} 63 | {$time} min remaining 64 | {else} 65 | {$time} mins remaining 66 | {/if} 67 |
68 | {/template} 69 | 70 | 71 | /** 72 | * Renders the drawer 73 | */ 74 | {template .drawer} 75 | {@param steps: list} 76 | {@param? feedback: string} 77 |
78 |
    79 | {for $step in $steps} 80 |
  1. {$step}
  2. 81 | {/for} 82 |
83 |
84 | {if $feedback} 85 | 89 | {/if} 90 | {/template} 91 | -------------------------------------------------------------------------------- /google-codelab/google_codelab_def.js: -------------------------------------------------------------------------------- 1 | goog.module('googlecodelabs.CodelabDef'); 2 | const Codelab = goog.require('googlecodelabs.Codelab'); 3 | 4 | try { 5 | window.customElements.define(Codelab.getTagName(), Codelab); 6 | } catch (e) { 7 | console.warn('googlecodelabs.Codelab', e); 8 | } -------------------------------------------------------------------------------- /google-codelab/img/25c5ac88e3641e75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/25c5ac88e3641e75.png -------------------------------------------------------------------------------- /google-codelab/img/350dceb89c6e3968.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/350dceb89c6e3968.png -------------------------------------------------------------------------------- /google-codelab/img/3f1ab21e1e5c772b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/3f1ab21e1e5c772b.png -------------------------------------------------------------------------------- /google-codelab/img/53b42d1efc0e0295.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/53b42d1efc0e0295.png -------------------------------------------------------------------------------- /google-codelab/img/5c79e3f467c21ce6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/5c79e3f467c21ce6.png -------------------------------------------------------------------------------- /google-codelab/img/7c7f4389428d02f9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/7c7f4389428d02f9.png -------------------------------------------------------------------------------- /google-codelab/img/9dec2e61f3d3b641.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/9dec2e61f3d3b641.png -------------------------------------------------------------------------------- /google-codelab/img/a21ac67adf427ddc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/a21ac67adf427ddc.png -------------------------------------------------------------------------------- /google-codelab/img/a322aaec88da31f0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/a322aaec88da31f0.png -------------------------------------------------------------------------------- /google-codelab/img/afb844ab04c5e37a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/afb844ab04c5e37a.png -------------------------------------------------------------------------------- /google-codelab/img/b79cf053ec60b7a4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/b79cf053ec60b7a4.png -------------------------------------------------------------------------------- /google-codelab/img/dd9ae517d0d8e68f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/dd9ae517d0d8e68f.png -------------------------------------------------------------------------------- /google-codelab/img/f43aa9981defd294.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/f43aa9981defd294.png -------------------------------------------------------------------------------- /google-codelab/img/fb8ec99e99f182ac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/codelab-elements/716b16f56a0be02abdf3c51b9ab4eb0ab28f15b6/google-codelab/img/fb8ec99e99f182ac.png -------------------------------------------------------------------------------- /third_party/BUILD.es6shim: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | filegroup( 4 | name = "es6all", 5 | srcs = [ 6 | "es6-sham.min.js", 7 | "es6-sham.map", 8 | "es6-shim.min.js", 9 | "es6-shim.map", 10 | ] 11 | ) 12 | -------------------------------------------------------------------------------- /third_party/BUILD.polyfill: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | filegroup( 4 | name = "custom_elements", 5 | srcs = [ 6 | "custom-elements.min.js", 7 | "custom-elements.min.js.map", 8 | ] 9 | ) 10 | 11 | filegroup( 12 | name = "native_shim", 13 | srcs = ["src/native-shim.js"], 14 | ) 15 | -------------------------------------------------------------------------------- /third_party/BUILD.prettify: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | filegroup( 4 | name = "prettify", 5 | srcs = ["src/prettify.js"], 6 | ) -------------------------------------------------------------------------------- /tools/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary") 2 | 3 | licenses(["notice"]) 4 | 5 | # The gen_test_html.template is used by js_test rule. 6 | # See defs.bzl for details. 7 | exports_files(["LICENSE", "gen_test_html.template"]) 8 | 9 | # The JS tests runner. 10 | # See defs.bzl for details. 11 | go_binary( 12 | name = "webtest", 13 | testonly = 1, 14 | srcs = ["webtest.go"], 15 | visibility = ["//:__subpackages__"], 16 | deps = [ 17 | "@com_github_tebeka_selenium//:go_default_library", 18 | "@io_bazel_rules_webtesting//go/webtest:go_default_library", 19 | ], 20 | ) 21 | 22 | # A simple static file server. Handy for running tests manually or inspecting 23 | # content from a browser. 24 | # Execute "bazel run //tools:server" from command line 25 | # and open http://localhost:8080 in a browser. 26 | go_binary( 27 | name = "server", 28 | testonly = 1, 29 | srcs = ["server.go"], 30 | data = [ 31 | "//demo:demo_files", 32 | "//demo:hello_bin", 33 | "//demo:hello_test", 34 | "//google-codelab:google_codelab_files", 35 | "//google-codelab-analytics:google_codelab_analytics_files", 36 | "//google-codelab-index:google_codelab_index_files", 37 | "//google-codelab-step:google_codelab_step_files", 38 | "//google-codelab-survey:google_codelab_survey_files", 39 | "@polyfill//:custom_elements", 40 | "@polyfill//:native_shim", 41 | "@prettify//:prettify", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tools/bazel.rc: -------------------------------------------------------------------------------- 1 | build --strategy=Closure=worker 2 | -------------------------------------------------------------------------------- /tools/ci-continuous.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2018 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -ex 17 | 18 | use_bazel.sh 0.19.1 19 | 20 | # Make this repo's bazel workspace the current work dir. 21 | # Relative to this script location. 22 | cd $(dirname $0)/.. 23 | bazel info 24 | 25 | BAZEL_FLAGS="--color=no \ 26 | --curses=no \ 27 | --verbose_failures \ 28 | --show_task_finish \ 29 | --show_timestamps" 30 | 31 | bazel build -s $BAZEL_FLAGS ... 32 | bazel test -s $BAZEL_FLAGS --test_output=all --test_arg=-debug ... 33 | -------------------------------------------------------------------------------- /tools/ci-presubmit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2018 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -ex 17 | 18 | use_bazel.sh 0.19.1 19 | 20 | # Make this repo's bazel workspace the current work dir. 21 | # Relative to this script location. 22 | cd $(dirname $0)/.. 23 | bazel info 24 | 25 | BAZEL_FLAGS="--color=no \ 26 | --curses=no \ 27 | --verbose_failures \ 28 | --show_task_finish \ 29 | --show_timestamps" 30 | 31 | # TODO(#2): Use more sensitive build/test targets when CI is working. 32 | bazel build -s $BAZEL_FLAGS ... 33 | bazel test -s $BAZEL_FLAGS --test_output=all --test_arg=-debug ... 34 | -------------------------------------------------------------------------------- /tools/defs.bzl: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Repo's bazel rules and macros.""" 16 | 17 | load("@io_bazel_rules_closure//closure:defs.bzl", 18 | _closure_js_binary_alias="closure_js_binary") 19 | load("@io_bazel_rules_closure//closure:defs.bzl", 20 | _closure_js_library_alias="closure_js_library") 21 | load("@io_bazel_rules_webtesting//web:web.bzl", "web_test_suite") 22 | 23 | def concat(ext): 24 | """Returns a genrule command to concat files with the extension ext.""" 25 | return "ls $(SRCS) | grep -E '\.{ext}$$' | xargs cat > $@".format(ext=ext) 26 | 27 | def closure_js_library(**kwargs): 28 | """Invokes actual closure_js_library with defaults suitable 29 | for non-test JS source files. 30 | """ 31 | kwargs.setdefault("convention", "GOOGLE") 32 | suppress = kwargs.pop("suppress", []) 33 | suppress.append("JSC_UNKNOWN_EXPR_TYPE") 34 | kwargs.update(dict(suppress=suppress)) 35 | _closure_js_library_alias(**kwargs) 36 | 37 | def closure_js_binary(**kwargs): 38 | """Invokes actual closure_js_binary with defaults suitable 39 | for non-test JS files compilation. 40 | """ 41 | kwargs.setdefault("compilation_level", "ADVANCED") 42 | kwargs.setdefault("dependency_mode", "STRICT") 43 | kwargs.setdefault("language", "ECMASCRIPT5_STRICT") 44 | kwargs.setdefault("defs", [ 45 | "--assume_function_wrapper", 46 | "--rewrite_polyfills=false", 47 | "--new_type_inf", 48 | "--export_local_property_definitions", 49 | "--language_out=ES5_STRICT", 50 | "--isolation_mode=IIFE", 51 | "--generate_exports", 52 | "--jscomp_warning=newCheckTypes", 53 | "--jscomp_off=newCheckTypesExtraChecks", 54 | "--hide_warnings_for=closure/goog", 55 | ]) 56 | _closure_js_binary_alias(**kwargs) 57 | 58 | def _gen_test_html_impl(ctx): 59 | """Implementation of the gen_test_html rule.""" 60 | ctx.actions.expand_template( 61 | template=ctx.file._template, 62 | output=ctx.outputs.html_file, 63 | substitutions={ 64 | "{{TEST_FILE_JS}}": ctx.attr.test_file_js 65 | }) 66 | runfiles = ctx.runfiles(files=[ctx.outputs.html_file], collect_default=True) 67 | return [DefaultInfo(runfiles=runfiles)] 68 | 69 | # A rule used by js_test to generate default test.html file 70 | # suitable for running Closure-based JS tests. 71 | # The test_file_js argument specifies the name of the JS file containing tests, 72 | # typically created with closure_js_binary. 73 | # The output is created from gen_test_html.template file. 74 | gen_test_html = rule( 75 | implementation=_gen_test_html_impl, 76 | attrs={ 77 | "test_file_js": attr.string(mandatory=True), 78 | "_template": attr.label(default=Label("//tools:gen_test_html.template"), 79 | allow_files=True, single_file=True), 80 | }, 81 | outputs={"html_file": "%{name}.html"}, 82 | ) 83 | 84 | def js_test(name, 85 | srcs, 86 | browsers, 87 | data=None, 88 | deps=None, 89 | compilation_level=None, 90 | css=None, 91 | entry_points=None, 92 | html=None, 93 | suppress=None, 94 | visibility=None, 95 | **kwargs): 96 | """A lower level macro which creates JS tests suite. 97 | 98 | It creates three targets: _lib closure_js_library, 99 | _bin closure_js_binary with the former as a dependencies, 100 | and web_test_suite with the _bin in its data dependencies. 101 | All targets have testonly attribute set to True. 102 | 103 | For more details about closure_js_library and closure_js_binary, 104 | see https://github.com/bazelbuild/rules_closure. 105 | 106 | Args: 107 | name: The name of the test target. 108 | srcs: A list of test source files with _test.js suffix. 109 | browsers: A list of browsers to run on. See rules_webtesting 110 | for list of supported browsers: https://goo.gl/VVH8tP. 111 | data: A list of data dependencies passed to closure_js_library. 112 | deps: list of code dependencies passed to closure_js_library. 113 | compilation_level: Closure compiler compilation level. 114 | css: A CSS class renaming target passed to closure_js_binary. 115 | It must point to a closure_css_binary rule. 116 | entry_points: List of unreferenced namespaces which should not 117 | be pruned by the compiler. See //demo:hello_test 118 | for a usage example. 119 | html: An HTML file which declares the generated closure_js_binary 120 | target it its 26 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tools/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The server command starts a simple static file server using current work dir 16 | // as the root directory. 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "net/http" 23 | "os" 24 | ) 25 | 26 | var addr = flag.String("addr", "localhost:8080", "Server address to bind to.") 27 | 28 | func main() { 29 | flag.Parse() 30 | dir, err := os.Getwd() 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | h := http.FileServer(http.Dir(dir)) 35 | log.Printf("serving from %q on http://%s", dir, *addr) 36 | log.Fatal(http.ListenAndServe(*addr, h)) 37 | } 38 | -------------------------------------------------------------------------------- /tools/webtest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The webtest command runs a Google Closure based JS tests. 16 | // It exits with non-zero code if any of the tests fail. 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "flag" 22 | "fmt" 23 | "log" 24 | "net" 25 | "net/http" 26 | "os" 27 | "strings" 28 | "time" 29 | 30 | "github.com/bazelbuild/rules_webtesting/go/webtest" 31 | "github.com/tebeka/selenium" 32 | ) 33 | 34 | var ( 35 | testURL = flag.String("test_url", "/demo/hello_test.html", "Initial URL to start the tests.") 36 | host = flag.String("host", "localhost", "Host to bind static server to.") 37 | debug = flag.Bool("debug", false, "Enable verbose logging.") 38 | ) 39 | 40 | func main() { 41 | log.SetPrefix("[tools/webtest] ") 42 | flag.Parse() 43 | if *debug { 44 | log.Printf("arguments: \n\t%s", strings.Join(os.Args, "\n\t")) 45 | info, err := webtest.GetBrowserInfo() 46 | if err != nil { 47 | log.Fatalf("get browser info: %v", err) 48 | } 49 | log.Printf("webtest browser: %s", info.BrowserLabel) 50 | log.Printf("webtest environment: %s", info.Environment) 51 | } 52 | if *testURL == "" { 53 | log.Fatal("--test_url argument is required") 54 | } 55 | if !strings.HasPrefix(*testURL, "/") { 56 | *testURL = "/" + *testURL 57 | } 58 | 59 | cwdir, err := os.Getwd() 60 | if err != nil { 61 | log.Fatalf("getwd: %v", err) 62 | } 63 | addr, err := serve(cwdir, *host) 64 | if err != nil { 65 | log.Fatalf("serve: %v", err) 66 | } 67 | debugf("serving on %s; root dir: %s", addr, cwdir) 68 | 69 | // TODO: Consider making capabilities configurable. 70 | drv, err := webtest.NewWebDriverSession(selenium.Capabilities{}) 71 | if err != nil { 72 | log.Fatalf("webdriver new session: %v", err) 73 | } 74 | runURL := fmt.Sprintf("http://%s%s", addr, *testURL) 75 | debugf("running tests on URL: %s", runURL) 76 | runErr := run(drv, runURL) 77 | if err := drv.Quit(); err != nil { 78 | log.Printf("webdriver quit: %v", err) 79 | } 80 | if runErr != nil { 81 | log.Fatal(runErr) 82 | } 83 | } 84 | 85 | func serve(root, host string) (addr string, err error) { 86 | l, err := net.Listen("tcp", host+":") 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | go func() { 92 | fs := http.FileServer(http.Dir(root)) 93 | h := func(w http.ResponseWriter, r *http.Request) { 94 | debugf("%s %s", r.Method, r.URL) 95 | fs.ServeHTTP(w, r) 96 | } 97 | errServe := http.Serve(l, http.HandlerFunc(h)) 98 | log.Fatalf("serve(%q): %v", root, errServe) 99 | }() 100 | 101 | return l.Addr().String(), nil 102 | } 103 | 104 | // TODO: Make run timeout and configurable based on test size (small, large, etc.) 105 | func run(drv selenium.WebDriver, testURL string) error { 106 | if err := drv.Get(testURL); err != nil { 107 | return fmt.Errorf("webdriver GET %s: %v", testURL, err) 108 | } 109 | if err := waitTests(drv); err != nil { 110 | return fmt.Errorf("waitTests: %v", err) 111 | } 112 | v, err := drv.ExecuteScript(`return window.top.G_testRunner.getTestResultsAsJson();`, nil) 113 | if err != nil { 114 | return fmt.Errorf("G_testRunner.getTestResultsAsJson: %v", err) 115 | } 116 | var results map[string][]*struct{ Message, Stacktrace string } 117 | if err := json.Unmarshal([]byte(v.(string)), &results); err != nil { 118 | return fmt.Errorf("G_testRunner.getTestResultsAsJson unmarshal: %v", err) 119 | } 120 | var ( 121 | nfail int 122 | fails strings.Builder 123 | ) 124 | for name, res := range results { 125 | switch { 126 | default: 127 | debugf("%s: OK", name) 128 | case len(res) > 0: 129 | nfail++ 130 | fmt.Fprintf(&fails, "%s: FAIL\n", name) 131 | for i, item := range res { 132 | fmt.Fprintf(&fails, "%d. %s\n%s\n", i+1, item.Message, item.Stacktrace) 133 | } 134 | } 135 | } 136 | if nfail > 0 { 137 | return fmt.Errorf("%d out of %d test(s) failed:\n%s", nfail, len(results), &fails) 138 | } 139 | return nil 140 | } 141 | 142 | func waitTests(drv selenium.WebDriver) error { 143 | f := func(drv selenium.WebDriver) (bool, error) { 144 | v, err := drv.ExecuteScript(`return window.top.G_testRunner.isFinished();`, nil) 145 | if err != nil { 146 | return false, fmt.Errorf("G_testRunner.isFinished: %v", err) 147 | } 148 | return v.(bool), nil 149 | } 150 | // TODO: make interval configurable based on test size (small, large, etc.) 151 | return drv.WaitWithTimeoutAndInterval(f, time.Minute, 100*time.Millisecond) 152 | } 153 | 154 | func debugf(format string, args ...interface{}) { 155 | if *debug { 156 | log.Printf(format, args...) 157 | } 158 | } 159 | --------------------------------------------------------------------------------