@@ -35,7 +34,11 @@
{/if}
-
About this codelab
+
+ {msg desc="Text explaining that this card is about this codelab"}
+ About this codelab
+ {/msg}
+
{if $lastUpdated}
@@ -56,9 +59,5 @@
{/msg}
{/if}
-
- {if $badgePath}
-
- {/if}
{/template}
diff --git a/codelab-elements/google-codelab-analytics/google_codelab_analytics.js b/codelab-elements/google-codelab-analytics/google_codelab_analytics.js
index 629325f2f..6a5e32365 100644
--- a/codelab-elements/google-codelab-analytics/google_codelab_analytics.js
+++ b/codelab-elements/google-codelab-analytics/google_codelab_analytics.js
@@ -17,7 +17,10 @@
goog.module('googlecodelabs.CodelabAnalytics');
+const Const = goog.require('goog.string.Const');
const EventHandler = goog.require('goog.events.EventHandler');
+const TrustedResourceUrl = goog.require('goog.html.TrustedResourceUrl');
+const {safeScriptEl} = goog.require('safevalues.dom');
/**
* The general codelab action event fired for trackable interactions.
@@ -38,12 +41,39 @@ const PAGEVIEW_EVENT = 'google-codelab-pageview';
*/
const GAID_ATTR = 'gaid';
+/**
+ * The Google Analytics GA4 ID.
+ * @const {string}
+ */
+const GA4ID_ATTR = 'ga4id';
+
+/** @const {string} */
+const GTAG = 'gtag';
+
+/**
+ * Namespaced data layer for use with GA4 properties. Allows for independent
+ * data layers so that other data layers, like that for GTM, don't receive data
+ * they don't need.
+ *
+ * @const {string}
+ */
+const CODELAB_DATA_LAYER = 'codelabDataLayer';
+
+/** @const {string} */
+const CODELAB_ID_ATTR = 'codelab-id';
+
/**
* The GAID defined by the current codelab.
* @const {string}
*/
const CODELAB_GAID_ATTR = 'codelab-gaid';
+/**
+ * The GA4ID defined by the current codelab.
+ * @const {string}
+ */
+const CODELAB_GA4ID_ATTR = 'codelab-ga4id';
+
/** @const {string} */
const CODELAB_ENV_ATTR = 'environment';
@@ -100,6 +130,12 @@ class CodelabAnalytics extends HTMLElement {
/** @private {?string} */
this.gaid_;
+ /** @private {?string} */
+ this.ga4Id_;
+
+ /** @private {?string} */
+ this.codelabId_;
+
/**
* @private {!EventHandler}
* @const
@@ -119,8 +155,9 @@ class CodelabAnalytics extends HTMLElement {
*/
connectedCallback() {
this.gaid_ = this.getAttribute(GAID_ATTR) || '';
+ this.ga4Id_ = this.getAttribute(GA4ID_ATTR) || '';
- if (this.hasSetup_ || !this.gaid_) {
+ if (this.hasSetup_ || (!this.gaid_ && !this.ga4Id_)) {
return;
}
@@ -133,6 +170,14 @@ class CodelabAnalytics extends HTMLElement {
} else {
this.init_();
}
+
+ if (this.ga4Id_) {
+ this.initializeGa4_();
+ }
+
+ if (this.ga4Id_ && !this.gaid_) {
+ this.addEventListeners_();
+ }
}
/** @private */
@@ -147,7 +192,7 @@ class CodelabAnalytics extends HTMLElement {
addEventListeners_() {
this.eventHandler_.listen(document.body, ACTION_EVENT,
(e) => {
- const detail = /** @type {AnalyticsTrackingEvent} */ (
+ const detail = /** @type {!AnalyticsTrackingEvent} */ (
e.getBrowserEvent().detail);
// Add tracking...
this.trackEvent_(
@@ -156,7 +201,7 @@ class CodelabAnalytics extends HTMLElement {
this.eventHandler_.listen(document.body, PAGEVIEW_EVENT,
(e) => {
- const detail = /** @type {AnalyticsPageview} */ (
+ const detail = /** @type {!AnalyticsPageview} */ (
e.getBrowserEvent().detail);
this.trackPageview_(detail['page'], detail['title']);
});
@@ -167,7 +212,8 @@ class CodelabAnalytics extends HTMLElement {
* @export
*/
static get observedAttributes() {
- return [CODELAB_GAID_ATTR, CODELAB_ENV_ATTR, CODELAB_CATEGORY_ATTR];
+ return [CODELAB_GAID_ATTR, CODELAB_ENV_ATTR, CODELAB_CATEGORY_ATTR,
+ CODELAB_ID_ATTR];
}
/**
@@ -194,6 +240,10 @@ class CodelabAnalytics extends HTMLElement {
case CODELAB_CATEGORY_ATTR:
this.codelabCategory_ = newValue;
break;
+ case CODELAB_ID_ATTR:
+ this.codelabId_ = newValue;
+ break;
+ default:
}
}
@@ -205,16 +255,42 @@ class CodelabAnalytics extends HTMLElement {
* @private
*/
trackEvent_(category, opt_action, opt_label) {
+ // UA related section.
const params = {
// Always event for trackEvent_ method
'hitType': 'event',
'dimension1': this.codelabEnv_,
'dimension2': this.codelabCategory_ || '',
+ 'dimension4': this.codelabId_ || undefined,
'eventCategory': category,
'eventAction': opt_action || '',
'eventLabel': opt_label || '',
};
this.gaSend_(params);
+
+ // GA4 related section.
+ if (!this.getGa4Ids_().length) {
+ return;
+ }
+
+ window[CODELAB_DATA_LAYER] = window[CODELAB_DATA_LAYER] || [];
+ window[GTAG] = window[GTAG] || function() {
+ window[CODELAB_DATA_LAYER].push(arguments);
+ };
+
+ for (const ga4Id of this.getGa4Ids_()) {
+ window[GTAG]('event', category, {
+ // Snakecase naming convention is followed for all built-in GA4 event
+ // properties.
+ 'send_to': ga4Id,
+ // Camelcase naming convention is followed for all custom dimensions
+ // constructed in the custom element.
+ 'eventAction': opt_action || '',
+ 'eventLabel': opt_label || '',
+ 'codelabEnv': this.codelabEnv_ || '',
+ 'codelabId': this.codelabId_ || '',
+ });
+ }
}
/**
@@ -223,14 +299,43 @@ class CodelabAnalytics extends HTMLElement {
* @private
*/
trackPageview_(opt_page, opt_title) {
+ // UA related section.
const params = {
'hitType': 'pageview',
'dimension1': this.codelabEnv_,
'dimension2': this.codelabCategory_,
+ 'dimension4': this.codelabId_ || undefined,
'page': opt_page || '',
'title': opt_title || ''
};
this.gaSend_(params);
+
+ // GA4 related section.
+ if (!this.getGa4Ids_().length) {
+ return;
+ }
+
+ window[CODELAB_DATA_LAYER] = window[CODELAB_DATA_LAYER] || [];
+ window[GTAG] = window[GTAG] || function() {
+ window[CODELAB_DATA_LAYER].push(arguments);
+ };
+
+ for (const ga4Id of this.getGa4Ids_()) {
+ window[GTAG]('event', 'page_view', {
+ // Snakecase naming convention is followed for all built-in GA4 event
+ // properties.
+ 'send_to': ga4Id,
+ 'page_location':
+ `${document.location.origin}${document.location.pathname}`,
+ 'page_path': opt_page || '',
+ 'page_title': opt_title || '',
+ // Camelcase naming convention is followed for all custom dimensions
+ // constructed in the custom element.
+ 'codelabCategory': this.codelabCategory_ || '',
+ 'codelabEnv': this.codelabEnv_ || '',
+ 'codelabId': this.codelabId_ || '',
+ });
+ }
}
/**
@@ -372,6 +477,68 @@ class CodelabAnalytics extends HTMLElement {
}
return isCreated;
}
+
+ /**
+ * Gets all GA4 IDs for the current page.
+ * @return {!Array
}
+ * @private
+ */
+ getGa4Ids_() {
+ if (!this.ga4Id_) {
+ return [];
+ }
+ const ga4Ids = [];
+ ga4Ids.push(this.ga4Id_);
+ const codelabGa4Id = this.getAttribute(CODELAB_GA4ID_ATTR);
+ if (codelabGa4Id) {
+ ga4Ids.push(codelabGa4Id);
+ }
+ if (ga4Ids.length) {
+ return ga4Ids;
+ }
+ return [];
+ }
+
+ /**
+ * Initialize the gtag script element and namespaced data layer based on the
+ * codelabs primary GA4 ID.
+ * @private
+ */
+ initializeGa4_() {
+ if (!this.ga4Id_) {
+ return;
+ }
+
+ // First, set the GTAG data layer before pushing anything to it.
+ window[CODELAB_DATA_LAYER] = window[CODELAB_DATA_LAYER] || [];
+
+ const firstScriptElement = document.querySelector('script');
+ const gtagScriptElement = /** @type {!HTMLScriptElement} */ (
+ document.createElement('script'));
+ gtagScriptElement.async = true;
+ // Key for the formatted params below:
+ // 'id': the stream id for the GA4 analytics property. The gtag script
+ // element must only be created once, and only the ID of the primary
+ // stream is appended when creating the src for that element.
+ // Additional streams are initialized via the function call
+ // `window[GTAG]('config', ga4Id...`
+ // 'l': the namespaced dataLayer used to separate codelabs related GA4
+ // data from other data layers that may exist on a site or page.
+ safeScriptEl.setSrc(
+ gtagScriptElement, TrustedResourceUrl.formatWithParams(
+ Const.from('//www.googletagmanager.com/gtag/js'),
+ {}, {'id': this.ga4Id_, 'l': CODELAB_DATA_LAYER}));
+ firstScriptElement.parentNode.insertBefore(
+ gtagScriptElement, firstScriptElement);
+
+ window[GTAG] = function() {
+ window[CODELAB_DATA_LAYER].push(arguments);
+ };
+ window[GTAG]('js', new Date(Date.now()));
+
+ // Set send_page_view to false. We send pageviews manually.
+ window[GTAG]('config', this.ga4Id_, {send_page_view: false});
+ }
}
exports = CodelabAnalytics;
diff --git a/codelab-elements/google-codelab-index/BUILD.bazel b/codelab-elements/google-codelab-index/BUILD.bazel
index b49ec158d..92df4bd63 100644
--- a/codelab-elements/google-codelab-index/BUILD.bazel
+++ b/codelab-elements/google-codelab-index/BUILD.bazel
@@ -79,5 +79,5 @@ sass_binary(
closure_js_template_library(
name = "google_codelab_index_soy",
- srcs = ["google_codelab_index.soy"]
+ srcs = ["google_codelab_index.soy"],
)
diff --git a/codelab-elements/google-codelab-index/google_codelab_index.soy b/codelab-elements/google-codelab-index/google_codelab_index.soy
index 4179c9318..5aa6257a9 100644
--- a/codelab-elements/google-codelab-index/google_codelab_index.soy
+++ b/codelab-elements/google-codelab-index/google_codelab_index.soy
@@ -26,12 +26,24 @@
{@param duration: number}
{@param updated: string}
{@param authors: string}
+
@@ -41,26 +53,55 @@
{/template}
-
{template .sortby}
{@param sort: string}
{@param categories: list}
+
diff --git a/codelab-elements/google-codelab-step/google_codelab_step.js b/codelab-elements/google-codelab-step/google_codelab_step.js
index 336c85138..a569d3a22 100644
--- a/codelab-elements/google-codelab-step/google_codelab_step.js
+++ b/codelab-elements/google-codelab-step/google_codelab_step.js
@@ -62,9 +62,9 @@ class CodelabStep extends HTMLElement {
this.hasSetup_ = false;
/**
- * @private {string}
+ * @private {number}
*/
- this.step_ = '0';
+ this.step_ = 0;
/**
* @private {string}
@@ -96,6 +96,12 @@ class CodelabStep extends HTMLElement {
this.setupDom_();
}
+ /**
+ * @export
+ * @override
+ */
+ disconnectedCallback() {}
+
/**
* @return {!Array}
* @export
@@ -127,7 +133,7 @@ class CodelabStep extends HTMLElement {
}
if (this.hasAttribute(STEP_ATTR)) {
- this.step_ = this.getAttribute(STEP_ATTR);
+ this.step_ = parseInt(this.getAttribute(STEP_ATTR) || '', 10);
}
if (!this.title_) {
@@ -169,11 +175,15 @@ class CodelabStep extends HTMLElement {
dom.appendChild(this.instructions_, this.inner_);
dom.removeChildren(this);
- // Generate the title using a soy template.
- const title = soy.renderAsElement(Templates.title, {
- step: this.step_,
- label: this.label_,
- });
+ // Get the rendered title.
+ let title = this.inner_.querySelector('.step-title');
+ if (!title) {
+ // Generate the title using a soy template.
+ title = soy.renderAsElement(Templates.title, {
+ step: this.step_,
+ label: this.label_,
+ });
+ }
this.title_ = title;
// Inject the title in the containers.
diff --git a/codelab-elements/google-codelab-step/google_codelab_step.scss b/codelab-elements/google-codelab-step/google_codelab_step.scss
index fe23eebc4..d24c33ed7 100644
--- a/codelab-elements/google-codelab-step/google_codelab_step.scss
+++ b/codelab-elements/google-codelab-step/google_codelab_step.scss
@@ -50,6 +50,17 @@ google-codelab-step h2.step-title {
margin: 0 0 30px !important;
}
+google-codelab-step .step-title a {
+ color: #3C4043;
+ text-decoration: none;
+}
+
+google-codelab-step .step-title a:focus,
+google-codelab-step .step-title a:hover {
+ color: #212121;
+ text-decoration: underline;
+}
+
google-codelab:not([theme="minimal"]) google-codelab-step .instructions {
box-shadow: 0px 1px 2px 0px rgba(60, 64, 67, 0.3), 0px 2px 6px 2px rgba(60, 64, 67, 0.15);
background: $google-codelab-step-background;
@@ -74,8 +85,8 @@ google-codelab[theme="minimal"] google-codelab-step .instructions .inner {
}
}
-google-codelab:not([theme="minimal"]) google-codelab-step .instructions a,
-google-codelab:not([theme="minimal"]) google-codelab-step .instructions a:visited {
+google-codelab:not([theme="minimal"]) google-codelab-step .instructions :not(.step-title) > a,
+google-codelab:not([theme="minimal"]) google-codelab-step .instructions :not(.step-title) > a:visited {
color: $google-codelab-step-link-color;
}
@@ -163,8 +174,7 @@ google-codelab-step .instructions ul.checklist {
padding: 0 0 0 1em;
}
-google-codelab-step .instructions ul.checklist li,
-google-codelab-step .instructions ::content ul.checklist li {
+google-codelab-step .instructions ul.checklist li {
padding-left: 24px;
background-size: 20px;
background-repeat: no-repeat;
diff --git a/codelab-elements/google-codelab-step/google_codelab_step.soy b/codelab-elements/google-codelab-step/google_codelab_step.soy
index a9fc43170..6511d07d6 100644
--- a/codelab-elements/google-codelab-step/google_codelab_step.soy
+++ b/codelab-elements/google-codelab-step/google_codelab_step.soy
@@ -21,7 +21,14 @@
* Renders the step title
*/
{template .title}
- {@param step: string}
+ {@param step: number}
{@param label: string}
- {$step}. {$label}
+
+
{/template}
diff --git a/codelab-elements/google-codelab-step/google_codelab_step_test.js b/codelab-elements/google-codelab-step/google_codelab_step_test.js
index 2688d637f..b35f6f071 100644
--- a/codelab-elements/google-codelab-step/google_codelab_step_test.js
+++ b/codelab-elements/google-codelab-step/google_codelab_step_test.js
@@ -100,17 +100,17 @@ testSuite({
const codelabStep = new CodelabStep();
document.body.appendChild(codelabStep);
-
+
let title = codelabStep.querySelector('h2.step-title');
- assertEquals('0. ', title.textContent);
+ assertEquals('1. ', title.textContent);
codelabStep.setAttribute('step', '3');
title = codelabStep.querySelector('h2.step-title');
- assertEquals('3. ', title.textContent);
+ assertEquals('4. ', title.textContent);
codelabStep.setAttribute('label', 'test label');
title = codelabStep.querySelector('h2.step-title');
- assertEquals('3. test label', title.textContent);
+ assertEquals('4. test label', title.textContent);
document.body.removeChild(codelabStep);
}
diff --git a/codelab-elements/google-codelab/BUILD.bazel b/codelab-elements/google-codelab/BUILD.bazel
index 73dc8c116..ca4fcbde7 100644
--- a/codelab-elements/google-codelab/BUILD.bazel
+++ b/codelab-elements/google-codelab/BUILD.bazel
@@ -67,5 +67,5 @@ sass_binary(
closure_js_template_library(
name = "google_codelab_soy",
- srcs = ["google_codelab.soy"]
+ srcs = ["google_codelab.soy"],
)
diff --git a/codelab-elements/google-codelab/_drawer.scss b/codelab-elements/google-codelab/_drawer.scss
index 59f01958a..7dd7040ef 100644
--- a/codelab-elements/google-codelab/_drawer.scss
+++ b/codelab-elements/google-codelab/_drawer.scss
@@ -16,14 +16,14 @@
*/
google-codelab #drawer {
- background: #fff;
width: 256px;
- flex-shrink: 0;
+ grid-area: drawer;
position: relative;
z-index: 100;
display: flex;
flex-direction: column;
background: #F8F9FA;
+ overflow: auto;
}
google-codelab #drawer .steps {
@@ -31,18 +31,12 @@ google-codelab #drawer .steps {
flex-grow: 1;
overflow-x: visible;
display: flex;
- max-height: calc(100% - 54px);
}
google-codelab #drawer .steps:only-child {
max-height: 100%;
}
-google-codelab #drawer .metadata .material-icons {
- top: 6px;
- position: relative;
-}
-
google-codelab #drawer ol {
margin: 0;
padding: 16px 12px;
@@ -136,18 +130,6 @@ google-codelab #drawer ol li[completed] .step:before {
color: #fff;
}
-google-codelab #drawer .metadata {
- color: #777;
- font-size: 14px;
- padding: 16px;
- flex-shrink: 0;
-}
-
-google-codelab #drawer .metadata a {
- color: currentcolor;
- margin-left: 4px;
-}
-
google-codelab #codelab-nav-buttons #menu {
display: none;
}
@@ -165,15 +147,6 @@ google-codelab #drawer ol {
}
@media (max-width: 768px) {
- google-codelab {
- display: block;
- position: relative;
- }
-
- google-codelab #main {
- height: 100%;
- }
-
google-codelab #codelab-nav-buttons #arrow-back {
display: none;
}
@@ -183,6 +156,7 @@ google-codelab #drawer ol {
}
google-codelab #drawer {
+ grid-area: auto;
width: 256px;
position: absolute;
left: 0;
@@ -220,4 +194,13 @@ google-codelab #drawer ol {
opacity: 1;
pointer-events: all;
}
+
+ google-codelab #drawer .codelab-time-container {
+ display: block;
+ padding: 20px 10px 10px 23px;
+ }
+
+ google-codelab #drawer .time-remaining i {
+ margin-right: 9px;
+ }
}
diff --git a/codelab-elements/google-codelab/_steps.scss b/codelab-elements/google-codelab/_steps.scss
index 2e4e496bb..2b3a55524 100644
--- a/codelab-elements/google-codelab/_steps.scss
+++ b/codelab-elements/google-codelab/_steps.scss
@@ -48,3 +48,9 @@ google-codelab google-codelab-step[animating] {
position: absolute;
overflow: hidden;
}
+
+@media (max-width: 768px) {
+ google-codelab google-codelab-step {
+ padding-top: 8px;
+ }
+}
diff --git a/codelab-elements/google-codelab/google_codelab.js b/codelab-elements/google-codelab/google_codelab.js
index 52d39e1df..132ef48ae 100644
--- a/codelab-elements/google-codelab/google_codelab.js
+++ b/codelab-elements/google-codelab/google_codelab.js
@@ -28,8 +28,8 @@ const events = goog.require('goog.events');
const soy = goog.require('goog.soy');
/**
- * Deprecated. Title causes the bowser to display a tooltip over the whole codelab.
- * Use codelab-title instead.
+ * Deprecated. Title causes the bowser to display a tooltip over the whole
+ * codelab. Use codelab-title instead.
* @const {string}
*/
const TITLE_ATTR = 'title';
@@ -46,6 +46,12 @@ const CATEGORY_ATTR = 'category';
/** @const {string} */
const GAID_ATTR = 'codelab-gaid';
+/** @const {string} */
+const GA4ID_ATTR = 'codelab-ga4id';
+
+/** @const {string} */
+const CODELAB_ID_ATTR = 'codelab-id';
+
/** @const {string} */
const FEEDBACK_LINK_ATTR = 'feedback-link';
@@ -109,12 +115,20 @@ const CODELAB_PAGEVIEW_EVENT = 'google-codelab-pageview';
*/
const CODELAB_READY_EVENT = 'google-codelab-ready';
+/** @const {string} */
+const ARIA_HIDDEN_ATTR = 'aria-hidden';
+
+/** @const {string} */
+const TAB_INDEX_ATTR = 'tabindex';
+
/**
* @extends {HTMLElement}
*/
class Codelab extends HTMLElement {
/** @return {string} */
- static getTagName() { return 'google-codelab'; }
+ static getTagName() {
+ return 'google-codelab';
+ }
constructor() {
super();
@@ -152,11 +166,11 @@ class Codelab extends HTMLElement {
/** @private {number} */
this.setFocusTimeoutId_ = -1;
- /** @private {!Array} */
- this.steps_ = [];
+ /** @protected {!Array} */
+ this.steps = [];
- /** @private {number} */
- this.currentSelectedStep_ = -1;
+ /** @protected {number} */
+ this.currentSelectedStep = -1;
/**
* @private {!EventHandler}
@@ -170,9 +184,6 @@ class Codelab extends HTMLElement {
*/
this.transitionEventHandler_ = new EventHandler();
- /** @private {boolean} */
- this.hasSetup_ = false;
-
/** @private {boolean} */
this.ready_ = false;
@@ -182,9 +193,6 @@ class Codelab extends HTMLElement {
/** @private {?Transition} */
this.transitionOut_ = null;
- /** @private {boolean} */
- this.resumed_ = false;
-
/**
* @private {!HTML5LocalStorage}
* @const
@@ -197,22 +205,10 @@ class Codelab extends HTMLElement {
* @override
*/
connectedCallback() {
- if (!this.hasSetup_) {
- this.setupDom_();
- }
-
- this.addEvents_();
-
- this.configureAnalytics_();
- this.showSelectedStep_();
- this.updateTitle_();
- this.toggleArrows_();
- this.toggleToolbar_();
-
- if (this.resumed_) {
- console.log('resumed');
- // TODO Show resume dialog
- }
+ this.init_();
+ this.setupDom();
+ this.addEvents();
+ this.saveStep();
if (!this.ready_) {
this.ready_ = true;
@@ -235,9 +231,11 @@ class Codelab extends HTMLElement {
* @export
*/
static get observedAttributes() {
- return [TITLE_ATTR, CODELAB_TITLE_ATTR, ENVIRONMENT_ATTR, CATEGORY_ATTR,
- FEEDBACK_LINK_ATTR, SELECTED_ATTR, LAST_UPDATED_ATTR, NO_TOOLBAR_ATTR,
- NO_ARROWS_ATTR, ANALYTICS_READY_ATTR];
+ return [
+ TITLE_ATTR, CODELAB_TITLE_ATTR, ENVIRONMENT_ATTR, CATEGORY_ATTR,
+ FEEDBACK_LINK_ATTR, SELECTED_ATTR, LAST_UPDATED_ATTR, NO_TOOLBAR_ATTR,
+ NO_ARROWS_ATTR, ANALYTICS_READY_ATTR
+ ];
}
/**
@@ -263,6 +261,7 @@ class Codelab extends HTMLElement {
break;
case SELECTED_ATTR:
this.showSelectedStep_();
+ this.saveStep();
break;
case NO_TOOLBAR_ATTR:
this.toggleToolbar_();
@@ -275,8 +274,8 @@ class Codelab extends HTMLElement {
if (this.ready_) {
this.firePageLoadEvents_();
} else {
- this.addEventListener(CODELAB_READY_EVENT,
- () => this.firePageLoadEvents_());
+ this.eventHandler_.listen(
+ this, CODELAB_READY_EVENT, () => this.firePageLoadEvents_());
}
}
break;
@@ -291,14 +290,6 @@ class Codelab extends HTMLElement {
return this.eventHandler_;
}
- /**
- * @return {!Array}
- * @export
- */
- get steps() {
- return this.steps_;
- }
-
/**
* @private
*/
@@ -309,9 +300,16 @@ class Codelab extends HTMLElement {
if (gaid) {
analytics.setAttribute(GAID_ATTR, gaid);
}
+ const ga4id = this.getAttribute(GA4ID_ATTR);
+ if (ga4id) {
+ analytics.setAttribute(GA4ID_ATTR, ga4id);
+ }
+ if (this.id_) {
+ analytics.setAttribute(CODELAB_ID_ATTR, this.id_);
+ }
analytics.setAttribute(
- ENVIRONMENT_ATTR, this.getAttribute(ENVIRONMENT_ATTR));
+ ENVIRONMENT_ATTR, this.getAttribute(ENVIRONMENT_ATTR));
analytics.setAttribute(CATEGORY_ATTR, this.getAttribute(CATEGORY_ATTR));
}
}
@@ -320,14 +318,14 @@ class Codelab extends HTMLElement {
* @export
*/
selectNext() {
- this.setAttribute(SELECTED_ATTR, this.currentSelectedStep_ + 1);
+ this.setAttribute(SELECTED_ATTR, this.currentSelectedStep + 1);
}
/**
* @export
*/
selectPrevious() {
- this.setAttribute(SELECTED_ATTR, this.currentSelectedStep_ - 1);
+ this.setAttribute(SELECTED_ATTR, this.currentSelectedStep - 1);
}
/**
@@ -339,32 +337,52 @@ class Codelab extends HTMLElement {
}
/**
- * @private
+ * @export
+ * @return{string}
+ */
+ get hash() {
+ return window.location.hash;
+ }
+
+ /**
+ * @export
+ * @param {string} newHash
*/
- addEvents_() {
+ set hash(newHash) {
+ if (newHash !== '' && window.location.hash !== newHash) {
+ window.history.replaceState({newHash}, document.title, newHash);
+ }
+ }
+
+ /**
+ * @protected
+ */
+ addEvents() {
if (this.prevStepBtn_) {
- this.eventHandler_.listen(this.prevStepBtn_, events.EventType.CLICK,
- (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.selectPrevious();
- });
+ this.eventHandler_.listen(
+ this.prevStepBtn_, events.EventType.CLICK, (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.selectPrevious();
+ });
}
if (this.nextStepBtn_) {
- this.eventHandler_.listen(this.nextStepBtn_, events.EventType.CLICK,
- (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.selectNext();
- });
+ this.eventHandler_.listen(
+ this.nextStepBtn_, events.EventType.CLICK, (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.selectNext();
+ });
}
if (this.drawer_) {
- this.eventHandler_.listen(this.drawer_, events.EventType.CLICK,
+ this.eventHandler_.listen(
+ this.drawer_, events.EventType.CLICK,
(e) => this.handleDrawerClick_(e));
- this.eventHandler_.listen(this.drawer_, events.EventType.KEYDOWN,
- (e) => this.handleDrawerKeyDown_(e));
+ this.eventHandler_.listen(
+ this.drawer_, events.EventType.KEYDOWN,
+ (e) => this.handleDrawerKeyDown_(e));
}
if (this.titleContainer_) {
@@ -380,21 +398,30 @@ class Codelab extends HTMLElement {
}
});
- this.eventHandler_.listen(document.body, events.EventType.CLICK, (e) => {
- if (this.hasAttribute(DRAWER_OPEN_ATTR)) {
- this.removeAttribute(DRAWER_OPEN_ATTR);
- }
- });
+ this.eventHandler_.listen(
+ document.body, events.EventType.CLICK, (e) => {
+ if (this.hasAttribute(DRAWER_OPEN_ATTR)) {
+ this.removeAttribute(DRAWER_OPEN_ATTR);
+ }
+ });
}
}
- this.eventHandler_.listen(dom.getWindow(), events.EventType.POPSTATE, (e) => {
- this.handlePopStateChanged_(e);
- });
-
this.eventHandler_.listen(document.body, events.EventType.KEYDOWN, (e) => {
this.handleKeyDown_(e);
});
+
+ // Start Google Feedback when the feedback link is clicked, if it exists.
+ const feedbackLink = this.querySelector('#codelab-feedback');
+ if (feedbackLink) {
+ this.eventHandler_.listen(feedbackLink, events.EventType.CLICK, (e) => {
+ if ('userfeedback' in window) {
+ window['userfeedback']['api']['startFeedback'](
+ {'productId': '5143948'});
+ e.preventDefault();
+ }
+ });
+ }
}
/**
@@ -482,33 +509,6 @@ class Codelab extends HTMLElement {
}
}
- /**
- * History popState callback
- * @param {!Event} e
- * @private
- */
- handlePopStateChanged_(e) {
- if (document.location.hash) {
- this.setAttribute(DONT_SET_HISTORY_ATTR, '');
- this.setAttribute(SELECTED_ATTR, document.location.hash.substring(1));
- this.removeAttribute(DONT_SET_HISTORY_ATTR);
- }
- }
-
- /**
- * Updates the browser history state
- * @param {string} path The new browser state
- * @param {boolean=} replaceState optionally replace state instead of pushing
- * @export
- */
- updateHistoryState(path, replaceState=false) {
- if (replaceState) {
- window.history.replaceState({path}, document.title, path);
- } else {
- window.history.pushState({path}, document.title, path);
- }
- }
-
/**
* @param {!Event} e
* @private
@@ -529,10 +529,10 @@ class Codelab extends HTMLElement {
return;
}
- const selected = new URL(target.getAttribute('href'), document.location.origin)
- .hash.substring(1);
-
- this.setAttribute(SELECTED_ATTR, selected);
+ const hash =
+ new URL(target.getAttribute('href'), document.location.origin).hash;
+ const step = this.getStepFromHash_(hash);
+ this.setAttribute(SELECTED_ATTR, `${step}`);
}
/**
@@ -542,8 +542,10 @@ class Codelab extends HTMLElement {
if (!this.title_ || !this.titleContainer_) {
return;
}
- const newTitleEl =
- soy.renderAsElement(Templates.title, {title: this.title_});
+ const url = new URL(document.location.href);
+ url.hash = '';
+ const newTitleEl = soy.renderAsElement(
+ Templates.title, {title: this.title_, url: url.href});
document.title = this.title_;
const oldTitleEl = this.titleContainer_.querySelector('h1');
const buttons = this.titleContainer_.querySelector('#codelab-nav-buttons');
@@ -564,8 +566,8 @@ class Codelab extends HTMLElement {
let time = 0;
- for (let i = this.currentSelectedStep_; i < this.steps_.length; i++) {
- const step = /** @type {!Element} */ (this.steps_[i]);
+ for (let i = this.currentSelectedStep; i < this.steps.length; i++) {
+ const step = /** @type {!Element} */ (this.steps[i]);
let n = parseInt(step.getAttribute(DURATION_ATTR), 10);
if (n) {
time += n;
@@ -580,7 +582,7 @@ class Codelab extends HTMLElement {
}
// Update the time container with remaining time.
- const newTimeEl = soy.renderAsElement(Templates.timeRemaining, {time});
+ const newTimeEl = soy.renderAsElement(Templates.timeRemaining, {time});
const oldTimeEl = timeContainer.querySelector('.time-remaining');
if (oldTimeEl) {
dom.replaceNode(newTimeEl, oldTimeEl);
@@ -594,9 +596,9 @@ class Codelab extends HTMLElement {
* @private
*/
setupSteps_() {
- this.steps_.forEach((step, index) => {
+ this.steps.forEach((step, index) => {
step = /** @type {!Element} */ (step);
- step.setAttribute('step', index+1);
+ step.setAttribute('step', index);
});
}
@@ -609,33 +611,23 @@ class Codelab extends HTMLElement {
let selected = 0;
if (this.hasAttribute(SELECTED_ATTR)) {
- selected = parseInt(this.getAttribute(SELECTED_ATTR), 0);
+ selected = parseInt(this.getAttribute(SELECTED_ATTR), 10);
} else {
this.setAttribute(SELECTED_ATTR, selected);
return;
}
- selected = Math.min(Math.max(0, parseInt(selected, 10)),
- this.steps_.length - 1);
+ selected = Math.min(Math.max(0, selected), this.steps.length - 1);
- if (this.currentSelectedStep_ === selected || isNaN(selected)) {
- // Either the current step is already selected or an invalid option was provided
- // do nothing and return.
+ if (this.currentSelectedStep === selected || isNaN(selected)) {
+ // Either the current step is already selected or an invalid option was
+ // provided do nothing and return.
return;
}
- const stepTitleEl = this.steps_[selected].querySelector('.step-title');
- const stepTitle = stepTitleEl ? stepTitleEl.textContent : '';
- const stepTitlePrefix = (selected + 1) + '.';
- const re = new RegExp(stepTitlePrefix, 'g');
- this.fireEvent_(CODELAB_PAGEVIEW_EVENT, {
- 'page': location.pathname + '#' + selected,
- 'title': stepTitle.replace(re, '').trim()
- });
-
- const stepToSelect = this.steps_[selected];
+ const stepToSelect = this.steps[selected];
- if (this.currentSelectedStep_ === -1) {
+ if (this.currentSelectedStep === -1) {
// No previous selected step, so select the correct step with no animation
stepToSelect.setAttribute(SELECTED_ATTR, '');
} else {
@@ -649,19 +641,15 @@ class Codelab extends HTMLElement {
this.transitionEventHandler_.removeAll();
const transitionInInitialStyle = {};
- const transitionInFinalStyle = {
- transform: 'translate3d(0, 0, 0)'
- };
+ const transitionInFinalStyle = {transform: 'translate3d(0, 0, 0)'};
- const transitionOutInitialStyle = {
- transform: 'translate3d(0, 0, 0)'
- };
+ const transitionOutInitialStyle = {transform: 'translate3d(0, 0, 0)'};
const transitionOutFinalStyle = {};
- const currentStep = this.steps_[this.currentSelectedStep_];
+ const currentStep = this.steps[this.currentSelectedStep];
stepToSelect.setAttribute(ANIMATING_ATTR, '');
- if (this.currentSelectedStep_ < selected) {
+ if (this.currentSelectedStep < selected) {
// Move new step in from the right
transitionInInitialStyle['transform'] = 'translate3d(110%, 0, 0)';
transitionOutFinalStyle['transform'] = 'translate3d(-110%, 0, 0)';
@@ -678,29 +666,34 @@ class Codelab extends HTMLElement {
timing: 'cubic-bezier(0.4, 0, 0.2, 1)'
}];
- this.transitionIn_ = new Transition(stepToSelect, ANIMATION_DURATION,
- transitionInInitialStyle, transitionInFinalStyle, animationProperties);
- this.transitionOut_ = new Transition(currentStep, ANIMATION_DURATION,
- transitionOutInitialStyle, transitionOutFinalStyle, animationProperties);
+ this.transitionIn_ = new Transition(
+ stepToSelect, ANIMATION_DURATION, transitionInInitialStyle,
+ transitionInFinalStyle, animationProperties);
+ this.transitionOut_ = new Transition(
+ currentStep, ANIMATION_DURATION, transitionOutInitialStyle,
+ transitionOutFinalStyle, animationProperties);
this.transitionIn_.play();
this.transitionOut_.play();
- this.transitionEventHandler_.listenOnce(this.transitionIn_,
- [TransitionEventType.FINISH, TransitionEventType.STOP], () => {
- stepToSelect.setAttribute(SELECTED_ATTR, '');
- stepToSelect.removeAttribute(ANIMATING_ATTR);
- });
+ this.transitionEventHandler_.listenOnce(
+ this.transitionIn_,
+ [TransitionEventType.FINISH, TransitionEventType.STOP], () => {
+ stepToSelect.setAttribute(SELECTED_ATTR, '');
+ stepToSelect.removeAttribute(ANIMATING_ATTR);
+ });
- this.transitionEventHandler_.listenOnce(this.transitionOut_,
- [TransitionEventType.FINISH, TransitionEventType.STOP], () => {
- currentStep.removeAttribute(SELECTED_ATTR);
- });
+ this.transitionEventHandler_.listenOnce(
+ this.transitionOut_,
+ [TransitionEventType.FINISH, TransitionEventType.STOP], () => {
+ currentStep.removeAttribute(SELECTED_ATTR);
+ });
}
- this.currentSelectedStep_ = selected;
+ this.currentSelectedStep = selected;
+ this.firePageViewEvent();
- // Set the focus on the new step after the animation is finished becasue it
+ // Set the focus on the new step after the animation is finished because it
// messes up the animation.
clearTimeout(this.setFocusTimeoutId_);
this.setFocusTimeoutId_ = setTimeout(() => {
@@ -709,11 +702,15 @@ class Codelab extends HTMLElement {
if (this.nextStepBtn_ && this.prevStepBtn_ && this.doneBtn_) {
if (selected === 0) {
+ this.prevStepBtn_.setAttribute(ARIA_HIDDEN_ATTR, '');
this.prevStepBtn_.setAttribute(DISAPPEAR_ATTR, '');
+ this.prevStepBtn_.setAttribute(TAB_INDEX_ATTR, '-1');
} else {
+ this.prevStepBtn_.removeAttribute(ARIA_HIDDEN_ATTR);
this.prevStepBtn_.removeAttribute(DISAPPEAR_ATTR);
+ this.prevStepBtn_.removeAttribute(TAB_INDEX_ATTR);
}
- if (selected === this.steps_.length - 1) {
+ if (selected === this.steps.length - 1) {
this.nextStepBtn_.setAttribute(HIDDEN_ATTR, '');
this.doneBtn_.removeAttribute(HIDDEN_ATTR);
this.fireEvent_(CODELAB_ACTION_EVENT, {
@@ -744,35 +741,14 @@ class Codelab extends HTMLElement {
}
this.updateTimeRemaining_();
- if (!this.hasAttribute(DONT_SET_HISTORY_ATTR)) {
- this.updateHistoryState(`#${selected}`, true);
- }
-
- if (this.id_) {
- this.storage_.set(`progress_${this.id_}`,
- String(this.currentSelectedStep_));
- }
}
/**
* @private
*/
renderDrawer_() {
- const feedback = this.getAttribute(FEEDBACK_LINK_ATTR);
- const steps = this.steps_.map((step) => step.getAttribute(LABEL_ATTR));
- soy.renderElement(this.drawer_, Templates.drawer, {steps, feedback});
- // Start Google Feedback when the feedback link is clicked, if it exists.
- const feedbackLink = this.drawer_.querySelector('#codelab-feedback');
- if (feedbackLink) {
- this.eventHandler_.listen(feedbackLink, events.EventType.CLICK,
- (e) => {
- if ('userfeedback' in window) {
- window['userfeedback']['api']['startFeedback']
- ({'productId': '5143948'});
- e.preventDefault();
- }
- });
- }
+ const steps = this.steps.map((step) => step.getAttribute(LABEL_ATTR));
+ soy.renderElement(this.drawer_, Templates.drawer, {steps});
}
/**
@@ -780,7 +756,7 @@ class Codelab extends HTMLElement {
* @return {string}
*/
getHomeUrl_() {
- const url = new URL(document.location.toString());
+ const url = new URL(document.location.href);
let index = url.searchParams.get('index');
if (!index) {
return '/';
@@ -803,7 +779,7 @@ class Codelab extends HTMLElement {
* @param {!Object=} detail
* @protected
*/
- fireEvent_(eventName, detail={}) {
+ fireEvent_(eventName, detail = {}) {
const event = new CustomEvent(eventName, {
detail: detail,
bubbles: true,
@@ -816,28 +792,25 @@ class Codelab extends HTMLElement {
* @private
*/
firePageLoadEvents_() {
- this.fireEvent_(CODELAB_PAGEVIEW_EVENT, {
- 'page': location.pathname + '#' + this.currentSelectedStep_,
- 'title': this.steps_[this.currentSelectedStep_].getAttribute(LABEL_ATTR)
- });
+ this.firePageViewEvent();
window.requestAnimationFrame(() => {
document.body.removeAttribute('unresolved');
- this.fireEvent_(CODELAB_ACTION_EVENT, {
- 'category': 'codelab',
- 'action': 'ready'
- });
+ this.fireEvent_(
+ CODELAB_ACTION_EVENT, {'category': 'codelab', 'action': 'ready'});
});
}
/**
- * @private
+ * @protected
*/
- setupDom_() {
- this.steps_ = Array.from(this.querySelectorAll('google-codelab-step'));
+ setupDom() {
+ this.steps = Array.from(this.querySelectorAll('google-codelab-step'));
+ const feedback = this.getAttribute(FEEDBACK_LINK_ATTR);
soy.renderElement(this, Templates.structure, {
- homeUrl: this.getHomeUrl_()
+ feedback,
+ homeUrl: this.getHomeUrl_(),
});
this.drawer_ = this.querySelector('#drawer');
@@ -847,27 +820,72 @@ class Codelab extends HTMLElement {
this.prevStepBtn_ = this.querySelector('#controls #previous-step');
this.nextStepBtn_ = this.querySelector('#controls #next-step');
this.doneBtn_ = this.querySelector('#controls #done');
-
- this.steps_.forEach((step) => dom.appendChild(this.stepsContainer_, step));
+ this.steps.forEach((step) => dom.appendChild(this.stepsContainer_, step));
this.setupSteps_();
this.renderDrawer_();
this.timeContainer_ = this.querySelectorAll('.codelab-time-container');
+ this.configureAnalytics_();
+ this.showSelectedStep_();
+ this.updateTitle_();
+ this.toggleArrows_();
+ this.toggleToolbar_();
+ }
- if (document.location.hash) {
- const h = parseInt(document.location.hash.substring(1), 10);
- if (!isNaN(h) && h) {
- this.setAttribute(SELECTED_ATTR, document.location.hash.substring(1));
+ /**
+ * @private
+ * @param {string} hash
+ * @return {number}
+ */
+ getStepFromHash_(hash) {
+ if (hash) {
+ const step = parseInt(hash.substring(1), 10);
+ if (!isNaN(step) && step) {
+ return step;
}
}
+ return 0;
+ }
+ /**
+ * @private
+ * @return {number}
+ */
+ getStepFromStorage_() {
+ const step = parseInt(this.storage_.get(`progress_${this.id_}`), 10);
+ if (!isNaN(step) && step) {
+ return step;
+ }
+ return 0;
+ }
+
+ /**
+ * @private
+ */
+ init_() {
this.id_ = this.getAttribute(ID_ATTR);
- const progress = this.storage_.get(`progress_${this.id_}`);
- if (progress && progress !== '0') {
- this.resumed_ = true;
- this.setAttribute(SELECTED_ATTR, progress);
+ let step = this.getStepFromHash_(this.hash) || this.getStepFromStorage_();
+ this.setAttribute(SELECTED_ATTR, `${step}`);
+ }
+
+ /**
+ * @protected
+ */
+ saveStep() {
+ this.hash = `#${this.currentSelectedStep}`;
+ if (this.id_) {
+ this.storage_.set(
+ `progress_${this.id_}`, String(this.currentSelectedStep));
}
+ }
- this.hasSetup_ = true;
+ /**
+ * @protected
+ */
+ firePageViewEvent() {
+ this.fireEvent_(CODELAB_PAGEVIEW_EVENT, {
+ 'page': location.pathname + '#' + this.currentSelectedStep,
+ 'title': this.steps[this.currentSelectedStep].getAttribute(LABEL_ATTR)
+ });
}
}
diff --git a/codelab-elements/google-codelab/google_codelab.scss b/codelab-elements/google-codelab/google_codelab.scss
index 1e209900e..41a94ba43 100644
--- a/codelab-elements/google-codelab/google_codelab.scss
+++ b/codelab-elements/google-codelab/google_codelab.scss
@@ -16,36 +16,40 @@
*/
google-codelab {
- display: flex;
width: 100%;
height: 100%;
- padding-top: 64px;
+ display: grid;
+ grid-template: 64px 1fr 56px/ 256px 1fr;
+ grid-template-areas:
+ 'title title'
+ 'drawer main'
+ 'metadata main';
}
+// stylelint-disable selector-max-id
+
google-codelab #main {
display: flex;
flex-direction: column;
- flex-grow: 1;
+ grid-area: main;
position: relative;
background: #F8F9FA;
}
google-codelab #codelab-title {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
background: #FFFFFF;
box-shadow: 0px 1px 2px 0px rgba(60, 64, 67, .3),
0px 2px 6px 2px rgba(60, 64, 67, .15);
color: #3C4043;
display: flex;
+ grid-area: title;
align-items: center;
justify-content: space-between;
height: 64px;
padding: 0 36px 0 16px;
-webkit-font-smoothing: antialiased;
z-index: 1000;
+ max-width: 100vw;
}
google-codelab #codelab-title h1 {
@@ -62,6 +66,17 @@ google-codelab #codelab-title h1 {
display: inline-block;
}
+google-codelab #codelab-title h1 a {
+ color: #3C4043;
+ text-decoration: none;
+}
+
+google-codelab #codelab-title h1 a:focus,
+google-codelab #codelab-title h1 a:hover {
+ color: #212121;
+ text-decoration: underline;
+}
+
google-codelab #codelab-title .time-remaining {
flex-shrink: 0;
flex-grow: 0;
@@ -99,26 +114,20 @@ google-codelab #controls {
bottom: 32px;
left: 0;
right: 0;
- display: flex;
- justify-content: center;
padding: 0 32px;
- flex-direction: column;
z-index: 1001;
}
google-codelab #fabs {
display: flex;
- flex-grow: 1;
- max-width: 1025px;
- width: 100%;
+ justify-content: space-between;
margin: 0 auto;
+ max-width: 1025px;
}
-google-codelab #fabs .spacer {
- flex-grow: 1;
-}
-
-#previous-step, #next-step, #done {
+google-codelab #previous-step,
+google-codelab #next-step,
+google-codelab #done {
border-radius: 4px;
font-family: 'Google Sans', Arial, sans-serif;
font-size: 14px;
@@ -140,12 +149,12 @@ google-codelab #fabs .spacer {
-webkit-font-smoothing: antialiased;
}
-#next-step {
+google-codelab #next-step {
color: #fff;
background: #1A73E8;
}
-#done {
+google-codelab #done {
background: #1E8E3E;
color: #fff;
}
@@ -154,7 +163,7 @@ google-codelab #fabs a[disappear] {
transform: scale(0, 0);
}
-#done {
+google-codelab #done {
background: #0f9d58;
}
@@ -162,18 +171,40 @@ google-codelab #drawer .codelab-time-container {
display: none;
}
+google-codelab .metadata {
+ background: #F8F9FA;
+ color: #777;
+ font-size: 14px;
+ grid-area: metadata;
+ padding: 16px;
+}
+
+google-codelab .metadata a {
+ color: currentcolor;
+ margin-left: 4px;
+}
+
+google-codelab .metadata a:hover,
+google-codelab .metadata a:focus {
+ color: #212121;
+}
+
@media (max-width: 768px) {
- google-codelab #codelab-title .codelab-time-container {
- display: none;
+ google-codelab {
+ grid-template: 64px 1fr 56px/1fr;
+ grid-template-areas:
+ 'title'
+ 'main'
+ 'metadata';
+ padding-top: 0;
}
- google-codelab #drawer .codelab-time-container {
- display: block;
- padding: 20px 10px 10px 23px;
+ google-codelab #codelab-title {
+ padding: 0 16px;
}
- google-codelab #drawer .time-remaining i {
- margin-right: 9px;
+ google-codelab #codelab-title .codelab-time-container {
+ display: none;
}
}
diff --git a/codelab-elements/google-codelab/google_codelab.soy b/codelab-elements/google-codelab/google_codelab.soy
index 8cc44ae62..27450c834 100644
--- a/codelab-elements/google-codelab/google_codelab.soy
+++ b/codelab-elements/google-codelab/google_codelab.soy
@@ -21,72 +21,128 @@
* Renders the main structure
*/
{template .structure}
+ {@param? feedback: string}
{@param homeUrl: string}
- Drawer
+
+
+
{/template}
-
/**
* Renders the title
*/
{template .title}
{@param title: string}
- {$title}
-{/template}
+ {@param url: string}
+
+{/template}
/**
* Renders the time remaining
*/
{template .timeRemaining}
{@param time: number}
-
-
access_time
- {if $time == 1}
- {$time} min remaining
- {else}
- {$time} mins remaining
- {/if}
+
+
+ access_time
+ {if $time == 1}
+ {msg desc="Label indicating expected time remaining (1 minute) for user to complete codelab."}
+ {$time} min remaining
+ {/msg}
+ {else}
+ {msg desc="Label indicating expected time remaining in minutes for user to complete codelab."}
+ {$time} mins remaining
+ {/msg}
+ {/if}
{/template}
-
/**
* Renders the drawer
*/
{template .drawer}
{@param steps: list
}
- {@param? feedback: string}
+
-
{/template}
diff --git a/codelab-elements/tools/defs.bzl b/codelab-elements/tools/defs.bzl
index 9457bcf16..7ba59af4b 100644
--- a/codelab-elements/tools/defs.bzl
+++ b/codelab-elements/tools/defs.bzl
@@ -26,7 +26,7 @@ load("@io_bazel_rules_webtesting//web:web.bzl", "web_test_suite")
def concat(ext):
"""Returns a genrule command to concat files with the extension ext."""
- return "ls $(SRCS) | grep -E '\.{ext}$$' | xargs cat > $@".format(ext = ext)
+ return "ls $(SRCS) | grep -E '\\.{ext}$$' | xargs cat > $@".format(ext = ext)
def closure_js_library(**kwargs):
"""Invokes closure_js_library with non-test compilation defaults.
diff --git a/site/app/views/default/index.html b/site/app/views/default/index.html
index 88c3fe45a..06990439d 100644
--- a/site/app/views/default/index.html
+++ b/site/app/views/default/index.html
@@ -151,7 +151,7 @@ {{view.title}}
- {% if view.id == 'default' %}
+ {% if view.id == 'default' && views|length > 1 %}
[a]Test comment.