Merge pull request #186 from squidfunk/chore/flow-type-checking

Integrate static type-checking with JSDoc and Flow
This commit is contained in:
Martin Donath 2017-02-25 17:30:02 +01:00 committed by GitHub
commit 968516a9c8
38 changed files with 921 additions and 1204 deletions

View File

@ -23,6 +23,10 @@
/material /material
/site /site
# Files used and generated by flow
/lib/declarations
/tmp
# Files generated by visual tests # Files generated by visual tests
/gemini-report /gemini-report
/tests/visual/data /tests/visual/data

View File

@ -169,7 +169,7 @@
"space-unary-ops": 2, "space-unary-ops": 2,
"spaced-comment": [2, "always", { "spaced-comment": [2, "always", {
"line": { "line": {
"markers": ["/"], "markers": ["/", ":"],
"exceptions": ["-", "+"] "exceptions": ["-", "+"]
}, },
"block": { "block": {

8
.flowconfig Normal file
View File

@ -0,0 +1,8 @@
[ignore]
.*/node_modules/.*
[libs]
lib/declarations/
[options]
strip_root=true

View File

@ -45,17 +45,25 @@ git checkout -- .
FILES=$(git diff --cached --name-only --diff-filter=ACMR | \ FILES=$(git diff --cached --name-only --diff-filter=ACMR | \
grep "\.\(js\|jsx\|scss\)$") grep "\.\(js\|jsx\|scss\)$")
# Run the check and print indicator # Run check and print indicator
if [ "$FILES" ]; then if [ "$FILES" ]; then
npm run lint --silent
# If we're on master, abort commit # If linter terminated with errors, abort commit
if [ $? -gt 0 ]; then if [ $? -gt 0 ]; then
echo -e "\x1B[31m✗\x1B[0m Linter - \x1B[31m$MESSAGE\x1B[0m" echo -e "\x1B[31m✗\x1B[0m Linter - \x1B[31m$MESSAGE\x1B[0m"
exit 1 exit 1
else else
echo -e "\x1B[32m✓\x1B[0m Linter" echo -e "\x1B[32m✓\x1B[0m Linter"
fi fi
# If flow terminated with errors, abort commit
npm run flow --silent > /dev/null
if [ $? -gt 0 ]; then
echo -e "\x1B[31m✗\x1B[0m Flow - \x1B[31m$MESSAGE\x1B[0m"
exit 1
else
echo -e "\x1B[32m✓\x1B[0m Flow"
fi
fi fi
# We're good # We're good

4
.gitignore vendored
View File

@ -24,6 +24,7 @@
# NPM-related # NPM-related
/node_modules /node_modules
/npm-debug.log* /npm-debug.log*
/yarn-error.log
# Files generated by build # Files generated by build
/build /build
@ -31,6 +32,9 @@
/MANIFEST /MANIFEST
/site /site
# Files generated by flow typechecker
/tmp
# Files generated by visual tests # Files generated by visual tests
/gemini-report /gemini-report
/tests/visual/baseline/local /tests/visual/baseline/local

View File

@ -55,9 +55,11 @@ let args = yargs
.default("sourcemaps", false) /* Create sourcemaps */ .default("sourcemaps", false) /* Create sourcemaps */
.argv .argv
/* Only use the last value seen, so overrides are possible */ /* Only use the last seen value if boolean, so overrides are possible */
args = Object.keys(args).reduce((result, arg) => { args = Object.keys(args).reduce((result, arg) => {
result[arg] = [].concat(args[arg]).pop() result[arg] = Array.isArray(args[arg]) && typeof args[arg][0] === "boolean"
? [].concat(args[arg]).pop()
: args[arg]
return result return result
}, {}) }, {})
@ -147,19 +149,28 @@ gulp.task("assets:images:clean",
/* /*
* Build application logic * Build application logic
*
* When revisioning, the build must be serialized due to race conditions
* happening when two tasks try to write manifest.json simultaneously
*/ */
gulp.task("assets:javascripts:build:application", gulp.task("assets:javascripts:build:application", args.revision ? [
load("assets/javascripts/build/application")) "assets:stylesheets:build"
] : [], load("assets/javascripts/build/application"))
/* /*
* Build custom modernizr * Build custom modernizr
*
* When revisioning, the build must be serialized due to race conditions
* happening when two tasks try to write manifest.json simultaneously
*/ */
gulp.task("assets:javascripts:build:modernizr", [ gulp.task("assets:javascripts:build:modernizr", [
"assets:stylesheets:build" "assets:stylesheets:build"
], load("assets/javascripts/build/modernizr")) ].concat(args.revision ? [
"assets:javascripts:build:application"
] : []), load("assets/javascripts/build/modernizr"))
/* /*
* Build application logic and modernizr * Build application logic and Modernizr
*/ */
gulp.task("assets:javascripts:build", (args.clean ? [ gulp.task("assets:javascripts:build", (args.clean ? [
"assets:javascripts:clean" "assets:javascripts:clean"
@ -178,6 +189,12 @@ gulp.task("assets:javascripts:build", (args.clean ? [
gulp.task("assets:javascripts:clean", gulp.task("assets:javascripts:clean",
load("assets/javascripts/clean")) load("assets/javascripts/clean"))
/*
* Annotate JavaScript
*/
gulp.task("assets:javascripts:annotate",
load("assets/javascripts/annotate"))
/* /*
* Lint JavaScript * Lint JavaScript
*/ */

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare module "fastclick" {
/* Type: FastClick */
declare type FastClick = {
attach(name: HTMLElement): void
}
/* Exports */
declare export default FastClick
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare module "js-cookie" {
/* Type: Options for setting cookie values */
declare type Options = {
path?: string,
expires?: number | string
}
/* Type: Cookie */
declare type Cookie = {
getJSON(json: string): Object,
set(key: string, value: string, options?: Options): string
}
/* Exports */
declare export default Cookie
}

34
lib/declarations/jsx.js Normal file
View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare class Jsx {
static createElement(tag: string, properties?: Object,
...children?: Array<string | number | Array<HTMLElement>>
): HTMLElement
}
/* Exports */
declare export default Jsx

34
lib/declarations/lunr.js Normal file
View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
/*
* Currently, it's not possible to export a function that returns a class type,
* as the imports just don't correctly work with flow. As a workaround we
* export an object until this error is fixed.
*/
declare module "lunr" {
declare function exports(name: () => void): Object
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare class Modernizr {
static addTest(name: string, test: () => boolean): void
}
/* Exports */
declare export default Modernizr

View File

@ -24,13 +24,13 @@
* Module * Module
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
export default /* JSX */ { export default /* Jsx */ {
/** /**
* Create a native DOM node from JSX's intermediate representation * Create a native DOM node from JSX's intermediate representation
* *
* @param {string} tag - Tag name * @param {string} tag - Tag name
* @param {object} properties - Properties * @param {?Object} properties - Properties
* @param {...(string|number|Array)} children - Child nodes * @param {...(string|number|Array)} children - Child nodes
* @return {HTMLElement} Native DOM node * @return {HTMLElement} Native DOM node
*/ */

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { transform } from "babel-core"
import jsdoc2flow from "flow-jsdoc"
import through from "through2"
/* ----------------------------------------------------------------------------
* Task: annotate JavaScript
* ------------------------------------------------------------------------- */
export default (gulp, config) => {
return () => {
return gulp.src(`${config.assets.src}/javascripts/**/*.{js,jsx}`)
/* Linting */
.pipe(
through.obj(function(file, enc, done) {
if (file.isNull() || file.isStream())
return done()
/* Perform Babel transformation to resolve JSX calls */
const transformed = transform(file.contents.toString(), {
plugins: [
["transform-react-jsx", {
"pragma": "Jsx.createElement"
}]
]
})
/* Annotate contents */
file.contents = new Buffer(jsdoc2flow(
`/* @flow */\n\n${transformed.code}`
).toString())
/* Push file to next stage */
this.push(file)
done()
}))
/* Print errors */
.pipe(gulp.dest("tmp/assets/javascripts"))
}
}

View File

@ -70,7 +70,7 @@ export default (gulp, config, args) => {
/* Provide JSX helper */ /* Provide JSX helper */
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
JSX: path.join(process.cwd(), `${config.lib}/providers/jsx.js`) Jsx: path.join(process.cwd(), `${config.lib}/providers/jsx.js`)
}) })
].concat( ].concat(

View File

@ -39,7 +39,7 @@ const format = eslint.getFormatter()
export default (gulp, config) => { export default (gulp, config) => {
return () => { return () => {
return gulp.src(`${config.assets.src}/javascripts/**/*.js`) return gulp.src(`${config.assets.src}/javascripts/**/*.{js,jsx}`)
/* Linting */ /* Linting */
.pipe( .pipe(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -141,7 +141,7 @@
{% endblock %} {% endblock %}
</div> </div>
{% block scripts %} {% block scripts %}
<script src="{{ base_url }}/assets/javascripts/application-0dae3d4464.js"></script> <script src="{{ base_url }}/assets/javascripts/application-8dc3dfc020.js"></script>
<script>app.initialize({url:{base:"{{ base_url }}"}})</script> <script>app.initialize({url:{base:"{{ base_url }}"}})</script>
{% for path in extra_javascript %} {% for path in extra_javascript %}
<script src="{{ path }}"></script> <script src="{{ path }}"></script>

View File

@ -3,7 +3,7 @@
<div class="md-search__overlay"></div> <div class="md-search__overlay"></div>
<div class="md-search__inner"> <div class="md-search__inner">
<form class="md-search__form" name="search"> <form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" placeholder="{{ lang.t('search.placeholder') }}" accesskey="s" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false"> <input type="text" class="md-search__input" name="query" placeholder="{{ lang.t('search.placeholder') }}" accesskey="s" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="query">
<label class="md-icon md-search__icon" for="search"></label> <label class="md-icon md-search__icon" for="search"></label>
</form> </form>
<div class="md-search__output"> <div class="md-search__output">

View File

@ -25,6 +25,7 @@
"scripts": { "scripts": {
"build": "scripts/build", "build": "scripts/build",
"clean": "scripts/clean", "clean": "scripts/clean",
"flow": "scripts/flow",
"lint": "scripts/lint", "lint": "scripts/lint",
"start": "scripts/start", "start": "scripts/start",
"test:visual:run": "scripts/test/visual/run", "test:visual:run": "scripts/test/visual/run",
@ -34,7 +35,7 @@
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.7.3", "autoprefixer": "^6.7.3",
"babel-core": "^6.0.0", "babel-core": "^6.23.0",
"babel-eslint": "^7.1.1", "babel-eslint": "^7.1.1",
"babel-loader": "^6.3.1", "babel-loader": "^6.3.1",
"babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-add-module-exports": "^0.2.1",
@ -48,8 +49,10 @@
"custom-event-polyfill": "^0.3.0", "custom-event-polyfill": "^0.3.0",
"del": "^2.2.2", "del": "^2.2.2",
"ecstatic": "^2.1.0", "ecstatic": "^2.1.0",
"eslint": "^3.14.0", "eslint": "^3.16.0",
"fastclick": "^1.0.6", "fastclick": "^1.0.6",
"flow-bin": "^0.39.0",
"flow-jsdoc": "^0.2.2",
"git-hooks": "^1.1.7", "git-hooks": "^1.1.7",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-changed": "^2.0.0", "gulp-changed": "^2.0.0",
@ -93,14 +96,6 @@
"chai": "^3.5.0", "chai": "^3.5.0",
"eslint-plugin-mocha": "^4.8.0", "eslint-plugin-mocha": "^4.8.0",
"gemini": "^4.14.3", "gemini": "^4.14.3",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-notify-reporter": "^1.0.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26",
"karma-webpack": "^2.0.1",
"mocha": "^3.2.0", "mocha": "^3.2.0",
"moniker": "^0.1.2", "moniker": "^0.1.2",
"saucelabs": "^1.4.0", "saucelabs": "^1.4.0",

44
scripts/flow Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
# Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "npm install" was executed
if [[ ! -d `npm bin` ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
exit 1
fi
# Annotate source files
`npm bin`/gulp assets:javascripts:annotate "$@"
FLOW_JSDOC=$?
# Run flow typecheck
`npm bin`/flow check tmp
FLOW=$?
# If one command failed, exit with error
if [ $FLOW_JSDOC -gt 0 ] || [ $FLOW -gt 0 ]; then
exit 1
fi;
# Otherwise return with success
exit 0

View File

@ -4,7 +4,7 @@
], ],
"plugins": [ "plugins": [
["transform-react-jsx", { ["transform-react-jsx", {
"pragma": "JSX.createElement" "pragma": "Jsx.createElement"
}] }]
] ]
} }

View File

@ -27,24 +27,26 @@ import Material from "./components/Material"
* Application * Application
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
export const initialize = config => { /**
* Initialize Material for MkDocs
*
* @param {Object} config - Configuration
*/
function initialize(config) { // eslint-disable-line func-style
/* Initialize Modernizr and FastClick */ /* Initialize Modernizr and FastClick */
new Material.Event.Listener(document, "DOMContentLoaded", () => { new Material.Event.Listener(document, "DOMContentLoaded", () => {
if (!(document.body instanceof HTMLElement))
throw new ReferenceError
/* Attach FastClick to mitigate 300ms delay on touch devices */
FastClick.attach(document.body)
/* Test for iOS */ /* Test for iOS */
Modernizr.addTest("ios", () => { Modernizr.addTest("ios", () => {
return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g) return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
}) })
/* Test for web application context */
Modernizr.addTest("standalone", () => {
return !!navigator.standalone
})
/* Attach FastClick to mitigate 300ms delay on touch devices */
FastClick.attach(document.body)
/* Wrap all data tables for better overflow scrolling */ /* Wrap all data tables for better overflow scrolling */
const tables = document.querySelectorAll("table:not([class])") const tables = document.querySelectorAll("table:not([class])")
Array.prototype.forEach.call(tables, table => { Array.prototype.forEach.call(tables, table => {
@ -119,7 +121,7 @@ export const initialize = config => {
new Material.Search.Lock("[data-md-toggle=search]"))) new Material.Search.Lock("[data-md-toggle=search]")))
/* Component: search results */ /* Component: search results */
new Material.Event.Listener(document.forms.search.query, [ new Material.Event.Listener("[data-md-component=query]", [
"focus", "keyup" "focus", "keyup"
], new Material.Search.Result("[data-md-component=result]", () => { ], new Material.Search.Result("[data-md-component=result]", () => {
return fetch(`${config.url.base}/mkdocs/search_index.json`, { return fetch(`${config.url.base}/mkdocs/search_index.json`, {
@ -143,6 +145,8 @@ export const initialize = config => {
new Material.Event.Listener("[data-md-component=navigation] [href^='#']", new Material.Event.Listener("[data-md-component=navigation] [href^='#']",
"click", () => { "click", () => {
const toggle = document.querySelector("[data-md-toggle=drawer]") const toggle = document.querySelector("[data-md-toggle=drawer]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) { if (toggle.checked) {
toggle.checked = false toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change")) toggle.dispatchEvent(new CustomEvent("change"))
@ -152,16 +156,23 @@ export const initialize = config => {
/* Listener: focus input after opening search */ /* Listener: focus input after opening search */
new Material.Event.Listener("[data-md-toggle=search]", "change", ev => { new Material.Event.Listener("[data-md-toggle=search]", "change", ev => {
setTimeout(toggle => { setTimeout(toggle => {
const query = document.forms.search.query if (!(toggle instanceof HTMLInputElement))
if (toggle.checked) throw new ReferenceError
if (toggle.checked) {
const query = document.querySelector("[data-md-component=query]")
if (!(query instanceof HTMLInputElement))
throw new ReferenceError
query.focus() query.focus()
}
}, 400, ev.target) }, 400, ev.target)
}).listen() }).listen()
/* Listener: open search on focus */ /* Listener: open search on focus */
new Material.Event.MatchMedia("(min-width: 960px)", new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(document.forms.search.query, "focus", () => { new Material.Event.Listener("[data-md-component=query]", "focus", () => {
const toggle = document.querySelector("[data-md-toggle=search]") const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (!toggle.checked) { if (!toggle.checked) {
toggle.checked = true toggle.checked = true
toggle.dispatchEvent(new CustomEvent("change")) toggle.dispatchEvent(new CustomEvent("change"))
@ -172,6 +183,8 @@ export const initialize = config => {
new Material.Event.MatchMedia("(min-width: 960px)", new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(document.body, "click", () => { new Material.Event.Listener(document.body, "click", () => {
const toggle = document.querySelector("[data-md-toggle=search]") const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) { if (toggle.checked) {
toggle.checked = false toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change")) toggle.dispatchEvent(new CustomEvent("change"))
@ -183,10 +196,15 @@ export const initialize = config => {
const code = ev.keyCode || ev.which const code = ev.keyCode || ev.which
if (code === 27) { if (code === 27) {
const toggle = document.querySelector("[data-md-toggle=search]") const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) { if (toggle.checked) {
toggle.checked = false toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change")) toggle.dispatchEvent(new CustomEvent("change"))
document.forms.search.query.blur() const query = document.querySelector("[data-md-component=query]")
if (!(query instanceof HTMLInputElement))
throw new ReferenceError
query.focus()
} }
} }
}).listen() }).listen()
@ -204,13 +222,16 @@ export const initialize = config => {
/* Retrieve facts for the given repository type */ /* Retrieve facts for the given repository type */
;(() => { ;(() => {
const el = document.querySelector("[data-md-source]") const el = document.querySelector("[data-md-source]")
if (!el) return Promise.resolve([]) if (!el)
return Promise.resolve([])
else if (!(el instanceof HTMLAnchorElement))
throw new ReferenceError
switch (el.dataset.mdSource) { switch (el.dataset.mdSource) {
case "github": return new Material.Source.Adapter.GitHub(el).fetch() case "github": return new Material.Source.Adapter.GitHub(el).fetch()
default: return Promise.resolve([]) default: return Promise.resolve([])
} }
/* Render repository source information */ /* Render repository information */
})().then(facts => { })().then(facts => {
const sources = document.querySelectorAll("[data-md-source]") const sources = document.querySelectorAll("[data-md-source]")
Array.prototype.forEach.call(sources, source => { Array.prototype.forEach.call(sources, source => {
@ -219,3 +240,11 @@ export const initialize = config => {
}) })
}) })
} }
/* ----------------------------------------------------------------------------
* Exports
* ------------------------------------------------------------------------- */
export {
initialize
}

View File

@ -30,14 +30,22 @@ export default class Listener {
* Generic event listener * Generic event listener
* *
* @constructor * @constructor
* @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements *
* @param {Array.<string>} events - Event names * @property {(Array<EventTarget>)} els_ - Event targets
* @param {(object|function)} handler - Handler to be invoked * @property {Object} handler_- Event handlers
* @property {Array<string>} events_ - Event names
* @property {Function} update_ - Update handler
*
* @param {?(string|EventTarget|NodeList<EventTarget>)} els -
* Selector or Event targets
* @param {(string|Array<string>)} events - Event names
* @param {(Object|Function)} handler - Handler to be invoked
*/ */
constructor(els, events, handler) { constructor(els, events, handler) {
this.els_ = (typeof els === "string") this.els_ = Array.prototype.slice.call(
(typeof els === "string")
? document.querySelectorAll(els) ? document.querySelectorAll(els)
: [].concat(els) : [].concat(els))
/* Set handler as function or directly as object */ /* Set handler as function or directly as object */
this.handler_ = typeof handler === "function" this.handler_ = typeof handler === "function"
@ -53,7 +61,7 @@ export default class Listener {
* Register listener for all relevant events * Register listener for all relevant events
*/ */
listen() { listen() {
Array.prototype.forEach.call(this.els_, el => { this.els_.forEach(el => {
this.events_.forEach(event => { this.events_.forEach(event => {
el.addEventListener(event, this.update_, false) el.addEventListener(event, this.update_, false)
}) })
@ -68,7 +76,7 @@ export default class Listener {
* Unregister listener for all relevant events * Unregister listener for all relevant events
*/ */
unlisten() { unlisten() {
Array.prototype.forEach.call(this.els_, el => { this.els_.forEach(el => {
this.events_.forEach(event => { this.events_.forEach(event => {
el.removeEventListener(event, this.update_) el.removeEventListener(event, this.update_)
}) })

View File

@ -20,6 +20,8 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import Listener from "./Listener" // eslint-disable-line no-unused-vars
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Class * Class
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -33,6 +35,9 @@ export default class MatchMedia {
* switches the given listeners on or off. * switches the given listeners on or off.
* *
* @constructor * @constructor
*
* @property {Function} handler_ - Media query event handler
*
* @param {string} query - Media query to test for * @param {string} query - Media query to test for
* @param {Listener} listener - Event listener * @param {Listener} listener - Event listener
*/ */

View File

@ -27,9 +27,16 @@
export default class Blur { export default class Blur {
/** /**
* Blur anchors within the navigation above current page y-offset * Blur links within the table of contents above current page y-offset
* *
* @constructor * @constructor
*
* @property {NodeList<HTMLElement>} els_ - Table of contents links
* @property {Array<HTMLElement>} anchors_ - Referenced anchor nodes
* @property {number} index_ - Current link index
* @property {number} offset_ - Current page y-offset
* @property {boolean} dir_ - Scroll direction change
*
* @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements * @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements
*/ */
constructor(els) { constructor(els) {
@ -45,20 +52,21 @@ export default class Blur {
this.dir_ = false this.dir_ = false
/* Index anchor node offsets for fast lookup */ /* Index anchor node offsets for fast lookup */
this.anchors_ = [].map.call(this.els_, el => { this.anchors_ = [].reduce.call(this.els_, (anchors, el) => {
return document.getElementById(el.hash.substring(1)) return anchors.concat(
}) document.getElementById(el.hash.substring(1)) || [])
}, [])
} }
/** /**
* Initialize anchor states * Initialize blur states
*/ */
setup() { setup() {
this.update() this.update()
} }
/** /**
* Update anchor states * Update blur states
* *
* Deduct the static offset of the header (56px) and sidebar offset (24px), * Deduct the static offset of the header (56px) and sidebar offset (24px),
* see _permalinks.scss for more information. * see _permalinks.scss for more information.
@ -67,7 +75,7 @@ export default class Blur {
const offset = window.pageYOffset const offset = window.pageYOffset
const dir = this.offset_ - offset < 0 const dir = this.offset_ - offset < 0
/* Hack: reset index if direction changed, to catch very fast scrolling, /* Hack: reset index if direction changed to catch very fast scrolling,
because otherwise we would have to register a timer and that sucks */ because otherwise we would have to register a timer and that sucks */
if (this.dir_ !== dir) if (this.dir_ !== dir)
this.index_ = dir this.index_ = dir
@ -109,7 +117,7 @@ export default class Blur {
} }
/** /**
* Reset anchor states * Reset blur states
*/ */
reset() { reset() {
Array.prototype.forEach.call(this.els_, el => { Array.prototype.forEach.call(this.els_, el => {

View File

@ -30,12 +30,18 @@ export default class Collapse {
* Expand or collapse navigation on toggle * Expand or collapse navigation on toggle
* *
* @constructor * @constructor
*
* @property {HTMLElement} el_ - Navigation list
*
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLElement)} el - Selector or HTML element
*/ */
constructor(el) { constructor(el) {
this.el_ = (typeof el === "string") const ref = (typeof el === "string")
? document.querySelector(el) ? document.querySelector(el)
: el : el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
} }
/** /**
@ -75,11 +81,16 @@ export default class Collapse {
/* Remove state on end of transition */ /* Remove state on end of transition */
const end = ev => { const end = ev => {
ev.target.removeAttribute("data-md-state") const target = ev.target
ev.target.style.maxHeight = "" if (!(target instanceof HTMLElement))
throw new ReferenceError
/* Reset height and state */
target.removeAttribute("data-md-state")
target.style.maxHeight = ""
/* Only fire once, so directly remove event listener */ /* Only fire once, so directly remove event listener */
ev.target.removeEventListener("transitionend", end) target.removeEventListener("transitionend", end)
} }
this.el_.addEventListener("transitionend", end, false) this.el_.addEventListener("transitionend", end, false)
} }

View File

@ -30,12 +30,18 @@ export default class Scrolling {
* Set overflow scrolling on the current active pane (for iOS) * Set overflow scrolling on the current active pane (for iOS)
* *
* @constructor * @constructor
*
* @property {HTMLElement} el_ - Primary navigation
*
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLElement)} el - Selector or HTML element
*/ */
constructor(el) { constructor(el) {
this.el_ = (typeof el === "string") const ref = (typeof el === "string")
? document.querySelector(el) ? document.querySelector(el)
: el : el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
} }
/** /**
@ -49,13 +55,22 @@ export default class Scrolling {
/* Find all toggles and check which one is active */ /* Find all toggles and check which one is active */
const toggles = this.el_.querySelectorAll("[data-md-toggle]") const toggles = this.el_.querySelectorAll("[data-md-toggle]")
Array.prototype.forEach.call(toggles, toggle => { Array.prototype.forEach.call(toggles, toggle => {
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) { if (toggle.checked) {
/* Find corresponding navigational pane */ /* Find corresponding navigational pane */
let pane = toggle.nextElementSibling let pane = toggle.nextElementSibling
while (pane.tagName !== "NAV") if (!(pane instanceof HTMLElement))
throw new ReferenceError
while (pane.tagName !== "NAV" && pane.nextElementSibling)
pane = pane.nextElementSibling pane = pane.nextElementSibling
/* Check references */
if (!(toggle.parentNode instanceof HTMLElement) ||
!(toggle.parentNode.parentNode instanceof HTMLElement))
throw new ReferenceError
/* Find current and parent list elements */ /* Find current and parent list elements */
const parent = toggle.parentNode.parentNode const parent = toggle.parentNode.parentNode
const target = pane.children[pane.children.length - 1] const target = pane.children[pane.children.length - 1]
@ -73,34 +88,48 @@ export default class Scrolling {
* @param {Event} ev - Change event * @param {Event} ev - Change event
*/ */
update(ev) { update(ev) {
const target = ev.target
if (!(target instanceof HTMLElement))
throw new ReferenceError
/* Find corresponding navigational pane */ /* Find corresponding navigational pane */
let pane = ev.target.nextElementSibling let pane = target.nextElementSibling
while (pane.tagName !== "NAV") if (!(pane instanceof HTMLElement))
throw new ReferenceError
while (pane.tagName !== "NAV" && pane.nextElementSibling)
pane = pane.nextElementSibling pane = pane.nextElementSibling
/* Find current and parent list elements */ /* Check references */
const parent = ev.target.parentNode.parentNode if (!(target.parentNode instanceof HTMLElement) ||
const target = pane.children[pane.children.length - 1] !(target.parentNode.parentNode instanceof HTMLElement))
throw new ReferenceError
/* Find parent and active panes */
const parent = target.parentNode.parentNode
const active = pane.children[pane.children.length - 1]
/* Always reset all lists when transitioning */ /* Always reset all lists when transitioning */
parent.style.webkitOverflowScrolling = "" parent.style.webkitOverflowScrolling = ""
target.style.webkitOverflowScrolling = "" active.style.webkitOverflowScrolling = ""
/* Set overflow scrolling on parent */ /* Set overflow scrolling on parent pane */
if (!ev.target.checked) { if (!target.checked) {
const end = () => { const end = () => {
if (pane instanceof HTMLElement) {
parent.style.webkitOverflowScrolling = "touch" parent.style.webkitOverflowScrolling = "touch"
pane.removeEventListener("transitionend", end) pane.removeEventListener("transitionend", end)
} }
}
pane.addEventListener("transitionend", end, false) pane.addEventListener("transitionend", end, false)
} }
/* Set overflow scrolling on target */ /* Set overflow scrolling on active pane */
if (ev.target.checked) { if (target.checked) {
const end = () => { const end = () => {
target.style.webkitOverflowScrolling = "touch" if (pane instanceof HTMLElement) {
pane.removeEventListener("transitionend", end, false) active.style.webkitOverflowScrolling = "touch"
pane.removeEventListener("transitionend", end)
}
} }
pane.addEventListener("transitionend", end, false) pane.addEventListener("transitionend", end, false)
} }
@ -117,20 +146,29 @@ export default class Scrolling {
/* Find all toggles and check which one is active */ /* Find all toggles and check which one is active */
const toggles = this.el_.querySelectorAll("[data-md-toggle]") const toggles = this.el_.querySelectorAll("[data-md-toggle]")
Array.prototype.forEach.call(toggles, toggle => { Array.prototype.forEach.call(toggles, toggle => {
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) { if (toggle.checked) {
/* Find corresponding navigational pane */ /* Find corresponding navigational pane */
let pane = toggle.nextElementSibling let pane = toggle.nextElementSibling
while (pane.tagName !== "NAV") if (!(pane instanceof HTMLElement))
throw new ReferenceError
while (pane.tagName !== "NAV" && pane.nextElementSibling)
pane = pane.nextElementSibling pane = pane.nextElementSibling
/* Find current and parent list elements */ /* Check references */
if (!(toggle.parentNode instanceof HTMLElement) ||
!(toggle.parentNode.parentNode instanceof HTMLElement))
throw new ReferenceError
/* Find parent and active panes */
const parent = toggle.parentNode.parentNode const parent = toggle.parentNode.parentNode
const target = pane.children[pane.children.length - 1] const active = pane.children[pane.children.length - 1]
/* Always reset all lists when transitioning */ /* Always reset all lists when transitioning */
parent.style.webkitOverflowScrolling = "" parent.style.webkitOverflowScrolling = ""
target.style.webkitOverflowScrolling = "" active.style.webkitOverflowScrolling = ""
} }
}) })
} }

View File

@ -30,12 +30,25 @@ export default class Lock {
* Lock body for full-screen search modal * Lock body for full-screen search modal
* *
* @constructor * @constructor
*
* @property {HTMLInputElement} el_ - Lock toggle
* @property {HTMLElement} lock_ - Element to lock (document body)
* @property {number} offset_ - Current page y-offset
*
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLElement)} el - Selector or HTML element
*/ */
constructor(el) { constructor(el) {
this.el_ = (typeof el === "string") const ref = (typeof el === "string")
? document.querySelector(el) ? document.querySelector(el)
: el : el
if (!(ref instanceof HTMLInputElement))
throw new ReferenceError
this.el_ = ref
/* Retrieve element to lock (= body) */
if (!document.body)
throw new ReferenceError
this.lock_ = document.body
} }
/** /**
@ -60,13 +73,13 @@ export default class Lock {
/* Lock body after finishing transition */ /* Lock body after finishing transition */
if (this.el_.checked) { if (this.el_.checked) {
document.body.dataset.mdState = "lock" this.lock_.dataset.mdState = "lock"
} }
}, 400) }, 400)
/* Exiting search mode */ /* Exiting search mode */
} else { } else {
document.body.dataset.mdState = "" this.lock_.dataset.mdState = ""
/* Scroll to former position, but wait for 100ms to prevent flashes on /* Scroll to former position, but wait for 100ms to prevent flashes on
iOS. A short timeout seems to do the trick */ iOS. A short timeout seems to do the trick */
@ -81,8 +94,8 @@ export default class Lock {
* Reset locked state and page y-offset * Reset locked state and page y-offset
*/ */
reset() { reset() {
if (document.body.dataset.mdState === "lock") if (this.lock_.dataset.mdState === "lock")
window.scrollTo(0, this.offset_) window.scrollTo(0, this.offset_)
document.body.dataset.mdState = "" this.lock_.dataset.mdState = ""
} }
} }

View File

@ -32,13 +32,24 @@ export default class Result {
* Perform search and update results on keyboard events * Perform search and update results on keyboard events
* *
* @constructor * @constructor
*
* @property {HTMLElement} el_ - Search result container
* @property {(Array<Object>|Function)} data_ - Raw document data
* @property {Object} docs_ - Indexed documents
* @property {HTMLElement} meta_ - Search meta information
* @property {HTMLElement} list_ - Search result list
* @property {Object} index_ - Search index
*
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLElement)} el - Selector or HTML element
* @param {(Array.<object>|Function)} data - Promise or array providing data * @param {(Array<Object>|Function)} data - Function providing data or array
*/ */
constructor(el, data) { constructor(el, data) {
this.el_ = (typeof el === "string") const ref = (typeof el === "string")
? document.querySelector(el) ? document.querySelector(el)
: el : el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
/* Set data and create metadata and list elements */ /* Set data and create metadata and list elements */
this.data_ = data this.data_ = data
@ -54,12 +65,20 @@ export default class Result {
/* Inject created elements */ /* Inject created elements */
this.el_.appendChild(this.meta_) this.el_.appendChild(this.meta_)
this.el_.appendChild(this.list_) this.el_.appendChild(this.list_)
}
/* Truncate a string after the given number of characters - this is not /**
a reasonable approach, since the summaries kind of suck. It would be * Truncate a string after the given number of character
better to create something more intelligent, highlighting the search *
occurrences and making a better summary out of it */ * This is not a reasonable approach, since the summaries kind of suck. It
this.truncate_ = function(string, n) { * would be better to create something more intelligent, highlighting the
* search occurrences and making a better summary out of it
*
* @param {string} string - String to be truncated
* @param {number} n - Number of characters
* @return {string} Truncated string
*/
truncate_(string, n) {
let i = n let i = n
if (string.length > i) { if (string.length > i) {
while (string[i] !== " " && --i > 0); while (string[i] !== " " && --i > 0);
@ -67,7 +86,6 @@ export default class Result {
} }
return string return string
} }
}
/** /**
* Update search results * Update search results
@ -90,7 +108,7 @@ export default class Result {
}) })
/* Index documents */ /* Index documents */
this.data_ = data.reduce((docs, doc) => { this.docs_ = data.reduce((docs, doc) => {
this.index_.add(doc) this.index_.add(doc)
docs[doc.location] = doc docs[doc.location] = doc
return docs return docs
@ -104,15 +122,20 @@ export default class Result {
: init(this.data_) : init(this.data_)
}, 250) }, 250)
/* Execute search on new input event after clearing current list */ /* Execute search on new input event */
} else if (ev.type === "keyup") { } else if (ev.type === "keyup") {
const target = ev.target
if (!(target instanceof HTMLInputElement))
throw new ReferenceError
/* Clear current list */
while (this.list_.firstChild) while (this.list_.firstChild)
this.list_.removeChild(this.list_.firstChild) this.list_.removeChild(this.list_.firstChild)
/* Perform search on index and render documents */ /* Perform search on index and render documents */
const result = this.index_.search(ev.target.value) const result = this.index_.search(target.value)
result.forEach(item => { result.forEach(item => {
const doc = this.data_[item.ref] const doc = this.docs_[item.ref]
/* Check if it's a anchor link on the current page */ /* Check if it's a anchor link on the current page */
let [pathname] = doc.location.split("#") let [pathname] = doc.location.split("#")
@ -143,6 +166,8 @@ export default class Result {
Array.prototype.forEach.call(anchors, anchor => { Array.prototype.forEach.call(anchors, anchor => {
anchor.addEventListener("click", ev2 => { anchor.addEventListener("click", ev2 => {
const toggle = document.querySelector("[data-md-toggle=search]") const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) { if (toggle.checked) {
toggle.checked = false toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change")) toggle.dispatchEvent(new CustomEvent("change"))

View File

@ -30,15 +30,25 @@ export default class Position {
* Set sidebars to locked state and limit height to parent node * Set sidebars to locked state and limit height to parent node
* *
* @constructor * @constructor
*
* @property {HTMLElement} el_ - Sidebar
* @property {HTMLElement} parent_ - Sidebar container
* @property {number} height_ - Current sidebar height
* @property {number} offset_ - Current page y-offset
*
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLElement)} el - Selector or HTML element
*/ */
constructor(el) { constructor(el) {
this.el_ = (typeof el === "string") const ref = (typeof el === "string")
? document.querySelector(el) ? document.querySelector(el)
: el : el
if (!(ref instanceof HTMLElement) ||
!(ref.parentNode instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
/* Initialize parent container and current height */ /* Initialize parent container and current height */
this.parent_ = this.el_.parentNode this.parent_ = ref.parentNode
this.height_ = 0 this.height_ = 0
} }
@ -65,15 +75,15 @@ export default class Position {
const visible = window.innerHeight const visible = window.innerHeight
/* Calculate bounds of sidebar container */ /* Calculate bounds of sidebar container */
this.bounds_ = { const bounds = {
top: this.parent_.offsetTop, top: this.parent_.offsetTop,
bottom: this.parent_.offsetTop + this.parent_.offsetHeight bottom: this.parent_.offsetTop + this.parent_.offsetHeight
} }
/* Calculate new offset and height */ /* Calculate new offset and height */
const height = visible - this.bounds_.top const height = visible - bounds.top
- Math.max(0, this.offset_ - offset) - Math.max(0, this.offset_ - offset)
- Math.max(0, offset + visible - this.bounds_.bottom) - Math.max(0, offset + visible - bounds.bottom)
/* If height changed, update element */ /* If height changed, update element */
if (height !== this.height_) if (height !== this.height_)

View File

@ -29,16 +29,25 @@ import Cookies from "js-cookie"
export default class Abstract { export default class Abstract {
/** /**
* Retrieve source information * Retrieve repository information
* *
* @constructor * @constructor
* @param {(string|HTMLElement)} el - Selector or HTML element *
* @property {HTMLAnchorElement} el_ - Link to repository
* @property {string} base_ - API base URL
* @property {number} salt_ - Unique identifier
*
* @param {(string|HTMLAnchorElement)} el - Selector or HTML element
*/ */
constructor(el) { constructor(el) {
this.el_ = (typeof el === "string") const ref = (typeof el === "string")
? document.querySelector(el) ? document.querySelector(el)
: el : el
if (!(ref instanceof HTMLAnchorElement))
throw new ReferenceError
this.el_ = ref
/* Retrieve base URL */ /* Retrieve base URL */
this.base_ = this.el_.href this.base_ = this.el_.href
this.salt_ = this.hash_(this.base_) this.salt_ = this.hash_(this.base_)
@ -47,7 +56,7 @@ export default class Abstract {
/** /**
* Retrieve data from Cookie or fetch from respective API * Retrieve data from Cookie or fetch from respective API
* *
* @return {Promise} Promise that returns an array of facts * @return {Promise<Array<string>>} Promise that returns an array of facts
*/ */
fetch() { fetch() {
return new Promise(resolve => { return new Promise(resolve => {
@ -70,7 +79,6 @@ export default class Abstract {
* Abstract private function that fetches relevant repository information * Abstract private function that fetches relevant repository information
* *
* @abstract * @abstract
* @return {Promise} Promise that provides the facts in an array
*/ */
fetch_() { fetch_() {
throw new Error("fetch_(): Not implemented") throw new Error("fetch_(): Not implemented")
@ -79,15 +87,15 @@ export default class Abstract {
/** /**
* Format a number with suffix * Format a number with suffix
* *
* @param {Number} number - Number to format * @param {number} number - Number to format
* @return {Number} Formatted number * @return {string} Formatted number
*/ */
format_(number) { format_(number) {
if (number > 10000) if (number > 10000)
return `${(number / 1000).toFixed(0)}k` return `${(number / 1000).toFixed(0)}k`
else if (number > 1000) else if (number > 1000)
return `${(number / 1000).toFixed(1)}k` return `${(number / 1000).toFixed(1)}k`
return number return `${number}`
} }
/** /**
@ -96,7 +104,7 @@ export default class Abstract {
* Taken from http://stackoverflow.com/a/7616484/1065584 * Taken from http://stackoverflow.com/a/7616484/1065584
* *
* @param {string} str - Input string * @param {string} str - Input string
* @return {string} Hashed string * @return {number} Hashed string
*/ */
hash_(str) { hash_(str) {
let hash = 0 let hash = 0

View File

@ -29,10 +29,10 @@ import Abstract from "./Abstract"
export default class GitHub extends Abstract { export default class GitHub extends Abstract {
/** /**
* Retrieve source information from GitHub * Retrieve repository information from GitHub
* *
* @constructor * @constructor
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLAnchorElement)} el - Selector or HTML element
*/ */
constructor(el) { constructor(el) {
super(el) super(el)
@ -42,9 +42,9 @@ export default class GitHub extends Abstract {
} }
/** /**
* Fetch relevant source information from GitHub * Fetch relevant repository information from GitHub
* *
* @return {function} Promise returning an array of facts * @return {Promise<Array<string>>} Promise returning an array of facts
*/ */
fetch_() { fetch_() {
return fetch(this.base_) return fetch(this.base_)

View File

@ -30,21 +30,27 @@ export default class Repository {
* Render repository information * Render repository information
* *
* @constructor * @constructor
*
* @property {HTMLElement} el_ - Repository information
*
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLElement)} el - Selector or HTML element
*/ */
constructor(el) { constructor(el) {
this.el_ = (typeof el === "string") const ref = (typeof el === "string")
? document.querySelector(el) ? document.querySelector(el)
: el : el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
} }
/** /**
* Initialize the source repository * Initialize the repository
* *
* @param {Array.<string>} facts - Facts to be rendered * @param {Array<string>} facts - Facts to be rendered
*/ */
initialize(facts) { initialize(facts) {
if (facts.length) if (facts.length && this.el_.children.length)
this.el_.children[this.el_.children.length - 1].appendChild( this.el_.children[this.el_.children.length - 1].appendChild(
<ul class="md-source__facts"> <ul class="md-source__facts">
{facts.map(fact => <li class="md-source__fact">{fact}</li>)} {facts.map(fact => <li class="md-source__fact">{fact}</li>)}

View File

@ -64,7 +64,7 @@
line-height: 1.2; line-height: 1.2;
white-space: nowrap; white-space: nowrap;
// Hovered source information // Hovered source container
&:hover { &:hover {
opacity: 0.7; opacity: 0.7;
} }

View File

@ -30,7 +30,7 @@
<input type="text" class="md-search__input" name="query" <input type="text" class="md-search__input" name="query"
placeholder="{{ lang.t('search.placeholder') }}" placeholder="{{ lang.t('search.placeholder') }}"
accesskey="s" autocapitalize="off" autocorrect="off" accesskey="s" autocapitalize="off" autocorrect="off"
autocomplete="off" spellcheck="false" /> autocomplete="off" spellcheck="false" data-md-component="query" />
<label class="md-icon md-search__icon" for="search"></label> <label class="md-icon md-search__icon" for="search"></label>
</form> </form>
<div class="md-search__output"> <div class="md-search__output">

1356
yarn.lock

File diff suppressed because it is too large Load Diff