├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jscsrc ├── .jshintrc ├── art ├── favicon.fw.png └── favicon.png ├── deploy.sh ├── gulp ├── clean.js ├── css.js ├── ghpages.js ├── html.js ├── js.js └── static-assets.js ├── gulpfile.js ├── package.json └── src ├── css ├── !base │ ├── _fontface.scss │ ├── _reset.scss │ ├── _utilities.scss │ └── _variables.scss ├── !general │ ├── _icons.scss │ └── _main.scss ├── atoms │ ├── _buttons.scss │ ├── _containers.scss │ ├── _form.scss │ ├── _headings.scss │ └── _scroll-to-top.scss ├── molecules │ ├── _columns.scss │ ├── _footer.scss │ ├── _header.scss │ ├── _notifications.scss │ └── _show-hide.scss ├── organisms │ ├── _about.scss │ ├── _browser-filter.scss │ ├── _code-input.scss │ ├── _logger.scss │ ├── _report-section.scss │ └── _reporter.scss └── site.scss ├── html ├── about.html ├── index.html └── partials │ ├── behaviour-about.html │ ├── behaviour.html │ ├── code-input.html │ ├── footer.html │ ├── head.html │ ├── header.html │ └── logger.html ├── scripts ├── About.js ├── Ajax.js ├── AjaxLoader.js ├── App.js ├── BrowserFilter.js ├── CodeAnalyzer.js ├── CodeInput.js ├── DataStore.js ├── Filter.js ├── LocalStorage.js ├── Reporter.js ├── ScrollTo.js ├── ShowHide.js ├── StickyHeader.js ├── SupportFilter.js ├── test.js └── vendor │ ├── Intermediary.js │ └── require.js └── static ├── CNAME ├── apple-touch-icon-114x114.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-144x144.png ├── apple-touch-icon-152x152.png ├── apple-touch-icon-180x180.png ├── apple-touch-icon-57x57.png ├── apple-touch-icon-60x60.png ├── apple-touch-icon-72x72.png ├── apple-touch-icon-76x76.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── browserconfig.xml ├── data ├── additional.json ├── caniuse2.json └── example.json ├── favicon-160x160.png ├── favicon-16x16.png ├── favicon-192x192.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── fonts ├── icomoon.eot ├── icomoon.svg ├── icomoon.ttf └── icomoon.woff ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png └── mstile-70x70.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Set default charset 12 | [*.{js,scss,mustache,html}] 13 | charset = utf-8 14 | 15 | # Tab indentation (no size specified) 16 | [*.{js,scss,mustache,html}] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Possible errors 4 | 5 | 6 | // Best practices 7 | "complexity" : [ 1, 6 ], 8 | "consistent-return" : [ 2 ], 9 | "curly" : [ 2 ], 10 | "eqeqeq" : [ 2, "allow-null" ], 11 | "no-invalid-this" : [ 2 ], 12 | "no-loop-func" : [ 2 ], 13 | 14 | // Strict mode 15 | "strict" : [ 2, "function" ], 16 | 17 | // Variables 18 | "no-shadow-restricted-names" : [ 2 ], 19 | "no-undef-init" : [ 2 ], 20 | "no-unused-vars" : [ 2, { "args": "none" } ], 21 | "no-use-before-define" : [ 1, "nofunc" ], 22 | 23 | // Stylistic issues (TODO) 24 | "brace-style" : [ 2 ], 25 | "eol-last" : [ 1, "windows" ], 26 | "indent" : [ 2, "tab" ], 27 | "linebreak-style" : [ 2, "unix" ], 28 | "max-depth" : [ 2, 3 ], 29 | "no-mixed-spaces-and-tabs" : [ 2, "smart-tabs"], 30 | "no-multiple-empty-lines" : [ 2, { "max": 3} ], 31 | "quotes" : [ 2, "single" ], 32 | "semi" : [ 2, "always" ] 33 | }, 34 | "env": { 35 | "amd" : true, 36 | "browser" : true, 37 | "commonjs" : true, 38 | "es6" : true 39 | }, 40 | "extends": "eslint:recommended" 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders to ignore 2 | .DS_Store 3 | .cache 4 | .hg 5 | .svn 6 | .CVS 7 | .idea 8 | node_modules 9 | Thumbs.db 10 | _SpecRunner.html 11 | npm-debug.log 12 | .sass-cache 13 | .grunt 14 | _site 15 | bin 16 | src/static/site/css 17 | js.min 18 | !node_modules/dyson-generators/index.js 19 | dist/static/site/sass 20 | out 21 | dist 22 | src/files/OverUnder/index.html 23 | src/files/OverUnder/static/css/site.css 24 | src/files/OverUnder/static/fonts/coffeetin_initials-webfont.eot 25 | src/files/OverUnder/static/fonts/coffeetin_initials-webfont.svg 26 | src/files/OverUnder/static/fonts/coffeetin_initials-webfont.ttf 27 | src/files/OverUnder/static/fonts/coffeetin_initials-webfont.woff 28 | src/files/OverUnder/static/fonts/icomoon.eot 29 | src/files/OverUnder/static/fonts/icomoon.svg 30 | src/files/OverUnder/static/fonts/icomoon.ttf 31 | src/files/OverUnder/static/fonts/icomoon.woff 32 | src/files/OverUnder/static/fonts/jfringmaster.eot 33 | src/files/OverUnder/static/fonts/jfringmaster.svg 34 | src/files/OverUnder/static/fonts/jfringmaster.ttf 35 | src/files/OverUnder/static/fonts/jfringmaster.woff 36 | src/files/OverUnder/static/img/felt.jpg 37 | src/files/OverUnder/static/js/game.js 38 | src/files/OverUnder/static/js/vendor/require.js 39 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces" : [ 3 | "catch", 4 | "do", 5 | "else", 6 | "for", 7 | "if", 8 | "try", 9 | "while" 10 | ], 11 | "requireSpaceAfterKeywords" : [ 12 | "case", 13 | "catch", 14 | "do", 15 | "else", 16 | "for", 17 | "if", 18 | "return", 19 | "switch", 20 | "try", 21 | "typeof", 22 | "void", 23 | "while", 24 | "with" 25 | ], 26 | "requireSpaceBeforeBlockStatements" : true, 27 | "requireParenthesesAroundIIFE" : true, 28 | "requireSpacesInConditionalExpression" : { 29 | "afterTest" : true, 30 | "beforeConsequent" : true, 31 | "afterConsequent" : true, 32 | "beforeAlternate" : true 33 | }, 34 | "requireSpacesInFunctionExpression" : { 35 | "beforeOpeningCurlyBrace": true 36 | }, 37 | "disallowSpacesInFunctionExpression" : { 38 | "beforeOpeningRoundBrace": true 39 | }, 40 | "requireSpacesInAnonymousFunctionExpression" : { 41 | "beforeOpeningCurlyBrace": true 42 | }, 43 | "disallowSpacesInAnonymousFunctionExpression" : { 44 | "beforeOpeningRoundBrace": true 45 | }, 46 | "requireSpacesInNamedFunctionExpression" : { 47 | "beforeOpeningCurlyBrace": true 48 | }, 49 | "disallowSpacesInNamedFunctionExpression" : { 50 | "beforeOpeningRoundBrace": true 51 | }, 52 | "requireSpacesInFunctionDeclaration" : { 53 | "beforeOpeningCurlyBrace": true 54 | }, 55 | "disallowSpacesInFunctionDeclaration" : { 56 | "beforeOpeningRoundBrace": true 57 | }, 58 | "requireSpacesInFunction" : { 59 | "beforeOpeningCurlyBrace": true 60 | }, 61 | "disallowSpacesInFunction" : { 62 | "beforeOpeningRoundBrace": true 63 | }, 64 | "requireMultipleVarDecl" : true, 65 | "requireBlocksOnNewline" : 1, 66 | "disallowPaddingNewlinesInBlocks" : true, 67 | "disallowSpacesInsideObjectBrackets" : "nested", 68 | "disallowSpacesInsideArrayBrackets" : true, 69 | "disallowSpacesInsideParentheses" : true, 70 | "disallowQuotedKeysInObjects" : true, 71 | "requireSpaceBeforeObjectValues" : true, 72 | "requireCommaBeforeLineBreak" : true, 73 | "requireAlignedObjectValues" : "ignoreFunction", 74 | "disallowSpaceAfterPrefixUnaryOperators" : [ 75 | "++", 76 | "--", 77 | "+", 78 | "-", 79 | "~", 80 | "!" 81 | ], 82 | "disallowSpaceBeforePostfixUnaryOperators" : [ 83 | "++", 84 | "--" 85 | ], 86 | "disallowSpaceBeforeBinaryOperators" : [ 87 | "," 88 | ], 89 | "requireSpaceBeforeBinaryOperators" : true, 90 | "requireSpaceAfterBinaryOperators" : true, 91 | "requireCamelCaseOrUpperCaseIdentifiers" : "ignoreProperties", 92 | "disallowKeywords" : [ 93 | "with" 94 | ], 95 | "disallowMultipleLineStrings" : true, 96 | "disallowMixedSpacesAndTabs" : "smart", 97 | "disallowTrailingWhitespace" : true, 98 | "disallowTrailingComma" : true, 99 | "disallowKeywordsOnNewLine" : [ 100 | "else" 101 | ], 102 | "requireLineFeedAtFileEnd" : true, 103 | "maximumLineLength" : { 104 | "value" : 200, 105 | "tabSize" : 4, 106 | "allowComments" : true, 107 | "allowUrlComments" : true, 108 | "allowRegex" : true 109 | }, 110 | "requireCapitalizedConstructors" : true, 111 | "requireDotNotation" : true, 112 | "disallowYodaConditions" : true, 113 | "requireSpaceAfterLineComment" : true, 114 | "disallowNewlineBeforeBlockStatements" : true, 115 | "validateLineBreaks" : "LF", 116 | "validateQuoteMarks" : { 117 | "mark" : true, 118 | "escape" : true 119 | }, 120 | "validateIndentation" : "\t", 121 | "validateParameterSeparator" : ", ", 122 | "safeContextKeyword" : [ 123 | "ref", 124 | "self" 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Enforcing options 3 | "bitwise" : true, // Warn when using bitwise operators, most of the time it is a typo 4 | "camelcase" : false, // This is a style issue and should be checked by JSCS 5 | "curly" : true, // Always put curly braces around blocks in loops and conditionals 6 | "eqeqeq" : true, // Always compare using === and !== 7 | "forin" : true, // Force the use of hasOwnProperty() checks when iterating object properties 8 | "freeze" : true, // Prohibit overwriting prototypes of native objects like Array 9 | "immed" : true, // Prohibits the use of immediate function invocations without wrapping them in parentheses 10 | "indent" : 4, // Tab width for the code 11 | "latedef" : true, // Prohibit the use of a variable before it was defined 12 | "newcap" : true, // Force constructor methods to start with a capital 13 | "noarg" : true, // Prohibit the use of arguments.caller and arguments.callee 14 | "noempty" : false, // Do not warn about empty blocks of code 15 | "nonbsp" : true, // Warn about "non-breaking whitespace" characters, could be Mac specific 16 | "nonew" : true, // Prohibit the use of constructor functions for side-effects 17 | "plusplus" : false, // Allow the use of unary increment and decrement operators 18 | "quotmark" : true, // Force a consistent use of quote marks but do not enforce single or double quotes 19 | "undef" : true, // Warn about the use of undeclared variables, handy for finding typos 20 | "unused" : "vars", // Warn about unused variables, but not about unused params as methods might get an event param and that would trigger a warning 21 | "strict" : true, // Force all methods to run in ECMAScript 5's strict mode 22 | "maxcomplexity" : 10, // Warn when the cyclomatic complexity of the code becomes more than 10 23 | "maxdepth" : 3, // Warn when a method has more than 3 nested blocks 24 | 25 | // Relaxing options 26 | "asi" : false, // Always warn about missing semicolons 27 | "boss" : false, // Always warn when assigning where a comparison is expected 28 | "debug" : false, // Always warn about debugger statements 29 | "eqnull" : true, // Do not warn when using == or != to check for null, this also checks for undefined in the same statement 30 | "esnext" : false, // Give warning when using ECMAScript 6 syntax 31 | "evil" : false, // Always warn when using eval 32 | "expr" : false, // Always warn when expressions are used where an assignement or function call is expected 33 | "funcscope" : false, // Always warn when using variables outside of the control structures they were defined in 34 | "globalstrict" : false, // Always warn when using a global use strict 35 | "iterator" : false, // Always warn when using the __iterator__ property 36 | "lastsemic" : false, // Always warn when missing the semicolon in a one-line block 37 | "laxbreak" : false, // Always warn about possibly unsafe line breaks 38 | "laxcomma" : false, // Always warn when using the comma-first code style 39 | "loopfunc" : false, // Always warn when declaring functions inside loops 40 | "maxerr" : 50, // The max number of errors JSHint will generate before giving up 41 | "multistr" : false, // Always warn when using multi-line strings 42 | "notypeof" : false, // Always warn when using invalid typeof operator values 43 | "proto" : false, // Always warn when using the __proto__ property 44 | "scripturl" : false, // Always warn when using script-targeted URLs 45 | "shadow" : false, // Always warn when declaring a variable that is already defined in the outer scope 46 | "sub" : true, // Do not warn when using obj['prop'] instead of obj.prop 47 | "supernew" : false, // Always warn when using "weird" constructions 48 | "validthis" : false, // Always warn about possible strict violations with the usage of this 49 | 50 | // Environments 51 | "browser" : true, // Define globals exposed by modern browsers like document and navigator 52 | "couch" : false, // Do not define globals exposed by CouchDB 53 | "devel" : false, // Do not define globals usually used for debugging (console, alert, etc) 54 | "dojo" : false, // Do not define globals exposed by Dojo Toolkit 55 | "jquery" : false, // Do not define globals exposed by jQuery 56 | "mootools" : false, // Do not define globals exposed by MooTools 57 | "node" : false, // Do not assume the code will be running within Node.js runtime environment 58 | "nonstandard" : false, // Do not define non-standard but widely adopted globals like escape and unescape 59 | "prototypejs" : false, // Do not define globals exposed by PrototypeJS 60 | "rhino" : false, // Do not assume the code will be running within Rhino runtime environment 61 | "worker" : false, // Do not assume the code will be running within a Web Worker environment 62 | "wsh" : false, // Do not define globals available when running as a script for Windows Shell Host 63 | "yui" : false, // Do not define globals exposed by YUI 64 | 65 | // Globals 66 | "predef" : [ 67 | "define", 68 | "module", 69 | "require" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /art/favicon.fw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/art/favicon.fw.png -------------------------------------------------------------------------------- /art/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/art/favicon.png -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit #abort if any command fails 3 | me=$(basename "$0") 4 | 5 | help_message="\ 6 | Usage: $me [-c FILE] [] 7 | Deploy generated files to a git branch. 8 | 9 | Options: 10 | 11 | -h, --help Show this help information. 12 | -v, --verbose Increase verbosity. Useful for debugging. 13 | -e, --allow-empty Allow deployment of an empty directory. 14 | -m, --message MESSAGE Specify the message used when committing on the 15 | deploy branch. 16 | -n, --no-hash Don't append the source commit's hash to the deploy 17 | commit's message. 18 | -c, --config-file PATH Override default & environment variables' values 19 | with those in set in the file at 'PATH'. Must be the 20 | first option specified. 21 | 22 | Variables: 23 | 24 | GIT_DEPLOY_DIR Folder path containing the files to deploy. 25 | GIT_DEPLOY_BRANCH Commit deployable files to this branch. 26 | GIT_DEPLOY_REPO Push the deploy branch to this repository. 27 | 28 | These variables have default values defined in the script. The defaults can be 29 | overridden by environment variables. Any environment variables are overridden 30 | by values set in a '.env' file (if it exists), and in turn by those set in a 31 | file specified by the '--config-file' option." 32 | 33 | parse_args() { 34 | # Set args from a local environment file. 35 | if [ -e ".env" ]; then 36 | source .env 37 | fi 38 | 39 | # Set args from file specified on the command-line. 40 | if [[ $1 = "-c" || $1 = "--config-file" ]]; then 41 | source "$2" 42 | shift 2 43 | fi 44 | 45 | # Parse arg flags 46 | # If something is exposed as an environment variable, set/overwrite it 47 | # here. Otherwise, set/overwrite the internal variable instead. 48 | while : ; do 49 | if [[ $1 = "-h" || $1 = "--help" ]]; then 50 | echo "$help_message" 51 | return 0 52 | elif [[ $1 = "-v" || $1 = "--verbose" ]]; then 53 | verbose=true 54 | shift 55 | elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then 56 | allow_empty=true 57 | shift 58 | elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then 59 | commit_message=$2 60 | shift 2 61 | elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then 62 | GIT_DEPLOY_APPEND_HASH=false 63 | shift 64 | else 65 | break 66 | fi 67 | done 68 | 69 | # Set internal option vars from the environment and arg flags. All internal 70 | # vars should be declared here, with sane defaults if applicable. 71 | 72 | # Source directory & target branch. 73 | deploy_directory=${GIT_DEPLOY_DIR:-dist} 74 | deploy_branch=${GIT_DEPLOY_BRANCH:-gh-pages} 75 | 76 | #if no user identity is already set in the current git environment, use this: 77 | default_username=${GIT_DEPLOY_USERNAME:-deploy.sh} 78 | default_email=${GIT_DEPLOY_EMAIL:-tbusser@gmail.com} 79 | 80 | #repository to deploy to. must be readable and writable. 81 | repo=${GIT_DEPLOY_REPO:-origin} 82 | 83 | #append commit hash to the end of message by default 84 | append_hash=${GIT_DEPLOY_APPEND_HASH:-true} 85 | } 86 | 87 | main() { 88 | parse_args "$@" 89 | 90 | enable_expanded_output 91 | 92 | if ! git diff --exit-code --quiet --cached; then 93 | echo Aborting due to uncommitted changes in the index >&2 94 | return 1 95 | fi 96 | 97 | commit_title=`git log -n 1 --format="%s" HEAD` 98 | commit_hash=` git log -n 1 --format="%H" HEAD` 99 | 100 | #default commit message uses last title if a custom one is not supplied 101 | if [[ -z $commit_message ]]; then 102 | commit_message="publish: $commit_title" 103 | fi 104 | 105 | #append hash to commit message unless no hash flag was found 106 | if [ $append_hash = true ]; then 107 | commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash" 108 | fi 109 | 110 | previous_branch=`git rev-parse --abbrev-ref HEAD` 111 | 112 | if [ ! -d "$deploy_directory" ]; then 113 | echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 114 | return 1 115 | fi 116 | 117 | # must use short form of flag in ls for compatibility with OS X and BSD 118 | if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then 119 | echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2 120 | return 1 121 | fi 122 | 123 | if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then 124 | # deploy_branch exists in $repo; make sure we have the latest version 125 | 126 | disable_expanded_output 127 | git fetch --force $repo $deploy_branch:$deploy_branch 128 | enable_expanded_output 129 | fi 130 | 131 | # check if deploy_branch exists locally 132 | if git show-ref --verify --quiet "refs/heads/$deploy_branch" 133 | then incremental_deploy 134 | else initial_deploy 135 | fi 136 | 137 | restore_head 138 | } 139 | 140 | initial_deploy() { 141 | git --work-tree "$deploy_directory" checkout --orphan $deploy_branch 142 | git --work-tree "$deploy_directory" add --all 143 | commit+push 144 | } 145 | 146 | incremental_deploy() { 147 | #make deploy_branch the current branch 148 | git symbolic-ref HEAD refs/heads/$deploy_branch 149 | #put the previously committed contents of deploy_branch into the index 150 | git --work-tree "$deploy_directory" reset --mixed --quiet 151 | git --work-tree "$deploy_directory" add --all 152 | 153 | set +o errexit 154 | diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$? 155 | set -o errexit 156 | case $diff in 157 | 0) echo No changes to files in $deploy_directory. Skipping commit.;; 158 | 1) commit+push;; 159 | *) 160 | echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to master, use: git symbolic-ref HEAD refs/heads/master && git reset --mixed >&2 161 | return $diff 162 | ;; 163 | esac 164 | } 165 | 166 | commit+push() { 167 | set_user_id 168 | git --work-tree "$deploy_directory" commit -m "$commit_message" 169 | 170 | disable_expanded_output 171 | #--quiet is important here to avoid outputting the repo URL, which may contain a secret token 172 | git push --quiet $repo $deploy_branch 173 | enable_expanded_output 174 | } 175 | 176 | #echo expanded commands as they are executed (for debugging) 177 | enable_expanded_output() { 178 | if [ $verbose ]; then 179 | set -o xtrace 180 | set +o verbose 181 | fi 182 | } 183 | 184 | #this is used to avoid outputting the repo URL, which may contain a secret token 185 | disable_expanded_output() { 186 | if [ $verbose ]; then 187 | set +o xtrace 188 | set -o verbose 189 | fi 190 | } 191 | 192 | set_user_id() { 193 | if [[ -z `git config user.name` ]]; then 194 | git config user.name "$default_username" 195 | fi 196 | if [[ -z `git config user.email` ]]; then 197 | git config user.email "$default_email" 198 | fi 199 | } 200 | 201 | restore_head() { 202 | if [[ $previous_branch = "HEAD" ]]; then 203 | #we weren't on any branch before, so just set HEAD back to the commit it was on 204 | git update-ref --no-deref HEAD $commit_hash $deploy_branch 205 | else 206 | git symbolic-ref HEAD refs/heads/$previous_branch 207 | fi 208 | 209 | git reset --mixed 210 | } 211 | 212 | filter() { 213 | sed -e "s|$repo|\$repo|g" 214 | } 215 | 216 | sanitize() { 217 | "$@" 2> >(filter 1>&2) | filter 218 | } 219 | 220 | [[ $1 = --source-only ]] || main "$@" 221 | -------------------------------------------------------------------------------- /gulp/clean.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config, plugins) { 2 | 'use strict'; 3 | 4 | gulp.task('clean', function() { 5 | if (config.environment === 'src') { 6 | return; 7 | } 8 | 9 | var paths = config[config.environment]; 10 | return plugins.del(paths.base + '**'); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /gulp/css.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config, plugins) { 2 | 'use strict'; 3 | 4 | gulp.task('build:asset:css', function() { 5 | var paths = config[config.environment]; 6 | 7 | return gulp.src([config.src.css + '**/*.scss']) 8 | .pipe(plugins.plumber({ 9 | errorHandler: plugins.notify.onError('SCSS: <%= error.message %>') 10 | })) 11 | .pipe(plugins.if(config.deploy, plugins.sourcemaps.init())) 12 | .pipe(plugins.sass({ 13 | outputStyle: config.deploy ? 'compressed' : 'expanded' 14 | })) 15 | .pipe(plugins.autoprefixer( 16 | config.autoprefix.support.split(', ') 17 | )) 18 | .pipe(plugins.if(config.deploy, plugins.sourcemaps.write('./'))) 19 | .pipe(gulp.dest(paths.css)) 20 | .pipe(plugins.size({title: 'styles:scss'})); 21 | }); 22 | 23 | gulp.task('watch:css', function() { 24 | gulp.watch(config.src.css + '**/*.scss', ['build:asset:css']); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /gulp/ghpages.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config, plugins) { 2 | 'use strict'; 3 | 4 | gulp.task('punlish', function(taskReady) { 5 | var paths = config.dist; 6 | return gulp.src(paths.base + '**/*') 7 | .pipe(plugins.ghPages(config.ghPages)); 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /gulp/html.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config, plugins) { 2 | 'use strict'; 3 | 4 | gulp.task('build:asset:html', function() { 5 | var paths = config[config.environment]; 6 | 7 | return gulp.src(config.src.html + '*.html') 8 | .pipe(plugins.fileInclude({ 9 | prefix: '@@', 10 | basePath: '@file', 11 | context: { 12 | deploy: config.deploy 13 | } 14 | })) 15 | .pipe(plugins.if(config.deploy, plugins.stripComments())) 16 | .pipe(plugins.if(config.deploy, plugins.save('before-sitemap'))) 17 | .pipe(plugins.if(config.deploy, plugins.sitemap(config.sitemap))) 18 | .pipe(plugins.if(config.deploy, gulp.dest(paths.html))) 19 | .pipe(plugins.if(config.deploy, plugins.save.restore('before-sitemap'))) 20 | .pipe(gulp.dest(paths.html)); 21 | }); 22 | 23 | gulp.task('watch:html', function() { 24 | gulp.watch(config.src.html + '**/*.htm*', ['build:asset:html']); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /gulp/js.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config, plugins) { 2 | 'use strict'; 3 | 4 | gulp.task('build:asset:js', function(taskReady) { 5 | var paths = config[config.environment]; 6 | 7 | if (config.environment === 'dist') { 8 | plugins.rjs.optimize(config.requirejs, function(buildResponse) { 9 | plugins.rjs.optimize(config.requirejsAbout, function(buildResponse) { 10 | taskReady(); 11 | },function(error) { 12 | plugins.notify(error); 13 | taskReady(); 14 | }); 15 | }, function(error) { 16 | plugins.notify(error); 17 | taskReady(); 18 | }); 19 | } else { 20 | return gulp.src(config.src.js + '**/*.js') 21 | .pipe(gulp.dest(paths.js)); 22 | } 23 | }); 24 | 25 | gulp.task('watch:js', function() { 26 | gulp.watch(config.src.js + '**/*.js', ['build:asset:js']); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /gulp/static-assets.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config, plugins) { 2 | 'use strict'; 3 | 4 | gulp.task('build:asset:static', function() { 5 | var paths = config[config.environment]; 6 | 7 | // Copy all the files in the static folder and its subfolders to the 8 | // output folder 9 | gulp.src(config.src.static + '**/*') 10 | .pipe(gulp.dest(paths.static)); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | plugins = require('gulp-load-plugins')(), 5 | runSequence = require('run-sequence'), 6 | config = { 7 | autoprefix : { 8 | support : 'last 2 versions, Explorer >= 9, Firefox >= 25' 9 | }, 10 | deploy : false, 11 | environment : 'dev', 12 | sitemap : { 13 | siteUrl : 'http://jscc.info' 14 | }, 15 | requirejs : { 16 | baseUrl: './src/scripts', 17 | paths: { 18 | Intermediary: './vendor/Intermediary', 19 | requireLib : './vendor/require' 20 | }, 21 | name: 'app', 22 | include: 'requireLib', 23 | out: './dist/scripts/App-built.js', 24 | optimize: 'uglify2' 25 | }, 26 | requirejsAbout : { 27 | baseUrl: './src/scripts', 28 | paths: { 29 | Intermediary: './vendor/Intermediary', 30 | requireLib : './vendor/require' 31 | }, 32 | name: 'About', 33 | include: 'requireLib', 34 | out: './dist/scripts/About-built.js', 35 | optimize: 'uglify2' 36 | }, 37 | ghPages : { 38 | remoteUrl : 'https://github.com/tbusser/jscc.git', 39 | origin : 'origin', 40 | branch : 'gh-pages' 41 | }, 42 | dev : { 43 | base : './out/', 44 | css : './out/styles/', 45 | html : './out/', 46 | js : './out/scripts/', 47 | static : './out/' 48 | }, 49 | dist : { 50 | base : './dist/', 51 | css : './dist/styles/', 52 | html : './dist/', 53 | js : './dist/scripts/', 54 | static : './dist/' 55 | }, 56 | src : { 57 | base : './src/', 58 | css : './src/css/', 59 | html : './src/html/', 60 | js : './src/scripts/', 61 | static : './src/static/' 62 | } 63 | }; 64 | 65 | plugins.del = require('del'); 66 | plugins.rjs = require('requirejs'); 67 | 68 | /* ========================================================================== *\ 69 | GULP TASK IMPORTS 70 | \* ========================================================================== */ 71 | require('./gulp/clean')(gulp, config, plugins); 72 | require('./gulp/css')(gulp, config, plugins); 73 | require('./gulp/ghpages')(gulp, config, plugins); 74 | require('./gulp/html')(gulp, config, plugins); 75 | require('./gulp/js')(gulp, config, plugins); 76 | require('./gulp/static-assets')(gulp, config, plugins); 77 | /* == GULP TASK IMPORTS ===================================================== */ 78 | 79 | 80 | 81 | /* ========================================================================== *\ 82 | UTILITY TASKS 83 | \* ========================================================================== */ 84 | gulp.task('set:deploy', function() { 85 | 'use strict'; 86 | 87 | config.deploy = true; 88 | config.environment = 'dist'; 89 | }); 90 | 91 | gulp.task('webserver', function(taskReady) { 92 | 'use strict'; 93 | 94 | gulp.src(config[config.environment].html) 95 | .pipe(plugins.serverLivereload({ 96 | directoryListing : false, 97 | livereload : true, 98 | open : true 99 | })); 100 | }); 101 | /* == UTILITY TASKS ========================================================= */ 102 | 103 | gulp.task('build', function(taskReady) { 104 | 'use strict'; 105 | 106 | runSequence( 107 | 'clean', 108 | [ 109 | 'build:asset:html', 110 | 'build:asset:css', 111 | 'build:asset:js', 112 | 'build:asset:static' 113 | ], 114 | taskReady 115 | ); 116 | }); 117 | 118 | gulp.task('deploy', function(taskReady) { 119 | 'use strict'; 120 | 121 | runSequence( 122 | 'set:deploy', 123 | 'build', 124 | taskReady 125 | ); 126 | }); 127 | 128 | gulp.task('publish', function(taskReady) { 129 | 'use strict'; 130 | 131 | runSequence( 132 | 'publish', 133 | taskReady 134 | ); 135 | }); 136 | 137 | gulp.task('deploy:test', function(ready) { 138 | 'use strict'; 139 | 140 | runSequence( 141 | 'set:deploy', 142 | 'webserver' 143 | ); 144 | }); 145 | 146 | gulp.task('develop', function(taskReady) { 147 | 'use strict'; 148 | 149 | runSequence( 150 | 'build', 151 | [ 152 | 'watch:css', 153 | 'watch:html', 154 | 'watch:js', 155 | 'webserver' 156 | ], 157 | taskReady 158 | ); 159 | }); 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jscc", 3 | "version": "1.10.0", 4 | "description": "JavaScript Compatibility Checker website", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/tbusser/jscc.git" 11 | }, 12 | "author": "tbusser@gmail.com", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "del": "^2.2.2", 16 | "gulp": "^3.9.1", 17 | "gulp-autoprefixer": "^3.1.1", 18 | "gulp-file-include": "^1.0.0", 19 | "gulp-if": "^2.0.1", 20 | "gulp-load-plugins": "^1.4.0", 21 | "gulp-notify": "^2.2.0", 22 | "gulp-plumber": "^1.1.0", 23 | "gulp-sass": "^2.3.2", 24 | "gulp-save": "^1.2.1", 25 | "gulp-server-livereload": "^1.9.1", 26 | "gulp-sitemap": "^4.2.0", 27 | "gulp-size": "^2.1.0", 28 | "gulp-sourcemaps": "^2.2.0", 29 | "gulp-strip-comments": "^2.4.3", 30 | "gulp-uglify": "^2.0.0", 31 | "requirejs": "^2.3.2", 32 | "run-sequence": "^1.2.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/css/!base/_fontface.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #FONT-FACE 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | @font-face { 5 | font-family: 'icomoon'; 6 | src:url('../fonts/icomoon.eot?-ezxqxq'); 7 | src:url('../fonts/icomoon.eot?#iefix-ezxqxq') format('embedded-opentype'), 8 | url('../fonts/icomoon.woff?-ezxqxq') format('woff'), 9 | url('../fonts/icomoon.ttf?-ezxqxq') format('truetype'), 10 | url('../fonts/icomoon.svg?-ezxqxq#icomoon') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | -------------------------------------------------------------------------------- /src/css/!base/_reset.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #RESET 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | a { 9 | border-bottom: 1px solid; 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | -------------------------------------------------------------------------------- /src/css/!base/_utilities.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #UTILITIES 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .hidden { 5 | display: none; 6 | } 7 | 8 | .visually-hidden { 9 | 10 | } 11 | 12 | .visually-hidden { 13 | border: none; 14 | clip: rect(0 0 0 0); 15 | height: 1px; 16 | margin: -1px; 17 | overflow: hidden; 18 | padding: 0; 19 | position: absolute; 20 | width: 1px; 21 | } 22 | -------------------------------------------------------------------------------- /src/css/!base/_variables.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #VARIABLES 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | $font-heading: Courier, monospace; //"Lucida Console", "Lucida Sans Typewriter", Monaco, "Bitstream Vera Sans Mono", monospace; //Monaco, Courier, monospace; 5 | $font-code: Monaco, Consolas, "Lucida Console", monospace; 6 | $font-icons: Icomoon; 7 | 8 | $color-border: #586e75; 9 | $color-border-light: lighten($color-border, 30%); 10 | $color-border-dark: darken($color-border, 30%); 11 | 12 | $color-base-light: #fdf6e3; 13 | $color-background-light: lighten($color-border, 49%); 14 | $color-background-medium: lighten($color-border, 40%); 15 | 16 | $color-heading-secondary: #1277be; 17 | 18 | $color-attention: #CFA31A; 19 | 20 | $color-accent-blue: deepskyblue; // #167bc2; //#268bd2; 21 | $color-accent-yellow: #b58900; 22 | 23 | $size-max-width: 80em; 24 | 25 | 26 | $color-grey-light: #e4e9eb; 27 | -------------------------------------------------------------------------------- /src/css/!general/_icons.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #ICONS 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | $icon-chain : '\f08e'; 5 | $icon-gear : '\f013'; 6 | $icon-check : '\f00c'; 7 | $icon-cross : '\f00d'; 8 | $icon-chevron-right : '\f054'; 9 | $icon-arrow-up : '\f077'; 10 | $icon-index : '\f0cb'; 11 | $icon-filter : '\f0b0'; 12 | 13 | @mixin icon($char) { 14 | content: $char; 15 | font-family: $font-icons; 16 | font-style: normal; 17 | font-variant: normal; 18 | font-weight: normal; 19 | speak: none; 20 | text-transform: none; 21 | } 22 | -------------------------------------------------------------------------------- /src/css/!general/_main.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #MAIN 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | body { 5 | background-color: #585858; //dimgrey; // #fff; // $color-base-light; 6 | color: #4b4b4b; 7 | font: 16px/1.5 Arial, sans-serif; 8 | margin: 0; 9 | //position: relative; 10 | } 11 | 12 | main { 13 | } 14 | 15 | .container-blue { 16 | background-color: #DFEAEE; 17 | border-bottom: 1px solid #999; 18 | color: #333; 19 | 20 | h2 { 21 | color: inherit; 22 | } 23 | } 24 | 25 | .container-white { 26 | background-color: #fff; 27 | color: #4b4b4b; 28 | 29 | h2 { 30 | color: #333; 31 | } 32 | } 33 | .container-medium { 34 | background-color: $color-background-medium; 35 | } 36 | 37 | .size-constraint { 38 | margin-left: auto; 39 | margin-right: auto; 40 | max-width: $size-max-width; 41 | } 42 | -------------------------------------------------------------------------------- /src/css/atoms/_buttons.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #BUTTONS 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | %base-button { 5 | background: #fff; 6 | border: none; 7 | cursor: pointer; 8 | font: inherit; 9 | text-decoration: none; 10 | padding: 0; 11 | } 12 | 13 | .button { 14 | @extend %base-button; 15 | 16 | &.filter, 17 | &.index, 18 | &.config { 19 | color: $color-border-light; 20 | padding-left: 1.25em; 21 | position: relative; 22 | transition: color .3s ease-out; 23 | 24 | &.active-trigger { 25 | border-bottom: 3px solid #CFA31A; 26 | color: #4b4b4b; 27 | } 28 | 29 | &:hover { 30 | color: inherit; 31 | } 32 | 33 | &:focus { 34 | outline: none; 35 | } 36 | 37 | &:before { 38 | bottom: 0; 39 | display: block; 40 | height: 1.8em; 41 | font-size: 1.25em; 42 | left: 0; 43 | line-height: 1.8; 44 | margin: auto 0; 45 | position: absolute; 46 | top: 0; 47 | } 48 | } 49 | 50 | &.filter:before { 51 | @include icon($icon-filter); 52 | } 53 | 54 | &.config:before { 55 | @include icon($icon-gear); 56 | } 57 | 58 | &.index { 59 | padding-left: 1.5em; 60 | 61 | &:before { 62 | @include icon($icon-index); 63 | } 64 | } 65 | } 66 | 67 | 68 | .toggle-button { 69 | background-color: darken(lightgrey, 5%); 70 | box-shadow: 0 0 1px #000; 71 | color: #333; 72 | cursor: pointer; 73 | display: block; 74 | font: inherit; 75 | font-weight: bold; 76 | min-width: 16em; 77 | padding: .25em 1em .25em 3em; 78 | position: relative; 79 | transition: box-shadow .2s ease-out, background-color .2s ease-out; 80 | 81 | 82 | &:before { 83 | @include icon($icon-cross); 84 | bottom: 0; 85 | color: red; 86 | display: block; 87 | font-size: 1.5em; 88 | height: 1em; 89 | left: .5em; 90 | line-height: 1; 91 | margin: auto 0; 92 | position: absolute; 93 | text-shadow: 0 -1px 0 #fff, 0 1px 0 #000; 94 | top: 0; 95 | } 96 | 97 | &:hover { 98 | background-color: lighten(lightgrey, 5%); 99 | box-shadow: 0 0 5px #000; 100 | } 101 | } 102 | 103 | input:checked + .toggle-button { 104 | background-color: #e4e9eb; //$color-accent-blue; 105 | box-shadow: inset 0 0 3px #000; 106 | 107 | &:before { 108 | @include icon($icon-check); 109 | color: green; 110 | text-shadow: 0 1px 0 #fff, 0 -1px 0 #000; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/css/atoms/_containers.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #CONTAINERS 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .widget-container { 5 | padding: 1.5em 1em; 6 | position: relative; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/atoms/_form.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #FORM 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | form { 5 | button { 6 | background-color: $color-accent-blue; 7 | border: 3px solid #fff; 8 | box-shadow: 0 0 1px #000; 9 | color: #fff; 10 | cursor: pointer; 11 | font: inherit; 12 | font-weight: bold; 13 | padding: .5em 1em; 14 | transition: box-shadow .2s ease-out, background-color .2s ease-out; 15 | 16 | &:hover { 17 | background-color: lighten($color-accent-blue, 10%); 18 | box-shadow: 0 0 3px #000; 19 | } 20 | 21 | &:focus { 22 | box-shadow: 0 0 5px #ff9200; 23 | outline: none; 24 | } 25 | } 26 | 27 | fieldset { 28 | border: none; 29 | padding: 0; 30 | } 31 | 32 | textarea { 33 | border: 3px solid $color-border; 34 | display: block; 35 | font: 14px/1.2 Monaco, Consolas, "Lucida Console", monospace; 36 | resize: vertical; 37 | padding: .5em; 38 | width: 100%; 39 | } 40 | 41 | input { 42 | display: block; 43 | 44 | &[type="file"] { 45 | height: 0; 46 | width: 0; 47 | 48 | &:focus { 49 | outline: none; 50 | } 51 | } 52 | } 53 | 54 | label { 55 | display: block; 56 | } 57 | 58 | fieldset + button, 59 | label + label, 60 | label + button, 61 | label + fieldset { 62 | margin-top: 1em; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/css/atoms/_headings.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #HEADINGS 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | h1 { 5 | color: #333; 6 | margin: 0 0 1em; 7 | 8 | 9 | & > span { 10 | //border-bottom: 3px solid #4b4b4b; 11 | //padding-bottom: .25em; 12 | } 13 | } 14 | 15 | h2 { 16 | margin-bottom: .25em; 17 | margin-top: 0; 18 | } 19 | 20 | h3, 21 | h4 { 22 | color: #333; 23 | margin-bottom: 0; 24 | 25 | & + p { 26 | margin-top: 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/css/atoms/_scroll-to-top.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #SCROLL-TO-TOP 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .scroll-to-top { 5 | background-color: rgba(255,255,255,.2); 6 | border-radius: .25em; 7 | bottom: .5em; 8 | box-shadow: 0 0 3px rgba(0,0,0,.2); 9 | color: rgba(0,0,0,.2); 10 | display: inline-block; 11 | font-size: 2em; 12 | height: 1.5em; 13 | line-height: 1.4em; 14 | position: fixed; 15 | right: .5em; 16 | text-align: center; 17 | transition: background-color .5s ease-out, color .5s ease-out, display .5s ease-out; 18 | width: 1.5em; 19 | z-index: 300; 20 | 21 | span { 22 | @extend .visually-hidden; 23 | } 24 | 25 | &.not-available { 26 | display: none; 27 | } 28 | 29 | &:before { 30 | @include icon($icon-arrow-up); 31 | } 32 | 33 | &:hover { 34 | background-color: #fff; 35 | color: #4b4b4b; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/css/molecules/_columns.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #COLUMNS 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .two-column { 5 | .column { 6 | float: left; 7 | padding-left: 1em; 8 | padding-right: 1em; 9 | width: 50%; 10 | } 11 | 12 | &:after { 13 | clear: both; 14 | content: ''; 15 | display: table; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/css/molecules/_footer.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #FOOTER 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .site-footer { 5 | //bottom: 0; 6 | color: #fff; 7 | //height: 4.5em; 8 | //left: 0; 9 | padding: 1.5em 0; 10 | //position: absolute; 11 | //right: 0; 12 | 13 | h2 { 14 | color: #CFA31A; 15 | font-size: 1.25em; 16 | } 17 | 18 | p { 19 | font-size: .825em; 20 | margin: 0; 21 | max-width: 75%; 22 | } 23 | 24 | a { 25 | color: inherit; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/css/molecules/_header.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #HEADER 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .site-header { 5 | background-color: #fff; 6 | box-shadow: 0 0 5px #000; 7 | position: relative; 8 | z-index: 1; 9 | 10 | .size-constraint { 11 | position: relative; 12 | } 13 | 14 | .large-text { 15 | display: block; 16 | float: left; 17 | font-size: 1.75em; 18 | line-height: 1.25; 19 | text-transform: uppercase; 20 | } 21 | 22 | .small-container { 23 | display: inline-block; 24 | float: left; 25 | font-size: .825em; 26 | line-height: 1; 27 | margin-left: .5em; 28 | max-width: 10em; 29 | padding-top: 8px; 30 | } 31 | 32 | h1 { 33 | font-family: $font-code; 34 | margin: 0; 35 | padding: .25em 0; 36 | 37 | &:after { 38 | clear: both; 39 | content: ''; 40 | display: table; 41 | } 42 | } 43 | nav { 44 | bottom: 1em; 45 | position: absolute; 46 | right: 1em; 47 | 48 | a { 49 | &:hover { 50 | border-bottom: 1px solid $color-attention; 51 | } 52 | } 53 | } 54 | 55 | .active { 56 | border-bottom: 3px solid $color-attention; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/css/molecules/_notifications.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #NOTIFICATIONS 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .notifications { 5 | border: 1px solid $color-border-light; 6 | font: 14px/1.2 $font-code; 7 | list-style: none; 8 | padding: .5em; 9 | margin: 0; 10 | min-height: 5em; 11 | } 12 | -------------------------------------------------------------------------------- /src/css/molecules/_show-hide.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #SHOW-HIDE 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | [data-sh-container] { 5 | overflow: hidden; 6 | 7 | &.transition { 8 | transition: all .3s ease-out; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/css/organisms/_about.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #ABOUT 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .api-list { 5 | column-count: 3; 6 | list-style-position: inside; 7 | margin: 0; 8 | } 9 | 10 | .medium-paragraph { 11 | max-width: 75%; 12 | } 13 | -------------------------------------------------------------------------------- /src/css/organisms/_browser-filter.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #BROWSER-FILTER 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .filter-widget { 5 | background-color: rgba(0,0,0,.125); //#e4e9eb; 6 | box-shadow: inset 0 0 5px #000; 7 | color: #333; 8 | 9 | input { 10 | font-size: 1em; 11 | height: 1em; 12 | margin: 0 .5em 0 0; 13 | } 14 | 15 | [data-sh-content] { 16 | padding: 1em; 17 | } 18 | 19 | h3, 20 | h4 { 21 | color: #00060b; 22 | } 23 | 24 | h3 { 25 | margin: 1em 0 .5em; 26 | } 27 | h4 { 28 | margin: 0; 29 | } 30 | 31 | ul { 32 | display: flex; 33 | flex-direction: row; 34 | flex-wrap: wrap; 35 | justify-content: space-between; 36 | list-style: none; 37 | margin: .5em 0 .75em; 38 | padding: 0; 39 | } 40 | 41 | li { 42 | display: block; 43 | float: left; 44 | margin-bottom: .5em; 45 | 46 | input { 47 | display: none; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/css/organisms/_code-input.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #CODE-INPUT 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .input-widget { 5 | @extend .widget-container; 6 | 7 | .drop-target { 8 | border: 3px dashed $color-border; // lightgrey; 9 | background-color: #fff; 10 | padding: 1.5em 0; 11 | text-align: center; 12 | transition: background-color .3s ease-out, border-color .3s ease-out, color .3s ease-out; 13 | 14 | &:hover, 15 | &.ondrag { 16 | background-color: $color-base-light; 17 | border-color: $color-border-dark; 18 | color: $color-border-dark; 19 | text-decoration: underline; 20 | } 21 | } 22 | 23 | button { 24 | background-color: $color-grey-light; 25 | color: inherit; 26 | display: block; 27 | margin-left: auto; 28 | margin-right: auto; 29 | min-width: 10em; 30 | padding-right: 3em; 31 | position: relative; 32 | 33 | &:after { 34 | @include icon($icon-chevron-right); 35 | bottom: 0; 36 | font-size: 1.25em; 37 | height: 1em; 38 | line-height: 1; 39 | margin: auto 0; 40 | position: absolute; 41 | right: .75em; 42 | text-shadow: 0 1px 0 #fff, 0 -1px 0 #000; 43 | top: 0; 44 | } 45 | 46 | &:hover { 47 | background-color: darken($color-grey-light, 10%); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/css/organisms/_logger.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #LOGGER 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | .logging-widget { 5 | @extend .widget-container; 6 | 7 | .notifications { 8 | background-color: #fff; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/css/organisms/_report-section.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | #REPORT-SECTION 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | @keyframes flash { 5 | 0% { background-color: transparent; color: inherit;} 6 | 50% { background-color: $color-border; color: #000; } 7 | 100% { background-color: transparent; color: inherit; } 8 | } 9 | 10 | .report-section { 11 | padding: 1em 0 2em; 12 | 13 | h3 { 14 | a { 15 | border-bottom: none; 16 | color: #002B36; 17 | position: relative; 18 | 19 | &:after { 20 | @include icon($icon-chain); 21 | font-size: .75em; 22 | margin-left: .5em; 23 | } 24 | } 25 | 26 | & + p { 27 | margin-top: .5em; 28 | } 29 | } 30 | 31 | .inline-code { 32 | background-color: lightyellow; 33 | padding: 0 .25em; 34 | } 35 | 36 | &:nth-child(even) { 37 | background-color: rgba(0,0,0,.05); // $color-base-light; //#e3e7e7; //rgba(#93A1A1, .25); 38 | border-color: $color-border; 39 | border-style: solid; 40 | border-width: 1px 0; 41 | // color: #4b4b4b; 42 | margin-left: -2em; 43 | margin-right: -2em; 44 | padding-left: 2em; 45 | padding-right: 2em; 46 | } 47 | 48 | & + & { 49 | margin-top: 0em; 50 | } 51 | } 52 | 53 | .polyfills, 54 | .report-notes { 55 | p { 56 | margin: 0; 57 | 58 | & + p { 59 | margin-top: 1em; 60 | } 61 | } 62 | 63 | h4 { 64 | color: #000608; 65 | 66 | & + ol, 67 | & + p { 68 | margin-top: 0; 69 | } 70 | } 71 | 72 | ol { 73 | margin: 0; 74 | } 75 | 76 | li:target { 77 | animation: flash 1s; 78 | } 79 | } 80 | 81 | .support-section { 82 | margin-left: 1em; 83 | 84 | h4 { 85 | color: #000608; 86 | margin-bottom: .25em; 87 | } 88 | 89 | ol { 90 | display: flex; 91 | flex-direction: row; 92 | flex-wrap: wrap; 93 | list-style-type: none; 94 | margin: 0; 95 | padding: 0; 96 | 97 | &:after { 98 | clear: both; 99 | content: ''; 100 | display: table; 101 | } 102 | } 103 | 104 | li { 105 | background: linear-gradient(to bottom, rgba(238,238,238,1) 0%,rgba(204,204,204,1) 100%); 106 | box-shadow: 0 0 1px #000; 107 | color: #000; 108 | float: left; 109 | font-size: .825em; 110 | margin-bottom: .5em; 111 | margin-right: .5em; 112 | min-width: 16em; 113 | padding: .25em .5em; 114 | position: relative; 115 | 116 | &.mobile-browser { 117 | background: linear-gradient(to bottom, rgba(222,222,222,1) 0%,rgba(188,188,188,1) 100%); 118 | // background: linear-gradient(to bottom, rgba(204,204,204,1) 0%,rgba(238,238,238,1) 100%); 119 | // background: linear-gradient(to bottom, rgba(247,251,252,1) 0%,rgba(217,237,242,1) 40%,rgba(173,217,228,1) 100%); 120 | } 121 | } 122 | 123 | .disabled { 124 | border-right: 2em solid dimgrey; 125 | } 126 | 127 | .prefix { 128 | border-right: 2em solid darkblue; 129 | } 130 | 131 | .prefix:hover:before, 132 | .disabled:hover:before { 133 | background-color: #ffffe0; 134 | border-radius: .5em; 135 | bottom: 2.5em; 136 | box-shadow: 0 0 5px #000; 137 | content: attr(data-title); 138 | padding: .5em 1em; 139 | position: absolute; 140 | right: -2em; 141 | width: 16em; 142 | z-index: 30; 143 | } 144 | 145 | .disabled:after, 146 | .prefix:after { 147 | bottom: 0; 148 | color: #fff; 149 | display: block; 150 | height: 1em; 151 | line-height: 1; 152 | margin: auto 0; 153 | position: absolute; 154 | right: -2em; 155 | text-align: center; 156 | 157 | top: 0; 158 | width: 2em; 159 | } 160 | 161 | .disabled:after { 162 | content: 'D'; 163 | } 164 | 165 | .prefix:after { 166 | content: 'P'; 167 | } 168 | 169 | sup { 170 | line-height: 1; 171 | margin-left: .5em; 172 | } 173 | } 174 | 175 | .polyfills { 176 | ul { 177 | list-style-position: inside; 178 | margin: 0; 179 | padding: 0; 180 | } 181 | 182 | a { 183 | margin-right: .5em; 184 | } 185 | } 186 | 187 | .report-index { 188 | background-color: rgba(0,0,0,.125); //#e4e9eb; 189 | box-shadow: inset 0 0 5px #000; 190 | color: #333; 191 | } 192 | 193 | .index-list { 194 | column-count: 2; 195 | list-style-position: inside; 196 | } 197 | -------------------------------------------------------------------------------- /src/css/organisms/_reporter.scss: -------------------------------------------------------------------------------- 1 | /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*\ 2 | REPORTER 3 | \*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -*/ 4 | #widget-report { 5 | @extend .widget-container; 6 | padding-top: 0; 7 | 8 | header { 9 | padding-top: 1.5em; 10 | 11 | > div { 12 | @extend .size-constraint; 13 | &:after { 14 | clear: both; 15 | content: ''; 16 | display: table; 17 | } 18 | } 19 | 20 | h2, 21 | button { 22 | float: left; 23 | } 24 | 25 | button { 26 | line-height: 1.6; 27 | margin-left: 1em; 28 | margin-top: .3em; 29 | } 30 | 31 | &.fixed { 32 | background-color: #fff; 33 | box-shadow: 0 0 5px #000; 34 | left: 0; 35 | padding-bottom: 1.5em; 36 | position: fixed; 37 | right: 0; 38 | top: 0; 39 | z-index: 10; 40 | 41 | & > div { 42 | padding: 0 1em ; 43 | } 44 | 45 | & + div { 46 | padding-top: 68px; 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/css/site.scss: -------------------------------------------------------------------------------- 1 | @import "!base/reset"; 2 | @import "!base/fontface"; 3 | @import "!base/variables"; 4 | @import "!base/utilities"; 5 | 6 | @import "!general/icons"; 7 | @import "!general/main"; 8 | 9 | @import "atoms/buttons"; 10 | @import "atoms/containers"; 11 | @import "atoms/form"; 12 | @import "atoms/headings"; 13 | @import "atoms/scroll-to-top"; 14 | 15 | @import "molecules/columns"; 16 | @import "molecules/footer"; 17 | @import "molecules/header"; 18 | @import "molecules/notifications"; 19 | @import "molecules/show-hide"; 20 | 21 | @import "organisms/about"; 22 | @import "organisms/browser-filter"; 23 | @import "organisms/code-input"; 24 | @import "organisms/logger"; 25 | @import "organisms/report-section"; 26 | @import "organisms/reporter"; 27 | -------------------------------------------------------------------------------- /src/html/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | @@include('./partials/head.html', { 4 | "description" : "See what the JavaScript Compatibility Checker can detect and learn about its planned future.", 5 | "title" : "About | JS Compatibility Checker" 6 | }) 7 | 8 | @@include('./partials/header.html', { 9 | "isAbout": true 10 | }) 11 |
12 |
13 |
14 |

