1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2024-11-24 07:30:12 +01:00

Separated observables from components

This commit is contained in:
squidfunk 2020-02-12 19:13:03 +01:00
parent a018ed0297
commit 306530f668
84 changed files with 1523 additions and 1418 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
{ {
"assets/javascripts/bundle.js": "assets/javascripts/bundle.96943303.min.js", "assets/javascripts/bundle.js": "assets/javascripts/bundle.65b5374b.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.96943303.min.js.map", "assets/javascripts/bundle.js.map": "assets/javascripts/bundle.65b5374b.min.js.map",
"assets/javascripts/worker/packer.js": "assets/javascripts/worker/packer.819c2a16.min.js", "assets/javascripts/worker/packer.js": "assets/javascripts/worker/packer.c14659e8.min.js",
"assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.819c2a16.min.js.map", "assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.c14659e8.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.03c9bfda.min.js", "assets/javascripts/worker/search.js": "assets/javascripts/worker/search.ce66ce8d.min.js",
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.03c9bfda.min.js.map", "assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.ce66ce8d.min.js.map",
"assets/stylesheets/app-palette.scss": "assets/stylesheets/app-palette.8c25017f.min.css", "assets/stylesheets/app-palette.scss": "assets/stylesheets/app-palette.8c25017f.min.css",
"assets/stylesheets/app.scss": "assets/stylesheets/app.0d3546bb.min.css" "assets/stylesheets/app.scss": "assets/stylesheets/app.a6040c1b.min.css"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -43,7 +43,7 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block styles %} {% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/app.0d3546bb.min.css' | url }}"> <link rel="stylesheet" href="{{ 'assets/stylesheets/app.a6040c1b.min.css' | url }}">
{% if palette.primary or palette.accent %} {% if palette.primary or palette.accent %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/app-palette.8c25017f.min.css' | url }}"> <link rel="stylesheet" href="{{ 'assets/stylesheets/app-palette.8c25017f.min.css' | url }}">
{% endif %} {% endif %}
@ -188,7 +188,7 @@
{% endblock %} {% endblock %}
</div> </div>
{% block scripts %} {% block scripts %}
<script src="{{ 'assets/javascripts/bundle.96943303.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/bundle.65b5374b.min.js' | url }}"></script>
<script id="__lang" type="application/json"> <script id="__lang" type="application/json">
{%- set translations = {} -%} {%- set translations = {} -%}
{%- for key in [ {%- for key in [
@ -207,7 +207,7 @@
{%- endfor -%} {%- endfor -%}
{{ translations | tojson }} {{ translations | tojson }}
</script> </script>
<script>app=initialize({base:"{{ base_url }}",worker:{search:"{{ 'assets/javascripts/worker/search.03c9bfda.min.js' | url }}",packer:"{{ 'assets/javascripts/worker/packer.819c2a16.min.js' | url }}"}})</script> <script>app=initialize({base:"{{ base_url }}",worker:{search:"{{ 'assets/javascripts/worker/search.ce66ce8d.min.js' | url }}",packer:"{{ 'assets/javascripts/worker/packer.c14659e8.min.js' | url }}"}})</script>
{% for path in config["extra_javascript"] %} {% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script> <script src="{{ path | url }}"></script>
{% endfor %} {% endfor %}

407
package-lock.json generated
View File

@ -321,15 +321,6 @@
"integrity": "sha512-yWj3OnlKlwNpq9+Jh/nJkVAD3ta8Abk2kIRpjWpVkDlAD43tn6Q6xk5hurp84ndcq54jBDBGCD/WcIR0pspG0A==", "integrity": "sha512-yWj3OnlKlwNpq9+Jh/nJkVAD3ta8Abk2kIRpjWpVkDlAD43tn6Q6xk5hurp84ndcq54jBDBGCD/WcIR0pspG0A==",
"dev": true "dev": true
}, },
"@types/mini-css-extract-plugin": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz",
"integrity": "sha512-y+/Z/FtQ0h0Ps9PYbYeBd9fVl1z4215gpvGcYfl20+jMWBngEKDNzQ2an2kz+cLL47SS6Y2f6Z4axdSoyNgNAw==",
"dev": true,
"requires": {
"@types/webpack": "*"
}
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -639,51 +630,6 @@
"integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==",
"dev": true "dev": true
}, },
"adjust-sourcemap-loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz",
"integrity": "sha512-4hFsTsn58+YjrU9qKzML2JSSDqKvN8mUGQ0nNIrfPi8hmIONT4L3uUaT6MKdMsZ9AjsU6D2xDkZxCkbQPxChrA==",
"dev": true,
"requires": {
"assert": "1.4.1",
"camelcase": "5.0.0",
"loader-utils": "1.2.3",
"object-path": "0.11.4",
"regex-parser": "2.2.10"
},
"dependencies": {
"assert": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
"integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
"dev": true,
"requires": {
"util": "0.10.3"
}
},
"camelcase": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
"integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
"dev": true
},
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
"dev": true
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
"inherits": "2.0.1"
}
}
}
},
"ajv": { "ajv": {
"version": "6.10.2", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
@ -757,12 +703,6 @@
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"arity-n": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz",
"integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=",
"dev": true
},
"arr-diff": { "arr-diff": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -1571,6 +1511,16 @@
"source-map": "~0.6.0" "source-map": "~0.6.0"
} }
}, },
"clean-webpack-plugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz",
"integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==",
"dev": true,
"requires": {
"@types/webpack": "^4.4.31",
"del": "^4.1.1"
}
},
"cli-boxes": { "cli-boxes": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
@ -1664,15 +1614,6 @@
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"dev": true "dev": true
}, },
"compose-function": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
"integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=",
"dev": true,
"requires": {
"arity-n": "^1.0.4"
}
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1926,18 +1867,6 @@
"integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=",
"dev": true "dev": true
}, },
"css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"source-map": "^0.6.1",
"source-map-resolve": "^0.5.2",
"urix": "^0.1.0"
}
},
"css-loader": { "css-loader": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz",
@ -2029,16 +1958,6 @@
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
"dev": true "dev": true
}, },
"d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"dev": true,
"requires": {
"es5-ext": "^0.10.50",
"type": "^1.0.1"
}
},
"debug": { "debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -2129,6 +2048,50 @@
} }
} }
}, },
"del": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
"integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
"dev": true,
"requires": {
"@types/glob": "^7.1.1",
"globby": "^6.1.0",
"is-path-cwd": "^2.0.0",
"is-path-in-cwd": "^2.0.0",
"p-map": "^2.0.0",
"pify": "^4.0.1",
"rimraf": "^2.6.3"
},
"dependencies": {
"globby": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
"dev": true,
"requires": {
"array-union": "^1.0.1",
"glob": "^7.0.3",
"object-assign": "^4.0.1",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0"
},
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
}
},
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true
}
}
},
"delegate": { "delegate": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
@ -2335,38 +2298,6 @@
"is-arrayish": "^0.2.1" "is-arrayish": "^0.2.1"
} }
}, },
"es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
"dev": true,
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.3",
"next-tick": "~1.0.0"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"dev": true,
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"dev": true,
"requires": {
"d": "^1.0.1",
"ext": "^1.1.2"
}
},
"escape-html": { "escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -2510,23 +2441,6 @@
"integrity": "sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw==", "integrity": "sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw==",
"dev": true "dev": true
}, },
"ext": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
"integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
"dev": true,
"requires": {
"type": "^2.0.0"
},
"dependencies": {
"type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
"integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==",
"dev": true
}
}
},
"extend": { "extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -4163,6 +4077,32 @@
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true "dev": true
}, },
"is-path-cwd": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
"integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
"dev": true
},
"is-path-in-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
"integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
"dev": true,
"requires": {
"is-path-inside": "^2.1.0"
},
"dependencies": {
"is-path-inside": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
"integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
"dev": true,
"requires": {
"path-is-inside": "^1.0.2"
}
}
}
},
"is-path-inside": { "is-path-inside": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
@ -4643,30 +4583,12 @@
"brorand": "^1.0.1" "brorand": "^1.0.1"
} }
}, },
"mime": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
"dev": true
},
"mimic-fn": { "mimic-fn": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true "dev": true
}, },
"mini-css-extract-plugin": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz",
"integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0",
"normalize-url": "1.9.1",
"schema-utils": "^1.0.0",
"webpack-sources": "^1.1.0"
}
},
"minimalistic-assert": { "minimalistic-assert": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -4830,12 +4752,6 @@
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==",
"dev": true "dev": true
}, },
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
"dev": true
},
"nice-try": { "nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -5019,18 +4935,6 @@
"integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=",
"dev": true "dev": true
}, },
"normalize-url": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
"integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=",
"dev": true,
"requires": {
"object-assign": "^4.0.1",
"prepend-http": "^1.0.0",
"query-string": "^4.1.0",
"sort-keys": "^1.0.0"
}
},
"npm-run-path": { "npm-run-path": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@ -5084,12 +4988,6 @@
} }
} }
}, },
"object-path": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz",
"integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=",
"dev": true
},
"object-visit": { "object-visit": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
@ -5186,6 +5084,12 @@
"p-limit": "^1.1.0" "p-limit": "^1.1.0"
} }
}, },
"p-map": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
"integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
"dev": true
},
"p-try": { "p-try": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
@ -5356,6 +5260,21 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true "dev": true
}, },
"pinkie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
"integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
"dev": true
},
"pinkie-promise": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
"dev": true,
"requires": {
"pinkie": "^2.0.0"
}
},
"pkg-dir": { "pkg-dir": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
@ -5898,16 +5817,6 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true "dev": true
}, },
"query-string": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
"dev": true,
"requires": {
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
}
},
"querystring": { "querystring": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@ -6012,12 +5921,6 @@
"safe-regex": "^1.1.0" "safe-regex": "^1.1.0"
} }
}, },
"regex-parser": {
"version": "2.2.10",
"resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.10.tgz",
"integrity": "sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA==",
"dev": true
},
"registry-auth-token": { "registry-auth-token": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz",
@ -6195,76 +6098,12 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true "dev": true
}, },
"resolve-url-loader": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz",
"integrity": "sha512-K1N5xUjj7v0l2j/3Sgs5b8CjrrgtC70SmdCuZiJ8tSyb5J+uk3FoeZ4b7yTnH6j7ngI+Bc5bldHJIa8hYdu2gQ==",
"dev": true,
"requires": {
"adjust-sourcemap-loader": "2.0.0",
"camelcase": "5.3.1",
"compose-function": "3.0.3",
"convert-source-map": "1.7.0",
"es6-iterator": "2.0.3",
"loader-utils": "1.2.3",
"postcss": "7.0.21",
"rework": "1.0.1",
"rework-visit": "1.0.0",
"source-map": "0.6.1"
},
"dependencies": {
"postcss": {
"version": "7.0.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz",
"integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
}
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"ret": { "ret": {
"version": "0.1.15", "version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"dev": true "dev": true
}, },
"rework": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz",
"integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=",
"dev": true,
"requires": {
"convert-source-map": "^0.3.3",
"css": "^2.0.0"
},
"dependencies": {
"convert-source-map": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
"integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
"dev": true
}
}
},
"rework-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz",
"integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=",
"dev": true
},
"rimraf": { "rimraf": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@ -6607,15 +6446,6 @@
"kind-of": "^3.2.0" "kind-of": "^3.2.0"
} }
}, },
"sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"dev": true,
"requires": {
"is-plain-obj": "^1.0.0"
}
},
"source-list-map": { "source-list-map": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@ -6785,12 +6615,6 @@
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"dev": true "dev": true
}, },
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"dev": true
},
"string-width": { "string-width": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@ -7747,12 +7571,6 @@
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
"dev": true "dev": true
}, },
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
"dev": true
},
"typedarray": { "typedarray": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@ -8029,29 +7847,6 @@
} }
} }
}, },
"url-loader": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-3.0.0.tgz",
"integrity": "sha512-a84JJbIA5xTFTWyjjcPdnsu+41o/SNE8SpXMdUvXs6Q+LuhCD9E2+0VCiuDWqgo3GGXVlFHzArDmBpj9PgWn4A==",
"dev": true,
"requires": {
"loader-utils": "^1.2.3",
"mime": "^2.4.4",
"schema-utils": "^2.5.0"
},
"dependencies": {
"schema-utils": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
"integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
"dev": true,
"requires": {
"ajv": "^6.10.2",
"ajv-keywords": "^3.4.1"
}
}
}
},
"url-parse-lax": { "url-parse-lax": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",

View File

@ -24,12 +24,12 @@
"url": "https://github.com/squidfunk/mkdocs-material.git" "url": "https://github.com/squidfunk/mkdocs-material.git"
}, },
"scripts": { "scripts": {
"build": "npx webpack --mode production", "build": "npm run clean && npx webpack --mode production",
"clean": "rm -rf material", "clean": "rm -rf material",
"lint": "npm run lint:ts && npm run lint:scss", "lint": "npm run lint:ts && npm run lint:scss",
"lint:scss": "npx stylelint `find src/assets -name *.scss`", "lint:scss": "npx stylelint `find src/assets -name *.scss`",
"lint:ts": "npx tslint -p tsconfig.json 'src/**/*.ts'", "lint:ts": "npx tslint -p tsconfig.json 'src/**/*.ts'",
"start": "npx webpack --mode development --watch" "start": "npm run clean && npx webpack --mode development --watch"
}, },
"dependencies": { "dependencies": {
"clipboard": "^2.0.0", "clipboard": "^2.0.0",

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./anchor"
export * from "./header" export * from "./header"
export * from "./main" export * from "./main"
export * from "./search" export * from "./search"
export * from "./toc"

View File

@ -24,7 +24,7 @@ import { keys } from "ramda"
import { NEVER, Observable, OperatorFunction, of, pipe } from "rxjs" import { NEVER, Observable, OperatorFunction, of, pipe } from "rxjs"
import { map, scan, shareReplay, switchMap } from "rxjs/operators" import { map, scan, shareReplay, switchMap } from "rxjs/operators"
import { getElement } from "utilities" import { getElement } from "observables"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types

View File

@ -20,6 +20,4 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./_"
export * from "./offset"
export * from "./shadow" export * from "./shadow"

View File

@ -1,112 +0,0 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { Observable, combineLatest } from "rxjs"
import {
distinctUntilChanged,
map,
shareReplay,
switchMapTo
} from "rxjs/operators"
import { Agent, ViewportOffset } from "utilities"
import { HeaderState } from "../_"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
header$: Observable<HeaderState> /* Header state observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch viewport offset relative to an element's top
*
* This function returns an observable that computes the relative offset to the
* top of the given element based on the current viewport offset.
*
* @param el - HTML element
* @param agent - Agent
* @param options - Options
*
* @return Viewport offset observable
*/
export function watchViewportOffsetFromTopOf(
el: HTMLElement, { viewport }: Agent, { header$ }: Options
): Observable<ViewportOffset> {
/* Compute necessary adjustment for offset */
const adjust$ = viewport.size$
.pipe(
switchMapTo(header$),
map(({ height }) => el.offsetTop - height),
distinctUntilChanged()
)
/* Compute relative offset and return as hot observable */
return combineLatest([viewport.offset$, adjust$])
.pipe(
map(([{ x, y }, adjust]) => ({ x, y: y - adjust })),
shareReplay(1)
)
}
/**
* Watch viewport offset relative to an element's bottom
*
* This function returns an observable that computes the relative offset to the
* bottom of the given element based on the current viewport offset.
*
* @param el - HTML element
* @param agent - Agent
* @param options - Options
*
* @return Viewport offset observable
*/
export function watchViewportOffsetFromBottomOf(
el: HTMLElement, { viewport }: Agent, { header$ }: Options
): Observable<ViewportOffset> {
/* Compute necessary adjustment for offset */
const adjust$ = viewport.size$
.pipe(
switchMapTo(header$),
map(({ height }) => el.offsetTop + el.offsetHeight - height),
distinctUntilChanged()
)
/* Compute relative offset and return as hot observable */
return combineLatest([viewport.offset$, adjust$])
.pipe(
map(([{ x, y }, adjust]) => ({ x, y: y - adjust })),
shareReplay(1)
)
}

View File

@ -21,12 +21,14 @@
*/ */
import { Observable, OperatorFunction, pipe } from "rxjs" import { Observable, OperatorFunction, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators" import { map, shareReplay, switchMap } from "rxjs/operators"
import { switchMapIf } from "extensions" import {
import { Agent, paintHidden } from "utilities" Header,
Viewport,
import { HeaderState, watchViewportOffsetFromTopOf } from "../header" paintHideable,
watchViewportFrom
} from "observables"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -47,7 +49,9 @@ export interface HeroState {
* Options * Options
*/ */
interface Options { interface Options {
header$: Observable<HeaderState> /* Header state observable */ header$: Observable<Header> /* Header observable */
viewport$: Observable<Viewport>
screen$: Observable<boolean> /* Media screen observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -64,13 +68,13 @@ interface Options {
* @return Hero state * @return Hero state
*/ */
export function watchHero( export function watchHero(
el: HTMLElement, agent: Agent, { header$ }: Options el: HTMLElement, options: Options
): Observable<HeroState> { ): Observable<HeroState> {
/* Watch and paint visibility */ /* Watch and paint visibility */
const hidden$ = watchViewportOffsetFromTopOf(el, agent, { header$ }) const hidden$ = watchViewportFrom(el, options)
.pipe( .pipe(
paintHidden(el, 20) paintHideable(el, 20)
) )
/* Combine into a single hot observable */ /* Combine into a single hot observable */
@ -91,11 +95,10 @@ export function watchHero(
* @return Operator function * @return Operator function
*/ */
export function mountHero( export function mountHero(
agent: Agent, options: Options options: Options
): OperatorFunction<HTMLElement, HeroState> { ): OperatorFunction<HTMLElement, HeroState> {
const { media } = agent
return pipe( return pipe(
switchMapIf(media.screen$, el => watchHero(el, agent, options)), switchMap(el => watchHero(el, options)),
shareReplay(1) shareReplay(1)
) )
} }

View File

@ -23,8 +23,5 @@
export * from "./_" export * from "./_"
export * from "./header" export * from "./header"
export * from "./hero" export * from "./hero"
export * from "./navigation"
export * from "./main"
export * from "./search"
export * from "./tabs" export * from "./tabs"
export * from "./toc" export * from "./toc"

View File

@ -23,10 +23,13 @@
import { Observable, OperatorFunction, pipe } from "rxjs" import { Observable, OperatorFunction, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators" import { map, shareReplay } from "rxjs/operators"
import { switchMapIf } from "extensions" import {
import { Agent, paintHidden } from "utilities" Header,
Viewport,
import { HeaderState, watchViewportOffsetFromTopOf } from "../header" paintHideable,
watchViewportFrom
} from "observables"
import { switchMapIf } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -47,7 +50,9 @@ export interface TabsState {
* Options * Options
*/ */
interface Options { interface Options {
header$: Observable<HeaderState> /* Header state observable */ header$: Observable<Header> /* Header state observable */
viewport$: Observable<Viewport>
screen$: Observable<boolean> /* Media screen observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -61,19 +66,18 @@ interface Options {
* the tabs, currently only denoting whether the tabs are hidden or not. * the tabs, currently only denoting whether the tabs are hidden or not.
* *
* @param el - Tabs element * @param el - Tabs element
* @param agent - Agent
* @param options - Options * @param options - Options
* *
* @return Tabs state * @return Tabs state
*/ */
export function watchTabs( export function watchTabs(
el: HTMLElement, agent: Agent, { header$ }: Options el: HTMLElement, options: Options
): Observable<TabsState> { ): Observable<TabsState> {
/* Watch and paint visibility */ /* Watch and paint visibility */
const hidden$ = watchViewportOffsetFromTopOf(el, agent, { header$ }) const hidden$ = watchViewportFrom(el, options)
.pipe( .pipe(
paintHidden(el, 8) paintHideable(el, 8)
) )
/* Combine into a single hot observable */ /* Combine into a single hot observable */
@ -82,6 +86,7 @@ export function watchTabs(
map(hidden => ({ hidden })) map(hidden => ({ hidden }))
) )
} }
// TODO: generalize into watchHideable !!! or mountHideable...
/* ------------------------------------------------------------------------- */ /* ------------------------------------------------------------------------- */
@ -94,11 +99,10 @@ export function watchTabs(
* @return Operator function * @return Operator function
*/ */
export function mountTabs( export function mountTabs(
agent: Agent, options: Options options: Options
): OperatorFunction<HTMLElement, TabsState> { ): OperatorFunction<HTMLElement, TabsState> {
const { media } = agent
return pipe( return pipe(
switchMapIf(media.screen$, el => watchTabs(el, agent, options)), switchMapIf(options.screen$, el => watchTabs(el, options)),
shareReplay(1) shareReplay(1)
) )
} }

View File

@ -23,21 +23,19 @@
import { Observable, OperatorFunction, combineLatest, pipe } from "rxjs" import { Observable, OperatorFunction, combineLatest, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators" import { map, shareReplay } from "rxjs/operators"
import { switchMapIf } from "extensions"
import { Agent, getElements } from "utilities"
import { HeaderState } from "../../header"
import {
MainState,
SidebarState,
paintSidebar,
watchSidebar
} from "../../main"
import { import {
AnchorList, AnchorList,
Header,
Main,
Sidebar,
Viewport,
getElements,
paintAnchorList, paintAnchorList,
watchAnchorList paintSidebar,
} from "../anchor" watchAnchorList,
watchSidebar
} from "observables"
import { switchMapIf } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -47,7 +45,7 @@ import {
* Table of contents state * Table of contents state
*/ */
export interface TableOfContentsState { export interface TableOfContentsState {
sidebar: SidebarState /* Sidebar state */ sidebar: Sidebar /* Sidebar state */
anchors: AnchorList /* Anchor list */ anchors: AnchorList /* Anchor list */
} }
@ -59,8 +57,10 @@ export interface TableOfContentsState {
* Options * Options
*/ */
interface Options { interface Options {
header$: Observable<HeaderState> /* Header state observable */ header$: Observable<Header> /* Header observable */
main$: Observable<MainState> /* Main area state observable */ main$: Observable<Main> /* Main area observable */
viewport$: Observable<Viewport> /* Viewport observable */
tablet$: Observable<boolean> /* Media tablet observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -77,18 +77,18 @@ interface Options {
* @return Table of contents state observable * @return Table of contents state observable
*/ */
export function watchTableOfContents( export function watchTableOfContents(
el: HTMLElement, agent: Agent, { header$, main$ }: Options el: HTMLElement, { header$, main$, viewport$ }: Options
): Observable<TableOfContentsState> { ): Observable<TableOfContentsState> {
/* Watch and paint sidebar */ /* Watch and paint sidebar */
const sidebar$ = watchSidebar(el, agent, { main$ }) const sidebar$ = watchSidebar(el, { main$, viewport$ })
.pipe( .pipe(
paintSidebar(el) paintSidebar(el)
) )
/* Watch and paint anchor list (scroll spy) */ /* Watch and paint anchor list (scroll spy) */
const els = getElements<HTMLAnchorElement>(".md-nav__link", el) const els = getElements<HTMLAnchorElement>(".md-nav__link", el)
const anchors$ = watchAnchorList(els, agent, { header$ }) const anchors$ = watchAnchorList(els, { header$, viewport$ })
.pipe( .pipe(
paintAnchorList(els) paintAnchorList(els)
) )
@ -111,11 +111,10 @@ export function watchTableOfContents(
* @return Operator function * @return Operator function
*/ */
export function mountTableOfContents( export function mountTableOfContents(
agent: Agent, options: Options options: Options
): OperatorFunction<HTMLElement, TableOfContentsState> { ): OperatorFunction<HTMLElement, TableOfContentsState> {
const { media } = agent
return pipe( return pipe(
switchMapIf(media.tablet$, el => watchTableOfContents(el, agent, options)), switchMapIf(options.tablet$, el => watchTableOfContents(el, options)),
shareReplay(1) shareReplay(1)
) )
} }

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Component types
*/
export type ComponentType =
| "container" /* Container */
| "header" /* Header */
| "header-title" /* Header title */
| "hero" /* Hero */
| "main" /* Main area */
| "navigation" /* Navigation */
| "search" /* Search */
| "search-query" /* Search input */
| "search-reset" /* Search reset */
| "search-result" /* Search results */
| "tabs" /* Tabs */
| "toc" /* Table of contents */
/* ------------------------------------------------------------------------- */
/**
* Component
*
* @template T - Data type
*/
export interface Component<T extends {}> {
type: ComponentType /* Component type */
data?: T /* Component data */
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { Observable, OperatorFunction, of, pipe } from "rxjs"
import { map, shareReplay, switchMap } from "rxjs/operators"
import {
Main,
NavigationLayer,
Sidebar,
Viewport,
getElements,
paintNavigationLayer,
paintSidebar,
watchNavigationLayer,
watchSidebar
} from "observables"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Navigation for breakpoint below screen
*/
export interface NavigationBelowScreen {
layer: NavigationLayer /* Navigation layer */
}
/**
* Navigation for breakpoint above screen
*/
export interface NavigationAboveScreen {
sidebar: Sidebar /* Navigation sidebar */
}
/* ------------------------------------------------------------------------- */
/**
* Navigation
*/
export type Navigation =
| NavigationBelowScreen
| NavigationAboveScreen
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
main$: Observable<Main> /* Main area observable */
viewport$: Observable<Viewport> /* Viewport offset observable */
screen$: Observable<boolean> /* Screen media observable */
}
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Mount navigation below screen from source observable
*
* @param options - Options
*
* @return Operator function
*/
function mountNavigationBelowScreen(
_options: Options
): OperatorFunction<HTMLElement, NavigationBelowScreen> {
return pipe(
map(el => getElements("nav", el)),
switchMap(els => watchNavigationLayer(els)
.pipe(
paintNavigationLayer(els),
map(layer => ({ layer }))
)
)
)
}
/**
* Mount navigation above screen from source observable
*
* @param options - Options
*
* @return Operator function
*/
function mountNavigationAboveScreen(
options: Options
): OperatorFunction<HTMLElement, NavigationAboveScreen> {
return pipe(
switchMap(el => watchSidebar(el, options)
.pipe(
paintSidebar(el),
map(sidebar => ({ sidebar }))
)
)
)
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Mount navigation from source observable
*
* @param options - Options
*
* @return Operator function
*/
export function mountNavigation(
options: Options
): OperatorFunction<HTMLElement, Navigation> {
return pipe(
switchMap(el => options.screen$
.pipe(
switchMap(screen => screen
? of(el).pipe(mountNavigationAboveScreen(options))
: of(el).pipe(mountNavigationBelowScreen(options))
)
)
),
shareReplay(1)
)
}

View File

@ -31,10 +31,12 @@ import {
} from "rxjs/operators" } from "rxjs/operators"
import { SearchResult } from "modules" import { SearchResult } from "modules"
import { Agent, watchElementOffset } from "utilities" import {
SearchQuery,
import { paintSearchResultList } from "../list" Viewport,
import { paintSearchResultMeta } from "../meta" paintSearchResult,
watchElementOffset
} from "observables"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper types * Helper types
@ -44,59 +46,45 @@ import { paintSearchResultMeta } from "../meta"
* Options * Options
*/ */
interface Options { interface Options {
query$: Observable<SearchQuery> /* Search query observable */
result$: Observable<SearchResult[]> /* Search result observable */ result$: Observable<SearchResult[]> /* Search result observable */
query$: Observable<string> /* Search query observable */ viewport$: Observable<Viewport> /* Viewport observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/**
* Watch search result
*
* @param el - Search result element
* @param agent - Agent
* @param options - Options
*
* @return Search result state observable
*/
export function watchSearchResult(
el: HTMLElement, agent: Agent, { result$, query$ }: Options
): Observable<SearchResult[]> {
const container = el.parentElement!
/* Compute whether there are more search results elements */
const render$ = watchElementOffset(container, agent)
.pipe(
map(({ y }) => y >= container.scrollHeight - container.offsetHeight - 16),
distinctUntilChanged(),
filter(identity)
)
/* Paint search results */
return result$
.pipe(
paintSearchResultMeta(el, { query$ }),
paintSearchResultList(el, { render$ })
)
}
/* ------------------------------------------------------------------------- */
/** /**
* Mount search result from source observable * Mount search result from source observable
* *
* @param agent - Agent
* @param options - Options * @param options - Options
* *
* @return Operator function * @return Operator function
*/ */
export function mountSearchResult( export function mountSearchResult(
agent: Agent, options: Options { query$, result$, viewport$ }: Options
): OperatorFunction<HTMLElement, SearchResult[]> { ): OperatorFunction<HTMLElement, SearchResult[]> {
return pipe( return pipe(
switchMap(el => watchSearchResult(el, agent, options)), switchMap(el => {
const container = el.parentElement!
/* Compute whether there are more search results to fetch */
const fetch$ = watchElementOffset(container, { viewport$ })
.pipe(
map(({ y }) => {
return y >= container.scrollHeight - container.offsetHeight - 16
}),
distinctUntilChanged(),
filter(identity)
)
/* Paint search results */
return result$
.pipe(
paintSearchResult(el, { query$, fetch$ })
)
}),
shareReplay(1) shareReplay(1)
) )
} }

View File

@ -0,0 +1,128 @@
// /*
// * Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
// *
// * Permission is hereby granted, free of charge, to any person obtaining a copy
// * of this software and associated documentation files (the "Software"), to
// * deal in the Software without restriction, including without limitation the
// * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// * sell copies of the Software, and to permit persons to whom the Software is
// * furnished to do so, subject to the following conditions:
// *
// * The above copyright notice and this permission notice shall be included in
// * all copies or substantial portions of the Software.
// *
// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// * IN THE SOFTWARE.
// */
// import { Observable, OperatorFunction, combineLatest, pipe } from "rxjs"
// import { map, shareReplay, switchMap } from "rxjs/operators"
// import { switchMapIf } from "extensions"
// import { Agent, getElements } from "utilities"
// import { HeaderState } from "../../header"
// import {
// MainState,
// SidebarState,
// paintSidebar,
// watchSidebar
// } from "../../main"
// import {
// AnchorList,
// paintAnchorList,
// watchAnchorList
// } from "../anchor"
// /* ----------------------------------------------------------------------------
// * Types
// * ------------------------------------------------------------------------- */
// /**
// * Table of contents
// */
// export interface TableOfContents {
// sidebar: SidebarState /* Sidebar state */
// anchors: AnchorList /* Anchor list */
// }
// /* ----------------------------------------------------------------------------
// * Helper types
// * ------------------------------------------------------------------------- */
// /**
// * Options
// */
// interface Options {
// header$: Observable<Header> /* Header observable */
// main$: Observable<Main> /* Main area observable */
// }
// /* ----------------------------------------------------------------------------
// * Functions
// * ------------------------------------------------------------------------- */
// /**
// * Watch table of contents
// *
// * @param el - Table of contents element
// * @param agent - Agent
// * @param options - Options
// *
// * @return Table of contents observable
// */
// export function watchTableOfContents(
// el: HTMLElement, agent: Agent, { header$, main$ }: Options
// ): Observable<TableOfContents> {
// /* Watch and paint sidebar */
// const sidebar$ = watchSidebar(el, agent, { main$ })
// .pipe(
// paintSidebar(el)
// )
// /* Watch and paint anchor list (scroll spy) */
// const els = getElements<HTMLAnchorElement>(".md-nav__link", el)
// const anchors$ = watchAnchorList(els, agent, { header$ })
// .pipe(
// paintAnchorList(els)
// )
// /* Combine into a single hot observable */
// return combineLatest([sidebar$, anchors$])
// .pipe(
// map(([sidebar, anchors]) => ({ sidebar, anchors }))
// )
// }
// /* ------------------------------------------------------------------------- */
// /**
// * Mount table of contents from source observable
// *
// * @param agent - Agent
// * @param options - Options
// *
// * @return Operator function
// */
// export function mountTableOfContents(
// agent: Agent, options: Options
// ): OperatorFunction<HTMLElement, TableOfContents> {
// const { media } = agent
// return pipe(
// switchMap(el => media.tablet$
// .pipe(
// switchMap(tablet => {
// return watchTableOfContents(el, agent, options)
// })
// )
// ),
// switchMapIf(media.tablet$, el => watchTableOfContents(el, agent, options)),
// shareReplay(1)
// )
// }

View File

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

View File

@ -31,15 +31,12 @@ import { identity, values } from "ramda"
import { import {
EMPTY, EMPTY,
Observable, Observable,
Subject,
forkJoin,
merge, merge,
of, of,
fromEvent, fromEvent,
interval, OperatorFunction,
NEVER pipe
} from "rxjs" } from "rxjs"
import { ajax } from "rxjs/ajax"
import { import {
delay, delay,
filter, filter,
@ -47,53 +44,46 @@ import {
pluck, pluck,
switchMap, switchMap,
switchMapTo, switchMapTo,
take,
tap, tap,
withLatestFrom,
distinctUntilChanged,
distinctUntilKeyChanged, distinctUntilKeyChanged,
shareReplay
} from "rxjs/operators" } from "rxjs/operators"
import { import {
Component, Component,
paintHeaderShadow, paintHeaderShadow,
mountHero, mountHero,
mountMain,
mountNavigation,
mountSearchResult,
mountTableOfContents, mountTableOfContents,
mountTabs, mountTabs,
switchComponent, switchComponent,
watchComponentMap, watchComponentMap,
} from "./components"
import {
watchHeader, watchHeader,
watchSearchQuery, watchSearchQuery,
watchSearchReset watchSearchReset,
} from "./components"
import { SearchIndexOptions } from "./modules"
import {
getElement, getElement,
setupAgent,
watchToggle, watchToggle,
watchWorker,
setToggle, setToggle,
getElements, getElements,
watchMedia, watchMedia,
translate, watchDocument,
watchElementFocus watchLocationHash,
} from "./utilities" watchMain,
watchViewport,
watchKeyboard
} from "./observables"
import { import {
PackerMessage, isSearchResultMessage,
PackerMessageType, setupSearchWorker
SearchMessage,
SearchMessageType,
SearchSetupMessage,
isSearchDumpMessage,
isSearchResultMessage
} from "./workers" } from "./workers"
import { renderSource } from "templates" import { renderSource } from "templates"
import { switchMapIf, not, takeIf } from "extensions" import { not, takeIf } from "utilities"
import { renderClipboard } from "templates/clipboard" import { renderClipboard } from "templates/clipboard"
import { watchActiveLayer, paintActiveLayer } from "components/navigation/layer" import { fetchGitHubStats } from "modules/source/github"
import { mountNavigation } from "components2/navigation"
import { mountSearchResult } from "components2/search"
import { renderTable } from "templates/table"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -117,6 +107,10 @@ export interface Config {
document.documentElement.classList.remove("no-js") document.documentElement.classList.remove("no-js")
document.documentElement.classList.add("js") document.documentElement.classList.add("js")
/* Test for iOS */
if (navigator.userAgent.match(/(iPad|iPhone|iPod)/g))
document.documentElement.classList.add("ios")
const names: Component[] = [ const names: Component[] = [
"container", /* Container */ "container", /* Container */
"header", /* Header */ "header", /* Header */
@ -154,106 +148,6 @@ function isConfig(config: any): config is Config {
&& typeof config.worker.packer === "string" && typeof config.worker.packer === "string"
} }
/**
*
* Rogue control characters must be filtered before handing the query to the
* search index, as lunr will throw otherwise.
*/
function prepare(value: string): string {
const newvalue = value
.replace(/(?:^|\s+)[*+-:^~]+(?=\s+|$)/g, "")
.trim()
return newvalue ? newvalue.replace(/\s+|$/g, "* ") : ""
}
function setupWorkers(config: Config) {
// Remove trailing URL, or search might not work on the 404 page.
config.base = config.base.replace(/\/$/, "")
const worker = new Worker(config.worker.search)
const packer = new Worker(config.worker.packer)
const packerMessage$ = new Subject<PackerMessage>()
const packer$ = watchWorker(packer, { send$: packerMessage$ })
// send a message, then switchMapTo worker!
packer$.subscribe(message => {
// console.log("PACKER.MSG", message.data.length)
// is always packed!
if (message.type === PackerMessageType.BINARY && message.data[0] !== "{")
localStorage.setItem("index", message.data)
})
// storing = experimental feature
const searchMessage$ = new Subject<SearchMessage>()
const search$ = watchWorker(worker, { send$: searchMessage$ })
/* Link search to packer */
search$
.pipe(
filter(isSearchDumpMessage),
map(message => ({
type: PackerMessageType.STRING,
data: message.data
})),
tap(message => packerMessage$.next(message)) // send message and wait!
// switchMapTo(packer$)
)
.subscribe()
const data$ = ajax({
url: `${config.base}/search/search_index.json`,
responseType: "json",
withCredentials: true
})
.pipe<SearchIndexOptions>(
pluck("response"),
// map(res => {
// // search language... default for theme language...
// const override = translate("search.tokenizer")
// // TODO: ???
// if (override.length)
// res.config.separator = override
// return res
// })
// take(1)
)
const fromLocal = localStorage.getItem("index")
;
(fromLocal ? of({
type: PackerMessageType.BINARY,
data: localStorage.getItem("index")!
}) : EMPTY)
.subscribe(x => {
// console.log("send message to packer")
packerMessage$.next(x)
})
const index$ = fromLocal ? packer$.pipe(pluck("data"), take(1)) : of(undefined) // of(localStorage.getItem("index"))
// index$.subscribe(xx => console.log("INDEX", xx))
forkJoin([data$, index$])
.pipe<SearchSetupMessage>(
map(([data, index]) => ({
type: SearchMessageType.SETUP,
data: { ...data, index }
}))
)
.subscribe(message => {
searchMessage$.next(message) // TODO: this shall not complete
})
return [search$, searchMessage$] as const
}
/** /**
* Yes, this is a super hacky implementation. Needs clean up. * Yes, this is a super hacky implementation. Needs clean up.
*/ */
@ -279,42 +173,29 @@ function repository() {
// github repository... // github repository...
const [, user, repo] = el.href.match(/^.+github\.com\/([^\/]+)\/?([^\/]+)?.*$/i) const [, user, repo] = el.href.match(/^.+github\.com\/([^\/]+)\/?([^\/]+)?.*$/i)
// storage memoization!?
// get, if not available, exec and persist
// getOrRetrieve... storage$.
// Show repo stats // Show repo stats
if (user && repo) { if (user && repo) {
return ajax({ return fetchGitHubStats(user, repo)
url: `https://api.github.com/repos/${user}/${repo}`,
responseType: "json"
})
.pipe( .pipe(
map(({ status, response }) => { map(({ stargazers_count, forks_count }) => ([
if (status === 200) { `${format(stargazers_count || 0)} Stars`,
const { stargazers_count, forks_count } = response `${format(forks_count || 0)} Forks`
return [ ])),
`${format(stargazers_count)} Stars`,
`${format(forks_count)} Forks`
]
}
return []
}),
tap(data => sessionStorage.setItem("repository", JSON.stringify(data))) tap(data => sessionStorage.setItem("repository", JSON.stringify(data)))
) )
// Show user or organization stats // Show user or organization stats
} else if (user) { } else if (user) {
return ajax({ return fetchGitHubStats(user)
url: `https://api.github.com/users/${user}`,
responseType: "json"
})
.pipe( .pipe(
map(({ status, response }) => { map(({ public_repos }) => ([
if (status === 200) { `${format(public_repos || 0)} Repositories`
const { public_repos } = response ])),
return [
`${format(public_repos)} Repositories`
]
}
return []
}),
tap(data => sessionStorage.setItem("repository", JSON.stringify(data))) tap(data => sessionStorage.setItem("repository", JSON.stringify(data)))
) )
} }
@ -334,14 +215,6 @@ export function initialize(config: unknown) {
if (!isConfig(config)) if (!isConfig(config))
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`) throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
// pass config here!?
const agent = setupAgent() // TODO: add a config parameter here to configure media queries
const [
searchWorkerRecv$,
searchMessage$
] = setupWorkers(config)
// TODO: WIP repo rendering // TODO: WIP repo rendering
repository().subscribe(facts => { repository().subscribe(facts => {
if (facts.length) { if (facts.length) {
@ -355,10 +228,17 @@ export function initialize(config: unknown) {
} }
}) })
// pass config here!?
const document$ = watchDocument()
const hash$ = watchLocationHash()
const viewport$ = watchViewport()
const screen$ = watchMedia("(min-width: 960px)")
const tablet$ = watchMedia("(min-width: 1220px)")
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
/* Create component map observable */ /* Create component map observable */
const components$ = watchComponentMap(names, { document$: agent.document.load$ }) const components$ = watchComponentMap(names, { document$ })
const component = <T extends HTMLElement>(name: Component): Observable<T> => { const component = <T extends HTMLElement>(name: Component): Observable<T> => {
return components$ return components$
.pipe( .pipe(
@ -383,120 +263,104 @@ export function initialize(config: unknown) {
) )
.subscribe() .subscribe()
// ---------------------------------------------------------------------------- // DONE
// watchSearchResult // emit, if at bottom...
// receive results as a second observable!? filter stuff, paint
const result$ = searchWorkerRecv$ // move worker initialization into mountSearch ?
.pipe(
// tap(m => console.log("message from worker", m)),
filter(isSearchResultMessage),
pluck("data"),
// Prefix URLs with base URL
tap(result => result.forEach(item => {
item.article.location = `${config.base}/${item.article.location}`
item.sections.forEach(section => {
section.location = `${config.base}/${section.location}`
})
}))
)
// handleSearchResult <-- operator
const query$ = component<HTMLInputElement>("search-query")
.pipe(
switchMap(el => watchSearchQuery(el, { prepare }))
)
query$
.pipe<SearchMessage>(
map(query => ({ // put this into some function...
type: SearchMessageType.QUERY,
data: query.value
})), // TODO. ugly...
distinctUntilKeyChanged("data")
// distinctUntilKeyChanged("data")
)
.subscribe(searchMessage$)
// create the message subject internally... and link it to the worker...?
// watchSearchWorker(worker, agent, { query$ }) // message internally...
query$
.pipe(
tap(query => {
if (query.focus)
setToggle(search, true)
})
)
.subscribe()
/* ----------------------------------------------------------------------- */
const main$ = component("main") const main$ = component("main")
.pipe( .pipe(
mountMain(agent, { header$ }) switchMap(el => watchMain(el, { header$, viewport$ })),
) shareReplay(1) // TODO: mount!?
const navigation$ = component("navigation")
.pipe(
mountNavigation(agent, { main$ })
)
const toc$ = component("toc")
.pipe(
mountTableOfContents(agent, { header$, main$ })
)
// TODO: naming?
const resultComponent$ = component("search-result")
.pipe(
mountSearchResult(agent, { result$, query$: query$.pipe(
distinctUntilKeyChanged("value"),
pluck("value")
) })
) // temporary fix
const tabs$ = component("tabs")
.pipe(
mountTabs(agent, { header$ })
)
const hero$ = component("hero")
.pipe(
mountHero(agent, { header$ })
) )
// ---------------------------------------------------------------------------
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")! const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")!
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
/* ----------------------------------------------------------------------- */
// build a single search observable???
const query$ = component<HTMLInputElement>("search-query")
.pipe(
switchMap(el => watchSearchQuery(el))
)
const sw = setupSearchWorker(config.worker.search, {
base: config.base,
query$
})
const result$ = sw.rx$ // move worker initialization into mountSearch ?
.pipe(
filter(isSearchResultMessage),
pluck("data")
)
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
const searchActive$ = watchToggle(search) const searchActive$ = watchToggle(search)
.pipe( .pipe(
delay(400) delay(400)
) )
query$
.pipe(
distinctUntilKeyChanged("focus"),
tap(query => {
if (query.focus)
setToggle(search, query.focus) // paintSearchQuery?
// console.log(query)
})
)
.subscribe()
// implement toggle function that returns the toggles as observable...
const reset$ = component("search-reset") const reset$ = component("search-reset")
.pipe( .pipe(
switchMap(watchSearchReset) switchMap(watchSearchReset)
) )
const key$ = fromEvent<KeyboardEvent>(window, "keydown").pipe( /* ----------------------------------------------------------------------- */
filter(ev => !(ev.metaKey || ev.ctrlKey))
)
// filter arrow keys if search is active! // DONE (partly)
searchActive$.subscribe(console.log) const navigation$ = component("navigation")
.pipe(
mountNavigation({ main$, viewport$, screen$ })
)
const toc$ = component("toc")
.pipe(
mountTableOfContents({ header$, main$, viewport$, tablet$ })
)
// TODO: naming?
const resultComponent$ = component("search-result")
.pipe(
mountSearchResult({ viewport$, result$, query$: query$.pipe(
distinctUntilKeyChanged("value"),
) })
) // temporary fix
// mount hideable...
const tabs$ = component("tabs")
.pipe(
mountTabs({ header$, viewport$, screen$ })
)
const hero$ = component("hero")
.pipe(
mountHero({ header$, viewport$, screen$ })
)
// function watchKeyboard
const key$ = watchKeyboard()
// shortcodes // shortcodes
key$ key$
.pipe( .pipe(
takeIf(not(searchActive$)) takeIf(not(searchActive$))
) )
.subscribe(ev => { .subscribe(key => {
if ( if (
document.activeElement && ( document.activeElement && (
["TEXTAREA", "SELECT", "INPUT"].includes( ["TEXTAREA", "SELECT", "INPUT"].includes(
@ -508,7 +372,7 @@ export function initialize(config: unknown) {
) { ) {
// do nothing... // do nothing...
} else { } else {
if (ev.keyCode === 70 || ev.keyCode === 83) { if (key.type === "KeyS" || key.type === "KeyF") {
setToggle(search, true) setToggle(search, true)
} }
} }
@ -520,27 +384,27 @@ export function initialize(config: unknown) {
takeIf(searchActive$), takeIf(searchActive$),
/* Abort if meta key (macOS) or ctrl key (Windows) is pressed */ /* Abort if meta key (macOS) or ctrl key (Windows) is pressed */
tap(ev => { tap(key => {
if (ev.key === "Enter") { console.log("jo", key)
if (key.type === "Enter") {
if (document.activeElement === getElement("[data-md-component=search-query]")) { if (document.activeElement === getElement("[data-md-component=search-query]")) {
ev.preventDefault() key.claim()
// intercept hash change after search closed // intercept hash change after search closed
} else { } else {
setToggle(search, false) setToggle(search, false)
} }
} }
if (ev.key === "ArrowUp" || ev.key === "ArrowDown") { if (key.type === "ArrowUp" || key.type === "ArrowDown") {
const active = getElements("[data-md-component=search-query], [data-md-component=search-result] [href]") const active = getElements("[data-md-component=search-query], [data-md-component=search-result] [href]")
const i = Math.max(0, active.findIndex(el => el === document.activeElement)) const i = Math.max(0, active.findIndex(el => el === document.activeElement))
const x = Math.max(0, (i + active.length + (ev.keyCode === 38 ? -1 : +1)) % active.length) const x = Math.max(0, (i + active.length + (key.type === "ArrowUp" ? -1 : +1)) % active.length)
active[x].focus() active[x].focus()
/* Prevent scrolling of page */ /* Prevent scrolling of page */
ev.preventDefault() key.claim()
ev.stopPropagation()
} else if (ev.key === "Escape" || ev.key === "Tab") { } else if (key.type === "Escape" || key.type === "Tab") {
setToggle(search, false) setToggle(search, false)
getElement("[data-md-component=search-query]")!.blur() getElement("[data-md-component=search-query]")!.blur()
@ -564,6 +428,8 @@ export function initialize(config: unknown) {
) )
.subscribe() .subscribe()
// focusable -> setFocus(true, false)
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
/* Open details before printing */ /* Open details before printing */
@ -579,50 +445,109 @@ export function initialize(config: unknown) {
}) })
// Close drawer and search on hash change // Close drawer and search on hash change
agent.location.hash$.subscribe(() => { hash$.subscribe(() => {
setToggle(drawer, false) setToggle(drawer, false)
setToggle(search, false) // we probably need to delay the anchor jump for search setToggle(search, false) // we probably need to delay the anchor jump for search
}) })
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
/* Clipboard integration */ /* Clipboard.js integration */
if (Clipboard.isSupported()) { if (Clipboard.isSupported()) {
const blocks = getElements(".codehilite > pre, .highlight> pre, pre > code") const blocks = getElements("pre > code")
Array.prototype.forEach.call(blocks, (block, index) => { for (const [index, block] of blocks.entries()) {
const id = `__code_${index}` const parent = block.parentElement!
parent.id = `__code_${index}`
/* Create button with message container */ parent.insertBefore(renderClipboard(parent.id), block)
const button = renderClipboard(id) }
/* Link to block and insert button */
const parent = block.parentNode
parent.id = id
parent.insertBefore(button, block)
})
/* Initialize Clipboard listener */ /* Initialize Clipboard listener */
const copy = new Clipboard(".md-clipboard") const copy = new Clipboard(".md-clipboard") // create observable...
/* Success handler */ /* Success handler */
copy.on("success", action => { // copy.on("success", action => {
alert("Copied to clipboard") // TODO: integrate snackbar // alert("Copied to clipboard") // TODO: integrate snackbar
// TODO: add a snackbar/notification // // TODO: add a snackbar/notification
}) // })
} }
/* Wrap all data tables for better overflow scrolling */
const tables = getElements<HTMLTableElement>("table:not([class])")
const placeholder = document.createElement("table")
tables.forEach(table => {
table.replaceWith(placeholder)
placeholder.replaceWith(renderTable(table))
})
// search lock
let lastOffset = 0
tablet$.pipe(
switchMap(active => {
return !active ? watchToggle(search) : EMPTY
}),
switchMap(toggle => {
if (toggle) {
console.log("ACTIVE")
return of(document.body)
.pipe(
tap(() => lastOffset = window.pageYOffset),
delay(400),
tap(() => {
window.scrollTo(0, 0),
console.log("scrolled... to top, locked body")
document.body.dataset.mdState = "lock"
})
)
} else {
console.log("INACTIVE")
return of(document.body)
.pipe(
tap(() => document.body.dataset.mdState = ""),
delay(100),
tap(() => {
window.scrollTo(0, lastOffset)
})
)
}
return EMPTY
})
)
.subscribe(x => console.log("SEARCHLOCK", x))
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
const navigationlayer$ = component("navigation") // get headerHEIGHT! only if header is sticky!
.pipe(
switchMapIf(not(agent.media.tablet$), el => watchActiveLayer(el) // // lockHeader at...
.pipe( // const direction$ = agent.viewport.offset$.pipe(
paintActiveLayer() // bufferCount(2, 1), // determine scroll direction
) // map(([{ y: y0 }, { y: y1 }]) => y1 > y0),
) // distinctUntilChanged(),
) // )
.subscribe(console.log)
// document.body.style.minHeight = "100vh"
// // if true => then + HEADER. otherwise not
// let last = 0
// combineLatest([direction$, header$]).pipe(
// tap(([direction, { height }]) => { // TODO: only if sticky!
// const offset = 48
// console.log(window.pageYOffset, height, last)
// if (Math.abs(window.pageYOffset - last) < height + offset) { // TODO: add sensitivity offset!
// return
// }
// if (direction) {
// document.body.style.height = `${window.pageYOffset + offset + height}px`
// } else {
// document.body.style.height = `${window.pageYOffset - offset}px` // offset
// }
// last = window.pageYOffset
// })
// )
// .subscribe()
// // toiggle
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
@ -644,7 +569,15 @@ export function initialize(config: unknown) {
.subscribe() // potential memleak <-- use takeUntil .subscribe() // potential memleak <-- use takeUntil
return { return {
agent, // agent,
state state
} }
} }
// function mountSearchQuery(
// ): OperatorFunction<HTMLInputElement, SearchQuery> {
// return pipe(
// switchMap(el => watchSearchQuery(el))
// )
// }

View File

@ -95,12 +95,22 @@ export function setupSearchDocumentMap(
/* Add subsequent section */ /* Add subsequent section */
} else { } else {
documents.set(location, { location, title, text, parent }) documents.set(location, {
location,
title,
text,
parent
})
} }
/* Add article */ /* Add article */
} else { } else {
documents.set(location, { location, title, text, linked: false }) documents.set(location, {
location,
title,
text,
linked: false
})
} }
} }
return documents return documents

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { Repo, User } from "github-types"
import { NEVER, Observable } from "rxjs"
import { ajax } from "rxjs/ajax"
import { filter, map, pluck, shareReplay } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Round a number
*
* TODO: document
*/
function round(value: number) {
return value > 999
? `${(value / 1000).toFixed(1)}k`
: `${(value)}`
}
/**
* TODO: document
*/
export function fetchGitHubStats(
user: string
): Observable<User>
export function fetchGitHubStats(
user: string, repo: string
): Observable<Repo>
export function fetchGitHubStats(
user: string, repo?: string
): Observable<User | Repo> {
const endpoint = typeof repo !== "undefined"
? `repos/${user}/${repo}`
: `users/${user}`
return ajax({
url: `https://api.github.com/${endpoint}`,
responseType: "json"
})
.pipe(
filter(({ status }) => status === 200),
pluck("response"),
shareReplay(1)
)
}
// TODO: GitLab API:
// https://docs.gitlab.com/ee/api/projects.html#get-single-project
// curl "https://gitlab.com/api/v4/projects/johannes-z%2Fmkdocs-material"
/* ------------------------------------------------------------------------- */
/**
* Get repository information
*
* TODO: document
*/
export function getRepository(user: string, repo: string): Observable<string[]> {
return fetchGitHubStats(user, repo)
.pipe(
map(({ stargazers_count, forks_count }) => ([
`${round(stargazers_count || 0)} Stars`,
`${round(forks_count || 0)} Forks`
]))
)
}
/**
* Get user/organization information
*
* TODO: document
*/
export function getUser(user: string): Observable<string[]> {
return fetchGitHubStats(user)
.pipe(
map(({ public_repos }) => ([
`${round(public_repos || 0)} Repositories`
]))
)
}

View File

@ -21,10 +21,14 @@
*/ */
import { Observable, fromEvent, merge } from "rxjs" import { Observable, fromEvent, merge } from "rxjs"
import { map, shareReplay, startWith } from "rxjs/operators" import {
distinctUntilKeyChanged,
map,
shareReplay,
startWith
} from "rxjs/operators"
import { Agent } from "../../_" import { Viewport } from "../../viewport"
import { ViewportSize } from "../../viewport"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -46,7 +50,7 @@ export interface ElementOffset {
* Options * Options
*/ */
interface Options { interface Options {
size$: Observable<ViewportSize> /* Viewport size observable */ viewport$: Observable<Viewport> /* Viewport observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -56,7 +60,7 @@ interface Options {
/** /**
* Retrieve element offset * Retrieve element offset
* *
* @param el - HTML element * @param el - Element
* *
* @return Element offset * @return Element offset
*/ */
@ -73,15 +77,21 @@ export function getElementOffset(el: HTMLElement): ElementOffset {
* Watch element offset * Watch element offset
* *
* @param el - Element * @param el - Element
* @param agent - Agent * @param options - Options
* *
* @return Element offset observable * @return Element offset observable
*/ */
export function watchElementOffset( export function watchElementOffset(
el: HTMLElement, { viewport }: Agent el: HTMLElement, { viewport$ }: Options
): Observable<ElementOffset> { ): Observable<ElementOffset> {
const scroll$ = fromEvent(el, "scroll") const scroll$ = fromEvent(el, "scroll")
return merge(scroll$, viewport.size$) const size$ = viewport$
.pipe(
distinctUntilKeyChanged("size")
)
/* Merge into a single hot observable */
return merge(scroll$, size$)
.pipe( .pipe(
map(() => getElementOffset(el)), map(() => getElementOffset(el)),
startWith(getElementOffset(el)), startWith(getElementOffset(el)),

View File

@ -20,9 +20,9 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./_"
export * from "./document" export * from "./document"
export * from "./element" export * from "./element"
export * from "./keyboard"
export * from "./location" export * from "./location"
export * from "./media" export * from "./media"
export * from "./viewport" export * from "./viewport"

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { Observable, fromEvent } from "rxjs"
import { filter, map, share } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Key
*/
export interface Key {
type: string /* Key type */
claim(): void /* Key claim */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Observable for window keyboard events
*/
const keydown$ = fromEvent<KeyboardEvent>(window, "keydown")
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch keyboard
*
* @return Keyboard observable
*/
export function watchKeyboard(): Observable<Key> {
return keydown$
.pipe(
filter(ev => !(ev.shiftKey || ev.metaKey || ev.ctrlKey)),
map(ev => ({
type: ev.code,
claim() {
ev.preventDefault()
ev.stopPropagation()
}
})),
share()
)
}

View File

@ -20,23 +20,9 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, fromEvent, merge } from "rxjs" import { Observable, combineLatest, fromEvent, merge } from "rxjs"
import { map, shareReplay, startWith } from "rxjs/operators" import { map, shareReplay, startWith } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Observable for window scroll events
*/
const scroll$ = fromEvent<UIEvent>(window, "scroll")
/**
* Observable for window resize events
*/
const resize$ = fromEvent<UIEvent>(window, "resize")
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -57,6 +43,28 @@ export interface ViewportSize {
height: number /* Viewport height */ height: number /* Viewport height */
} }
/**
* Viewport
*/
export interface Viewport {
offset: ViewportOffset /* Viewport offset */
size: ViewportSize /* Viewport size */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Observable for window scroll events
*/
const scroll$ = fromEvent<UIEvent>(window, "scroll")
/**
* Observable for window resize events
*/
const resize$ = fromEvent<UIEvent>(window, "resize")
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -96,8 +104,7 @@ export function watchViewportOffset(): Observable<ViewportOffset> {
return merge(scroll$, resize$) return merge(scroll$, resize$)
.pipe( .pipe(
map(getViewportOffset), map(getViewportOffset),
startWith(getViewportOffset()), startWith(getViewportOffset())
shareReplay(1)
) )
} }
@ -110,7 +117,22 @@ export function watchViewportSize(): Observable<ViewportSize> {
return resize$ return resize$
.pipe( .pipe(
map(getViewportSize), map(getViewportSize),
startWith(getViewportSize()), startWith(getViewportSize())
)
}
/**
* Watch viewport
*
* @return Viewport observable
*/
export function watchViewport(): Observable<Viewport> {
return combineLatest([
watchViewportOffset(),
watchViewportSize()
])
.pipe(
map(([offset, size]) => ({ offset, size })),
shareReplay(1) shareReplay(1)
) )
} }

View File

@ -21,5 +21,4 @@
*/ */
export * from "./_" export * from "./_"
export * from "./list" export * from "./relative"
export * from "./meta"

View File

@ -20,15 +20,11 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { MonoTypeOperatorFunction, Observable, pipe } from "rxjs" import { Observable, combineLatest } from "rxjs"
import { map, withLatestFrom } from "rxjs/operators" import { map, shareReplay } from "rxjs/operators"
import { import { Header } from "../../../header"
resetSearchResultMeta, import { Viewport } from "../_"
setSearchResultMeta
} from "actions"
import { SearchResult } from "modules"
import { getElement } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper types * Helper types
@ -38,7 +34,8 @@ import { getElement } from "utilities"
* Options * Options
*/ */
interface Options { interface Options {
query$: Observable<string> /* Search query observable */ header$: Observable<Header> /* Header observable */
viewport$: Observable<Viewport> /* Viewport observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -46,26 +43,25 @@ interface Options {
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Paint search result metadata from source observable * Watch viewport relative to element
* *
* @param el - Search result metadata element * @param el - Element
* @param options - Options * @param options - Options
* *
* @return Operator function * @return Viewport observable
*/ */
export function paintSearchResultMeta( export function watchViewportFrom(
el: HTMLElement, { query$ }: Options el: HTMLElement, { header$, viewport$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> { ): Observable<Viewport> {
const meta = getElement(".md-search-result__meta", el)! return combineLatest([viewport$, header$])
return pipe( .pipe(
withLatestFrom(query$), map(([{ offset, size }, { height }]) => ({
map(([result, query]) => { offset: {
if (query) { x: offset.x - el.offsetLeft,
setSearchResultMeta(meta, result.length) y: offset.y - el.offsetTop + height
} else { },
resetSearchResultMeta(meta) size
} })),
return result shareReplay(1)
}) )
)
} }

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, fromEvent } from "rxjs" import { Observable, Subject, fromEvent } from "rxjs"
import { pluck, share, switchMapTo, tap, throttle } from "rxjs/operators" import { pluck, share, switchMapTo, tap, throttle } from "rxjs/operators"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -35,6 +35,18 @@ export interface WorkerMessage {
data: unknown /* Message data */ data: unknown /* Message data */
} }
/**
* Worker handler
*
* @template T - Message type
*/
export interface WorkerHandler<
T extends WorkerMessage
> {
tx$: Subject<T> /* Message transmission subject */
rx$: Observable<T> /* Message receive observable */
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper types * Helper types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -45,7 +57,7 @@ export interface WorkerMessage {
* @template T - Worker message type * @template T - Worker message type
*/ */
interface Options<T extends WorkerMessage> { interface Options<T extends WorkerMessage> {
send$: Observable<T> /* Message observable */ tx$: Observable<T> /* Message transmission observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -61,26 +73,26 @@ interface Options<T extends WorkerMessage> {
* emitted during a pending request are throttled, the last one is emitted. * emitted during a pending request are throttled, the last one is emitted.
* *
* @param worker - Web worker * @param worker - Web worker
* @param options - Options
* *
* @return Worker message observable * @return Worker message observable
*/ */
export function watchWorker<T extends WorkerMessage>( export function watchWorker<T extends WorkerMessage>(
worker: Worker, { send$ }: Options<T> worker: Worker, { tx$ }: Options<T>
): Observable<T> { ): Observable<T> {
/* Intercept messages from web worker */ /* Intercept messages from web worker */
const recv$ = fromEvent(worker, "message") const rx$ = fromEvent(worker, "message")
.pipe( .pipe(
pluck<Event, T>("data"), pluck<Event, T>("data")
share()
) )
/* Send and receive messages, return hot observable */ /* Send and receive messages, return hot observable */
return send$ return tx$
.pipe( .pipe(
throttle(() => recv$, { leading: true, trailing: true }), throttle(() => rx$, { leading: true, trailing: true }),
tap(message => worker.postMessage(message)), tap(message => worker.postMessage(message)),
switchMapTo(recv$), switchMapTo(rx$),
share() share()
) )
} }

View File

@ -30,6 +30,7 @@ import {
} from "rxjs" } from "rxjs"
import { import {
distinctUntilChanged, distinctUntilChanged,
distinctUntilKeyChanged,
finalize, finalize,
map, map,
observeOn, observeOn,
@ -45,9 +46,9 @@ import {
setAnchorActive, setAnchorActive,
setAnchorBlur setAnchorBlur
} from "actions" } from "actions"
import { Agent, getElement } from "utilities"
import { HeaderState } from "../../header" import { Viewport, getElement } from "../agent"
import { Header } from "../header"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -69,7 +70,8 @@ export interface AnchorList {
* Options * Options
*/ */
interface Options { interface Options {
header$: Observable<HeaderState> /* Header state observable */ header$: Observable<Header> /* Header observable */
viewport$: Observable<Viewport> /* Viewport observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -92,13 +94,12 @@ interface Options {
* Note that the current anchor is the last item of the `prev` anchor list. * Note that the current anchor is the last item of the `prev` anchor list.
* *
* @param els - Anchor elements * @param els - Anchor elements
* @param agent - Agent
* @param options - Options * @param options - Options
* *
* @return Anchor list observable * @return Anchor list observable
*/ */
export function watchAnchorList( export function watchAnchorList(
els: HTMLAnchorElement[], { viewport }: Agent, { header$ }: Options els: HTMLAnchorElement[], { header$, viewport$ }: Options
): Observable<AnchorList> { ): Observable<AnchorList> {
const table = new Map<HTMLAnchorElement, HTMLElement>() const table = new Map<HTMLAnchorElement, HTMLElement>()
for (const el of els) { for (const el of els) {
@ -115,8 +116,9 @@ export function watchAnchorList(
) )
/* Compute partition of previous and next anchors */ /* Compute partition of previous and next anchors */
const partition$ = viewport.size$ const partition$ = viewport$
.pipe( .pipe(
distinctUntilKeyChanged("size"),
/* Build index to map anchor paths to vertical offsets */ /* Build index to map anchor paths to vertical offsets */
map(() => { map(() => {
@ -138,9 +140,9 @@ export function watchAnchorList(
}), }),
/* Re-compute partition when viewport offset changes */ /* Re-compute partition when viewport offset changes */
switchMap(index => combineLatest(viewport.offset$, adjust$) switchMap(index => combineLatest(viewport$, adjust$)
.pipe( .pipe(
scan(([prev, next], [{ y }, adjust]) => { scan(([prev, next], [{ offset: { y } }, adjust]) => {
/* Look forward */ /* Look forward */
while (next.length) { while (next.length) {

View File

@ -20,16 +20,16 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, defer, of } from "rxjs" import { Observable, of } from "rxjs"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Header state * Header
*/ */
export interface HeaderState { export interface Header {
sticky: boolean /* Header stickyness */ sticky: boolean /* Header stickyness */
height: number /* Header visible height */ height: number /* Header visible height */
} }
@ -46,19 +46,15 @@ export interface HeaderState {
* *
* @param el - Header element * @param el - Header element
* *
* @return Header state observable * @return Header observable
*/ */
export function watchHeader( export function watchHeader(
el: HTMLElement el: HTMLElement
): Observable<HeaderState> { ): Observable<Header> {
return defer(() => { const styles = getComputedStyle(el)
const sticky = getComputedStyle(el) const sticky = styles.position === "sticky"
.getPropertyValue("position") === "sticky" return of({
sticky,
/* Return header as hot observable */ height: sticky ? el.offsetHeight : 0
return of({
sticky,
height: sticky ? el.offsetHeight : 0
})
}) })
} }

View File

@ -20,4 +20,10 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./agent"
export * from "./anchor" export * from "./anchor"
export * from "./header"
export * from "./main"
export * from "./navigation"
export * from "./search"
export * from "./toggle"

View File

@ -20,27 +20,25 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, OperatorFunction, combineLatest, pipe } from "rxjs" import { Observable, combineLatest } from "rxjs"
import { import {
distinctUntilChanged, distinctUntilChanged,
map, map,
pluck, pluck,
shareReplay, shareReplay
switchMap
} from "rxjs/operators" } from "rxjs/operators"
import { Agent } from "utilities" import { Viewport } from "../../agent"
import { Header } from "../../header"
import { HeaderState } from "../../header"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Main area state * Main area
*/ */
export interface MainState { export interface Main {
offset: number /* Main area top offset */ offset: number /* Main area top offset */
height: number /* Main area visible height */ height: number /* Main area visible height */
active: boolean /* Scrolled past top offset */ active: boolean /* Scrolled past top offset */
@ -54,7 +52,8 @@ export interface MainState {
* Options * Options
*/ */
interface Options { interface Options {
header$: Observable<HeaderState> /* Header state observable */ header$: Observable<Header> /* Header observable */
viewport$: Observable<Viewport> /* Viewport observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -65,18 +64,17 @@ interface Options {
* Watch main area * Watch main area
* *
* This function returns an observable that computes the visual parameters of * This function returns an observable that computes the visual parameters of
* the main area which depends on the viewport height and vertical offset, as * the main area which depends on the viewport vertical offset and height, as
* well as the height of the header element, if the header is fixed. * well as the height of the header element, if the header is fixed.
* *
* @param el - Main area element * @param el - Main area element
* @param agent - Agent
* @param options - Options * @param options - Options
* *
* @return Main area state observable * @return Main area observable
*/ */
export function watchMain( export function watchMain(
el: HTMLElement, { viewport }: Agent, { header$ }: Options el: HTMLElement, { header$, viewport$ }: Options
): Observable<MainState> { ): Observable<Main> {
/* Compute necessary adjustment for header */ /* Compute necessary adjustment for header */
const adjust$ = header$ const adjust$ = header$
@ -85,13 +83,9 @@ export function watchMain(
) )
/* Compute the main area's visible height */ /* Compute the main area's visible height */
const height$ = combineLatest([ const height$ = combineLatest([viewport$, adjust$])
viewport.offset$,
viewport.size$,
adjust$
])
.pipe( .pipe(
map(([{ y }, { height }, adjust]) => { map(([{ offset: { y }, size: { height } }, adjust]) => {
const top = el.offsetTop const top = el.offsetTop
const bottom = el.offsetHeight + top const bottom = el.offsetHeight + top
return height return height
@ -102,9 +96,9 @@ export function watchMain(
) )
/* Compute whether the viewport offset is past the main area's top */ /* Compute whether the viewport offset is past the main area's top */
const active$ = combineLatest([viewport.offset$, adjust$]) const active$ = combineLatest([viewport$, adjust$])
.pipe( .pipe(
map(([{ y }, adjust]) => y >= el.offsetTop - adjust), map(([{ offset: { y } }, adjust]) => y >= el.offsetTop - adjust),
distinctUntilChanged() distinctUntilChanged()
) )
@ -115,25 +109,7 @@ export function watchMain(
offset: el.offsetTop - adjust, offset: el.offsetTop - adjust,
height, height,
active active
})) })),
shareReplay(1)
) )
} }
/* ------------------------------------------------------------------------- */
/**
* Mount main area from source observable
*
* @param agent - Agent
* @param options - Options
*
* @return Operator function
*/
export function mountMain(
agent: Agent, options: Options
): OperatorFunction<HTMLElement, MainState> {
return pipe(
switchMap(el => watchMain(el, agent, options)),
shareReplay(1)
)
}

View File

@ -31,7 +31,7 @@ import {
import { resetHidden, setHidden } from "actions" import { resetHidden, setHidden } from "actions"
import { ViewportOffset } from "../agent" import { Viewport } from "../../agent"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
@ -45,11 +45,11 @@ import { ViewportOffset } from "../agent"
* *
* @return Operator function * @return Operator function
*/ */
export function paintHidden( export function paintHideable(
el: HTMLElement, offset: number = 0 el: HTMLElement, offset: number = 0
): OperatorFunction<ViewportOffset, boolean> { ): OperatorFunction<Viewport, boolean> {
return pipe( return pipe(
map(({ y }) => y >= offset), map(({ offset: { y } }) => y >= offset),
distinctUntilChanged(), distinctUntilChanged(),
/* Defer repaint to next animation frame */ /* Defer repaint to next animation frame */

View File

@ -21,4 +21,5 @@
*/ */
export * from "./_" export * from "./_"
export * from "./hideable"
export * from "./sidebar" export * from "./sidebar"

View File

@ -43,18 +43,18 @@ import {
setSidebarHeight, setSidebarHeight,
setSidebarLock setSidebarLock
} from "actions" } from "actions"
import { Agent } from "utilities"
import { MainState } from "../_" import { Viewport } from "../../agent"
import { Main } from "../_"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Sidebar state * Sidebar
*/ */
export interface SidebarState { export interface Sidebar {
height: number /* Sidebar height */ height: number /* Sidebar height */
lock: boolean /* Sidebar lock */ lock: boolean /* Sidebar lock */
} }
@ -67,7 +67,8 @@ export interface SidebarState {
* Options * Options
*/ */
interface Options { interface Options {
main$: Observable<MainState> /* Main area state observable */ main$: Observable<Main> /* Main area observable */
viewport$: Observable<Viewport> /* Viewport observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -83,14 +84,13 @@ interface Options {
* sidebar is locked and fills the remaining space. * sidebar is locked and fills the remaining space.
* *
* @param el - Sidebar element * @param el - Sidebar element
* @param agent - Agent
* @param options - Options * @param options - Options
* *
* @return Sidebar state observable * @return Sidebar observable
*/ */
export function watchSidebar( export function watchSidebar(
el: HTMLElement, { viewport }: Agent, { main$ }: Options el: HTMLElement, { main$, viewport$ }: Options
): Observable<SidebarState> { ): Observable<Sidebar> {
/* Adjust for internal main area offset */ /* Adjust for internal main area offset */
const adjust = parseFloat( const adjust = parseFloat(
@ -99,24 +99,24 @@ export function watchSidebar(
) )
/* Compute the sidebar's available height */ /* Compute the sidebar's available height */
const height$ = combineLatest([viewport.offset$, main$]) const height$ = combineLatest([viewport$, main$])
.pipe( .pipe(
map(([{ y }, { offset, height }]) => { map(([{ offset: { y } }, { offset, height }]) => {
return height - adjust + Math.min(adjust, Math.max(0, y - offset)) return height - adjust + Math.min(adjust, Math.max(0, y - offset))
}) })
) )
/* Compute whether the sidebar should be locked */ /* Compute whether the sidebar should be locked */
const lock$ = combineLatest([viewport.offset$, main$]) const lock$ = combineLatest([viewport$, main$])
.pipe( .pipe(
map(([{ y }, { offset }]) => y >= offset + adjust) map(([{ offset: { y } }, { offset }]) => y >= offset + adjust)
) )
/* Combine into single hot observable */ /* Combine into single hot observable */
return combineLatest([height$, lock$]) return combineLatest([height$, lock$])
.pipe( .pipe(
map(([height, lock]) => ({ height, lock })), map(([height, lock]) => ({ height, lock })),
distinctUntilChanged<SidebarState>(equals), distinctUntilChanged<Sidebar>(equals),
shareReplay(1) shareReplay(1)
) )
} }
@ -132,7 +132,7 @@ export function watchSidebar(
*/ */
export function paintSidebar( export function paintSidebar(
el: HTMLElement el: HTMLElement
): MonoTypeOperatorFunction<SidebarState> { ): MonoTypeOperatorFunction<Sidebar> {
return pipe( return pipe(
/* Defer repaint to next animation frame */ /* Defer repaint to next animation frame */

View File

@ -20,4 +20,4 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./_" export * from "./layer"

View File

@ -43,16 +43,17 @@ import {
resetOverflowScrolling, resetOverflowScrolling,
setOverflowScrolling setOverflowScrolling
} from "actions" } from "actions"
import { getElement } from "utilities"
import { getElement } from "../../agent"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Active layer * Navigation layer
*/ */
export interface ActiveLayer { export interface NavigationLayer {
prev?: HTMLElement /* Layer (previous) */ prev?: HTMLElement /* Layer (previous) */
next: HTMLElement /* Layer (next) */ next: HTMLElement /* Layer (next) */
} }
@ -62,20 +63,20 @@ export interface ActiveLayer {
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Watch active layer * Watch navigation layer
* *
* On iOS we want to add `-webkit-overflow-scrolling: touch` for the menus * On iOS we want to add `-webkit-overflow-scrolling: touch` for the menus
* contained in the drawer, but as the navigational layers are nested, we can * contained in the drawer, but as the navigational layers are nested, we can
* only add it to the active layer because otherwise weird cropping will occur. * only add it to the navigation layer or extremely weird cropping will occur.
* This implementation keeps track of the previous and currently active layer. * This implementation keeps track of the previous and current layer.
* *
* @param els - Navigation elements * @param els - Navigation elements
* *
* @return Active layer observable * @return Navigation layer observable
*/ */
export function watchActiveLayer( export function watchNavigationLayer(
els: HTMLElement[] els: HTMLElement[]
): Observable<ActiveLayer> { ): Observable<NavigationLayer> {
const table = new Map<HTMLInputElement, HTMLElement>() const table = new Map<HTMLInputElement, HTMLElement>()
for (const el of els) { for (const el of els) {
const label = getElement<HTMLLabelElement>("label", el) const label = getElement<HTMLLabelElement>("label", el)
@ -86,7 +87,7 @@ export function watchActiveLayer(
} }
/* Determine active layer */ /* Determine active layer */
const active$ = merge( const layer$ = merge(
...[...table.keys()].map(input => fromEvent(input, "change")) ...[...table.keys()].map(input => fromEvent(input, "change"))
) )
.pipe( .pipe(
@ -96,7 +97,7 @@ export function watchActiveLayer(
) )
/* Return previous and next layer */ /* Return previous and next layer */
return active$ return layer$
.pipe( .pipe(
map(next => ({ next })), map(next => ({ next })),
scan(({ next: prev }, { next }) => ({ prev, next })), scan(({ next: prev }, { next }) => ({ prev, next })),
@ -107,15 +108,15 @@ export function watchActiveLayer(
/* ------------------------------------------------------------------------- */ /* ------------------------------------------------------------------------- */
/** /**
* Paint active layer from source observable * Paint navigation layer from source observable
* *
* @param els - Navigation elements * @param els - Navigation elements
* *
* @return Operator function * @return Operator function
*/ */
export function paintActiveLayer( export function paintNavigationLayer(
els: HTMLElement[] els: HTMLElement[]
): MonoTypeOperatorFunction<ActiveLayer> { ): MonoTypeOperatorFunction<NavigationLayer> {
return pipe( return pipe(
/* Defer repaint to next animation frame */ /* Defer repaint to next animation frame */
@ -125,14 +126,6 @@ export function paintActiveLayer(
resetOverflowScrolling(prev) resetOverflowScrolling(prev)
}), }),
/* Reset on complete or error */
finalize(() => {
for (const el of els)
resetOverflowScrolling(
getElement(".md-nav__list", el)!
)
}),
/* Wait until transition has finished */ /* Wait until transition has finished */
delay(250), delay(250),
@ -140,6 +133,14 @@ export function paintActiveLayer(
observeOn(animationFrameScheduler), observeOn(animationFrameScheduler),
tap(({ next }) => { tap(({ next }) => {
setOverflowScrolling(next) setOverflowScrolling(next)
}),
/* Reset on complete or error */
finalize(() => {
for (const el of els)
resetOverflowScrolling(
getElement(".md-nav__list", el)!
)
}) })
) )
} }

View File

@ -28,18 +28,18 @@ import {
startWith startWith
} from "rxjs/operators" } from "rxjs/operators"
import { watchElementFocus } from "utilities" import { watchElementFocus } from "../../agent"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Search query state * Search query
*/ */
export interface SearchQueryState { export interface SearchQuery {
value: string /* Query value */ value: string /* Query value */
focus: boolean /* Query focus state */ focus: boolean /* Query focus */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -50,7 +50,28 @@ export interface SearchQueryState {
* Options * Options
*/ */
interface Options { interface Options {
prepare(value: string): string /* Preparation function */ transform?(value: string): string /* Transformation function */
}
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Default transformation function
*
* Rogue control characters are filtered before handing the query to the
* search index, as lunr will throw otherwise.
*
* @param value - Query value
*
* @return Transformed query value
*/
function defaultTransform(value: string): string {
return value
.replace(/(?:^|\s+)[*+-:^~]+(?=\s+|$)/g, "")
.trim()
.replace(/\s+|\b$/g, "* ")
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -63,17 +84,17 @@ interface Options {
* @param el - Search query element * @param el - Search query element
* @param options - Options * @param options - Options
* *
* @return Search query state observable * @return Search query observable
*/ */
export function watchSearchQuery( export function watchSearchQuery(
el: HTMLInputElement, { prepare }: Options el: HTMLInputElement, { transform = defaultTransform }: Options = {}
): Observable<SearchQueryState> { ): Observable<SearchQuery> {
/* Intercept keyboard events */ /* Intercept keyboard events */
const value$ = fromEvent(el, "keyup") const value$ = fromEvent(el, "keyup")
.pipe( .pipe(
map(() => prepare(el.value)), map(() => transform(el.value)),
startWith(""), startWith(el.value),
distinctUntilChanged() distinctUntilChanged()
) )

View File

@ -28,19 +28,25 @@ import {
} from "rxjs" } from "rxjs"
import { import {
finalize, finalize,
map,
mapTo, mapTo,
observeOn, observeOn,
scan, scan,
switchMap switchMap,
withLatestFrom
} from "rxjs/operators" } from "rxjs/operators"
import { import {
addToSearchResultList, addToSearchResultList,
resetSearchResultList resetSearchResultList,
resetSearchResultMeta,
setSearchResultMeta
} from "actions" } from "actions"
import { SearchResult } from "modules" import { SearchResult } from "modules"
import { renderSearchResult } from "templates" import { renderSearchResult } from "templates"
import { getElement } from "utilities"
import { getElement } from "../../agent"
import { SearchQuery } from "../query"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper types * Helper types
@ -50,7 +56,8 @@ import { getElement } from "utilities"
* Options * Options
*/ */
interface Options { interface Options {
render$: Observable<boolean> /* Render trigger observable */ query$: Observable<SearchQuery> /* Search query observable */
fetch$: Observable<boolean> /* Search trigger observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -58,25 +65,39 @@ interface Options {
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Paint search result list from source observable * Paint search results from source observable
* *
* @param el - Search result element * @param el - Search result element
* @param options - Options * @param options - Options
* *
* @return Operator function * @return Operator function
*/ */
export function paintSearchResultList( export function paintSearchResult(
el: HTMLElement, { render$ }: Options el: HTMLElement, { query$, fetch$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> { ): MonoTypeOperatorFunction<SearchResult[]> {
const container = el.parentElement!
const list = getElement(".md-search-result__list", el)! const list = getElement(".md-search-result__list", el)!
const meta = getElement(".md-search-result__meta", el)!
return pipe( return pipe(
switchMap(result => render$
/* Paint search result metadata */
withLatestFrom(query$),
map(([result, query]) => {
if (query.value) {
setSearchResultMeta(meta, result.length)
} else {
resetSearchResultMeta(meta)
}
return result
}),
/* Paint search result list */
switchMap(result => fetch$
.pipe( .pipe(
/* Defer repaint to next animation frame */ /* Defer repaint to next animation frame */
observeOn(animationFrameScheduler), observeOn(animationFrameScheduler),
scan(index => { scan(index => {
const container = el.parentElement!
while (index < result.length) { while (index < result.length) {
addToSearchResultList(list, renderSearchResult(result[index++])) addToSearchResultList(list, renderSearchResult(result[index++]))
if (container.scrollHeight - container.offsetHeight > 16) if (container.scrollHeight - container.offsetHeight > 16)

View File

@ -32,7 +32,7 @@ import { map, startWith } from "rxjs/operators"
* *
* Simulating a click event seems to be the most cross-browser compatible way * Simulating a click event seems to be the most cross-browser compatible way
* of changing the value while also emitting a `change` event. Before, Material * of changing the value while also emitting a `change` event. Before, Material
* used `CustomEvent` to programatically change the value of a toggle, but this * used `CustomEvent` to programmatically change the value of a toggle, but this
* is a much simpler and cleaner solution. * is a much simpler and cleaner solution.
* *
* @param el - Toggle element * @param el - Toggle element

View File

@ -20,8 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { h } from "extensions" import { h, translate } from "utilities"
import { translate } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Data * Data
@ -43,7 +42,7 @@ const css = {
* *
* @param id - Unique identifier * @param id - Unique identifier
* *
* @return HTML element * @return Element
*/ */
export function renderClipboard( export function renderClipboard(
id: string id: string

View File

@ -20,5 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./clipboard"
export * from "./search" export * from "./search"
export * from "./source" export * from "./source"
export * from "./table"

View File

@ -20,9 +20,8 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { h } from "extensions"
import { SearchResult } from "modules" import { SearchResult } from "modules"
import { truncate } from "utilities" import { h, truncate } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Data * Data
@ -49,7 +48,7 @@ const css = {
* *
* @param result - Search result * @param result - Search result
* *
* @return HTML element * @return Element
*/ */
export function renderSearchResult( export function renderSearchResult(
{ article, sections }: SearchResult { article, sections }: SearchResult

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { h } from "extensions" import { h } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Data * Data
@ -43,7 +43,7 @@ const css = {
* *
* @param facts - Source facts * @param facts - Source facts
* *
* @return HTML element * @return Element
*/ */
export function renderSource( export function renderSource(
facts: any // TODO: add typings facts: any // TODO: add typings

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { h } from "extensions" import { h } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Data * Data
@ -43,7 +43,7 @@ const css = {
* *
* @param table - Table element * @param table - Table element
* *
* @return HTML element * @return Element
*/ */
export function renderTable( export function renderTable(
table: HTMLTableElement table: HTMLTableElement

View File

@ -1,113 +0,0 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { Observable, Subject } from "rxjs"
import {
ViewportOffset,
ViewportSize,
watchDocument,
watchLocation,
watchLocationHash,
watchMedia,
watchViewportOffset,
watchViewportSize
} from "utilities"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Agent document
*/
export interface AgentDocument {
load$: Observable<Document> /* Document observable */
}
/**
* Agent location
*/
export interface AgentLocation {
href$: Subject<string> /* Location subject */
hash$: Observable<string> /* Location hash observable */
}
/**
* Agent media
*/
export interface AgentMedia {
tablet$: Observable<boolean> /* Media observable for tablet */
screen$: Observable<boolean> /* Media observable for screen */
}
/**
* Agent viewport
*/
export interface AgentViewport {
offset$: Observable<ViewportOffset> /* Viewport offset observable */
size$: Observable<ViewportSize> /* Viewport size observable */
}
/* ------------------------------------------------------------------------- */
/**
* Agent
*/
export interface Agent {
document: AgentDocument /* Document observables */
location: AgentLocation /* Location observables */
media: AgentMedia /* Media observables */
viewport: AgentViewport /* Viewport observables */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Create the agent
*
* This function returns a data structure that contains all observables that
* are related to the browser and/or environment.
*
* @return Agent
*/
export function setupAgent(): Agent {
return {
document: {
load$: watchDocument()
},
location: {
href$: watchLocation(),
hash$: watchLocationHash()
},
media: {
tablet$: watchMedia("(min-width: 960px)"),
screen$: watchMedia("(min-width: 1220px)")
},
viewport: {
offset$: watchViewportOffset(),
size$: watchViewportSize()
}
}
}

View File

@ -20,7 +20,6 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./agent" export * from "./jsx"
export * from "./hidden" export * from "./rxjs"
export * from "./string" export * from "./string"
export * from "./toggle"

View File

@ -38,7 +38,11 @@ type Attributes =
/** /**
* Child element * Child element
*/ */
type Child = Child[] | HTMLElement | Text | string | number type Child =
| HTMLElement
| Text
| string
| number
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper functions * Helper functions
@ -47,10 +51,10 @@ type Child = Child[] | HTMLElement | Text | string | number
/** /**
* Append a child node to an element * Append a child node to an element
* *
* @param el - HTML element * @param el - Element
* @param child - Child node * @param child - Child node(s)
*/ */
function appendChild(el: HTMLElement, child: Child): void { function appendChild(el: HTMLElement, child: Child | Child[]): void {
/* Handle primitive types (including raw HTML) */ /* Handle primitive types (including raw HTML) */
if (typeof child === "string" || typeof child === "number") { if (typeof child === "string" || typeof child === "number") {
@ -78,11 +82,10 @@ function appendChild(el: HTMLElement, child: Child): void {
* @param attributes - HTML attributes * @param attributes - HTML attributes
* @param children - Child elements * @param children - Child elements
* *
* @return HTML element * @return Element
*/ */
export function h( export function h(
tag: string, attributes: Attributes | null, tag: string, attributes: Attributes | null, ...children: Child[]
...children: Array<HTMLElement | Text | string | number>
): HTMLElement { ): HTMLElement {
const el = document.createElement(tag) const el = document.createElement(tag)

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { getElement } from "../agent" import { getElement } from "observables"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Data * Data
@ -35,23 +35,6 @@ let lang: Record<string, string>
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/**
* Truncate a string after the given number of characters
*
* @param value - Value to be truncated
* @param n - Number of characters
*
* @return Truncated value
*/
export function truncate(value: string, n: number): string {
let i = n
if (value.length > i) {
while (value[i] !== " " && --i > 0); // tslint:disable-line
return `${value.substring(0, i)}...`
}
return value
}
/** /**
* Translate the given key * Translate the given key
* *
@ -72,3 +55,20 @@ export function translate(key: string, value?: string): string {
? lang[key].replace("#", value) ? lang[key].replace("#", value)
: lang[key] : lang[key]
} }
/**
* Truncate a string after the given number of characters
*
* @param value - Value to be truncated
* @param n - Number of characters
*
* @return Truncated value
*/
export function truncate(value: string, n: number): string {
let i = n
if (value.length > i) {
while (value[i] !== " " && --i > 0); // tslint:disable-line
return `${value.substring(0, i)}...`
}
return value
}

View File

@ -1,4 +1,3 @@
/* /*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com> * Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
* *
@ -14,78 +13,37 @@
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
/* ---------------------------------------------------------------------------- import { Subject } from "rxjs"
* Types
* ------------------------------------------------------------------------- */
/** import { WorkerHandler, watchWorker } from "observables"
* Packer message type
*/
export const enum PackerMessageType {
STRING, /* String data */
BINARY /* Packed data */
}
/* ------------------------------------------------------------------------- */ import { PackerMessage } from "../message"
/**
* A message containing an unpacked string
*/
export interface PackerStringMessage {
type: PackerMessageType.STRING /* Message type */
data: string /* Message data */
}
/**
* A message containing a packed string
*/
export interface PackerBinaryMessage {
type: PackerMessageType.BINARY /* Message type */
data: string /* Message data */
}
/* ------------------------------------------------------------------------- */
/**
* A message exchanged with the packer worker
*/
export type PackerMessage =
| PackerStringMessage
| PackerBinaryMessage
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Type guard for packer binary messages * Setup packer web worker
* *
* @param message - Packer worker message * @param worker - Worker instance
* @param options - Options
* *
* @return Test result * @return Worker handler
*/ */
export function isPackerBinaryMessage( export function setupPackerWorker(
message: PackerMessage worker: Worker
): message is PackerBinaryMessage { ): WorkerHandler<PackerMessage> {
return message.type === PackerMessageType.BINARY const tx$ = new Subject<PackerMessage>()
} const rx$ = watchWorker(worker, { message$: tx$ })
/** /* Return worker handler */
* Type guard for packer string messages return { tx$, rx$ }
*
* @param message - Packer worker message
*
* @return Test result
*/
export function isPackerStringMessage(
message: PackerMessage
): message is PackerStringMessage {
return message.type === PackerMessageType.STRING
} }

View File

@ -21,3 +21,4 @@
*/ */
export * from "./_" export * from "./_"
export * from "./message"

View File

@ -28,7 +28,7 @@ import {
decompressFromUTF16 decompressFromUTF16
} from "lz-string" } from "lz-string"
import { PackerMessage, PackerMessageType } from "../_" import { PackerMessage, PackerMessageType } from "../message"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Data * Data

View File

@ -1,3 +1,4 @@
/* /*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com> * Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
* *
@ -20,87 +21,71 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, OperatorFunction, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators"
import { switchMapIf } from "extensions"
import { Agent } from "utilities"
import {
MainState,
SidebarState,
paintSidebar,
watchSidebar
} from "../../main"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Navigation state * Packer message type
*/ */
export interface NavigationState { export const enum PackerMessageType {
sidebar: SidebarState /* Sidebar state */ STRING, /* String data */
BINARY /* Packed data */
} }
/* ---------------------------------------------------------------------------- /* ------------------------------------------------------------------------- */
* Helper types
* ------------------------------------------------------------------------- */
/** /**
* Options * A message containing an unpacked string
*/ */
interface Options { export interface PackerStringMessage {
main$: Observable<MainState> /* Main area state observable */ type: PackerMessageType.STRING /* Message type */
data: string /* Message data */
} }
/**
* A message containing a packed string
*/
export interface PackerBinaryMessage {
type: PackerMessageType.BINARY /* Message type */
data: string /* Message data */
}
/* ------------------------------------------------------------------------- */
/**
* A message exchanged with the packer worker
*/
export type PackerMessage =
| PackerStringMessage
| PackerBinaryMessage
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Watch navigation * Type guard for packer binary messages
* *
* @param el - Navigation element * @param message - Packer worker message
* @param agent - Agent
* @param options - Options
* *
* @return Navigation state observable * @return Test result
*/ */
export function watchNavigation( export function isPackerBinaryMessage(
el: HTMLElement, agent: Agent, { main$ }: Options message: PackerMessage
): Observable<NavigationState> { ): message is PackerBinaryMessage {
return message.type === PackerMessageType.BINARY
/* Watch and paint sidebar */
const sidebar$ = watchSidebar(el, agent, { main$ })
.pipe(
paintSidebar(el)
)
/* Combine into a single hot observable */
return sidebar$
.pipe(
map(sidebar => ({ sidebar }))
)
} }
/* ------------------------------------------------------------------------- */
/** /**
* Mount navigation from source observable * Type guard for packer string messages
* *
* @param agent - Agent * @param message - Packer worker message
* @param options - Options
* *
* @return Operator function * @return Test result
*/ */
export function mountNavigation( export function isPackerStringMessage(
agent: Agent, options: Options message: PackerMessage
): OperatorFunction<HTMLElement, NavigationState> { ): message is PackerStringMessage {
const { media } = agent return message.type === PackerMessageType.STRING
return pipe(
switchMapIf(media.screen$, el => watchNavigation(el, agent, options)),
shareReplay(1)
)
} }

View File

@ -20,119 +20,97 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { SearchIndexOptions, SearchResult } from "modules" import { Observable, Subject } from "rxjs"
import { ajax } from "rxjs/ajax"
import { distinctUntilKeyChanged, map, pluck } from "rxjs/operators"
import { SearchIndexOptions } from "modules"
import {
SearchQuery,
WorkerHandler,
watchWorker
} from "observables"
import {
SearchMessage,
SearchMessageType,
SearchQueryMessage,
SearchSetupMessage,
isSearchResultMessage
} from "../message"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Helper types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Search message type * Options
*/ */
export const enum SearchMessageType { interface Options {
SETUP, /* Search index setup */ base: string /* Base url */
DUMP, /* Search index dump */ query$: Observable<SearchQuery> /* Search query observable */
QUERY, /* Search query */
RESULT /* Search results */
} }
/* ------------------------------------------------------------------------- */
/**
* A message containing the data necessary to setup the search index
*/
export interface SearchSetupMessage {
type: SearchMessageType.SETUP /* Message type */
data: SearchIndexOptions /* Message data */
}
/**
* A message containing the a dump of the search index
*/
export interface SearchDumpMessage {
type: SearchMessageType.DUMP /* Message type */
data: string /* Message data */
}
/**
* A message containing a search query
*/
export interface SearchQueryMessage {
type: SearchMessageType.QUERY /* Message type */
data: string /* Message data */
}
/**
* A message containing results for a search query
*/
export interface SearchResultMessage {
type: SearchMessageType.RESULT /* Message type */
data: SearchResult[] /* Message data */
}
/* ------------------------------------------------------------------------- */
/**
* A message exchanged with the search worker
*/
export type SearchMessage =
| SearchSetupMessage
| SearchDumpMessage
| SearchQueryMessage
| SearchResultMessage
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Type guard for search setup messages * Setup search web worker
* *
* @param message - Search worker message * @param url - Worker url
* @param options - Options
* *
* @return Test result * @return Worker handler
*/ */
export function isSearchSetupMessage( export function setupSearchWorker(
message: SearchMessage url: string, { base, query$ }: Options
): message is SearchSetupMessage { ): WorkerHandler<SearchMessage> {
return message.type === SearchMessageType.SETUP const worker = new Worker(url)
} const prefix = new URL(base, location.href)
/** /* Create communication channels and correct relative links */
* Type guard for search dump messages const tx$ = new Subject<SearchMessage>()
* const rx$ = watchWorker(worker, { tx$ })
* @param message - Search worker message .pipe(
* map(message => {
* @return Test result if (isSearchResultMessage(message)) {
*/ for (const { article, sections } of message.data) {
export function isSearchDumpMessage( article.location = `${prefix}/${article.location}`
message: SearchMessage for (const section of sections)
): message is SearchDumpMessage { section.location = `${prefix}/${section.location}`
return message.type === SearchMessageType.DUMP }
} }
return message
})
)
/** /* Fetch index and setup search worker */
* Type guard for search query messages ajax({
* url: `${base}/search/search_index.json`,
* @param message - Search worker message responseType: "json",
* withCredentials: true
* @return Test result })
*/ .pipe(
export function isSearchQueryMessage( pluck("response"),
message: SearchMessage map<SearchIndexOptions, SearchSetupMessage>(data => ({
): message is SearchQueryMessage { type: SearchMessageType.SETUP,
return message.type === SearchMessageType.QUERY data
} }))
)
.subscribe(tx$.next.bind(tx$))
/** /* Subscribe to search query */
* Type guard for search result messages query$
* .pipe(
* @param message - Search worker message distinctUntilKeyChanged("value"),
* map<SearchQuery, SearchQueryMessage>(query => ({
* @return Test result type: SearchMessageType.QUERY,
*/ data: query.value
export function isSearchResultMessage( }))
message: SearchMessage )
): message is SearchResultMessage { .subscribe(tx$.next.bind(tx$))
return message.type === SearchMessageType.RESULT
/* Return worker handler */
return { tx$, rx$ }
} }

View File

@ -21,3 +21,4 @@
*/ */
export * from "./_" export * from "./_"
export * from "./message"

View File

@ -22,7 +22,7 @@
import { SearchIndex, SearchIndexConfig } from "modules" import { SearchIndex, SearchIndexConfig } from "modules"
import { SearchMessage, SearchMessageType } from "../_" import { SearchMessage, SearchMessageType } from "../message"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Data * Data
@ -46,7 +46,7 @@ function setupLunrLanguages(config: SearchIndexConfig): void {
const base = "../lunr" const base = "../lunr"
/* Add scripts for languages */ /* Add scripts for languages */
const scripts = [`${base}/min/lunr.stemmer.support.min.js`] const scripts = []
for (const lang of config.lang) { for (const lang of config.lang) {
if (lang === "ja") scripts.push(`${base}/tinyseg.js`) if (lang === "ja") scripts.push(`${base}/tinyseg.js`)
if (lang !== "en") scripts.push(`${base}/min/lunr.${lang}.min.js`) if (lang !== "en") scripts.push(`${base}/min/lunr.${lang}.min.js`)
@ -57,7 +57,11 @@ function setupLunrLanguages(config: SearchIndexConfig): void {
scripts.push(`${base}/min/lunr.multi.min.js`) scripts.push(`${base}/min/lunr.multi.min.js`)
/* Load scripts synchronously */ /* Load scripts synchronously */
importScripts(...scripts) if (scripts.length)
importScripts(
`${base}/min/lunr.stemmer.support.min.js`,
...scripts
)
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { SearchIndexOptions, SearchResult } from "modules"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Search message type
*/
export const enum SearchMessageType {
SETUP, /* Search index setup */
DUMP, /* Search index dump */
QUERY, /* Search query */
RESULT /* Search results */
}
/* ------------------------------------------------------------------------- */
/**
* A message containing the data necessary to setup the search index
*/
export interface SearchSetupMessage {
type: SearchMessageType.SETUP /* Message type */
data: SearchIndexOptions /* Message data */
}
/**
* A message containing the a dump of the search index
*/
export interface SearchDumpMessage {
type: SearchMessageType.DUMP /* Message type */
data: string /* Message data */
}
/**
* A message containing a search query
*/
export interface SearchQueryMessage {
type: SearchMessageType.QUERY /* Message type */
data: string /* Message data */
}
/**
* A message containing results for a search query
*/
export interface SearchResultMessage {
type: SearchMessageType.RESULT /* Message type */
data: SearchResult[] /* Message data */
}
/* ------------------------------------------------------------------------- */
/**
* A message exchanged with the search worker
*/
export type SearchMessage =
| SearchSetupMessage
| SearchDumpMessage
| SearchQueryMessage
| SearchResultMessage
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Type guard for search setup messages
*
* @param message - Search worker message
*
* @return Test result
*/
export function isSearchSetupMessage(
message: SearchMessage
): message is SearchSetupMessage {
return message.type === SearchMessageType.SETUP
}
/**
* Type guard for search dump messages
*
* @param message - Search worker message
*
* @return Test result
*/
export function isSearchDumpMessage(
message: SearchMessage
): message is SearchDumpMessage {
return message.type === SearchMessageType.DUMP
}
/**
* Type guard for search query messages
*
* @param message - Search worker message
*
* @return Test result
*/
export function isSearchQueryMessage(
message: SearchMessage
): message is SearchQueryMessage {
return message.type === SearchMessageType.QUERY
}
/**
* Type guard for search result messages
*
* @param message - Search worker message
*
* @return Test result
*/
export function isSearchResultMessage(
message: SearchMessage
): message is SearchResultMessage {
return message.type === SearchMessageType.RESULT
}

View File

@ -34,7 +34,7 @@
border-left: px2rem(4px) solid $clr-blue-a200; border-left: px2rem(4px) solid $clr-blue-a200;
border-radius: px2rem(2px); border-radius: px2rem(2px);
font-size: ms(-1); font-size: ms(-1);
box-shadow: inset 0 0 0 px2rem(1px) transparentize($clr-blue-a200, 0.75); box-shadow: inset 0 0 0 px2rem(1px) transparentize($clr-blue-a200, 0.85);
overflow: auto; overflow: auto;
// Adjust for RTL languages // Adjust for RTL languages
@ -109,7 +109,7 @@
&%#{nth($names, 1)}, &%#{nth($names, 1)},
&.#{nth($names, 1)} { &.#{nth($names, 1)} {
border-left-color: $tint; border-left-color: $tint;
box-shadow: inset 0 0 0 px2rem(1px) transparentize($tint, 0.75); box-shadow: inset 0 0 0 px2rem(1px) transparentize($tint, 0.85);
// Adjust for RTL languages // Adjust for RTL languages
[dir="rtl"] & { [dir="rtl"] & {

View File

@ -76,6 +76,8 @@ body {
// Lock body to viewport height (e.g. in search mode) // Lock body to viewport height (e.g. in search mode)
&[data-md-state="lock"] { &[data-md-state="lock"] {
height: 100%;
min-height: auto;
overflow: hidden; overflow: hidden;
// Hide container on iOS, or the body will not be locked correctly // Hide container on iOS, or the body will not be locked correctly
@ -101,12 +103,6 @@ hr {
margin-left: auto; margin-left: auto;
} }
// Prevent collapse of margin when setting margin on child element
.md-container,
.md-main {
overflow: auto;
}
// Content wrapper // Content wrapper
.md-container { .md-container {
display: flex; display: flex;

View File

@ -24,8 +24,8 @@
"paths": { "paths": {
"actions": ["actions"], "actions": ["actions"],
"components": ["components"], "components": ["components"],
"extensions": ["extensions"],
"modules": ["modules"], "modules": ["modules"],
"observables": ["observables"],
"templates": ["templates"], "templates": ["templates"],
"utilities": ["utilities"], "utilities": ["utilities"],
"workers": ["workers"] "workers": ["workers"]