diff --git a/.eslintignore b/.eslintignore index 3728be369..28cbc6649 100644 --- a/.eslintignore +++ b/.eslintignore @@ -23,6 +23,9 @@ /material /site +# Files generated by flow typechecker +/tmp + # Files generated by visual tests /gemini-report /tests/visual/data diff --git a/.flowconfig b/.flowconfig index 5cca72a58..11e8eea21 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,2 +1,5 @@ [ignore] -/node_modules +.*/node_modules/.* + +[options] +strip_root=true diff --git a/.gitignore b/.gitignore index 2fd46324f..5ec033ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ /MANIFEST /site +# Files generated by flow typechecker +/tmp + # Files generated by visual tests /gemini-report /tests/visual/baseline/local diff --git a/Gulpfile.babel.js b/Gulpfile.babel.js index c091e950b..980f7b6af 100755 --- a/Gulpfile.babel.js +++ b/Gulpfile.babel.js @@ -145,12 +145,6 @@ gulp.task("assets:images:clean", * JavaScript * ------------------------------------------------------------------------- */ -/* - * Annotate javascript - */ -gulp.task("assets:javascripts:flow:annotate", - load("assets/javascripts/flow/annotate")) - /* * Build application logic */ @@ -165,7 +159,7 @@ gulp.task("assets:javascripts:build:modernizr", [ ], load("assets/javascripts/build/modernizr")) /* - * Build application logic and modernizr + * Build application logic and Modernizr */ gulp.task("assets:javascripts:build", (args.clean ? [ "assets:javascripts:clean" @@ -184,6 +178,12 @@ gulp.task("assets:javascripts:build", (args.clean ? [ gulp.task("assets:javascripts:clean", load("assets/javascripts/clean")) +/* + * Annotate JavaScript + */ +gulp.task("assets:javascripts:annotate", + load("assets/javascripts/annotate")) + /* * Lint JavaScript */ diff --git a/lib/tasks/assets/javascripts/flow/annotate.js b/lib/tasks/assets/javascripts/annotate.js similarity index 96% rename from lib/tasks/assets/javascripts/flow/annotate.js rename to lib/tasks/assets/javascripts/annotate.js index 3b41082a8..c0d2b26c2 100644 --- a/lib/tasks/assets/javascripts/flow/annotate.js +++ b/lib/tasks/assets/javascripts/annotate.js @@ -24,7 +24,7 @@ import jsdoc2flow from "flow-jsdoc" import through from "through2" /* ---------------------------------------------------------------------------- - * Task: lint JavaScript + * Task: annotate JavaScript * ------------------------------------------------------------------------- */ export default (gulp, config) => { @@ -48,6 +48,6 @@ export default (gulp, config) => { })) /* Print errors */ - .pipe(gulp.dest("tmp/javascripts")) + .pipe(gulp.dest("tmp/assets/javascripts")) } } diff --git a/material/assets/javascripts/application.js b/material/assets/javascripts/application.js index 41f7a8750..83ebd9788 100644 --- a/material/assets/javascripts/application.js +++ b/material/assets/javascripts/application.js @@ -880,11 +880,13 @@ var Listener = function () { * Generic event listener * * @constructor - * @property {(HTMLCollection)} els_ - Event targets + * + * @property {(Array)} els_ - Event targets * @property {Object} handler_- Event handlers * @property {Array} events_ - Event names * @property {Function} update_ - Update handler - * @param {?(string|EventTarget|HTMLCollection)} els - + * + * @param {?(string|EventTarget|NodeList)} els - * Selector or Event targets * @param {(string|Array)} events - Event names * @param {(Object|Function)} handler - Handler to be invoked @@ -894,8 +896,7 @@ var Listener = function () { _classCallCheck(this, Listener); - this.els_ = Array.prototype.slice.call(typeof els === "string" ? document.querySelectorAll(els) : [els]); - console.log(this.els_); + this.els_ = Array.prototype.slice.call(typeof els === "string" ? document.querySelectorAll(els) : [].concat(els)); /* Set handler as function or directly as object */ this.handler_ = typeof handler === "function" ? { update: handler } : handler; @@ -5825,7 +5826,9 @@ var MatchMedia = * switches the given listeners on or off. * * @constructor + * * @property {Function} handler_ - Media query event handler + * * @param {string} query - Media query to test for * @param {Listener} listener - Event listener */ @@ -5931,11 +5934,13 @@ var Blur = function () { * Blur links within the table of contents above current page y-offset * * @constructor + * * @property {NodeList} els_ - Table of contents links * @property {Array} 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)} els - Selector or HTML elements */ function Blur(els) { @@ -6078,13 +6083,17 @@ var Collapse = function () { * Expand or collapse navigation on toggle * * @constructor + * * @property {HTMLElement} el_ - Navigation list + * * @param {(string|HTMLElement)} el - Selector or HTML element */ function Collapse(el) { _classCallCheck(this, Collapse); - this.el_ = typeof el === "string" ? document.querySelector(el) || new HTMLElement() : el; + var ref = typeof el === "string" ? document.querySelector(el) : el; + if (!(ref instanceof HTMLElement)) throw new ReferenceError(); + this.el_ = ref; } /** @@ -6203,13 +6212,17 @@ var Scrolling = function () { * Set overflow scrolling on the current active pane (for iOS) * * @constructor + * * @property {HTMLElement} el_ - Navigation + * * @param {(string|HTMLElement)} el - Selector or HTML element */ function Scrolling(el) { _classCallCheck(this, Scrolling); - this.el_ = typeof el === "string" ? document.querySelector(el) : el; + var ref = typeof el === "string" ? document.querySelector(el) : el; + if (!(ref instanceof HTMLElement)) throw new ReferenceError(); + this.el_ = ref; } /** @@ -6227,13 +6240,17 @@ var Scrolling = function () { /* Find all toggles and check which one is active */ var toggles = this.el_.querySelectorAll("[data-md-toggle]"); Array.prototype.forEach.call(toggles, function (toggle) { - if (toggle.checked) { + if (toggle instanceof HTMLInputElement && toggle.checked) { /* Find corresponding navigational pane */ var pane = toggle.nextElementSibling; - while (pane.tagName !== "NAV") { + if (!(pane instanceof HTMLElement)) throw new ReferenceError(); + while (pane.tagName !== "NAV" && 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 current and parent list elements */ var parent = toggle.parentNode.parentNode; var target = pane.children[pane.children.length - 1]; @@ -6253,36 +6270,46 @@ var Scrolling = function () { }, { key: "update", value: function update(ev) { + var target = ev.target; + if (!(target instanceof HTMLElement)) throw new ReferenceError(); /* Find corresponding navigational pane */ - var pane = ev.target.nextElementSibling; - while (pane.tagName !== "NAV") { + var pane = target.nextElementSibling; + if (!(pane instanceof HTMLElement)) throw new ReferenceError(); + while (pane.tagName !== "NAV" && pane.nextElementSibling) { pane = pane.nextElementSibling; - } /* Find current and parent list elements */ - var parent = ev.target.parentNode.parentNode; - var target = pane.children[pane.children.length - 1]; + } /* Check references */ + if (!(target.parentNode instanceof HTMLElement) || !(target.parentNode.parentNode instanceof HTMLElement)) throw new ReferenceError(); + + /* Find parent and active panes */ + var parent = target.parentNode.parentNode; + var active = pane.children[pane.children.length - 1]; /* Always reset all lists when transitioning */ parent.style.webkitOverflowScrolling = ""; - target.style.webkitOverflowScrolling = ""; + active.style.webkitOverflowScrolling = ""; - /* Set overflow scrolling on parent */ - if (!ev.target.checked) { + /* Set overflow scrolling on parent pane */ + if (!target.checked) { (function () { var end = function end() { - parent.style.webkitOverflowScrolling = "touch"; - pane.removeEventListener("transitionend", end); + if (pane instanceof HTMLElement) { + parent.style.webkitOverflowScrolling = "touch"; + pane.removeEventListener("transitionend", end); + } }; pane.addEventListener("transitionend", end, false); })(); } - /* Set overflow scrolling on target */ - if (ev.target.checked) { + /* Set overflow scrolling on active pane */ + if (target.checked) { (function () { var end = function end() { - target.style.webkitOverflowScrolling = "touch"; - pane.removeEventListener("transitionend", end, false); + if (pane instanceof HTMLElement) { + active.style.webkitOverflowScrolling = "touch"; + pane.removeEventListener("transitionend", end); + } }; pane.addEventListener("transitionend", end, false); })(); @@ -6303,19 +6330,23 @@ var Scrolling = function () { /* Find all toggles and check which one is active */ var toggles = this.el_.querySelectorAll("[data-md-toggle]"); Array.prototype.forEach.call(toggles, function (toggle) { - if (toggle.checked) { + if (toggle instanceof HTMLInputElement && toggle.checked) { /* Find corresponding navigational pane */ var pane = toggle.nextElementSibling; - while (pane.tagName !== "NAV") { + if (!(pane instanceof HTMLElement)) throw new ReferenceError(); + while (pane.tagName !== "NAV" && 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 */ var parent = toggle.parentNode.parentNode; - var target = pane.children[pane.children.length - 1]; + var active = pane.children[pane.children.length - 1]; /* Always reset all lists when transitioning */ parent.style.webkitOverflowScrolling = ""; - target.style.webkitOverflowScrolling = ""; + active.style.webkitOverflowScrolling = ""; } }); } @@ -6408,12 +6439,18 @@ var Lock = function () { * Lock body for full-screen search modal * * @constructor + * + * @property {HTMLInputElement} el_ - TODO + * @property {number} offset_ - TODO + * * @param {(string|HTMLElement)} el - Selector or HTML element */ function Lock(el) { _classCallCheck(this, Lock); - this.el_ = typeof el === "string" ? document.querySelector(el) : el; + var ref = typeof el === "string" ? document.querySelector(el) : el; + if (!(ref instanceof HTMLInputElement)) throw new ReferenceError(); + this.el_ = ref; } /** @@ -6526,8 +6563,14 @@ var Result = function () { * Perform search and update results on keyboard events * * @constructor + * + * @property {HTMLElement} el_ - TODO + * @property {(Object|Array|Function)} data_ - TODO (very dirty) + * @property {*} meta_ - TODO (must be done like this, as React$Component does not return the correct thing) + * @property {*} list_ - TODO (must be done like this, as React$Component does not return the correct thing) + * * @param {(string|HTMLElement)} el - Selector or HTML element - * @param {(Array.|Function)} data - Promise or array providing data + * @param {(Array|Function)} data - Promise or array providing data // TODO ???? */ function Result(el, data) { _classCallCheck(this, Result); diff --git a/package.json b/package.json index 01677be27..eb300e25a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "scripts": { "build": "scripts/build", "clean": "scripts/clean", + "flow": "scripts/flow", "lint": "scripts/lint", "start": "scripts/start", "test:visual:run": "scripts/test/visual/run", @@ -39,7 +40,6 @@ "babel-loader": "^6.3.1", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-syntax-flow": "^6.18.0", - "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-plugin-transform-react-jsx": "^6.8.0", "babel-polyfill": "^6.20.0", "babel-preset-es2015": "^6.22.0", @@ -52,6 +52,7 @@ "ecstatic": "^2.1.0", "eslint": "^3.14.0", "fastclick": "^1.0.6", + "flow-bin": "^0.39.0", "flow-jsdoc": "^0.2.2", "git-hooks": "^1.1.7", "gulp": "^3.9.1", diff --git a/scripts/flow b/scripts/flow new file mode 100755 index 000000000..ecc7f103c --- /dev/null +++ b/scripts/flow @@ -0,0 +1,44 @@ +#!/bin/bash + +# Copyright (c) 2016-2017 Martin Donath + +# 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 diff --git a/src/assets/javascripts/components/Material/Nav/Collapse.js b/src/assets/javascripts/components/Material/Nav/Collapse.js index 1cc10d040..4491b63d0 100644 --- a/src/assets/javascripts/components/Material/Nav/Collapse.js +++ b/src/assets/javascripts/components/Material/Nav/Collapse.js @@ -36,9 +36,12 @@ export default class Collapse { * @param {(string|HTMLElement)} el - Selector or HTML element */ constructor(el) { - this.el_ = (typeof el === "string") + const ref = (typeof el === "string") ? document.querySelector(el) : el + if (!(ref instanceof HTMLElement)) + throw new ReferenceError + this.el_ = ref } /** diff --git a/src/assets/javascripts/components/Material/Nav/Scrolling.js b/src/assets/javascripts/components/Material/Nav/Scrolling.js index e766198db..4ea4e7181 100644 --- a/src/assets/javascripts/components/Material/Nav/Scrolling.js +++ b/src/assets/javascripts/components/Material/Nav/Scrolling.js @@ -36,9 +36,12 @@ export default class Scrolling { * @param {(string|HTMLElement)} el - Selector or HTML element */ constructor(el) { - this.el_ = (typeof el === "string") + const ref = (typeof el === "string") ? document.querySelector(el) : el + if (!(ref instanceof HTMLElement)) + throw new ReferenceError + this.el_ = ref } /** @@ -52,13 +55,20 @@ export default class Scrolling { /* Find all toggles and check which one is active */ const toggles = this.el_.querySelectorAll("[data-md-toggle]") Array.prototype.forEach.call(toggles, toggle => { - if (toggle.checked) { + if (toggle instanceof HTMLInputElement && toggle.checked) { /* Find corresponding navigational pane */ let pane = toggle.nextElementSibling - while (pane.tagName !== "NAV") + if (!(pane instanceof HTMLElement)) + throw new ReferenceError + while (pane.tagName !== "NAV" && 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 */ const parent = toggle.parentNode.parentNode const target = pane.children[pane.children.length - 1] @@ -76,34 +86,48 @@ export default class Scrolling { * @param {Event} ev - Change event */ update(ev) { + const target = ev.target + if (!(target instanceof HTMLElement)) + throw new ReferenceError /* Find corresponding navigational pane */ - let pane = ev.target.nextElementSibling - while (pane.tagName !== "NAV") + let pane = target.nextElementSibling + if (!(pane instanceof HTMLElement)) + throw new ReferenceError + while (pane.tagName !== "NAV" && pane.nextElementSibling) pane = pane.nextElementSibling - /* Find current and parent list elements */ - const parent = ev.target.parentNode.parentNode - const target = pane.children[pane.children.length - 1] + /* Check references */ + if (!(target.parentNode instanceof HTMLElement) || + !(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 */ parent.style.webkitOverflowScrolling = "" - target.style.webkitOverflowScrolling = "" + active.style.webkitOverflowScrolling = "" - /* Set overflow scrolling on parent */ - if (!ev.target.checked) { + /* Set overflow scrolling on parent pane */ + if (!target.checked) { const end = () => { - parent.style.webkitOverflowScrolling = "touch" - pane.removeEventListener("transitionend", end) + if (pane instanceof HTMLElement) { + parent.style.webkitOverflowScrolling = "touch" + pane.removeEventListener("transitionend", end) + } } pane.addEventListener("transitionend", end, false) } - /* Set overflow scrolling on target */ - if (ev.target.checked) { + /* Set overflow scrolling on active pane */ + if (target.checked) { const end = () => { - target.style.webkitOverflowScrolling = "touch" - pane.removeEventListener("transitionend", end, false) + if (pane instanceof HTMLElement) { + active.style.webkitOverflowScrolling = "touch" + pane.removeEventListener("transitionend", end) + } } pane.addEventListener("transitionend", end, false) } @@ -120,20 +144,27 @@ export default class Scrolling { /* Find all toggles and check which one is active */ const toggles = this.el_.querySelectorAll("[data-md-toggle]") Array.prototype.forEach.call(toggles, toggle => { - if (toggle.checked) { + if (toggle instanceof HTMLInputElement && toggle.checked) { /* Find corresponding navigational pane */ let pane = toggle.nextElementSibling - while (pane.tagName !== "NAV") + if (!(pane instanceof HTMLElement)) + throw new ReferenceError + while (pane.tagName !== "NAV" && 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 target = pane.children[pane.children.length - 1] + const active = pane.children[pane.children.length - 1] /* Always reset all lists when transitioning */ parent.style.webkitOverflowScrolling = "" - target.style.webkitOverflowScrolling = "" + active.style.webkitOverflowScrolling = "" } }) } diff --git a/src/assets/javascripts/components/Material/Search/Lock.js b/src/assets/javascripts/components/Material/Search/Lock.js index 2c4fb647e..b513bc896 100644 --- a/src/assets/javascripts/components/Material/Search/Lock.js +++ b/src/assets/javascripts/components/Material/Search/Lock.js @@ -37,9 +37,12 @@ export default class Lock { * @param {(string|HTMLElement)} el - Selector or HTML element */ constructor(el) { - this.el_ = (typeof el === "string") + const ref = (typeof el === "string") ? document.querySelector(el) : el + if (!(ref instanceof HTMLInputElement)) + throw new ReferenceError + this.el_ = ref } /** diff --git a/src/assets/javascripts/components/Material/Search/Result.jsx b/src/assets/javascripts/components/Material/Search/Result.jsx index 8df29a11f..4985cdc4b 100644 --- a/src/assets/javascripts/components/Material/Search/Result.jsx +++ b/src/assets/javascripts/components/Material/Search/Result.jsx @@ -35,7 +35,8 @@ export default class Result { * * @property {HTMLElement} el_ - TODO * @property {(Object|Array|Function)} data_ - TODO (very dirty) - * @property {React$Element|HTMLElement} meta_ - TODO + * @property {*} meta_ - TODO (must be done like this, as React$Component does not return the correct thing) + * @property {*} list_ - TODO (must be done like this, as React$Component does not return the correct thing) * * @param {(string|HTMLElement)} el - Selector or HTML element * @param {(Array|Function)} data - Promise or array providing data // TODO ???? diff --git a/tests/visual/helpers/spec.js b/tests/visual/helpers/spec.js index 41f9703d8..bb84cd342 100644 --- a/tests/visual/helpers/spec.js +++ b/tests/visual/helpers/spec.js @@ -140,16 +140,16 @@ const generate = (dirname, components) => { for (const state of states) { const test = subsuite => { - /* Resolve and apply relevant breakpoints */ + /* Resolve and apply relevant breakpoints */ const breakpoints = resolve(config.breakpoints, component.break) for (const breakpoint of breakpoints) { subsuite.capture(`@${breakpoint.name}`, actions => { - /* Set window size according to breakpoint */ + /* Set window size according to breakpoint */ actions.setWindowSize( - breakpoint.size.width, breakpoint.size.height) + breakpoint.size.width, breakpoint.size.height) - /* Add the name as a CSS class to the captured element */ + /* Add the name as a CSS class to the captured element */ if (state.name) actions.executeJS(new Function(` document.querySelector( @@ -157,22 +157,22 @@ const generate = (dirname, components) => { ).classList.add("${state.name}") `)) - /* Execute function inside an IIFE */ + /* Execute function inside an IIFE */ if (state.exec) actions.executeJS(new Function(`(${state.exec})()`)) - /* Wait the specified time before taking a screenshot */ + /* Wait the specified time before taking a screenshot */ if (state.wait) actions.wait(state.wait) }) } } - /* No state sub-suite if the name is empty */ + /* No state sub-suite if the name is empty */ if (state.name.length > 0) gemini.suite(state.name, subsuite => test(subsuite)) else - test(suite) + test(suite) } /* Generate sub-suites */