Intro

15 |

16 | Welcome and thank you for your interest. I won't take up too much of your time with a silly intro text, let me get right to the point. 17 |

18 |

19 | You will find here information on which API's can be detected by the JavaScript Compatibility Checker. I will also give some information on what I have planned for this tool in the future. If you have an idea on how I can make the JSCC even more useful, please contact me on Twitter or create an issue on GitHub. 20 |

21 |
22 |
23 | 24 |
25 |
26 |

Detectable APIs

27 |

28 | Below you will find a live list of the APIs the JavaScript Compatibility Checker can detect. If you are missing an API or think you're getting false positives, shoot me a message. 29 |

30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |

Roadmap

39 |

40 | What you're looking at is the first version of the JavaScript Compatibility Checker. When I started this website I had a few things in mind that I wanted to accomplish. Enough got done to make it already a useful tool but that doesn't mean it is done. At the moment I have planned the following features to be added: 41 |

42 |
    43 |
  1. Load in some polyfills so older browsers will be able to run the site
  2. 44 |
  3. Add the ability to filter by browser era so you can limit the report to just, say, the last two versions of each browser
  4. 45 |
  5. Allow more than a single file to be selected at a time. Ideally you should be able to select a project and have one report for everything instead of having to check each file separately
  6. 46 |
  7. Add some information in the report on which code triggered the API detection
  8. 47 |
  9. Add more links to polyfills and add more APIs to detect
  10. 48 |
  11. Add national browser usage figures in addition to the global usage figures
  12. 49 |
