This repository has been archived by the owner on Jan 13, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(demos): Add theme switcher to theme demo page #1975
Merged
Merged
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
ee38ec1
WIP: Copy experimental/theme/demo branch at 7e588ce
acdvorak 6de574e
Merge branch 'master' into experimental/theme/demo-v2
acdvorak 96703fa
WIP: Squashed commit of the following:
acdvorak 29f120c
WIP: Refactor RTL toggling out into separate class
acdvorak c024802
WIP: Remove RTL toggle button from toolbar
acdvorak fa725d8
Merge remote-tracking branch 'origin/master' into experimental/theme/…
acdvorak 53da253
WIP: JSDoc
acdvorak 8777138
WIP: Remove RtlToggler; add it in future PR
acdvorak 1594af7
WIP: Remove unneeded ID
acdvorak 096e779
WIP: Undo RTL fixes from #1974
acdvorak 85cc99f
chore(demos): Add `root` param to demoReady
acdvorak eca2972
WIP: Don't subclass HotSwapper; use single shared instance
acdvorak d2ea4c7
WIP: Refactoring
acdvorak 2c5bf94
WIP: Refactoring
acdvorak 2188c65
WIP: Refactoring
acdvorak a06a54a
Merge remote-tracking branch 'origin/master' into experimental/theme/…
acdvorak f064477
WIP: Rename methods
acdvorak f5c9690
WIP: Reorder params
acdvorak 13ff649
WIP: Refactoring
acdvorak 6da0a67
WIP: Remove unnecessary class
acdvorak ed8d8aa
WIP: It ain't over until the async method completes
acdvorak 871d783
WIP: Fix botched auto-merge
acdvorak e72566e
WIP: Code style
acdvorak 8838f93
WIP: Rename optional args and use default values
acdvorak 3792390
WIP: Remove pointless `getElementById_` method
acdvorak bbe077e
WIP: Remove duplicate styles
acdvorak f2670cb
WIP: Rename `initialize()` to `lazyInit()` for clarity
acdvorak bd0d961
WIP: Add missing @param to JSDoc
acdvorak a98ceaa
WIP: Remove TODO
acdvorak 2029047
Merge branch 'master' into experimental/theme/demo-v2
acdvorak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/** | ||
* @license | ||
* Copyright 2017 Google Inc. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import * as interactivity from './interactivity.js'; | ||
|
||
demoReady((root) => { | ||
interactivity.init(root); | ||
}); | ||
|
||
// Export useful libs to aid debugging/experimentation in the browser's dev tools console. | ||
import * as dom from './dom.js'; | ||
import * as pony from './ponyfill.js'; | ||
import * as util from './util.js'; | ||
export {dom, pony, util}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
* @license | ||
* Copyright 2017 Google Inc. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
/** | ||
* @param {string} query | ||
* @param {!Document|!Element=} opt_root | ||
* @return {!Array<!Element>} | ||
*/ | ||
export function getAll(query, opt_root) { | ||
opt_root = opt_root || document; | ||
return [].slice.call(opt_root.querySelectorAll(query)); | ||
} | ||
|
||
/** | ||
* @param {!Window|!Document|!Element} root | ||
* @return {!Document|undefined} | ||
*/ | ||
export function getDocument(root) { | ||
return root.ownerDocument || root.document || (root.documentElement ? root : undefined); | ||
} | ||
|
||
/** | ||
* @param {!Window|!Document|!Element} root | ||
* @return {!Window|undefined} | ||
*/ | ||
export function getWindow(root) { | ||
const doc = getDocument(root); | ||
return doc.defaultView || doc.parentWindow || (root.document ? root : undefined); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
/** | ||
* @license | ||
* Copyright 2017 Google Inc. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import * as dom from './dom.js'; | ||
import * as util from './util.js'; | ||
|
||
const classes = { | ||
TOOLBAR_PROGRESS_BAR_ACTIVE: 'demo-toolbar-progress-bar--active', | ||
}; | ||
|
||
const attrs = { | ||
HOT_SWAP: 'data-hot', | ||
IS_LOADING: 'data-is-loading', | ||
}; | ||
|
||
const ids = { | ||
TOOLBAR_PROGRESS_BAR: 'demo-toolbar-progress-bar', | ||
}; | ||
|
||
/** @abstract */ | ||
export class InteractivityProvider { | ||
constructor(root) { | ||
/** @protected {!Document|!Element} */ | ||
this.root_ = root; | ||
|
||
/** @protected {!Document} */ | ||
this.document_ = dom.getDocument(this.root_); | ||
|
||
/** @protected {!Window} */ | ||
this.window_ = dom.getWindow(this.root_); | ||
} | ||
|
||
initialize() {} | ||
|
||
/** | ||
* @param {string} id | ||
* @param {!Document|!Element=} opt_root | ||
* @return {?Element} | ||
* @protected | ||
*/ | ||
getElementById_(id, opt_root) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this more complicated than simply calling For that matter, are these various Also, same idea here and below RE There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
const root = opt_root || this.root_; | ||
return root.querySelector(`#${id}`); | ||
} | ||
|
||
/** | ||
* @param {string} selector | ||
* @param {!Document|!Element=} opt_root | ||
* @return {!Array<!Element>} | ||
* @protected | ||
*/ | ||
querySelectorAll_(selector, opt_root) { | ||
const root = opt_root || this.root_; | ||
return dom.getAll(selector, root); | ||
} | ||
} | ||
|
||
export class ToolbarProvider extends InteractivityProvider { | ||
/** @param {!Document|!Element} root */ | ||
static attachTo(root) { | ||
const instance = new ToolbarProvider(root); | ||
instance.initialize(); | ||
return instance; | ||
} | ||
|
||
/** @override */ | ||
initialize() { | ||
/** @type {?Element} */ | ||
this.progressBarEl_ = this.getElementById_(ids.TOOLBAR_PROGRESS_BAR); | ||
} | ||
|
||
/** @param {boolean} isLoading */ | ||
setIsLoading(isLoading) { | ||
if (!this.progressBarEl_) { | ||
return; | ||
} | ||
|
||
if (isLoading) { | ||
this.progressBarEl_.classList.add(classes.TOOLBAR_PROGRESS_BAR_ACTIVE); | ||
} else { | ||
this.progressBarEl_.classList.remove(classes.TOOLBAR_PROGRESS_BAR_ACTIVE); | ||
} | ||
} | ||
} | ||
|
||
export class HotSwapper extends InteractivityProvider { | ||
/** | ||
* @param {!Document|!Element} root | ||
* @param {!ToolbarProvider} toolbarProvider | ||
*/ | ||
static attachTo(root, toolbarProvider) { | ||
const instance = new HotSwapper(root); | ||
instance.initialize(toolbarProvider); | ||
return instance; | ||
} | ||
|
||
/** @private {number} */ | ||
static get hotUpdateWaitPeriodMs_() { | ||
return 250; | ||
} | ||
|
||
/** | ||
* @param {!ToolbarProvider} toolbarProvider | ||
* @override | ||
*/ | ||
initialize(toolbarProvider) { | ||
/** @type {!ToolbarProvider} */ | ||
this.toolbarProvider_ = toolbarProvider; | ||
|
||
/** @type {!Array<string>} */ | ||
this.pendingRequests_ = []; | ||
|
||
this.registerHotUpdateHandler_(); | ||
} | ||
|
||
/** @private */ | ||
registerHotUpdateHandler_() { | ||
const hotSwapAllStylesheets = util.debounce(() => { | ||
this.hotSwapAllStylesheets_(); | ||
}, HotSwapper.hotUpdateWaitPeriodMs_); | ||
|
||
this.window_.addEventListener('message', (evt) => { | ||
if (this.isWebpackRecompileStart_(evt)) { | ||
this.toolbarProvider_.setIsLoading(true); | ||
} else if (this.isWebpackRecompileEnd_(evt)) { | ||
hotSwapAllStylesheets(); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* @param {!Event} evt | ||
* @return {boolean} | ||
* @private | ||
*/ | ||
isWebpackRecompileStart_(evt) { | ||
return Boolean(evt.data) && evt.data.type === 'webpackInvalid'; | ||
} | ||
|
||
/** | ||
* @param {!Event} evt | ||
* @return {boolean} | ||
* @private | ||
*/ | ||
isWebpackRecompileEnd_(evt) { | ||
return typeof evt.data === 'string' && evt.data.indexOf('webpackHotUpdate') === 0; | ||
} | ||
|
||
/** @private */ | ||
hotSwapAllStylesheets_() { | ||
this.querySelectorAll_(`link[${attrs.HOT_SWAP}]:not([${attrs.IS_LOADING}])`).forEach((link) => { | ||
this.hotSwapStylesheet(link); | ||
}); | ||
} | ||
|
||
/** | ||
* @param {!Element} oldLink | ||
* @param {string|undefined=} newUri | ||
*/ | ||
hotSwapStylesheet(oldLink, newUri) { | ||
const oldUri = oldLink.getAttribute('href'); | ||
|
||
// Reload existing stylesheet | ||
if (!newUri) { | ||
newUri = oldUri; | ||
} | ||
|
||
// Force IE 11 and Edge to bypass the cache and request a fresh copy of the CSS. | ||
newUri = this.bustCache_(newUri); | ||
|
||
this.swapItLikeItsHot_(oldLink, oldUri, newUri); | ||
} | ||
|
||
/** | ||
* @param {!Element} oldLink | ||
* @param {string} oldUri | ||
* @param {string} newUri | ||
* @private | ||
*/ | ||
swapItLikeItsHot_(oldLink, oldUri, newUri) { | ||
this.logHotSwap_('swapping', oldUri, newUri, '...'); | ||
|
||
// Ensure that oldLink has a unique ID so we can remove all stale stylesheets from the DOM after newLink loads. | ||
// This is a more robust approach than holding a reference to oldLink and removing it directly, because a user might | ||
// quickly switch themes several times before the first stylesheet finishes loading (especially over a slow network) | ||
// and each new stylesheet would try to remove the first one, leaving multiple conflicting stylesheets in the DOM. | ||
if (!oldLink.id) { | ||
oldLink.id = `stylesheet-${Math.floor(Math.random() * Date.now())}`; | ||
} | ||
|
||
const newLink = /** @type {!Element} */ (oldLink.cloneNode(false)); | ||
newLink.setAttribute('href', newUri); | ||
newLink.setAttribute(attrs.IS_LOADING, 'true'); | ||
|
||
// IE 11 and Edge fire the `load` event twice for `<link>` elements. | ||
newLink.addEventListener('load', util.debounce(() => { | ||
this.handleStylesheetLoad_(newLink, newUri, oldUri); | ||
}, 50)); | ||
|
||
oldLink.parentNode.insertBefore(newLink, oldLink); | ||
|
||
this.pendingRequests_.push(newUri); | ||
this.toolbarProvider_.setIsLoading(true); | ||
} | ||
|
||
/** | ||
* @param {!Element} newLink | ||
* @param {string} newUri | ||
* @param {string} oldUri | ||
* @private | ||
*/ | ||
handleStylesheetLoad_(newLink, newUri, oldUri) { | ||
this.pendingRequests_.splice(this.pendingRequests_.indexOf(newUri), 1); | ||
if (this.pendingRequests_.length === 0) { | ||
this.toolbarProvider_.setIsLoading(false); | ||
} | ||
|
||
setTimeout(() => { | ||
this.purgeOldStylesheets_(newLink); | ||
|
||
// Remove the 'loading' attribute *after* purging old stylesheets to avoid purging this one. | ||
newLink.removeAttribute(attrs.IS_LOADING); | ||
|
||
this.logHotSwap_('swapped', oldUri, newUri, '!'); | ||
}); | ||
} | ||
|
||
/** | ||
* @param {!Element} newLink | ||
* @private | ||
*/ | ||
purgeOldStylesheets_(newLink) { | ||
let oldLinks; | ||
|
||
const getOldLinks = () => this.querySelectorAll_(`link[id="${newLink.id}"]:not([${attrs.IS_LOADING}])`); | ||
|
||
while ((oldLinks = getOldLinks()).length > 0) { | ||
oldLinks.forEach((oldLink) => { | ||
// Link has already been detached from the DOM. I'm not sure what causes this to happen; I've only seen it in | ||
// IE 11 and/or Edge so far, and only occasionally. | ||
if (!oldLink.parentNode) { | ||
return; | ||
} | ||
oldLink.parentNode.removeChild(oldLink); | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Adds a timestamp to the given URI to force IE 11 and Edge to bypass the cache and request a fresh copy of the CSS. | ||
* @param oldUri | ||
* @return {string} | ||
* @private | ||
*/ | ||
bustCache_(oldUri) { | ||
const newUri = oldUri | ||
// Remove previous timestamp param (if present) | ||
.replace(/[?&]timestamp=\d+(&|$)/, '') | ||
// Remove trailing '?' or '&' char (if present) | ||
.replace(/[?&]$/, ''); | ||
const separator = newUri.indexOf('?') === -1 ? '?' : '&'; | ||
return `${newUri}${separator}timestamp=${Date.now()}`; | ||
} | ||
|
||
/** | ||
* @param {string} verb | ||
* @param {string} oldUri | ||
* @param {string} newUri | ||
* @param {string} trailingPunctuation | ||
* @private | ||
*/ | ||
logHotSwap_(verb, oldUri, newUri, trailingPunctuation) { | ||
const swapMessage = `"${oldUri}"${newUri ? ` with "${newUri}"` : ''}`; | ||
console.log(`Hot ${verb} stylesheet ${swapMessage}${trailingPunctuation}`); | ||
} | ||
|
||
/** | ||
* @param {!Document|!Element} root | ||
* @return {!HotSwapper} | ||
*/ | ||
static getInstance(root) { | ||
// Yeah, I know, this is gross. | ||
if (!root.demoHotSwapperRootMap_) { | ||
/** @type {?Map<{key:*, value:*}>} @private */ | ||
root.demoHotSwapperRootMap_ = new Map(); | ||
} | ||
let instance = root.demoHotSwapperRootMap_.get(root); | ||
if (!instance) { | ||
instance = HotSwapper.attachTo(root, ToolbarProvider.attachTo(root)); | ||
root.demoHotSwapperRootMap_.set(root, instance); | ||
} | ||
return instance; | ||
} | ||
} | ||
|
||
/** @param {!Document|!Element} root */ | ||
export function init(root) { | ||
HotSwapper.getInstance(root); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drop
opt_
prefix and inline as default parameter?getAll(query, root = document)
The
opt_
prefix is discouraged now in favor of default parameters. (See reference)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done