50 |

51 | Suggestions on what would make a good addition to the JSCC are always welcome. 52 |

53 |
54 |
55 |
56 | @@include('./partials/footer.html') 57 | @@include('./partials/behaviour-about.html') 58 | 59 | Go back to top 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | @@include('./partials/head.html', { 4 | "description" : "A tool to check your JavaScript code for browser compatibility issues", 5 | "title" : "JS Compatibility Checker" 6 | }) 7 | 8 | @@include('./partials/header.html', { 9 | "isAbout": false 10 | }) 11 |
12 |
13 |
14 | @@include('./partials/code-input.html') 15 |
16 |
17 | 18 |
19 | 101 |
102 | 103 |
104 | @@include('./partials/logger.html') 105 |
106 |
107 | @@include('./partials/footer.html') 108 | @@include('./partials/behaviour.html') 109 | 110 | Go back to top 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/html/partials/behaviour-about.html: -------------------------------------------------------------------------------- 1 | @@if (!deploy) { 2 | 3 | } 4 | 5 | @@if (deploy) { 6 | 7 | 16 | } 17 | -------------------------------------------------------------------------------- /src/html/partials/behaviour.html: -------------------------------------------------------------------------------- 1 | @@if (!deploy) { 2 | 3 | } 4 | 5 | @@if (deploy) { 6 | 7 | 16 | } 17 | -------------------------------------------------------------------------------- /src/html/partials/code-input.html: -------------------------------------------------------------------------------- 1 |
2 |

Please provide some JavaScript to check

3 |
4 | 8 |
9 | Upload a JavaScript file 10 | 13 | 14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/html/partials/footer.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/html/partials/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @@title 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/html/partials/header.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/html/partials/logger.html: -------------------------------------------------------------------------------- 1 |
2 |

Logging

3 |
    4 |
5 |
6 | -------------------------------------------------------------------------------- /src/scripts/About.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | baseUrl : '/scripts', 3 | paths : { 4 | Intermediary : 'vendor/Intermediary' 5 | } 6 | }); 7 | 8 | requirejs(['Intermediary', 'DataStore', 'ScrollTo', 'AjaxLoader'], function(Intermediary, DataStore, ScrollTo, AjaxLoader) { 9 | 'use strict'; 10 | 11 | /** 12 | * Handles the messages received from the loader. 13 | */ 14 | function _onDataLoaderMessage(message, channel) { 15 | // Check the channel on which the message was posted 16 | switch (channel) { 17 | case 'dataloader:download-completed': 18 | // The loader was able to download all the data we need, we can proceed 19 | // with intializing the data store and analyzing the code 20 | DataStore.init(ajaxLoader.getData()); 21 | _listApis(); 22 | break; 23 | } 24 | 25 | // Unsubscribe from message from the data loader 26 | Intermediary.unsubscribe('dataloader', dataLoaderHandler); 27 | // Relaease the data loader and its handler 28 | dataLoaderHandler = null; 29 | ajaxLoader = null; 30 | } 31 | 32 | function _listApis() { 33 | var features = DataStore.getData(), 34 | keys = Object.keys(features), 35 | list = document.createElement('ol'), 36 | index, 37 | ubound; 38 | 39 | list.classList.add('api-list'); 40 | 41 | keys.sort(function(a, b) { 42 | a = features[a].title.toLowerCase(); 43 | b = features[b].title.toLowerCase(); 44 | 45 | if (a < b) { 46 | return -1; 47 | } else if (a > b) { 48 | return 1; 49 | } 50 | return 0; 51 | }); 52 | 53 | for (index = 0, ubound = keys.length; index < ubound; index++) { 54 | var feature = keys[index], 55 | data = features[feature], 56 | item = document.createElement('li'); 57 | 58 | item.appendChild(document.createTextNode(data.title)); 59 | list.appendChild(item); 60 | } 61 | 62 | outputElem.appendChild(list); 63 | } 64 | 65 | var scrollToController = new ScrollTo(), 66 | outputElem = document.getElementById('api-list'), 67 | ajaxLoader = new AjaxLoader(), 68 | dataLoaderHandler = Intermediary.subscribe('dataloader', _onDataLoaderMessage), 69 | sources = { 70 | '/data/additional.json' : null, 71 | '/data/caniuse2.json' : null 72 | }; 73 | 74 | // Try to load the compatibility data 75 | ajaxLoader.loadData(sources); 76 | 77 | scrollToController.init(); 78 | }); 79 | -------------------------------------------------------------------------------- /src/scripts/Ajax.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['Intermediary'], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(require('Intermediary')); 9 | } else { 10 | root.Ajax = factory(root.Intermediary); 11 | } 12 | }(this, function(Module1Intermediary) { 13 | 'use strict'; 14 | 15 | /** 16 | * Iterates over the keys of an object and calls a callback function when the 17 | * key belongs to he object itself and not to its prototype. 18 | */ 19 | function iterate(object, callback) { 20 | for (var key in object) { 21 | if (object.hasOwnProperty(key)) { 22 | callback(key, object[key]); 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * Helper method to merge the default options with the overrides passed 29 | * along to the constructor. 30 | * 31 | * @param {object} overrides The overrides for the default options. These 32 | * values will take precedence over the default 33 | * values. 34 | * 35 | * @returns {object} The method returns an object containing the default 36 | * options with the values override by those of the 37 | * overrides object. 38 | */ 39 | function mergeOptions(overrides) { 40 | var result = {}; 41 | 42 | // Copy the default options to the result object 43 | iterate(exports.options, function(key, value) { 44 | result[key] = value; 45 | }); 46 | 47 | // Iterate over the keys in the overrides object 48 | iterate(overrides, function(key, value) { 49 | // Check if the key for an existing configuration property 50 | if (result[key] !== undefined) { 51 | // Override the default value 52 | result[key] = value; 53 | } 54 | }); 55 | 56 | // Return the merge result 57 | return result; 58 | } 59 | 60 | var exports = function(overrides) { 61 | this._options = mergeOptions(overrides); 62 | }; 63 | 64 | exports.options = { 65 | callbackOnError : null, 66 | callbackOnSuccess : null, 67 | timeout : 5000 68 | }; 69 | 70 | function _clearTimeout(timeoutId) { 71 | if (timeoutId != null) { 72 | clearTimeout(timeoutId); 73 | } 74 | } 75 | 76 | function _sendError(error, callback, url) { 77 | if (callback != null && typeof callback === 'function') { 78 | callback.call({}, { 79 | error : error, 80 | url : url 81 | }); 82 | } 83 | } 84 | 85 | exports.prototype = { 86 | _onError: function() { 87 | this._clearTimeout(this._timeoutId); 88 | _sendError(event.target.status, this._options.callbackOnError, this._url); 89 | }, 90 | 91 | _onLoad: function() { 92 | _clearTimeout(this._timeoutId); 93 | var result; 94 | 95 | // Check if the request completed succesfully 96 | if (this._request.status >= 200 && this._request.status < 400) { 97 | if (this._options.callbackOnSuccess) { 98 | var type = this._request.getResponseHeader('Content-Type'), 99 | regex = /json/gi; 100 | 101 | if (regex.test(type)) { 102 | try { 103 | result = JSON.parse(this._request.response); 104 | } catch (e) { 105 | result = this._request.responseText; 106 | } 107 | } else { 108 | result = this._request.responseText; 109 | } 110 | this._options.callbackOnSuccess.call({}, { 111 | url : this._url, 112 | result : result 113 | }); 114 | } 115 | } else { 116 | 117 | } 118 | }, 119 | 120 | onTimeout: function() { 121 | if (this._request.readyState < 4) { 122 | this._request.abort(); 123 | } 124 | this._timeoutId = null; 125 | }, 126 | 127 | makeRequest: function(url) { 128 | this._url = url; 129 | this._request = new XMLHttpRequest(); 130 | this._request.open('GET', url, true); 131 | 132 | this._request.addEventListener('load', this._onLoad.bind(this)); 133 | this._request.addEventListener('error', this._onError.bind(this)); 134 | 135 | this._timeoutId = setTimeout(this._onTimeout, this._options.timeout); 136 | 137 | this._request.send(); 138 | } 139 | }; 140 | 141 | return exports; 142 | })); 143 | -------------------------------------------------------------------------------- /src/scripts/AjaxLoader.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['Intermediary', 'Ajax'], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(require('Intermediary'), require('Ajax')); 9 | } else { 10 | root.AjaxLoader = factory(root.Intermediary, root.Ajax); 11 | } 12 | }(this, function(Intermediary, Ajax) { 13 | 'use strict'; 14 | 15 | /** 16 | * Iterates over the keys of an object and calls a callback function when the 17 | * key belongs to he object itself and not to its prototype. 18 | */ 19 | function iterate(object, callback) { 20 | for (var key in object) { 21 | if (object.hasOwnProperty(key)) { 22 | callback(key, object[key]); 23 | } 24 | } 25 | } 26 | 27 | var exports = function(overrides) { 28 | this._options = overrides; 29 | 30 | this._attempts = 0; 31 | this._callCount = 0; 32 | this._isReady = false; 33 | }; 34 | 35 | exports.prototype = { 36 | /** 37 | * Load the JSON files containing the compatibility data. 38 | */ 39 | _loadData: function() { 40 | var ref = this; 41 | 42 | this._isReady = false; 43 | this._attempts++; 44 | iterate(this._sources, function(key) { 45 | if (ref._sources[key] == null) { 46 | ref._callCount++; 47 | var ajax = new Ajax({ 48 | callbackOnSuccess : ref._onAjaxSuccess.bind(ref), 49 | callbackOnError : ref._onAjaxError.bind(ref) 50 | }); 51 | 52 | Intermediary.publish('notification:info', { 53 | level : 9, 54 | message : 'Downloading compatibility data from "' + key + '" (attempt ' + ref._attempts + ').' 55 | }); 56 | 57 | ajax.makeRequest(key); 58 | } 59 | }); 60 | }, 61 | 62 | _onAjaxError: function(event) { 63 | // Decrease the number of outstanding calls 64 | this._callCount--; 65 | 66 | // Send out a message about the failed attempt 67 | Intermediary.publish('dataloader:download-failed', { 68 | level : 1, 69 | message : 'Unable to download compatibility data from (' + event.url + ')', 70 | error : event 71 | }); 72 | 73 | // Call the call completed method for some housekeeping 74 | this._onCallCompleted(); 75 | }, 76 | 77 | _onAjaxSuccess: function(event) { 78 | Intermediary.publish('notification:info', { 79 | level : 9, 80 | message : 'Compatibility data from "' + event.url + '" downloaded.' 81 | }); 82 | 83 | // Decrease the number of outstanding calls 84 | this._callCount--; 85 | 86 | // Store the raw data 87 | this._sources[event.url] = event.result; 88 | 89 | // Call the call completed method for some housekeeping 90 | this._onCallCompleted(); 91 | }, 92 | 93 | _onCallCompleted: function() { 94 | var ref = this; 95 | // Check if the call count is 0, if not we need to wait for more data to 96 | // arrive 97 | if (this._callCount !== 0) { 98 | return; 99 | } 100 | 101 | var retry = false; 102 | iterate(this._sources, function(key, data) { 103 | if (data == null) { 104 | retry = true; 105 | } 106 | }); 107 | 108 | if (retry) { 109 | if (this._attempts < 5) { 110 | this._loadData(); 111 | } else { 112 | Intermediary.publish('dataloader:too-many-attempts'); 113 | } 114 | } else { 115 | // Set the ready flag 116 | this._isReady = true; 117 | 118 | // Inform subscribers of the fact that the data has been downloaded 119 | Intermediary.publish('dataloader:download-completed', { 120 | level : 9, 121 | message : 'Compatibility data successfully downloaded' 122 | }); 123 | } 124 | }, 125 | 126 | getData: function() { 127 | return this._sources; 128 | }, 129 | 130 | isReady: function() { 131 | return this._isReady; 132 | }, 133 | 134 | loadData: function(sources) { 135 | this._isReady = false; 136 | this._sources = sources; 137 | this._attempts = 0; 138 | this._loadData(); 139 | } 140 | }; 141 | 142 | return exports; 143 | })); 144 | -------------------------------------------------------------------------------- /src/scripts/App.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | paths : { 3 | Intermediary : 'vendor/Intermediary' 4 | } 5 | }); 6 | 7 | requirejs(['Intermediary', 'CodeInput', 'CodeAnalyzer', 'Reporter', 'DataStore', 'BrowserFilter', 'ShowHide', 'ScrollTo', 'StickyHeader', 'SupportFilter', 'AjaxLoader'], function(Intermediary, CodeInput, CodeAnalyzer, Reporter, DataStore, BrowserFilter, ShowHide, ScrollTo, StickyHeader, SupportFilter, AjaxLoader) { 8 | 'use strict'; 9 | 10 | // 1: We need a correction of -88px, this will ensure the scroll to target 11 | // is positioned below the top of the viewport. If we don't do this the 12 | // top of scroll target will fall under the fixed report header 13 | // 2: Show the scroll to top element as soon as half the viewport height is 14 | // scrolled 15 | // 3: In dire need of a refactor action, this is way too brittle 16 | var codeInputWidget = document.getElementById('code-input'), 17 | codeInputController = new CodeInput(codeInputWidget), 18 | notifications = document.querySelector('.notifications'), 19 | scrollToController, 20 | analyzer, 21 | ajaxLoader, 22 | browserFilter, 23 | reportController, 24 | stickyHeaderController, 25 | showHideController, 26 | supportFilterController, 27 | scrollToConfig = { 28 | correction : -88, /* [1] */ 29 | topThresholdRatio : 2 /* [2] */ 30 | }, 31 | sources = { /* [3] */ 32 | '/data/additional.json' : null, 33 | '/data/caniuse2.json' : null 34 | }, 35 | dataLoaderHandler, 36 | jsCode; 37 | 38 | /* ====================================================================== *\ 39 | LOGGING 40 | \* ====================================================================== */ 41 | function _logMessage(message) { 42 | var item = document.createElement('li'), 43 | date = new Date(); 44 | 45 | item.textContent = ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2) + ':' + ('0' + date.getSeconds()).substr(-2) + '.' + ('00' + date.getMilliseconds()).substr(-3) + ' - ' + message; 46 | notifications.insertBefore(item, notifications.firstChild); 47 | } 48 | 49 | /** 50 | * This method gets calles when the Intermediary has received a message on 51 | * the 'notification' channel 52 | */ 53 | function _onNotification(event, channel) { 54 | if (channel === 'notification:clear') { 55 | while (notifications.firstChild) { 56 | notifications.removeChild(notifications.firstChild); 57 | } 58 | } else { 59 | _logMessage(event.message); 60 | } 61 | } 62 | 63 | 64 | /* ====================================================================== *\ 65 | APPLICATION FLOW 66 | \* ====================================================================== */ 67 | /** 68 | * This handles the message which signals the CodeInput module has code 69 | * which is ready to be analyzed. 70 | */ 71 | function _onCodeInputReady(message) { 72 | // Check if we already have an analyzer, if not create an instance 73 | if (analyzer == null) { 74 | analyzer = new CodeAnalyzer(); 75 | } 76 | 77 | // If the data store is ready we're good to go, we can immediately 78 | // analyze the code the visitor has provided us with 79 | if (DataStore.isReady()) { 80 | // Check the code that was passed on from the CodeInput module 81 | analyzer.check(message.jsCode); 82 | } else { 83 | // Keep the code for later 84 | jsCode = message.jsCode; 85 | // Create a new loader to load the compatibility data 86 | ajaxLoader = new AjaxLoader(); 87 | // Listen to messages from the data loader 88 | dataLoaderHandler = Intermediary.subscribe('dataloader', _onDataLoaderMessage); 89 | // Try to load the compatibility data 90 | ajaxLoader.loadData(sources); 91 | } 92 | } 93 | 94 | function _onCodeAnalyzed(message) { 95 | _logMessage('Code analysis completed'); 96 | _initializeFilters(); 97 | 98 | // Check if the showHideController still needs to be initialized 99 | if (showHideController == null) { 100 | // Create and initialize the ShowHide controller 101 | showHideController = new ShowHide(); 102 | showHideController.init(); 103 | } 104 | 105 | if (stickyHeaderController == null) { 106 | stickyHeaderController = new StickyHeader(document.getElementById('report')); 107 | stickyHeaderController.init(); 108 | } 109 | 110 | var widget = document.getElementById('widget-report'); 111 | reportController = new Reporter(document.getElementById('report-output'), {showFullySupported: false}, DataStore.getAgents()); 112 | reportController.buildReport(analyzer.getMatches(), browserFilter.getFilter()); 113 | reportController.filterSupportSections(supportFilterController.getFilter()); 114 | widget.classList.remove('hidden'); 115 | 116 | scrollToController = new ScrollTo(scrollToConfig); 117 | scrollToController.init(); 118 | 119 | scrollToController.scrollToElement(document.getElementById('report')); 120 | } 121 | 122 | /** 123 | * Handles the messages received from the loader. 124 | */ 125 | function _onDataLoaderMessage(message, channel) { 126 | var cleanUp = false; 127 | 128 | // Check the channel on which the message was posted 129 | switch (channel) { 130 | case 'dataloader:download-completed': 131 | // The loader was able to download all the data we need, we can proceed 132 | // with intializing the data store and analyzing the code 133 | DataStore.init(ajaxLoader.getData()); 134 | analyzer.check(jsCode); 135 | jsCode = null; 136 | cleanUp = true; 137 | break; 138 | case 'dataloader:too-many-attempts': 139 | _logMessage('Exhausted compatibility data download attempts. (' + channel + ')'); 140 | cleanUp = true; 141 | break; 142 | default: 143 | // The compatibility data didn't get downloaded, this needs some 144 | // more to better deal with situations where things didn't go as 145 | // planned 146 | _logMessage('Unable to download necessary data for analysis. (' + channel + ')'); 147 | break; 148 | } 149 | 150 | if (cleanUp) { 151 | // Unsubscribe from message from the data loader 152 | Intermediary.unsubscribe('dataloader', dataLoaderHandler); 153 | // Relaease the data loader and its handler 154 | dataLoaderHandler = null; 155 | ajaxLoader = null; 156 | } 157 | } 158 | 159 | /** 160 | * This handles the message which indicates a filter has been changed 161 | */ 162 | function _onFilterChanged(message) { 163 | // The message will have a sender property, we can use this to determine 164 | // which filter (browser or support) has been changed 165 | switch (message.sender) { 166 | case 'jscc-browser-filter': 167 | reportController.filterBrowsers(browserFilter.getFilter()); 168 | break; 169 | case 'jscc-support-filter': 170 | reportController.filterSupportSections(supportFilterController.getFilter()); 171 | break; 172 | } 173 | } 174 | 175 | /** 176 | * This handles the click events from the index list 177 | */ 178 | function _onClickHandlerIndex(event) { 179 | // We've attached a click event on the list element and not on the 180 | // individual anchors. Before we continue we will have to check if the 181 | // target is an anchor or not 182 | if (event.target.nodeName.toUpperCase() === 'A') { 183 | // The event if from an anchor element, the document will scroll to 184 | // the matching ID. We just need to hide the index from view at 185 | // this point 186 | showHideController.hideDetailView('index', true); 187 | } 188 | } 189 | 190 | function _initializeFilters() { 191 | // Check if the browser filter hasn't yet been initialized 192 | if (browserFilter == null) { 193 | // Create the browser filter 194 | browserFilter = new BrowserFilter(document.getElementById('browser-filter')); 195 | // Initialize it with the agents we got from the compatibility data 196 | browserFilter.init(DataStore.getAgents()); 197 | } 198 | 199 | // Check if the support filter hasn't yet been initialized 200 | if (supportFilterController == null) { 201 | // Create the support filter and initialize it 202 | supportFilterController = new SupportFilter(document.getElementById('support-filter')); 203 | supportFilterController.init(); 204 | } 205 | } 206 | 207 | 208 | /* ====================================================================== *\ 209 | INITIALIZATION 210 | \* ====================================================================== */ 211 | function initInputLabels() { 212 | // Create an input element to check if it has a placeholder property, if 213 | // it does not it means the browser does not support placeholders and we 214 | // need to show the label text 215 | if (document.createElement('input').placeholder == null) { 216 | // Get all the spans inside label elements 217 | var spans = document.querySelectorAll('label > span'); 218 | // Loop over all the elements we found and remove the visually-hidden 219 | // class so the label text will be visible to the visitor 220 | for (var index = 0, ubound = spans.length; index < ubound; index++) { 221 | spans[index].classList.remove('visually-hidden'); 222 | } 223 | } 224 | } 225 | 226 | function init() { 227 | // Initialize the module responsible for letting the user select some JS code 228 | codeInputController.init(); 229 | Intermediary.subscribe('codeinput:ready', _onCodeInputReady); 230 | 231 | // Check if there is an element for writing log messages to, if there is 232 | // we need to subscribe to the channel 233 | if (notifications != null) { 234 | Intermediary.subscribe('notification', _onNotification); 235 | } 236 | 237 | // Create and initialize the scrollTo module 238 | scrollToController = new ScrollTo(scrollToConfig); 239 | scrollToController.init(); 240 | 241 | // Try to get the index list element 242 | var index = document.getElementById('index-list'); 243 | // Add an event listener for the click event if we were able to locate 244 | // the index list element 245 | if (index != null) { 246 | index.addEventListener('click', _onClickHandlerIndex); 247 | } 248 | 249 | // Subscribe to the message indicating the code has been analyzed and a 250 | // report can be generated 251 | Intermediary.subscribe('codeAnalyzed', _onCodeAnalyzed); 252 | // Subscribe to the message indicating a filter has changed and needs to 253 | // be applied on the report 254 | Intermediary.subscribe('filter:filter-changed', _onFilterChanged); 255 | 256 | initInputLabels(); 257 | 258 | _logMessage('Reporting for duty'); 259 | } 260 | 261 | init(); 262 | }); 263 | -------------------------------------------------------------------------------- /src/scripts/BrowserFilter.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['Filter'], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(require('Filter')); 9 | } else { 10 | root.BrowserFilter = factory(root.Filter); 11 | } 12 | }(this, function(Filter) { 13 | 'use strict'; 14 | 15 | /** 16 | * Iterates over the keys of an object and calls a callback function when the 17 | * key belongs to he object itself and not to its prototype. 18 | */ 19 | function iterate(object, callback) { 20 | for (var key in object) { 21 | if (object.hasOwnProperty(key)) { 22 | callback(key, object[key]); 23 | } 24 | } 25 | } 26 | 27 | function _convertAgentsToArray(agents) { 28 | var result = []; 29 | 30 | iterate(agents, function(agent, agentData) { 31 | result.push({ 32 | key : agent, 33 | title : agentData.browser.toLowerCase() 34 | }); 35 | }); 36 | 37 | result.sort(function(a, b) { 38 | if (a.title < b.title) { 39 | return -1; 40 | } else if (a.title > b.title) { 41 | return 1; 42 | } 43 | return 0; 44 | }); 45 | 46 | return result; 47 | } 48 | 49 | var exports = function(element, overrides) { 50 | Filter.call(this, element, exports.options, overrides); 51 | }; 52 | 53 | exports.prototype = Object.create(Filter.prototype); 54 | 55 | exports.options = { 56 | filter : { 57 | and_ch : true, 58 | and_ff : true, 59 | and_uc : true, 60 | chrome : true, 61 | edge : true, 62 | firefox : true, 63 | ie : true, 64 | ios_saf : true, 65 | op_mini : true, 66 | opera : true, 67 | safari : true 68 | }, 69 | storageKey : 'jscc-browser-filter' 70 | }; 71 | 72 | function _emptyElement(element) { 73 | while (element.firstChild) { 74 | element.removeChild(element.firstChild); 75 | } 76 | } 77 | 78 | function _renderFilter(target, agents, agentArray, options) { 79 | var listDesktop = document.getElementById('bf-desktop'), 80 | listMobile = document.getElementById('bf-mobile'); 81 | 82 | // Make sure we found the two lists we need to show the user agents 83 | if (listDesktop == null || listMobile == null) { 84 | return; 85 | } 86 | 87 | // Empty the container 88 | _emptyElement(listDesktop); 89 | _emptyElement(listMobile); 90 | 91 | // Loop over the user agents, this way the user agents will be shown in 92 | // alphabetical order 93 | for (var index = 0, ubound = agentArray.length; index < ubound; index++) { 94 | var agent = agentArray[index].key, 95 | agentData = agents[agent], 96 | item = document.createElement('li'), 97 | checkBox = document.createElement('input'), 98 | label = document.createElement('label'), 99 | textNode = document.createTextNode(agentData.browser); 100 | 101 | checkBox.setAttribute('id', 'chkbox_bf_' + agent); 102 | checkBox.setAttribute('type', 'checkbox'); 103 | checkBox.setAttribute('data-filter-value', agent); 104 | 105 | if (options.filter[agent]) { 106 | checkBox.checked = true; 107 | } else if (options.filter[agent === undefined]) { 108 | options.filter[agent] = false; 109 | } 110 | 111 | label.setAttribute('for', 'chkbox_bf_' + agent); 112 | label.classList.add('toggle-button'); 113 | item.appendChild(checkBox); 114 | label.appendChild(textNode); 115 | item.appendChild(label); 116 | if (agentData.type === 'mobile') { 117 | listMobile.appendChild(item); 118 | } else { 119 | listDesktop.appendChild(item); 120 | } 121 | } 122 | } 123 | 124 | exports.prototype.init = function(agents) { 125 | if (this._element == null) { 126 | return; 127 | } 128 | this.attachOnBeforeUnload(); 129 | this.attachClickHandler(); 130 | this.getFilterFromStorage(); 131 | 132 | // Take the agents object and create an array with the user agents 133 | // sorted alphabetically 134 | var agentarray = _convertAgentsToArray(agents); 135 | // Render the browser filters 136 | _renderFilter(this._element, agents, agentarray, this._options); 137 | }; 138 | 139 | return exports; 140 | })); 141 | -------------------------------------------------------------------------------- /src/scripts/CodeAnalyzer.js: -------------------------------------------------------------------------------- 1 | /* 2 | TODO: 3 | 4 | - Too difficult to properly detect: Dialog element (key: dialog) 5 | - Not a JS technique: Picture element (key: picture) 6 | */ 7 | (function(root, factory) { 8 | 'use strict'; 9 | 10 | if (typeof define === 'function' && define.amd) { 11 | // AMD 12 | define(['DataStore', 'Intermediary'], factory); 13 | } else if (typeof exports === 'object') { 14 | module.exports = factory(require('DataStore'), require('Intermediary')); 15 | } else { 16 | root.CodeAnalyzer = factory(root.DataStore, root.Intermediary); 17 | } 18 | }(this, function(DataStore, Intermediary) { 19 | 'use strict'; 20 | 21 | /** 22 | * Iterates over the keys of an object and calls a callback function when the 23 | * key belongs to he object itself and not to its prototype. 24 | */ 25 | function iterate(object, callback) { 26 | for (var key in object) { 27 | if (object.hasOwnProperty(key)) { 28 | callback(key, object[key]); 29 | } 30 | } 31 | } 32 | 33 | var exports = function(options) { 34 | this._options = options; 35 | this._code = null; 36 | this._fileName = null; 37 | this._matches = []; 38 | }; 39 | 40 | function _runRules(code) { 41 | var cleanCode = _stripComments(code), 42 | features = DataStore.getData(), 43 | matches = []; 44 | 45 | // Send the message to let everyone know we're starting the tests 46 | Intermediary.publish('notification:completed', { 47 | level : 9, 48 | message : 'Testing ' + DataStore.getCategoryCount() + ' features' 49 | }); 50 | 51 | iterate(features, function(feature, data) { 52 | var isMatch; 53 | 54 | for (var index = 0, ubound = data.tests.length; index < ubound; index++) { 55 | isMatch = data.tests[index].test(cleanCode); 56 | if (!isMatch) { 57 | break; 58 | } 59 | } 60 | 61 | if (isMatch) { 62 | matches.push(data); 63 | Intermediary.publish('notification:info', { 64 | level : 1, 65 | message : 'Detected the usage of ' + data.title + ' (' + feature + ')' 66 | }); 67 | } 68 | }); 69 | 70 | return matches; 71 | } 72 | 73 | function _stripComments(code) { 74 | // Strip all comments from the code file, this RegEx is something I have 75 | // not created myself but found on StackOverflow. This is the link to 76 | // the post: http://stackoverflow.com/a/15123777/1244780 77 | return code.replace(/(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm, ''); 78 | } 79 | 80 | exports.prototype = { 81 | check: function(code, fileName) { 82 | this._code = code; 83 | this._fileName = fileName; 84 | 85 | if (DataStore.isReady()) { 86 | this._matches = _runRules(code); 87 | Intermediary.publish('codeAnalyzed', { 88 | sender : this 89 | }); 90 | } else { 91 | Intermediary.publish('notification:error', { 92 | level : 1, 93 | message : 'Make sure compatibility data is loaded before running the CodeAnalyzer' 94 | }); 95 | } 96 | }, 97 | 98 | getFileName: function() { 99 | return this._fileName; 100 | }, 101 | 102 | getMatches: function() { 103 | return this._matches; 104 | } 105 | }; 106 | 107 | return exports; 108 | })); 109 | -------------------------------------------------------------------------------- /src/scripts/CodeInput.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['Intermediary'], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(require('Intermediary')); 9 | } else { 10 | root.CodeInput = factory(root.Intermediary); 11 | } 12 | }(this, function(Intermediary) { 13 | 'use strict'; 14 | 15 | var _element, 16 | _options, 17 | _file, 18 | _fileReader, 19 | _directLoad = false, 20 | exports = function(element, overrides) { 21 | _element = element; 22 | _options = overrides; 23 | }; 24 | 25 | function _checkTextArea() { 26 | var element = _element.querySelector('textarea'); 27 | if (element == null) { 28 | return false; 29 | } 30 | 31 | if (element.value === '') { 32 | Intermediary.publish('notification:info', { 33 | level : 1, 34 | message : 'There is no text in the textarea' 35 | }); 36 | return false; 37 | } else { 38 | _trackEvent('textarea'); 39 | _publishCode(element.value); 40 | return true; 41 | } 42 | } 43 | 44 | function _loadFile(file) { 45 | Intermediary.publish('notification:info', { 46 | level : 9, 47 | message : 'Starting download of "' + file.name + '"' 48 | }); 49 | 50 | _fileReader = new FileReader(); 51 | 52 | _fileReader.onerror = _onErrorFileReader; 53 | _fileReader.onload = _onLoadFileReader; 54 | _fileReader.readAsText(file); 55 | } 56 | 57 | function _onChangeFileInput(event) { 58 | var regex = /javascript/i; 59 | 60 | // Check if a file was selected 61 | if (event.target.files.length > 0) { 62 | // Get the first file from the selection 63 | var item = event.target.files[0]; 64 | 65 | // Check if the file is of the type JavaScript 66 | if (regex.test(item.type)) { 67 | // Show the current selected file 68 | _setSelectedFile(item); 69 | } else { 70 | // Show that no file has been selected 71 | _setSelectedFile(null); 72 | } 73 | 74 | // Check if the direct load flag has been set, if so we will 75 | // automatically start loading the flag 76 | if (_directLoad) { 77 | _loadFile(_file); 78 | } 79 | } 80 | // Reset the direct load flag 81 | _directLoad = false; 82 | } 83 | 84 | function _onClickButton(event) { 85 | Intermediary.publish('notification:clear', {}); 86 | // Check if a file has been selected 87 | if (_file == null) { 88 | // There is no file, check if the textarea contains something we 89 | // can analyse 90 | if (!_checkTextArea()) { 91 | // There is nothing in the textarea, we will trigger the file 92 | // dialog of the input element via code and set the flag to 93 | // immediately load the file after a file has been selected 94 | 95 | // Get the input element 96 | var fileinput = _element.querySelector('input[type="file"]'); 97 | // Make sure we have the input element before we continue 98 | if (fileinput != null) { 99 | // Set the direct load flag 100 | _directLoad = true; 101 | // Trigger the file dialog 102 | fileinput.click(); 103 | } 104 | } 105 | } else { 106 | // A file has been selected, load it 107 | _loadFile(_file); 108 | } 109 | } 110 | 111 | function _onDragEnter(event) { 112 | var dataTransfer = event.dataTransfer, 113 | validFile = false; 114 | 115 | if (dataTransfer.types.indexOf) { 116 | validFile = dataTransfer.types.indexOf('Files') !== -1; 117 | } else { 118 | validFile = dataTransfer.types.contains('Files'); 119 | } 120 | 121 | if (validFile) { 122 | event.target.classList.add('on-drag'); 123 | } 124 | } 125 | 126 | function _onDragLeave(event) { 127 | event.target.classList.remove('on-drag'); 128 | } 129 | 130 | function _onDragOver(event) { 131 | // This is the most important one of the drag events. Forget to attach a 132 | // listener for the dragover event and you can kiss goodbye the drop 133 | // event. 134 | event.preventDefault(); 135 | event.stopPropagation(); 136 | event.dataTransfer.dropEffect = 'copy'; 137 | } 138 | 139 | function _onDrop(event) { 140 | event.preventDefault(); 141 | event.stopPropagation(); 142 | 143 | event.target.classList.remove('on-drag'); 144 | 145 | var item = event.dataTransfer.files[0]; 146 | if ((/javascript/gi).test(item.type)) { 147 | _setSelectedFile(item); 148 | } else { 149 | _setSelectedFile(null); 150 | } 151 | } 152 | 153 | function _onErrorFileReader(event) { 154 | var message; 155 | 156 | switch (event.target.error.name) { 157 | case 'NotFoundError': 158 | message = 'File not found'; 159 | break; 160 | case 'NotReadableError': 161 | message = 'Unable to read file'; 162 | break; 163 | case 'AbortError': 164 | message = 'File read aborted'; 165 | break; 166 | default: 167 | message = 'An error occured reading the file'; 168 | } 169 | 170 | Intermediary.publish('notification:error', { 171 | level : 1, 172 | message : message 173 | }); 174 | } 175 | 176 | function _onLoadFileReader(event) { 177 | Intermediary.publish('notification:info', { 178 | level : 9, 179 | message : 'File downloaded' 180 | }); 181 | 182 | var fileContent = _fileReader.result; 183 | _trackEvent('file'); 184 | _publishCode(fileContent); 185 | } 186 | 187 | function _publishCode(code) { 188 | Intermediary.publish('codeinput:ready', { 189 | jsCode : code 190 | }); 191 | } 192 | 193 | function _setSelectedFile(newValue) { 194 | var element = _element.querySelector('.drop-target'); 195 | 196 | _file = newValue; 197 | 198 | if (element == null) { 199 | return; 200 | } 201 | 202 | if (_file == null) { 203 | element.textContent = 'Selected file does not appear to be a javascript file. Click to select another file or try dropping another file.'; 204 | } else { 205 | element.textContent = '"' + _file.name + '" selected. Click to select another file or try dropping another file.'; 206 | } 207 | } 208 | 209 | function _trackEvent(value) { 210 | if (window.ga != null) { 211 | ga('send', 'event', 'code-input', 'code-submit', value); 212 | } 213 | } 214 | 215 | exports.prototype = { 216 | init: function() { 217 | if (_element == null) { 218 | return; 219 | } 220 | 221 | if (window.FileReader == null) { 222 | var fileInput = document.getElementById('file-selector'); 223 | if (fileInput != null) { 224 | fileInput.parentNode.removeChild(fileInput); 225 | fileInput = null; 226 | } 227 | return; 228 | } 229 | 230 | var element = _element.querySelector('.drop-target'); 231 | if (element != null) { 232 | element.addEventListener('dragenter', _onDragEnter); 233 | element.addEventListener('dragleave', _onDragLeave); 234 | element.addEventListener('dragover', _onDragOver); 235 | element.addEventListener('drop', _onDrop); 236 | } 237 | 238 | element = _element.querySelector('input[type="file"]'); 239 | if (element != null) { 240 | element.addEventListener('change', _onChangeFileInput); 241 | } 242 | 243 | element = _element.querySelector('button[type="button"]'); 244 | if (element) { 245 | element.addEventListener('click', _onClickButton); 246 | } 247 | } 248 | }; 249 | 250 | return exports; 251 | })); 252 | -------------------------------------------------------------------------------- /src/scripts/Filter.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['Intermediary', 'LocalStorage'], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(require('Intermediary'), require('LocalStorage')); 9 | } else { 10 | root.Filter = factory(root.Intermediary, root.LocalStorage); 11 | } 12 | }(this, function(Intermediary, LocalStorage) { 13 | 'use strict'; 14 | 15 | 16 | /** 17 | * Iterates over the keys of an object and calls a callback function when the 18 | * key belongs to he object itself and not to its prototype. 19 | */ 20 | function iterate(object, callback) { 21 | for (var key in object) { 22 | if (object.hasOwnProperty(key)) { 23 | callback(key, object[key]); 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * Helper method to merge the default options with the overrides passed 30 | * along to the constructor. 31 | * 32 | * @param {object} overrides The overrides for the default options. These 33 | * values will take precedence over the default 34 | * values. 35 | * 36 | * @returns {object} The method returns an object containing the default 37 | * options with the values override by those of the 38 | * overrides object. 39 | */ 40 | function mergeOptions(overrides, source) { 41 | var result = {}; 42 | 43 | source = (source != null) ? source : exports.options; 44 | 45 | // Copy the default options to the result object 46 | iterate(source, function(key, value) { 47 | result[key] = value; 48 | }); 49 | 50 | // Iterate over the keys in the overrides object 51 | iterate(overrides, function(key, value) { 52 | // Check if the key for an existing configuration property 53 | if (result[key] !== undefined) { 54 | // Override the default value 55 | result[key] = value; 56 | } 57 | }); 58 | 59 | // Return the merge result 60 | return result; 61 | } 62 | 63 | var exports = function(element, options, overrides) { 64 | this._element = element; 65 | this._options = mergeOptions(options); 66 | this._options = mergeOptions(overrides, this._options); 67 | this._consent = false; 68 | }; 69 | 70 | exports.options = { 71 | filter : {}, 72 | storageKey : '' 73 | }; 74 | 75 | exports.prototype = { 76 | _onBeforeUnload: function(event) { 77 | var store = new LocalStorage(); 78 | if (this._consent) { 79 | if (store.isAvailable) { 80 | store.set(this._options.storageKey, this.getFilter(true)); 81 | } 82 | } else { 83 | if (store.isAvailable) { 84 | store.remove(this._options.storageKey); 85 | } 86 | } 87 | }, 88 | 89 | _onClickHandler: function(event) { 90 | var target = event.target; 91 | // We only want to process the click event for input elements 92 | if (target.tagName.toLowerCase() === 'input') { 93 | // Check if the input element has a filter value 94 | if (target.hasAttribute('data-filter-value')) { 95 | this._options.filter[target.getAttribute('data-filter-value')] = target.checked; 96 | Intermediary.publish('filter:filter-changed', { 97 | sender : this._options.storageKey 98 | }); 99 | } else if (target.hasAttribute('data-consent')) { 100 | // The input element has the data-consent attribute, we need 101 | // to flip the value 102 | this._consent = target.checked; 103 | // Alert everyone of the change in the consent 104 | Intermediary.publish('filter:consent-changed', { 105 | sender : this._options.storageKey 106 | }); 107 | } 108 | } 109 | }, 110 | 111 | _onConsentChanged: function(event) { 112 | // Ignore the message if this instance was the sender of the event 113 | if (event.sender !== this._options.storageKey) { 114 | this._consent = !this._consent; 115 | var element = this._element.querySelector('[data-consent]'); 116 | if (element != null) { 117 | element.checked = this._consent; 118 | } 119 | } 120 | }, 121 | 122 | attachClickHandler: function() { 123 | if (this._element != null) { 124 | this._element.addEventListener('click', this._onClickHandler.bind(this)); 125 | Intermediary.subscribe('filter:consent-changed', this._onConsentChanged, this); 126 | } 127 | }, 128 | 129 | attachOnBeforeUnload: function() { 130 | // Add an event listener for the beforeunload event, this should be fire 131 | // when the user navigates away from the page 132 | window.addEventListener('beforeunload', this._onBeforeUnload.bind(this)); 133 | }, 134 | 135 | getFilter: function(onlyEnabledKeys) { 136 | if (onlyEnabledKeys) { 137 | var result = {}; 138 | iterate(this._options.filter, function(key, isEnabled) { 139 | if (isEnabled) { 140 | result[key] = true; 141 | } 142 | }); 143 | return result; 144 | } else { 145 | return this._options.filter; 146 | } 147 | }, 148 | 149 | getFilterFromStorage: function() { 150 | var store = new LocalStorage(); 151 | 152 | // Check if the store is available 153 | if (store.isAvailable) { 154 | // Get the data from the local storage 155 | var value = store.get(this._options.storageKey); 156 | if (value != null) { 157 | this._options.filter = value; 158 | this._consent = true; 159 | } 160 | } 161 | if (this._consent) { 162 | var element = this._element.querySelector('[data-consent]'); 163 | if (element != null) { 164 | element.checked = this._consent; 165 | } 166 | } 167 | } 168 | }; 169 | 170 | return exports; 171 | })); 172 | -------------------------------------------------------------------------------- /src/scripts/LocalStorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module is a wrapper for the local storage object. It checks if local storage is available 3 | * and has some convenience methods to interact with the local storage. 4 | * 5 | * Even though at the start we check if the local storage is available this isn't enough ensurance 6 | * the local storage will actually be available when we use one of the methods to interact with it. 7 | * One of such scenarios is iOS7 where the user can switch private mode on and off at any given time. 8 | * If private mode was off when the module was instantiated and the user later switched private mode 9 | * on then any attempt to interact with the local storage will fail. 10 | * 11 | * 12 | * BROWSER COMPATIBILITY: 13 | * - This module uses Object.defineProperty and is compatible with IE9 and newer. 14 | */ 15 | (function(root, factory) { 16 | 'use strict'; 17 | 18 | /* istanbul ignore next */ 19 | if (typeof define === 'function' && define.amd) { 20 | // AMD 21 | define([], factory); 22 | } else if (typeof exports === 'object') { 23 | module.exports = factory(); 24 | } else { 25 | root.LocalStorage = factory(); 26 | } 27 | }(this, function() { 28 | 'use strict'; 29 | 30 | var exports = function() { 31 | // Intialize the _enabled flag to true 32 | this._enabled = true; 33 | // Test the availability of the local storage 34 | this.testAvailability(); 35 | 36 | Object.defineProperty(this, 'enabled', { 37 | get: function() { 38 | return (this._initialized && this._enabled); 39 | }, 40 | set: function(newValue) { 41 | this._enabled = newValue; 42 | } 43 | }); 44 | 45 | /** 46 | * The property isAvailable indicates whether or not local storge is available to the website. 47 | * 48 | * @returns {Boolean} The result is true when local storage is available; otherwise false. 49 | */ 50 | Object.defineProperty(this, 'isAvailable', { 51 | get: function() { 52 | return this._initialized; 53 | }, 54 | set: function(newValue) { 55 | throw new Error('The property isAvailable on the LocalStorage object is read-only'); 56 | } 57 | }); 58 | }; 59 | 60 | function deserialize(data) { 61 | try { 62 | return JSON.parse(data); 63 | } catch (error) { 64 | return data; 65 | } 66 | } 67 | 68 | function serialize(data) { 69 | // After extensive testing I was unable to find a case where JSON.stringify would 70 | // throw an error. It is therefor my opinion there is no need for a try...catch 71 | return JSON.stringify(data); 72 | } 73 | 74 | exports.prototype = { 75 | /** 76 | * Removes all keys in the local storage. 77 | */ 78 | clear: function() { 79 | // Check if the local storage has been initialized 80 | if (this.isAvailable) { 81 | // Clear the local storage 82 | try { 83 | localStorage.clear(); 84 | } catch (error) { 85 | this._initialized = false; 86 | } 87 | } 88 | return this._initialized; 89 | }, 90 | 91 | /** 92 | * Returns the value for the specified key. 93 | * 94 | * @param {String} key The key whose value should be retrieved from local storage. 95 | * 96 | * @returns {Object} Returns the value for the specified key. The value is deserialized 97 | * before it is returned. If local storage is not available this 98 | * method will return null. 99 | */ 100 | get: function(key) { 101 | // If the module has been intialized succesfully we don't have to continue 102 | if (!this.isAvailable) { 103 | return null; 104 | } 105 | 106 | try { 107 | // Return the deserialized value for the specified key 108 | return deserialize(localStorage.getItem(key)); 109 | } catch (error) { 110 | this._initialized = false; 111 | return null; 112 | } 113 | }, 114 | 115 | /** 116 | * Removes the provided key from local storage. 117 | * 118 | * @param {String} key The key which should be removed from local storage. 119 | */ 120 | remove: function(key) { 121 | // Check if the local storage has been initialized 122 | if (this.isAvailable) { 123 | // Local storage is available, remove the specified key 124 | try { 125 | localStorage.removeItem(key); 126 | } catch (error) { 127 | this._initialized = false; 128 | } 129 | } 130 | return this._initialized; 131 | }, 132 | 133 | /** 134 | * Stores the value for the specified key in local storage. If local storage is not 135 | * available the method will return with performing any action. 136 | * 137 | * @param {String} key The key which can be used to retrieved the data that will be stored. 138 | * @param {Object} value The value which should be stored under the specified key. The value will 139 | * be serialized before it is written to the local storage. If value is null 140 | * the key will be removed from local storage. This is the same as calling 141 | * the remove method. 142 | */ 143 | set: function(key, value) { 144 | // If the module has been intialized succesfully we don't have to continue 145 | if (!this.isAvailable) { 146 | return false; 147 | } 148 | // Check if the value is null, if so we should remove the specified key from local storage 149 | if (value == null) { 150 | // Remove the key from the local storage 151 | return this.remove(key); 152 | } else { 153 | // Store the value under the provided key after we first serialize the value 154 | try { 155 | localStorage.setItem(key, serialize(value)); 156 | } catch (error) { 157 | this._initialized = false; 158 | } 159 | } 160 | return this._initialized; 161 | }, 162 | 163 | /** 164 | * Tests the availability of the local storage. 165 | * 166 | * @returns Returns true when the local storage is available; otherwise false. 167 | */ 168 | testAvailability: function() { 169 | var testKey = '__lsp__'; 170 | 171 | try { 172 | // Attempt to set the test key 173 | localStorage.setItem(testKey, testKey); 174 | // The local storage is available if getItem returns the same value as we tried to store 175 | this._initialized = (localStorage.getItem(testKey) === testKey); 176 | // Remove our test key 177 | localStorage.removeItem(testKey); 178 | } catch (error) { 179 | // On error occured, the local storage is not available 180 | this._initialized = false; 181 | } 182 | 183 | return this._initialized; 184 | } 185 | }; 186 | 187 | return exports; 188 | })); 189 | -------------------------------------------------------------------------------- /src/scripts/Reporter.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define([], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(); 9 | } else { 10 | root.Reporter = factory(); 11 | } 12 | }(this, function() { 13 | 'use strict'; 14 | 15 | /** 16 | * Iterates over the keys of an object and calls a callback function when the 17 | * key belongs to he object itself and not to its prototype. 18 | */ 19 | function iterate(object, callback) { 20 | for (var key in object) { 21 | if (object.hasOwnProperty(key)) { 22 | callback(key, object[key]); 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * Helper method to merge the default options with the overrides passed 29 | * along to the constructor. 30 | * 31 | * @param {object} overrides The overrides for the default options. These 32 | * values will take precedence over the default 33 | * values. 34 | * 35 | * @returns {object} The method returns an object containing the default 36 | * options with the values override by those of the 37 | * overrides object. 38 | */ 39 | function mergeOptions(overrides) { 40 | var result = {}; 41 | 42 | // Copy the default options to the result object 43 | iterate(exports.options, function(key, value) { 44 | result[key] = value; 45 | }); 46 | 47 | // Iterate over the keys in the overrides object 48 | iterate(overrides, function(key, value) { 49 | // Check if the key for an existing configuration property 50 | if (result[key] !== undefined) { 51 | // Override the default value 52 | result[key] = value; 53 | } 54 | }); 55 | 56 | // Return the merge result 57 | return result; 58 | } 59 | 60 | function _convertAgentsToArray(agents) { 61 | var result = []; 62 | 63 | iterate(agents, function(agent, agentData) { 64 | result.push({ 65 | key : agent, 66 | title : agentData.browser.toLowerCase() 67 | }); 68 | }); 69 | 70 | result.sort(function(a, b) { 71 | if (a.title < b.title) { 72 | return -1; 73 | } else if (a.title > b.title) { 74 | return 1; 75 | } 76 | return 0; 77 | }); 78 | 79 | return result; 80 | } 81 | 82 | 83 | var exports = function(element, overrides, agents) { 84 | this._element = element; 85 | this._options = mergeOptions(overrides); 86 | this._agents = agents; 87 | this._agentsArray = _convertAgentsToArray(agents); 88 | this._index = document.getElementById('index-list'); 89 | }; 90 | 91 | exports.options = { 92 | groupVersions : true, 93 | showFullySupported : true, 94 | supportOrder : ['u', 'n', 'p', 'a', 'x', 'd', 'y'] 95 | }; 96 | 97 | 98 | /* ====================================================================== *\ 99 | # 100 | \* ====================================================================== */ 101 | 102 | function _addIndexEntry(title, key, index) { 103 | var anchor = document.createElement('a'), 104 | item = document.createElement('li'); 105 | 106 | anchor.appendChild(document.createTextNode(title)); 107 | anchor.setAttribute('href', '#' + key); 108 | 109 | item.appendChild(anchor); 110 | index.appendChild(item); 111 | } 112 | 113 | /** 114 | * Adds links to notes to an item 115 | * @param {HTMLElement} item The HTML element which is the parent to the 116 | * link(s) to the note(s) 117 | * @param {Array} versions The array with versions which need to be 118 | * checked for notes. 119 | */ 120 | function _addNoteLink(item, versions) { 121 | var notes = {}, 122 | insertLink = function(note) { 123 | // 1: The link to the note 124 | // 2: We will place the anchor in a super-element 125 | // 3: This regex will get the number to the note from the link 126 | // 4: This is the note number, it will be the text for the link 127 | var anchor = document.createElement('a'), /* [1] */ 128 | sup = document.createElement('sup'), /* [2] */ 129 | regex = /\d+/, /* [3] */ 130 | noteText = regex.exec(note); /* [4] */ 131 | 132 | anchor.setAttribute('href', note); 133 | anchor.setAttribute('title', 'Go to note ' + noteText); 134 | anchor.appendChild(document.createTextNode(noteText)); 135 | sup.appendChild(anchor); 136 | item.appendChild(sup); 137 | }; 138 | 139 | // Make sure versions has a value, if not we're done 140 | if (versions == null) { 141 | return; 142 | } 143 | 144 | // Check if versions is an array 145 | if (Array.isArray(versions)) { 146 | // Loop over the items in the array 147 | for (var index = 0, ubound = versions.length; index < ubound; index++) { 148 | // Get an easy reference to the current item in the array 149 | var currentVersion = versions[index]; 150 | // Check if the current version has a note and make sure the note is not 151 | // yet in the object we have created to keep track of the notes we've 152 | // made a link for 153 | if (currentVersion.note != null && notes[currentVersion.note] == null) { 154 | // Add an entry in the object, if we come across this link again the 155 | // if statement above will be false and thus we prevent the same link 156 | // from appearing more than once 157 | notes[currentVersion.note] = true; 158 | // Insert a link to note in the item 159 | insertLink(currentVersion.note); 160 | } 161 | } 162 | } else if (typeof versions === 'object') { 163 | // The versions is an object, check if it has a note property 164 | if (versions.note != null) { 165 | // The object has a note property, use it to create a link to 166 | // the to note 167 | insertLink(versions.note); 168 | } 169 | } else if (typeof versions === 'string') { 170 | // We've received a string, use it to create the link to the note 171 | insertLink(versions); 172 | } 173 | } 174 | 175 | function _applyFilter(filter, query) { 176 | iterate(filter, function(value, isVisible) { 177 | var elements = document.querySelectorAll('[' + query + '="' + value + '"]'); 178 | 179 | // Loop over the returned elements 180 | for (var index = 0, ubound = elements.length; index < ubound; index++) { 181 | // Check if this user agent should be visible and set/remove 182 | // the hide class accordingly 183 | if (isVisible) { 184 | elements[index].classList.remove('hidden'); 185 | } else { 186 | elements[index].classList.add('hidden'); 187 | } 188 | } 189 | }); 190 | } 191 | 192 | /** 193 | * Removes all the DOM children from an element. 194 | */ 195 | function _clearReport(element) { 196 | // As long as target has a first child we will remove the first child 197 | // from the target. There is little difference between removing the 198 | // first child vs removing the last child as seen in this perf test: 199 | // http://jsperf.com/innerhtml-vs-removechild/15 200 | while (element.firstChild) { 201 | element.removeChild(element.firstChild); 202 | } 203 | } 204 | 205 | function _createCategoryContainer(category) { 206 | var section = document.createElement('section'), 207 | title = document.createElement('h3'), 208 | desc = document.createElement('p'); 209 | 210 | // Set the texts for title and the description 211 | if (category.spec !== '') { 212 | var link = document.createElement('a'); 213 | link.textContent = category.title; 214 | link.setAttribute('title', 'Go to specification'); 215 | link.setAttribute('href', category.spec); 216 | title.appendChild(link); 217 | } else { 218 | title.textContent = category.title; 219 | } 220 | desc.textContent = category.description; 221 | 222 | // Add the elements to the section 223 | section.classList.add('report-section'); 224 | section.setAttribute('id', category.key); 225 | section.appendChild(title); 226 | section.appendChild(desc); 227 | 228 | return section; 229 | } 230 | 231 | /** 232 | * Creates a section with a title and list which can be used to represent a 233 | * single support section. 234 | */ 235 | function _createSupportSection(supportValue) { 236 | // Create the DOM elements we need to for the support section, it is the 237 | // section itself [1], a list to place the browsers in [2] and a title 238 | // element to show the support category [3] 239 | var section = document.createElement('section'), /* [1] */ 240 | list = document.createElement('ol'), /* [2] */ 241 | title = document.createElement('h4'); /* [3] */ 242 | 243 | // Add a class to the section so it can be styled the way we want to 244 | section.classList.add('support-section'); 245 | 246 | // Check what kind of support section we're creating 247 | switch (supportValue) { 248 | case 'n': 249 | title.appendChild(document.createTextNode('No support')); 250 | break; 251 | case 'y': 252 | title.appendChild(document.createTextNode('Full support')); 253 | break; 254 | case 'p': 255 | title.appendChild(document.createTextNode('Support through polyfill')); 256 | break; 257 | case 'u': 258 | title.appendChild(document.createTextNode('Unknown support')); 259 | break; 260 | case 'a': 261 | title.appendChild(document.createTextNode('Partial support')); 262 | break; 263 | default: 264 | title.appendChild(document.createTextNode('SUPPORT VALUE:' + supportValue)); 265 | } 266 | // Set the data-support attribute so we can use this to filter out 267 | // certain categories the visitor doesn't want to see 268 | section.setAttribute('data-support', supportValue); 269 | // Add the title to the section 270 | section.appendChild(title); 271 | // Add the list to the section 272 | section.appendChild(list); 273 | 274 | // Return the result object with a reference to the section [1], the 275 | // list for the browsers [2], then number of items in the list [3] and 276 | // the global browser share [4] 277 | return { 278 | section : section, /* [1] */ 279 | list : list, /* [2] */ 280 | count : 0, /* [3] */ 281 | usage : 0 /* [4] */ 282 | }; 283 | } 284 | 285 | function _renderBrowsers(key, list, browser, agentName, supportObject, collate, browserFilter) { 286 | var item, 287 | index, 288 | ubound, 289 | currentVersion, 290 | isVisible = browserFilter[browser], 291 | setDataTitle = function(version, item) { 292 | if (version.disabled) { 293 | item.classList.add('disabled'); 294 | item.setAttribute('data-title', 'Feature disabled by default, needs to be enabled through a flag or similar action'); 295 | } 296 | if (version.needsPrefix) { 297 | item.classList.add('prefix'); 298 | item.setAttribute('data-title', 'The prefix "' + version.needsPrefix + '" is required to use this feature'); 299 | } 300 | }; 301 | 302 | if (collate) { 303 | item = document.createElement('li'); 304 | if (supportObject.fromVersion === supportObject.toVersion) { 305 | item.appendChild(document.createTextNode(agentName + ' ' + supportObject.fromVersion)); 306 | } else { 307 | item.appendChild(document.createTextNode(agentName + ' ' + supportObject.fromVersion + ' to ' + supportObject.toVersion)); 308 | } 309 | item.setAttribute('data-browser', browser); 310 | if (!isVisible) { 311 | item.classList.add('hidden'); 312 | } 313 | _addNoteLink(item, supportObject.versions); 314 | 315 | if (supportObject.isMobileBrowser) { 316 | item.classList.add('mobile-browser'); 317 | } 318 | 319 | for (index = 0, ubound = supportObject.versions.length; index < ubound; index++) { 320 | setDataTitle(supportObject.versions[index], item); 321 | } 322 | 323 | list.appendChild(item); 324 | } else { 325 | for (index = 0, ubound = supportObject.versions.length; index < ubound; index++) { 326 | currentVersion = supportObject.versions[index]; 327 | item = document.createElement('li'); 328 | item.appendChild(document.createTextNode(agentName + ' ' + currentVersion.version)); 329 | if (currentVersion.note != null) { 330 | _addNoteLink(item, currentVersion.note); 331 | } 332 | if (supportObject.isMobileBrowser) { 333 | item.classList.add('mobile-browser'); 334 | } 335 | setDataTitle(currentVersion, item); 336 | item.setAttribute('data-browser', browser); 337 | if (!isVisible) { 338 | item.classList.add('hidden'); 339 | } 340 | list.appendChild(item); 341 | } 342 | } 343 | } 344 | 345 | /** 346 | * Creates a section and adds the notes to it. 347 | * @param {[type]} notes [description] 348 | */ 349 | function _renderNotes(notes) { 350 | var section; 351 | 352 | // Make sure there are notes to display before we create the section 353 | if (notes == null) { 354 | return; 355 | } 356 | 357 | // Create a section 358 | section = document.createElement('section'); 359 | // Add the CSS class report-notes so we can style it as we want 360 | section.classList.add('report-notes'); 361 | // Add the notes to the innerHTML as the notes is a string containing 362 | // HTML tags. This way the string will be parsed to HTML and all the 363 | // elements in the string will get created 364 | section.innerHTML = notes; 365 | 366 | // Return the section 367 | return section; 368 | } 369 | 370 | 371 | function _renderNoProblems(target) { 372 | var section = document.createElement('section'), 373 | title = document.createElement('h3'), 374 | desc = document.createElement('p'); 375 | 376 | title.textContent = 'Congrats!'; 377 | desc.textContent = 'The code you provided did not contain anything that might give compatibility problems. Yeey! (or perhaps there is but went undetected, don\'t you feel better now?)'; 378 | 379 | // Add the elements to the section 380 | section.appendChild(title); 381 | section.appendChild(desc); 382 | 383 | target.appendChild(section); 384 | } 385 | 386 | function _renderPolyfillLinks(links, target) { 387 | var section = document.createElement('section'), 388 | title = document.createElement('h4'), 389 | list = document.createElement('ul'), 390 | regex = /^http(?:s)?:\/\/(\S*?)\//; 391 | 392 | // Loop over the links 393 | for (var index = 0, ubound = links.length; index < ubound; index++) { 394 | // 1: This will be the link to the polyfill 395 | // 2: This will be the list item which holds the anchor 396 | var anchor = document.createElement('a'), /* [1] */ 397 | item = document.createElement('li'); /* [2] */ 398 | // Set the title of the anchor 399 | anchor.appendChild(document.createTextNode(links[index].title)); 400 | // Set the href attribute to the URL for the polyfill 401 | anchor.setAttribute('href', links[index].url); 402 | // Add the anchor to the list item 403 | item.appendChild(anchor); 404 | // Add the domain of the URL to the list item 405 | item.appendChild(document.createTextNode('[' + links[index].url.match(regex)[1] + ']')); 406 | // Add the list item to the list 407 | list.appendChild(item); 408 | } 409 | 410 | // Check how many polyfill links there are and make sure the text is in 411 | // singular or plural according to the number of links 412 | if (links.length === 1) { 413 | title.appendChild(document.createTextNode('Polyfill script:')); 414 | } else { 415 | title.appendChild(document.createTextNode('Polyfill scripts:')); 416 | } 417 | // Add the title to the polyfill section 418 | section.appendChild(title); 419 | // Add a class to the polyfill section so we an style it 420 | section.classList.add('polyfills'); 421 | // Add the list to the polyfill section 422 | section.appendChild(list); 423 | // Add the polyfill section to its parent 424 | target.appendChild(section); 425 | } 426 | 427 | exports.prototype = { 428 | _renderCategoryExt: function(category, browserFilter) { 429 | var section = _createCategoryContainer(category), 430 | notes, 431 | supportSections = {}, 432 | index, 433 | ubound, 434 | agents = this._agents, 435 | options = this._options; 436 | 437 | _addIndexEntry(category.title, category.key, this._index); 438 | 439 | for (index = 0, ubound = this._agentsArray.length; index < ubound; index++) { 440 | var browser = this._agentsArray[index].key, 441 | supportObjects = category.stats[browser]; 442 | 443 | for (var index2 = 0, ubound2 = supportObjects.length; index2 < ubound2; index2++) { 444 | var supportObject = supportObjects[index2], 445 | supportValue = supportObject.support.substr(0, 1).toLowerCase(); 446 | 447 | if (supportSections[supportValue] == null) { 448 | supportSections[supportValue] = _createSupportSection(supportValue); 449 | } 450 | 451 | var list = supportSections[supportValue].list; 452 | 453 | _renderBrowsers(category.key, list, browser, agents[browser].browser, supportObject, options.groupVersions, browserFilter); 454 | supportSections[supportValue].usage += supportObject.totalGlobalUsage; 455 | } 456 | } 457 | 458 | for (index = 0, ubound = this._options.supportOrder.length; index < ubound; index++) { 459 | var value = this._options.supportOrder[index]; 460 | if (supportSections[value] != null) { 461 | var title = supportSections[value].section.querySelector('h4'); 462 | if (title != null) { 463 | title.appendChild(document.createTextNode(' (' + supportSections[value].usage.toFixed(1) + '% global browser share)')); 464 | } 465 | section.appendChild(supportSections[value].section); 466 | } 467 | } 468 | 469 | // Check if there is a section for the polyfills and that there are links to polyfills 470 | if (supportSections.p != null && category.links != null && category.links.length > 0) { 471 | // Render the links to the polyfills 472 | _renderPolyfillLinks(category.links, section); 473 | } 474 | 475 | // Check if there is a section with notes for the category 476 | if (category.notes != null) { 477 | notes = _renderNotes(category.notes); 478 | // Add the notes section to the category section 479 | section.appendChild(notes); 480 | } 481 | // Add the category section to the document 482 | this._element.appendChild(section); 483 | }, 484 | 485 | buildReport: function(data, browserFilter) { 486 | // Make sure there is an element specified to render the report in 487 | if (this._element == null) { 488 | return; 489 | } 490 | 491 | // Remove any existing report 492 | _clearReport(this._element); 493 | 494 | if (this._index != null) { 495 | _clearReport(this._index); 496 | } 497 | 498 | // When there is no data it means no compatibility problems were found 499 | if (data == null || data.length === 0) { 500 | _renderNoProblems(this._element); 501 | } 502 | 503 | // Sort the data to make sure the features are listed alphabetically 504 | data = data.sort(function(a, b) { 505 | if (a.title.toLowerCase() < b.title.toLowerCase()) { 506 | return -1; 507 | } else if (a.title.toLowerCase() > b.title.toLowerCase()) { 508 | return 1; 509 | } 510 | return 0; 511 | }); 512 | 513 | // Loop over all the features found in the code 514 | for (var index = 0, ubound = data.length; index < ubound; index++) { 515 | var item = data[index]; 516 | // Render a report item for the feature 517 | this._renderCategoryExt(item, browserFilter); 518 | } 519 | }, 520 | 521 | filterBrowsers: function(browserFilter) { 522 | _applyFilter(browserFilter, 'data-browser'); 523 | }, 524 | 525 | filterSupportSections: function(supportFilter) { 526 | _applyFilter(supportFilter, 'data-support'); 527 | } 528 | }; 529 | 530 | return exports; 531 | })); 532 | -------------------------------------------------------------------------------- /src/scripts/ScrollTo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ScrollTo.js - version 1.0.0 3 | * 4 | * A class for animating the jump from an anchor to its target within the same 5 | * document. It also has support for a scroll to top element which can be 6 | * hidden/made visible based on the scroll position of the document. 7 | */ 8 | (function(root, factory) { 9 | 'use strict'; 10 | 11 | if (typeof define === 'function' && define.amd) { 12 | // AMD 13 | define([], factory); 14 | } else if (typeof exports === 'object') { 15 | module.exports = factory(); 16 | } else { 17 | root.ScrollTo = factory(); 18 | } 19 | }(this, function() { 20 | 'use strict'; 21 | 22 | var scrollToTopButton, 23 | _distance, 24 | _iteration, 25 | _options, 26 | _startPosition; 27 | 28 | /** 29 | * Iterates over the keys of an object and calls a callback function when the 30 | * key belongs to the object itself and not to its prototype. 31 | */ 32 | function iterate(object, callback) { 33 | for (var key in object) { 34 | if (object.hasOwnProperty(key)) { 35 | callback(key, object[key]); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Helper method to merge the default options with the overrides passed 42 | * along to the constructor. 43 | * 44 | * @param {object} overrides The overrides for the default options. These 45 | * values will take precedence over the default 46 | * values. 47 | * 48 | * @returns {object} The method returns an object containing the default 49 | * options with the values override by those of the 50 | * overrides object. 51 | */ 52 | function mergeOptions(overrides) { 53 | var result = {}; 54 | 55 | // Copy the default options to the result object 56 | iterate(exports.options, function(key, value) { 57 | result[key] = value; 58 | }); 59 | 60 | // Iterate over the keys in the overrides object 61 | iterate(overrides, function(key, value) { 62 | // Check if the key for an existing configuration property 63 | if (result[key] !== undefined) { 64 | // Override the default value 65 | result[key] = value; 66 | } 67 | }); 68 | 69 | // Return the merge result 70 | return result; 71 | } 72 | 73 | var exports = function(overrides) { 74 | _options = mergeOptions(overrides); 75 | }; 76 | 77 | // 1: The name of the data attribute which is used to indicate the anchor 78 | // should not animate the jump to its target 79 | // 2: The CSS class to apply to the scroll to top button when it is not 80 | // available to the visitor 81 | // 3: The number of iterations to use to scroll from the current y position 82 | // to the desired y position 83 | // 4: The query to use with document.querySelector to find the element which 84 | // the visitor can use to scroll back to the top of the document 85 | // 5: When an element is provided for the scroll to top functionality the 86 | // clientHeight will be divided by this number. Only when the current 87 | // scroll position exceeds the result of the division will the scroll top 88 | // top element be made available. Eg: with a value of 3 the scroll to top 89 | // element will become visible when a third of the clientHeight has been 90 | // scrolled. 91 | exports.options = { 92 | attrIgnoreAnchor : 'data-scrolltop-ignore', /* [1] */ 93 | correction : 0, 94 | cssScrollTopDisabled : 'not-available', /* [2] */ 95 | maxIterations : 50, /* [3] */ 96 | queryScrollTop : '.scroll-to-top', /* [4] */ 97 | topThresholdRatio : 3 /* [5] */ 98 | }; 99 | 100 | // Because of the numerous names of requestAnimationFrame we use this var to 101 | // point to the version that is available to us. When the browser doesn't 102 | // have one of the known requestAnimationFrame methods we will role our own 103 | // fallback method with a setTimeout. 104 | var requestAnimationFrame = window.requestAnimationFrame || 105 | window.mozRequestAnimationFrame || 106 | window.webkitRequestAnimationFrame || 107 | function(callback) { 108 | window.setTimeout(callback, 1000 / 60); 109 | }; 110 | 111 | /* 112 | * This method is used to scroll a bit closer to the top of the window. As long as the top 113 | * hasn't been reached the method will call itself at the next animation frame. 114 | */ 115 | function _animationLoop() { 116 | // Perform a step of the animation to go to the top of the window 117 | window.scrollTo(0, _easeOutCubic(_iteration, _startPosition, _distance, _options.maxIterations)); 118 | // Increase the iteration counter 119 | _iteration++; 120 | 121 | // As long as we haven't gone through the max number of iterations we will have to schedule 122 | // the next part of the animation 123 | if (_iteration <= _options.maxIterations) { 124 | // We're not there yet, request a new animation frame to move a little closer to the top 125 | requestAnimationFrame(function() { 126 | _animationLoop(); 127 | }); 128 | } 129 | } 130 | 131 | /** 132 | * Loops the body and finds all hrefs that contain an anchor and adds a 133 | * javascript function to provide animated scroll 134 | */ 135 | function _attachClickHandlers() { 136 | var items = document.querySelectorAll('a[href^="#"]'), 137 | index = 0, 138 | ubound = items.length, 139 | item; 140 | 141 | // Loop over all the anchors that link to an ID in the document 142 | for (; index < ubound; index++) { 143 | // Get the current item for easy reference 144 | item = items[index]; 145 | // Check if the item does not have the ignore attribute; if it does 146 | // we need to skip this anchor, otherwise we need to attach a click 147 | // handler 148 | if (!item.hasAttribute(_options.attrIgnoreAnchor)) { 149 | // Anchor should not be ignored, attach a click handler 150 | item.addEventListener('click', _onClickHandler); 151 | } 152 | } 153 | } 154 | 155 | /* 156 | * Robert Penner's algorithm for an cubic ease out function 157 | * @param {Number} currentIteration The current iteration of the animation, 158 | * on each subsequent call this should be 159 | * increased by 1 160 | * @param {Number} startValue The start value, this should be a 161 | * constant throughout the animation. 162 | * @param {Number} changeInValue The difference between the start value 163 | * and the desired end value 164 | * @param {Number} totalIterations The number of iterations over which we 165 | * want to go from start to end 166 | * @return {Number} The value for the current step in the 167 | * animation 168 | */ 169 | function _easeOutCubic(currentIteration, startValue, changeInValue, totalIterations) { 170 | return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 3) + 1) + startValue; 171 | } 172 | 173 | /** 174 | * Returns the height of the document in a cross browser safe way 175 | * @return {Number} The height of the document in pixels 176 | */ 177 | function _getDocumentHeight() { 178 | // There are quite a few variables that can return the height of the 179 | // current document. This method attempts to cover all the bases for a 180 | // reliable end result 181 | var scrollHeight = (document.documentElement || document.body).scrollHeight, 182 | offsetHeight = (document.documentElement || document.body).offsetHeight, 183 | clientHeight = (document.documentElement || document.body).clientHeight; 184 | // Return whichever value is the highest 185 | return Math.max(scrollHeight, offsetHeight, clientHeight); 186 | } 187 | 188 | /** 189 | * Returns the height of the viewport in a cross browser safe way 190 | * @return {Number} The height of the viewport in pixels. 191 | */ 192 | function _getViewportHeight() { 193 | return (document.documentElement || document.body).clientHeight; 194 | } 195 | 196 | /* 197 | * Returns the scroll position of the document in a cross browser safe way. 198 | * 199 | * @return {Number} The current y position of the document. 200 | */ 201 | function _getScrollPosition() { 202 | if (window.pageYOffset !== undefined) { 203 | return window.pageYOffset; 204 | } else { 205 | return (document.documentElement || document.body.parentNode || document.body).scrollTop; 206 | } 207 | } 208 | 209 | function _onClickHandler(event) { 210 | // Get the target, this is the link the user has clicked on 211 | var target = (event.currentTarget) ? event.currentTarget : event.srcElement; 212 | if (target == null) { 213 | return; 214 | } 215 | 216 | // Get the element the link referes to and make sure the link target 217 | // exists before we continue 218 | var hrefTarget = document.querySelector(target.getAttribute('href')); 219 | if (hrefTarget == null) { 220 | return; 221 | } 222 | 223 | var correction = 0; 224 | if (!target.hasAttribute('data-no-correction')) { 225 | correction = _options.correction; 226 | } 227 | // Start the animation, as a starting point we will use the current 228 | // position. We want to animate the document to the position of the 229 | // element which is the target of the anchor, we can get the distance 230 | // between the current position and the top of the target element by 231 | // getting its bounding client rect and using its top property. This 232 | // will be a positive value, the number of pixels we have to scroll down 233 | // to get to the target element. 234 | setTimeout(function() { 235 | _startAnimation(_getScrollPosition(), hrefTarget.getBoundingClientRect().top + correction); 236 | }, 10); 237 | // Prevent the default behaviour of the anchor element, otherwise IE8 238 | // will perform a nasty jump to the target only to perform the scroll 239 | // animation after that. 240 | event.preventDefault(); 241 | } 242 | 243 | /* 244 | * Handles the scroll event of the window, this is were we decide whether 245 | * the scroll to top button should be visible. 246 | */ 247 | function _scrollEventHandler(event) { 248 | // Check if we have a button (we should not be here otherwise but better 249 | // safe than sorry) 250 | if (scrollToTopButton) { 251 | // Get the viewport size 252 | var clientHeight = _getViewportHeight(); 253 | // Check if the user scrolled more than a third of the height of the 254 | // viewport 255 | if (_getScrollPosition() > clientHeight / _options.topThresholdRatio) { 256 | // The user scrolled more than the height of the viewport, we 257 | // will make the scroll to top button available 258 | scrollToTopButton.classList.remove(_options.cssScrollTopDisabled); 259 | } else { 260 | // The scroll position of the document is less than the height 261 | // of the viewport, we will hide the scroll to top button 262 | scrollToTopButton.classList.add(_options.cssScrollTopDisabled); 263 | } 264 | } 265 | } 266 | 267 | /* 268 | * This method will start the animation. Before staring the animation it 269 | * will check if it is possible to travel the requested distance. If the 270 | * distance is too big it will correct the distance to the nearest possible 271 | * distance. 272 | * 273 | * @param {Number} startPosition The position from which we should start 274 | * @param {Number} distance The number of pixels to move the document. 275 | * A negative number will move the document 276 | * closer to the top; a positive number will 277 | * scroll further down 278 | */ 279 | function _startAnimation(startPosition, distance) { 280 | // 1: The max position to scroll to is calculated by taking the height 281 | // of the document and substracting the height of the viewport. The 282 | // result is the y position that is in view at the top of the screen 283 | // when the user has scrolled all the way to the bottom of the 284 | // document 285 | // 2: The end position is the sum of the start position plus the 286 | // distance 287 | var maxYPos = _getDocumentHeight() - _getViewportHeight(), /* [1] */ 288 | endPosition = startPosition + distance; /* [2] */ 289 | 290 | // If the sum of the start position plus the distance is bigger than the 291 | // maxYPos we've calculated than we need to correct the distance as it 292 | // is not possible to scroll beyond the y position of maxYPos 293 | if (endPosition > maxYPos) { 294 | // The distance is less than we though, we need to get the 295 | // difference between the maxYPos and the current position of the 296 | // document 297 | distance = maxYPos - startPosition; 298 | } else if (endPosition < 0) { 299 | // We can't scroll beyond the top of the document, at the most we 300 | // can scroll to the top of document 301 | distance = -startPosition; 302 | } 303 | 304 | // Remember the information passed along 305 | _startPosition = startPosition; 306 | _distance = distance; 307 | // Reset the iteration count 308 | _iteration = 0; 309 | // Start the scroll animation 310 | _animationLoop(); 311 | } 312 | 313 | exports.prototype = { 314 | init: function() { 315 | // Did the implementer create a scroll to top button 316 | scrollToTopButton = document.querySelector(_options.queryScrollTop); 317 | // Only activate scroll tot top button if we have one 318 | if (scrollToTopButton) { 319 | window.addEventListener('scroll', _scrollEventHandler); 320 | } 321 | // Find all anchored links and attach a clickhandler 322 | _attachClickHandlers(); 323 | }, 324 | 325 | scrollToElement: function(element) { 326 | _startAnimation(_getScrollPosition(), element.getBoundingClientRect().top); 327 | } 328 | }; 329 | 330 | return exports; 331 | })); 332 | -------------------------------------------------------------------------------- /src/scripts/ShowHide.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ShowHide.js - version 1.0.1 3 | * 4 | * This module can be used to let the user toggle additional information in a 5 | * container. 6 | */ 7 | 8 | /* global module */ 9 | (function(root, factory) { 10 | 'use strict'; 11 | 12 | if (typeof define === 'function' && define.amd) { 13 | // AMD 14 | define([], factory); 15 | } else if (typeof exports === 'object') { 16 | // Common.js 17 | module.exports = factory(); 18 | } else { 19 | root.ShowHide = factory(); 20 | } 21 | }(this, function() { 22 | 'use strict'; 23 | 24 | // 1: The flag to indicate if the init method has been executed 25 | // 2: The object with the configuration for the module 26 | var initialized = false, /* [1] */ 27 | options, /* [2] */ 28 | NO_TRANSITIONS = 'no-transitions'; 29 | 30 | /** 31 | * Iterates over the keys of an object and calls a callback function when the 32 | * key belongs to he object itself and not to its prototype. 33 | */ 34 | function iterate(object, callback) { 35 | for (var key in object) { 36 | if (object.hasOwnProperty(key)) { 37 | callback(key, object[key]); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Helper method to merge the default options with the overrides passed 44 | * along to the constructor. 45 | * 46 | * @param {object} overrides The overrides for the default options. These 47 | * values will take precedence over the default 48 | * values. 49 | * 50 | * @returns {object} The method returns an object containing the default 51 | * options with the values override by those of the 52 | * overrides object. 53 | */ 54 | function mergeOptions(overrides) { 55 | var result = {}; 56 | 57 | // Copy the default options to the result object 58 | iterate(exports.options, function(key, value) { 59 | result[key] = value; 60 | }); 61 | 62 | // Iterate over the keys in the overrides object 63 | iterate(overrides, function(key, value) { 64 | // Check if the key for an existing configuration property 65 | if (result[key] !== undefined) { 66 | // Override the default value 67 | result[key] = value; 68 | } 69 | }); 70 | 71 | // Return the merge result 72 | return result; 73 | } 74 | 75 | /** 76 | * This method tries to determine the name of the transitionend event, it 77 | * could be the user's browser is using a prefixed version 78 | */ 79 | function transitionEndEventName() { 80 | // 1: The variable we use to keep track of the current key when 81 | // iterating over the transitions object 82 | // 2: An element we can use to test if a CSS property if known to the 83 | // browser 84 | // 3: The key:value pairs represent CSS-property:Event-name values. By 85 | // testing if the browser supports a CSS property we can tell what 86 | // name to use for the transitionend event 87 | var key, /* [1] */ 88 | el = document.createElement('div'), /* [2] */ 89 | transitions = { /* [3] */ 90 | WebkitTransition : 'webkitTransitionEnd', 91 | OTransition : 'otransitionend', // oTransitionEnd in very old Opera 92 | MozTransition : 'transitionend', 93 | transition : 'transitionend' 94 | }; 95 | 96 | // Loop over the keys in the transition object 97 | for (key in transitions) { 98 | // Check the key is a property of the object and check if the CSS 99 | // property does not return undefined. If the result is undefined it 100 | // means the browser doesn't recognize the (un)prefixed property 101 | if (transitions.hasOwnProperty(key) && el.style[key] !== undefined) { 102 | // The CSS property exists, this means we know which name of the 103 | // transitionend event we can use for this browser 104 | return transitions[key]; 105 | } 106 | } 107 | 108 | // If the method reaches this line it means that none of the CSS 109 | // properties were recognized by the browser. It is safe to conclude 110 | // there is no support for transitions (or at least none that we can use 111 | // in any meaningful way) 112 | return NO_TRANSITIONS; 113 | } 114 | 115 | var exports = function(overrides) { 116 | options = mergeOptions(overrides); 117 | // Try to determine the name of the transitionEnd event, it might be 118 | // prefixed 119 | options.transitionEnd = transitionEndEventName(); 120 | }; 121 | 122 | // These are the default configuration settings for the module, these can be 123 | // overriden by calling the module constructor with an object that has the 124 | // new value for the property 125 | exports.options = { 126 | attrContent : 'data-sh-content', 127 | attrContainer : 'data-sh-container', 128 | attrOpenOnInit : 'data-sh-open', 129 | attrTrigger : 'data-sh-trigger', 130 | cssActiveTrigger : 'active-trigger', 131 | cssClosed : 'closed', 132 | cssOpened : 'opened', 133 | cssTransition : 'transition' 134 | }; 135 | 136 | function _closeItem(id, skipTransition) { 137 | var container = document.querySelector('[' + options.attrContainer + '="' + id + '"]'), 138 | trigger = document.querySelector('[' + options.attrTrigger + '="' + id + '"]'), 139 | applyTransition = !skipTransition && options.transitionEnd !== NO_TRANSITIONS; 140 | 141 | if (container == null || trigger == null) { 142 | return; 143 | } 144 | 145 | container.classList.remove(options.cssOpened); 146 | container.classList.add(options.cssClosed); 147 | 148 | if (trigger.hasAttribute('data-sh-text-closed')) { 149 | var text = document.createTextNode(trigger.getAttribute('data-sh-text-closed')); 150 | trigger.textContent = text.textContent; 151 | } 152 | 153 | trigger.classList.remove(options.cssActiveTrigger); 154 | 155 | // Remove the transition class as the first style change we do should not be animated 156 | container.classList.remove(options.cssTransition); 157 | // Check of the module has been intialized, if it is not we want to forgo the close animation all together 158 | if (initialized && applyTransition) { 159 | // Add an eventlistner to detect when the transition of the container has ended 160 | container.addEventListener(options.transitionEnd, _onTransitionEndHandlerContainer); 161 | // Set the max height to the actual height of the filter item (it is set to 9999px at this point). If we 162 | // don't do this first the transition will to from 9999px to 32px and will be visible as the difference is 163 | // too large 164 | container.style.maxHeight = container.offsetHeight + 'px'; 165 | // This is not my own idea, I got this from http://jsfiddle.net/adambiggs/MAbD3/. Apparently it is 166 | // needed to disable/enable the transition. Anyway, we will wait for 10ms before we set the next set 167 | // of css properties 168 | setTimeout(function() { 169 | // The next style change should be animated, apply the transition class on the filter item 170 | container.classList.add(options.cssTransition); 171 | // The item should be just high enough to show the title 172 | container.style.maxHeight = _getCollapsedHeight(container) + 'px'; 173 | }, 10); 174 | } else { 175 | // The item should be just high enough to show the title. This change should be instantaneous as we're 176 | // initializing the module 177 | container.style.maxHeight = _getCollapsedHeight(container) + 'px'; 178 | setTimeout(function() { 179 | // Once the filter item has been given its initial height we need to add the transition class so the 180 | // opening of the filter item will be animated 181 | if (options.transitionEnd === NO_TRANSITIONS) { 182 | _makeContainerSane(container); 183 | } else { 184 | container.classList.add(options.cssTransition); 185 | } 186 | }, 10); 187 | } 188 | } 189 | 190 | function _getCollapsedHeight(container) { 191 | var trigger = container.querySelector('[' + options.attrTrigger + ']'), 192 | result = 0, 193 | styles; 194 | 195 | styles = window.getComputedStyle(container); 196 | 197 | if (trigger != null) { 198 | // We know the trigger is somewhere within in the container, we will 199 | // traverse its parents untill we get the element that is a direct 200 | // child of the container 201 | while (trigger.parentNode !== container) { 202 | trigger = trigger.parentNode; 203 | } 204 | styles = window.getComputedStyle(trigger); 205 | // If the position of the trigger is absolute it is taken out of the 206 | // flow and thus should be counted towards the height of the 207 | // container when the detail view is collapsed 208 | if (styles.position !== 'absolute') { 209 | result += trigger.offsetHeight; 210 | result += parseInt(styles['margin-bottom'], 10) + parseInt(styles['margin-top'], 10); 211 | } 212 | } 213 | 214 | styles = window.getComputedStyle(container); 215 | if (styles['box-sizing'] === 'border-box') { 216 | result += parseInt(styles['border-bottom-width'], 10) + parseInt(styles['border-top-width'], 10); 217 | } 218 | 219 | return result; 220 | } 221 | 222 | function _onClickHandlerTrigger(event) { 223 | var trigger = event.target, 224 | itemId = trigger.getAttribute(options.attrTrigger), 225 | container = document.querySelector('[' + options.attrContainer + '="' + itemId + '"]'); 226 | 227 | if (container == null) { 228 | return; 229 | } 230 | 231 | if (container.classList.contains(options.cssClosed)) { 232 | _openItem(itemId); 233 | 234 | if (trigger.hasAttribute('data-sh-group')) { 235 | var grouptriggers = document.querySelectorAll('[data-sh-group="' + trigger.getAttribute('data-sh-group') + '"]'); 236 | for (var index = 0, ubound = grouptriggers.length; index < ubound; index++) { 237 | var currentTrigger = grouptriggers[index]; 238 | if (currentTrigger !== trigger) { 239 | var currentContainer = document.querySelector('[' + options.attrContainer + '="' + currentTrigger.getAttribute(options.attrTrigger) + '"]'); 240 | if (currentContainer.classList.contains(options.cssOpened)) { 241 | _closeItem(currentTrigger.getAttribute(options.attrTrigger)); 242 | break; 243 | } 244 | } 245 | } 246 | } 247 | } else { 248 | _closeItem(itemId); 249 | } 250 | } 251 | 252 | /** 253 | * Handles the transition end event, this is where we need to do some finishing up 254 | * for the opening/closing of the container. 255 | * 256 | * @param {Event} event The event as fired 257 | */ 258 | function _onTransitionEndHandlerContainer(event) { 259 | // Get an easy reference to the filter item which has been opened or closed 260 | var container = event.currentTarget; 261 | 262 | // Remove the event listener from the container, the transition has ended 263 | container.removeEventListener(options.transitionEnd, _onTransitionEndHandlerContainer); 264 | _makeContainerSane(container); 265 | } 266 | 267 | function _openItem(id, skipTransition) { 268 | // 1: Inside the method we want to know if the animation should be applied, to make the 269 | // code more readable we will make 270 | var container = document.querySelector('[' + options.attrContainer + '="' + id + '"]'), 271 | content = container.querySelector('[' + options.attrContent + ']'), 272 | trigger = document.querySelector('[' + options.attrTrigger + '="' + id + '"]'), 273 | applyTransition = (!skipTransition && options.transitionEnd !== NO_TRANSITIONS); /* [1] */ 274 | 275 | // Check if we have all the elements we need to show the detail view 276 | if (container == null || content == null || trigger == null) { 277 | return; 278 | } 279 | skipTransition = (skipTransition === undefined) ? false : skipTransition; 280 | // Remove the closed class and add the opened class to the container 281 | container.classList.remove(options.cssClosed); 282 | container.classList.add(options.cssOpened); 283 | 284 | // Check if the trigger has a text for when the detail view is open 285 | if (trigger.hasAttribute('data-sh-text-opened')) { 286 | // Replace the trigger text for the text when the detail view is open 287 | var text = document.createTextNode(trigger.getAttribute('data-sh-text-opened')); 288 | trigger.textContent = text.textContent; 289 | } 290 | 291 | trigger.classList.add(options.cssActiveTrigger); 292 | 293 | // We need to set the max height for the filter item. We do this by taking the height of the 294 | // div which contains all the content of the filter item plus the height of the element 295 | // with the class block-header as this element is outside of the div. In the _onTransitionEnd 296 | // method we will remove the max height to make sure the filter item can grow taller if needed 297 | container.classList.remove(options.cssTransition); 298 | // Make the content visible again by setting its display to block 299 | content.style.display = 'block'; 300 | 301 | // Set the initial max-height, if this is not set the transition will not trigger when we set 302 | // the max height to include the detail view 303 | container.style.maxHeight = _getCollapsedHeight(container) + 'px'; 304 | setTimeout(function() { 305 | // Check if the transition should be applied; when it should be animated we need to 306 | // add the transition class on the the container 307 | if (applyTransition) { 308 | // Add an eventlistner to detect when the transition of the container has ended 309 | container.addEventListener(options.transitionEnd, _onTransitionEndHandlerContainer); 310 | container.classList.add(options.cssTransition); 311 | } 312 | 313 | // Adjust the height 314 | container.style.maxHeight = (content.offsetHeight + _getCollapsedHeight(container)) + 'px'; 315 | 316 | // Check if the transition was not applied to the opening of the container, in this case we need 317 | // to manually call the method to make the container sane again 318 | if (!applyTransition) { 319 | _makeContainerSane(container); 320 | } 321 | }, 10); 322 | } 323 | 324 | function _makeContainerSane(container) { 325 | if (container == null) { 326 | return; 327 | } 328 | 329 | if (container.classList.contains(options.cssClosed)) { 330 | // The container should contain only a single content element, we need this element 331 | var content = container.querySelector('[' + options.attrContent + ']'); 332 | if (content != null) { 333 | // The filter item is closed, we will make the container invisible 334 | content.style.display = 'none'; 335 | } 336 | } 337 | 338 | if (container.classList.contains(options.cssTransition)) { 339 | container.classList.remove(options.cssTransition); 340 | // There was a bug on OSX Safari which would show some odd behaviour 341 | // when opening the container. The source was the removal of the 342 | // max-height style. OSX Safari would transition this removal. The 343 | // way to solve this problem is by removing the transition class 344 | // before removing the max-height style. But in order for this to 345 | // take effect we need to apply 10ms timeouts in between 346 | // manipulating the styles. Ugly but functional. 347 | setTimeout(function() { 348 | // Remove the max-height value so the container can grow as big as it needs to to 349 | // accommodate all its content; 350 | container.style.removeProperty('max-height'); 351 | setTimeout(function() { 352 | container.classList.add(options.cssTransition); 353 | }, 10); 354 | }, 10); 355 | } else { 356 | // Remove the max-height value so the container can grow as big as it needs to to 357 | // accommodate all its content; 358 | container.style.removeProperty('max-height'); 359 | } 360 | } 361 | 362 | exports.prototype = { 363 | /** 364 | * Makes the detail view hidden. 365 | * 366 | * @param {string} id The ID of the Show/Hide content. 367 | * @param {boolean} [skipTransition=false] True to skip the open animation; pass 368 | * false to play the animation. 369 | */ 370 | hideDetailView: function(id, skipTransition) { 371 | // Try to get the container with the provided ID 372 | var container = document.querySelector('[' + options.attrContainer + '="' + id + '"]'); 373 | 374 | // Make sure we found the container and it is not yet open 375 | if (container == null || container.classList.contains(options.cssClosed)) { 376 | // Either the container was not found or it already contains the 377 | // class opened; either way we stop 378 | return; 379 | } 380 | 381 | // Close the detail view 382 | _closeItem(id, skipTransition); 383 | }, 384 | 385 | /** 386 | * This method will initialize the module. It will look for all elements 387 | * with the attribute [data-sh-trigger] and initialize their toggle 388 | * state. 389 | * 390 | * It is possible to specify a callback method to be called after each 391 | * toggle has been intialised. 392 | * 393 | * @param {Function} onInitCallback The method to be called when a toggle 394 | * has been initialised. This will not get 395 | * called when there was no matching container 396 | * for the trigger element. The callback will 397 | * receive an object with the property ID, its 398 | * value will be the same as the value of the 399 | * data-sh-trigger attribute. 400 | * @param {Object} context The context on which the callback should be 401 | * executed. It will be an empty object if no 402 | * context is provided. 403 | */ 404 | init: function(onInitCallback, context) { 405 | context = context || {}; 406 | 407 | // Get all the triggers from the DOM 408 | var triggers = document.querySelectorAll('[' + options.attrTrigger + ']'), 409 | index = 0, 410 | ubound = triggers.length; 411 | 412 | // Iterate over the triggers 413 | for (; index < ubound; index++) { 414 | // Get the trigger at the current index and try to get its container 415 | var trigger = triggers[index], 416 | triggerId = trigger.getAttribute(options.attrTrigger), 417 | container = document.querySelector('[' + options.attrContainer + '="' + triggerId + '"]'); 418 | 419 | // Make sure the container for the trigger has been found, if it could not 420 | // be found skip this trigger and continue with the next one 421 | if (container == null) { 422 | continue; 423 | } 424 | 425 | // Add a click handler for the trigger, this will allow the item to be opened 426 | // and closed 427 | trigger.addEventListener('click', _onClickHandlerTrigger); 428 | // Check if the item should be opened by default 429 | if (container.getAttribute(options.attrOpenOnInit) === '1') { 430 | // Remove the attribute, it no longer serves a purpose 431 | container.removeAttribute(options.attrOpenOnInit); 432 | _openItem(triggerId, true); 433 | } else { 434 | _closeItem(triggerId, true); 435 | } 436 | 437 | if (onInitCallback != null) { 438 | onInitCallback.call(context, { id: triggerId }); 439 | } 440 | } 441 | 442 | // Set the flag to indicate the module has been intialized 443 | initialized = true; 444 | 445 | // Remove the no-read-more class from the HTML element, this can be 446 | // used for styling purposes when there is a fallback scenario 447 | // implemented for when there is no JS available 448 | document.documentElement.classList.remove('no-read-more'); 449 | }, 450 | 451 | /** 452 | * Replaces the trigger element with a new element. It removes the 453 | * original trigger element from the DOM, removes its click handler, and 454 | * inserts the new element at the position of the original trigger. Once 455 | * the new element has been inserted a click handler will be attached to 456 | * it so it will toggle the detail view. 457 | */ 458 | replaceTrigger: function(id, newElement) { 459 | // Make sure the new element is actually something and that it has 460 | // the data attribute that makes it a trigger. 461 | if (newElement == null || !newElement.hasAttribute(options.attrTrigger)) { 462 | return; 463 | } 464 | 465 | // Try to get the original trigger and make sure we found it before 466 | // we continue 467 | var trigger = document.querySelector('[' + options.attrTrigger + '="' + id + '"]'); 468 | if (trigger == null || newElement == null) { 469 | return; 470 | } 471 | 472 | // Remove the click handler from the original trigger and attach it 473 | // to the new trigger 474 | trigger.removeEventListener('click', _onClickHandlerTrigger); 475 | newElement.addEventListener('click', _onClickHandlerTrigger); 476 | // Replace the old trigger element with the new element 477 | trigger.parentNode.replaceChild(newElement, trigger); 478 | // Trash the old trigger 479 | trigger = null; 480 | }, 481 | 482 | /** 483 | * Makes the detail view visible. 484 | * 485 | * @param {string} id The ID of the Show/Hide content. 486 | * @param {boolean} [skipTransition=false] True to skip the open animation; pass 487 | * false to play the animation. 488 | */ 489 | showDetailView: function(id, skipTransition) { 490 | // Try to get the container with the provided ID 491 | var container = document.querySelector('[' + options.attrContainer + '="' + id + '"]'); 492 | 493 | // Make sure we found the container and it is not yet open 494 | if (container == null || container.classList.contains(options.cssOpened)) { 495 | // Either the container was not found or it already contains the 496 | // class opened; either way we stop 497 | return; 498 | } 499 | 500 | // Open the detail view 501 | _openItem(id, skipTransition); 502 | } 503 | }; 504 | 505 | return exports; 506 | })); 507 | -------------------------------------------------------------------------------- /src/scripts/StickyHeader.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define([], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(); 9 | } else { 10 | root.StickyHeader = factory(); 11 | } 12 | }(this, function() { 13 | 'use strict'; 14 | 15 | // Because of the numerous names of requestAnimationFrame we use this var to 16 | // point to the version that is available to us. When the browser doesn't 17 | // have one of the known requestAnimationFrame methods we will role our own 18 | // fallback method with a setTimeout. 19 | var requestAnimationFrame = window.requestAnimationFrame || 20 | window.mozRequestAnimationFrame || 21 | window.webkitRequestAnimationFrame || 22 | function(callback) { 23 | window.setTimeout(callback, 1000 / 60); 24 | }; 25 | 26 | /** 27 | * Iterates over the keys of an object and calls a callback function when the 28 | * key belongs to he object itself and not to its prototype. 29 | */ 30 | function iterate(object, callback) { 31 | for (var key in object) { 32 | if (object.hasOwnProperty(key)) { 33 | callback(key, object[key]); 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Helper method to merge the default options with the overrides passed 40 | * along to the constructor. 41 | * 42 | * @param {object} overrides The overrides for the default options. These 43 | * values will take precedence over the default 44 | * values. 45 | * 46 | * @returns {object} The method returns an object containing the default 47 | * options with the values override by those of the 48 | * overrides object. 49 | */ 50 | function mergeOptions(overrides) { 51 | var result = {}; 52 | 53 | // Copy the default options to the result object 54 | iterate(exports.options, function(key, value) { 55 | result[key] = value; 56 | }); 57 | 58 | // Iterate over the keys in the overrides object 59 | iterate(overrides, function(key, value) { 60 | // Check if the key for an existing configuration property 61 | if (result[key] !== undefined) { 62 | // Override the default value 63 | result[key] = value; 64 | } 65 | }); 66 | 67 | // Return the merge result 68 | return result; 69 | } 70 | 71 | var exports = function(element, overrides) { 72 | this._element = element; 73 | this._options = mergeOptions(overrides); 74 | this._isFixed = false; 75 | }; 76 | 77 | exports.options = { 78 | cssFixed : 'fixed', 79 | queryHeader : '#widget-report-header' 80 | }; 81 | 82 | exports.prototype = { 83 | _checkElementPosition: function() { 84 | var rect = this._element.getBoundingClientRect(); 85 | if (rect.top < 0 && !this._isFixed) { 86 | this._isFixed = true; 87 | this._header.classList.add(this._options.cssFixed); 88 | } else if (rect.top >= 0 && this._isFixed) { 89 | this._isFixed = false; 90 | this._header.classList.remove(this._options.cssFixed); 91 | } 92 | }, 93 | 94 | _onScrollWindow: function(event) { 95 | requestAnimationFrame(this.checkHandler); 96 | }, 97 | 98 | init: function() { 99 | this._header = document.querySelector(this._options.queryHeader); 100 | 101 | if (this._element == null || this._header == null) { 102 | return; 103 | } 104 | this.checkHandler = this._checkElementPosition.bind(this); 105 | 106 | window.addEventListener('scroll', this._onScrollWindow.bind(this)); 107 | 108 | this._checkElementPosition(); 109 | } 110 | }; 111 | 112 | return exports; 113 | })); 114 | -------------------------------------------------------------------------------- /src/scripts/SupportFilter.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['Filter'], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(require('Filter')); 9 | } else { 10 | root.SupportFilter = factory(root.Filter); 11 | } 12 | }(this, function(Filter) { 13 | 'use strict'; 14 | 15 | /** 16 | * Iterates over the keys of an object and calls a callback function when the 17 | * key belongs to he object itself and not to its prototype. 18 | */ 19 | function iterate(object, callback) { 20 | for (var key in object) { 21 | if (object.hasOwnProperty(key)) { 22 | callback(key, object[key]); 23 | } 24 | } 25 | } 26 | 27 | var exports = function(element, overrides) { 28 | Filter.call(this, element, exports.options, overrides); 29 | this._keys = ['u', 'n', 'a', 'p', 'y']; 30 | }; 31 | 32 | exports.prototype = Object.create(Filter.prototype); 33 | 34 | exports.options = { 35 | filter : { 36 | u : true, 37 | n : true, 38 | a : true, 39 | p : true, 40 | y : true 41 | }, 42 | storageKey : 'jscc-support-filter' 43 | }; 44 | 45 | function _renderFilter(target, options, keys) { 46 | var list = document.getElementById('support-filter'); 47 | 48 | // Make sure we found the two lists we need to show the user agents 49 | if (list == null) { 50 | return; 51 | } 52 | 53 | for (var index = 0, ubound = keys.length; index < ubound; index++) { 54 | if (options.filter[keys[index]]) { 55 | var checkbox = target.querySelector('[data-filter-value="' + keys[index] + '"]'); 56 | if (checkbox != null) { 57 | checkbox.checked = true; 58 | } 59 | } else { 60 | options.filter[keys[index]] = false; 61 | } 62 | } 63 | } 64 | 65 | exports.prototype.init = function() { 66 | if (this._element == null) { 67 | return; 68 | } 69 | this.attachOnBeforeUnload(); 70 | this.attachClickHandler(); 71 | this.getFilterFromStorage(); 72 | 73 | _renderFilter(this._element, this._options, this._keys); 74 | }; 75 | 76 | return exports; 77 | })); 78 | -------------------------------------------------------------------------------- /src/scripts/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * [description] 3 | * @param {[type]} ) { 'use strict'; document.addEventListener('click', function() { console.log('document clicked'); });} [description] 4 | * @return {[type]} [description] 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * My comment 11 | */ 12 | document.addEventListener('click', function() { 13 | var context = window.AudioContext || window.webkitAudioContext, 14 | elem = document.getElementsByClassName('test'), 15 | worker = new Worker (); 16 | window.getContext( '2d' ); 17 | window.strokeText('Hello!'); 18 | //window.onhashchange = function(){}; 19 | window.addEventListener ( 'hashchange', function(){}); 20 | // Oh noes, a console statement! 21 | console.log('document clicked'); 22 | 23 | var db= window.openDatabase(); 24 | var db2 = window.indexedDB; 25 | var es = new EventSource(''); 26 | es.postMessage('test'); 27 | navigator.geolocation.getCurrentPosition(); 28 | var wgl = initWebGL(); 29 | wgl.createShadowRoot(); 30 | = new WebSocket () ; 31 | 32 | .querySelectorAll('link[rel="import"]'); 33 | 34 | elem.classList.add('test'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/scripts/vendor/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.1.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&& 19 | (f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a= 20 | this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f); 21 | if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval", 22 | "fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b, 23 | a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p, 24 | nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b, 25 | a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild= 26 | !0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!== 27 | e&&(!("."===k||".."===k)||1e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)): 34 | (e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl= 35 | O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b|| 36 | (b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this); 37 | -------------------------------------------------------------------------------- /src/static/CNAME: -------------------------------------------------------------------------------- 1 | jscc.info 2 | -------------------------------------------------------------------------------- /src/static/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/apple-touch-icon.png -------------------------------------------------------------------------------- /src/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #da532c 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/static/data/example.json: -------------------------------------------------------------------------------- 1 | "data":{ 2 | "png-alpha": { 3 | "title":"PNG alpha transparency", 4 | "description":"Semi-transparent areas in PNG files", 5 | "spec":"http://www.w3.org/TR/PNG/", 6 | "status":"rec", 7 | "links":[{"url":"http://en.wikipedia.org/wiki/Portable_Network_Graphics","title":"Wikipedia"},{"url":"http://dillerdesign.com/experiment/DD_belatedPNG/","title":"Workaround for IE6"}], 8 | "categories":["PNG"], 9 | "stats":{ 10 | "ie":{"5.5":"n","6":"p","7":"y","8":"y","9":"y","10":"y","11":"y"}, 11 | "firefox":{"2":"y","3":"y","3.5":"y","3.6":"y","4":"y","5":"y","6":"y","7":"y","8":"y","9":"y","10":"y","11":"y","12":"y","13":"y","14":"y","15":"y","16":"y","17":"y","18":"y","19":"y","20":"y","21":"y","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y"}, 12 | "chrome":{"4":"y","5":"y","6":"y","7":"y","8":"y","9":"y","10":"y","11":"y","12":"y","13":"y","14":"y","15":"y","16":"y","17":"y","18":"y","19":"y","20":"y","21":"y","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y"}, 13 | "safari":{"3.1":"y","3.2":"y","4":"y","5":"y","5.1":"y","6":"y","6.1":"y","7":"y","7.1":"y","8":"y"}, 14 | "opera":{"9":"y","9.5-9.6":"y","10.0-10.1":"y","10.5":"y","10.6":"y","11":"y","11.1":"y","11.5":"y","11.6":"y","12":"y","12.1":"y","15":"y","16":"y","17":"y","18":"y","19":"y","20":"y","21":"y","22":"y","23":"y","24":"y","25":"y","26":"y"}, 15 | "ios_saf":{"3.2":"y","4.0-4.1":"y","4.2-4.3":"y","5.0-5.1":"y","6.0-6.1":"y","7.0-7.1":"y","8":"y"}, 16 | "op_mini":{"5.0-7.0":"y"}, 17 | "android":{"2.1":"y","2.2":"y","2.3":"y","3":"y","4":"y","4.1":"y","4.2-4.3":"y","4.4":"y","4.4.3":"y"}, 18 | "bb":{"7":"y","10":"y"}, 19 | "op_mob":{"10":"y","11":"y","11.1":"y","11.5":"y","12":"y","12.1":"y","22":"y"}, 20 | "and_chr":{"37":"y"}, 21 | "and_ff":{"32":"y"}, 22 | "ie_mob":{"10":"y"}, 23 | "and_uc":{"9.9":"y"} 24 | }, 25 | "notes":"IE6 does support full transparency in 8-bit PNGs, which can sometimes be an alternative to 24-bit PNGs.", 26 | "notes_by_num":{}, 27 | "usage_perc_y":96.14, 28 | "usage_perc_a":0, 29 | "ucprefix":false, 30 | "parent":"", 31 | "keywords":"", 32 | "ie_id":"", 33 | "chrome_id":"" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/static/favicon-160x160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/favicon-160x160.png -------------------------------------------------------------------------------- /src/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/favicon-16x16.png -------------------------------------------------------------------------------- /src/static/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/favicon-192x192.png -------------------------------------------------------------------------------- /src/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/favicon-32x32.png -------------------------------------------------------------------------------- /src/static/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/favicon-96x96.png -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/fonts/icomoon.eot -------------------------------------------------------------------------------- /src/static/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/static/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/fonts/icomoon.ttf -------------------------------------------------------------------------------- /src/static/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/fonts/icomoon.woff -------------------------------------------------------------------------------- /src/static/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/mstile-144x144.png -------------------------------------------------------------------------------- /src/static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/mstile-150x150.png -------------------------------------------------------------------------------- /src/static/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/mstile-310x150.png -------------------------------------------------------------------------------- /src/static/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/mstile-310x310.png -------------------------------------------------------------------------------- /src/static/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbusser/jscc/3fcb5e7efa0023fb19dbb6d6c13d67c40935c2a0/src/static/mstile-70x70.png --------------------------------------------------------------------------------