1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2024-11-12 01:50:52 +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.map": "assets/javascripts/bundle.96943303.min.js.map",
"assets/javascripts/worker/packer.js": "assets/javascripts/worker/packer.819c2a16.min.js",
"assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.819c2a16.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.03c9bfda.min.js",
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.03c9bfda.min.js.map",
"assets/javascripts/bundle.js": "assets/javascripts/bundle.65b5374b.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.65b5374b.min.js.map",
"assets/javascripts/worker/packer.js": "assets/javascripts/worker/packer.c14659e8.min.js",
"assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.c14659e8.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.ce66ce8d.min.js",
"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.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 %}
{% endblock %}
{% 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 %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/app-palette.8c25017f.min.css' | url }}">
{% endif %}
@ -188,7 +188,7 @@
{% endblock %}
</div>
{% 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">
{%- set translations = {} -%}
{%- for key in [
@ -207,7 +207,7 @@
{%- endfor -%}
{{ translations | tojson }}
</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"] %}
<script src="{{ path | url }}"></script>
{% endfor %}

407
package-lock.json generated
View File

@ -321,15 +321,6 @@
"integrity": "sha512-yWj3OnlKlwNpq9+Jh/nJkVAD3ta8Abk2kIRpjWpVkDlAD43tn6Q6xk5hurp84ndcq54jBDBGCD/WcIR0pspG0A==",
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -639,51 +630,6 @@
"integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==",
"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": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
@ -757,12 +703,6 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -1571,6 +1511,16 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
@ -1664,15 +1614,6 @@
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1926,18 +1867,6 @@
"integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=",
"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": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz",
@ -2029,16 +1958,6 @@
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
"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": {
"version": "2.6.9",
"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": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
@ -2335,38 +2298,6 @@
"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": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -2510,23 +2441,6 @@
"integrity": "sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw==",
"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": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -4163,6 +4077,32 @@
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
@ -4643,30 +4583,12 @@
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -4830,12 +4752,6 @@
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==",
"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": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -5019,18 +4935,6 @@
"integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=",
"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": {
"version": "2.0.2",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
@ -5186,6 +5084,12 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
@ -5356,6 +5260,21 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"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": {
"version": "3.0.0",
"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==",
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@ -6012,12 +5921,6 @@
"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": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz",
@ -6195,76 +6098,12 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"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": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"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": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@ -6607,15 +6446,6 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@ -6785,12 +6615,6 @@
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@ -7747,12 +7571,6 @@
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
"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": {
"version": "0.0.6",
"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": {
"version": "1.0.0",
"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"
},
"scripts": {
"build": "npx webpack --mode production",
"build": "npm run clean && npx webpack --mode production",
"clean": "rm -rf material",
"lint": "npm run lint:ts && npm run lint:scss",
"lint:scss": "npx stylelint `find src/assets -name *.scss`",
"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": {
"clipboard": "^2.0.0",

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
export * from "./anchor"
export * from "./header"
export * from "./main"
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 { map, scan, shareReplay, switchMap } from "rxjs/operators"
import { getElement } from "utilities"
import { getElement } from "observables"
/* ----------------------------------------------------------------------------
* Types

View File

@ -20,6 +20,4 @@
* IN THE SOFTWARE.
*/
export * from "./_"
export * from "./offset"
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 { map, shareReplay } from "rxjs/operators"
import { map, shareReplay, switchMap } from "rxjs/operators"
import { switchMapIf } from "extensions"
import { Agent, paintHidden } from "utilities"
import { HeaderState, watchViewportOffsetFromTopOf } from "../header"
import {
Header,
Viewport,
paintHideable,
watchViewportFrom
} from "observables"
/* ----------------------------------------------------------------------------
* Types
@ -47,7 +49,9 @@ export interface HeroState {
* 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
*/
export function watchHero(
el: HTMLElement, agent: Agent, { header$ }: Options
el: HTMLElement, options: Options
): Observable<HeroState> {
/* Watch and paint visibility */
const hidden$ = watchViewportOffsetFromTopOf(el, agent, { header$ })
const hidden$ = watchViewportFrom(el, options)
.pipe(
paintHidden(el, 20)
paintHideable(el, 20)
)
/* Combine into a single hot observable */
@ -91,11 +95,10 @@ export function watchHero(
* @return Operator function
*/
export function mountHero(
agent: Agent, options: Options
options: Options
): OperatorFunction<HTMLElement, HeroState> {
const { media } = agent
return pipe(
switchMapIf(media.screen$, el => watchHero(el, agent, options)),
switchMap(el => watchHero(el, options)),
shareReplay(1)
)
}

View File

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

View File

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

View File

@ -23,21 +23,19 @@
import { Observable, OperatorFunction, combineLatest, pipe } from "rxjs"
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 {
AnchorList,
Header,
Main,
Sidebar,
Viewport,
getElements,
paintAnchorList,
watchAnchorList
} from "../anchor"
paintSidebar,
watchAnchorList,
watchSidebar
} from "observables"
import { switchMapIf } from "utilities"
/* ----------------------------------------------------------------------------
* Types
@ -47,7 +45,7 @@ import {
* Table of contents state
*/
export interface TableOfContentsState {
sidebar: SidebarState /* Sidebar state */
sidebar: Sidebar /* Sidebar state */
anchors: AnchorList /* Anchor list */
}
@ -59,8 +57,10 @@ export interface TableOfContentsState {
* Options
*/
interface Options {
header$: Observable<HeaderState> /* Header state observable */
main$: Observable<MainState> /* Main area state observable */
header$: Observable<Header> /* Header 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
*/
export function watchTableOfContents(
el: HTMLElement, agent: Agent, { header$, main$ }: Options
el: HTMLElement, { header$, main$, viewport$ }: Options
): Observable<TableOfContentsState> {
/* Watch and paint sidebar */
const sidebar$ = watchSidebar(el, agent, { main$ })
const sidebar$ = watchSidebar(el, { main$, viewport$ })
.pipe(
paintSidebar(el)
)
/* Watch and paint anchor list (scroll spy) */
const els = getElements<HTMLAnchorElement>(".md-nav__link", el)
const anchors$ = watchAnchorList(els, agent, { header$ })
const anchors$ = watchAnchorList(els, { header$, viewport$ })
.pipe(
paintAnchorList(els)
)
@ -111,11 +111,10 @@ export function watchTableOfContents(
* @return Operator function
*/
export function mountTableOfContents(
agent: Agent, options: Options
options: Options
): OperatorFunction<HTMLElement, TableOfContentsState> {
const { media } = agent
return pipe(
switchMapIf(media.tablet$, el => watchTableOfContents(el, agent, options)),
switchMapIf(options.tablet$, el => watchTableOfContents(el, options)),
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"
import { SearchResult } from "modules"
import { Agent, watchElementOffset } from "utilities"
import { paintSearchResultList } from "../list"
import { paintSearchResultMeta } from "../meta"
import {
SearchQuery,
Viewport,
paintSearchResult,
watchElementOffset
} from "observables"
/* ----------------------------------------------------------------------------
* Helper types
@ -44,59 +46,45 @@ import { paintSearchResultMeta } from "../meta"
* Options
*/
interface Options {
query$: Observable<SearchQuery> /* Search query observable */
result$: Observable<SearchResult[]> /* Search result observable */
query$: Observable<string> /* Search query observable */
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
* 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
*
* @param agent - Agent
* @param options - Options
*
* @return Operator function
*/
export function mountSearchResult(
agent: Agent, options: Options
{ query$, result$, viewport$ }: Options
): OperatorFunction<HTMLElement, SearchResult[]> {
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)
)
}

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 {
EMPTY,
Observable,
Subject,
forkJoin,
merge,
of,
fromEvent,
interval,
NEVER
OperatorFunction,
pipe
} from "rxjs"
import { ajax } from "rxjs/ajax"
import {
delay,
filter,
@ -47,53 +44,46 @@ import {
pluck,
switchMap,
switchMapTo,
take,
tap,
withLatestFrom,
distinctUntilChanged,
distinctUntilKeyChanged,
shareReplay
} from "rxjs/operators"
import {
Component,
paintHeaderShadow,
mountHero,
mountMain,
mountNavigation,
mountSearchResult,
mountTableOfContents,
mountTabs,
switchComponent,
watchComponentMap,
} from "./components"
import {
watchHeader,
watchSearchQuery,
watchSearchReset
} from "./components"
import { SearchIndexOptions } from "./modules"
import {
watchSearchReset,
getElement,
setupAgent,
watchToggle,
watchWorker,
setToggle,
getElements,
watchMedia,
translate,
watchElementFocus
} from "./utilities"
watchDocument,
watchLocationHash,
watchMain,
watchViewport,
watchKeyboard
} from "./observables"
import {
PackerMessage,
PackerMessageType,
SearchMessage,
SearchMessageType,
SearchSetupMessage,
isSearchDumpMessage,
isSearchResultMessage
isSearchResultMessage,
setupSearchWorker
} from "./workers"
import { renderSource } from "templates"
import { switchMapIf, not, takeIf } from "extensions"
import { not, takeIf } from "utilities"
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
@ -117,6 +107,10 @@ export interface Config {
document.documentElement.classList.remove("no-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[] = [
"container", /* Container */
"header", /* Header */
@ -154,106 +148,6 @@ function isConfig(config: any): config is Config {
&& 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.
*/
@ -279,42 +173,29 @@ function repository() {
// github repository...
const [, user, repo] = el.href.match(/^.+github\.com\/([^\/]+)\/?([^\/]+)?.*$/i)
// storage memoization!?
// get, if not available, exec and persist
// getOrRetrieve... storage$.
// Show repo stats
if (user && repo) {
return ajax({
url: `https://api.github.com/repos/${user}/${repo}`,
responseType: "json"
})
return fetchGitHubStats(user, repo)
.pipe(
map(({ status, response }) => {
if (status === 200) {
const { stargazers_count, forks_count } = response
return [
`${format(stargazers_count)} Stars`,
`${format(forks_count)} Forks`
]
}
return []
}),
map(({ stargazers_count, forks_count }) => ([
`${format(stargazers_count || 0)} Stars`,
`${format(forks_count || 0)} Forks`
])),
tap(data => sessionStorage.setItem("repository", JSON.stringify(data)))
)
// Show user or organization stats
} else if (user) {
return ajax({
url: `https://api.github.com/users/${user}`,
responseType: "json"
})
return fetchGitHubStats(user)
.pipe(
map(({ status, response }) => {
if (status === 200) {
const { public_repos } = response
return [
`${format(public_repos)} Repositories`
]
}
return []
}),
map(({ public_repos }) => ([
`${format(public_repos || 0)} Repositories`
])),
tap(data => sessionStorage.setItem("repository", JSON.stringify(data)))
)
}
@ -334,14 +215,6 @@ export function initialize(config: unknown) {
if (!isConfig(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
repository().subscribe(facts => {
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 */
const components$ = watchComponentMap(names, { document$: agent.document.load$ })
const components$ = watchComponentMap(names, { document$ })
const component = <T extends HTMLElement>(name: Component): Observable<T> => {
return components$
.pipe(
@ -383,120 +263,104 @@ export function initialize(config: unknown) {
)
.subscribe()
// ----------------------------------------------------------------------------
// 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()
/* ----------------------------------------------------------------------- */
// DONE
const main$ = component("main")
.pipe(
mountMain(agent, { header$ })
)
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$ })
switchMap(el => watchMain(el, { header$, viewport$ })),
shareReplay(1) // TODO: mount!?
)
// ---------------------------------------------------------------------------
/* ----------------------------------------------------------------------- */
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)
.pipe(
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")
.pipe(
switchMap(watchSearchReset)
)
const key$ = fromEvent<KeyboardEvent>(window, "keydown").pipe(
filter(ev => !(ev.metaKey || ev.ctrlKey))
)
/* ----------------------------------------------------------------------- */
// filter arrow keys if search is active!
searchActive$.subscribe(console.log)
// DONE (partly)
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
key$
.pipe(
takeIf(not(searchActive$))
)
.subscribe(ev => {
.subscribe(key => {
if (
document.activeElement && (
["TEXTAREA", "SELECT", "INPUT"].includes(
@ -508,7 +372,7 @@ export function initialize(config: unknown) {
) {
// do nothing...
} else {
if (ev.keyCode === 70 || ev.keyCode === 83) {
if (key.type === "KeyS" || key.type === "KeyF") {
setToggle(search, true)
}
}
@ -520,27 +384,27 @@ export function initialize(config: unknown) {
takeIf(searchActive$),
/* Abort if meta key (macOS) or ctrl key (Windows) is pressed */
tap(ev => {
if (ev.key === "Enter") {
tap(key => {
console.log("jo", key)
if (key.type === "Enter") {
if (document.activeElement === getElement("[data-md-component=search-query]")) {
ev.preventDefault()
key.claim()
// intercept hash change after search closed
} else {
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 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()
/* Prevent scrolling of page */
ev.preventDefault()
ev.stopPropagation()
key.claim()
} else if (ev.key === "Escape" || ev.key === "Tab") {
} else if (key.type === "Escape" || key.type === "Tab") {
setToggle(search, false)
getElement("[data-md-component=search-query]")!.blur()
@ -564,6 +428,8 @@ export function initialize(config: unknown) {
)
.subscribe()
// focusable -> setFocus(true, false)
/* ----------------------------------------------------------------------- */
/* Open details before printing */
@ -579,50 +445,109 @@ export function initialize(config: unknown) {
})
// Close drawer and search on hash change
agent.location.hash$.subscribe(() => {
hash$.subscribe(() => {
setToggle(drawer, false)
setToggle(search, false) // we probably need to delay the anchor jump for search
})
/* ----------------------------------------------------------------------- */
/* Clipboard integration */
/* Clipboard.js integration */
if (Clipboard.isSupported()) {
const blocks = getElements(".codehilite > pre, .highlight> pre, pre > code")
Array.prototype.forEach.call(blocks, (block, index) => {
const id = `__code_${index}`
/* Create button with message container */
const button = renderClipboard(id)
/* Link to block and insert button */
const parent = block.parentNode
parent.id = id
parent.insertBefore(button, block)
})
const blocks = getElements("pre > code")
for (const [index, block] of blocks.entries()) {
const parent = block.parentElement!
parent.id = `__code_${index}`
parent.insertBefore(renderClipboard(parent.id), block)
}
/* Initialize Clipboard listener */
const copy = new Clipboard(".md-clipboard")
const copy = new Clipboard(".md-clipboard") // create observable...
/* Success handler */
copy.on("success", action => {
alert("Copied to clipboard") // TODO: integrate snackbar
// TODO: add a snackbar/notification
// copy.on("success", action => {
// alert("Copied to clipboard") // TODO: integrate snackbar
// // 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")
.pipe(
switchMapIf(not(agent.media.tablet$), el => watchActiveLayer(el)
.pipe(
paintActiveLayer()
)
)
)
.subscribe(console.log)
// get headerHEIGHT! only if header is sticky!
// // lockHeader at...
// const direction$ = agent.viewport.offset$.pipe(
// bufferCount(2, 1), // determine scroll direction
// map(([{ y: y0 }, { y: y1 }]) => y1 > y0),
// distinctUntilChanged(),
// )
// 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
return {
agent,
// agent,
state
}
}
// function mountSearchQuery(
// ): OperatorFunction<HTMLInputElement, SearchQuery> {
// return pipe(
// switchMap(el => watchSearchQuery(el))
// )
// }

View File

@ -95,12 +95,22 @@ export function setupSearchDocumentMap(
/* Add subsequent section */
} else {
documents.set(location, { location, title, text, parent })
documents.set(location, {
location,
title,
text,
parent
})
}
/* Add article */
} else {
documents.set(location, { location, title, text, linked: false })
documents.set(location, {
location,
title,
text,
linked: false
})
}
}
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 { map, shareReplay, startWith } from "rxjs/operators"
import {
distinctUntilKeyChanged,
map,
shareReplay,
startWith
} from "rxjs/operators"
import { Agent } from "../../_"
import { ViewportSize } from "../../viewport"
import { Viewport } from "../../viewport"
/* ----------------------------------------------------------------------------
* Types
@ -46,7 +50,7 @@ export interface ElementOffset {
* Options
*/
interface Options {
size$: Observable<ViewportSize> /* Viewport size observable */
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
@ -56,7 +60,7 @@ interface Options {
/**
* Retrieve element offset
*
* @param el - HTML element
* @param el - Element
*
* @return Element offset
*/
@ -73,15 +77,21 @@ export function getElementOffset(el: HTMLElement): ElementOffset {
* Watch element offset
*
* @param el - Element
* @param agent - Agent
* @param options - Options
*
* @return Element offset observable
*/
export function watchElementOffset(
el: HTMLElement, { viewport }: Agent
el: HTMLElement, { viewport$ }: Options
): Observable<ElementOffset> {
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(
map(() => getElementOffset(el)),
startWith(getElementOffset(el)),

View File

@ -20,9 +20,9 @@
* IN THE SOFTWARE.
*/
export * from "./_"
export * from "./document"
export * from "./element"
export * from "./keyboard"
export * from "./location"
export * from "./media"
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.
*/
import { Observable, fromEvent, merge } from "rxjs"
import { Observable, combineLatest, fromEvent, merge } from "rxjs"
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
* ------------------------------------------------------------------------- */
@ -57,6 +43,28 @@ export interface ViewportSize {
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
* ------------------------------------------------------------------------- */
@ -96,8 +104,7 @@ export function watchViewportOffset(): Observable<ViewportOffset> {
return merge(scroll$, resize$)
.pipe(
map(getViewportOffset),
startWith(getViewportOffset()),
shareReplay(1)
startWith(getViewportOffset())
)
}
@ -110,7 +117,22 @@ export function watchViewportSize(): Observable<ViewportSize> {
return resize$
.pipe(
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)
)
}

View File

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

View File

@ -20,15 +20,11 @@
* IN THE SOFTWARE.
*/
import { MonoTypeOperatorFunction, Observable, pipe } from "rxjs"
import { map, withLatestFrom } from "rxjs/operators"
import { Observable, combineLatest } from "rxjs"
import { map, shareReplay } from "rxjs/operators"
import {
resetSearchResultMeta,
setSearchResultMeta
} from "actions"
import { SearchResult } from "modules"
import { getElement } from "utilities"
import { Header } from "../../../header"
import { Viewport } from "../_"
/* ----------------------------------------------------------------------------
* Helper types
@ -38,7 +34,8 @@ import { getElement } from "utilities"
* 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
*
* @return Operator function
* @return Viewport observable
*/
export function paintSearchResultMeta(
el: HTMLElement, { query$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> {
const meta = getElement(".md-search-result__meta", el)!
return pipe(
withLatestFrom(query$),
map(([result, query]) => {
if (query) {
setSearchResultMeta(meta, result.length)
} else {
resetSearchResultMeta(meta)
}
return result
})
)
export function watchViewportFrom(
el: HTMLElement, { header$, viewport$ }: Options
): Observable<Viewport> {
return combineLatest([viewport$, header$])
.pipe(
map(([{ offset, size }, { height }]) => ({
offset: {
x: offset.x - el.offsetLeft,
y: offset.y - el.offsetTop + height
},
size
})),
shareReplay(1)
)
}

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { Observable, fromEvent } from "rxjs"
import { Observable, Subject, fromEvent } from "rxjs"
import { pluck, share, switchMapTo, tap, throttle } from "rxjs/operators"
/* ----------------------------------------------------------------------------
@ -35,6 +35,18 @@ export interface WorkerMessage {
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
* ------------------------------------------------------------------------- */
@ -45,7 +57,7 @@ export interface WorkerMessage {
* @template T - Worker message type
*/
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.
*
* @param worker - Web worker
* @param options - Options
*
* @return Worker message observable
*/
export function watchWorker<T extends WorkerMessage>(
worker: Worker, { send$ }: Options<T>
worker: Worker, { tx$ }: Options<T>
): Observable<T> {
/* Intercept messages from web worker */
const recv$ = fromEvent(worker, "message")
const rx$ = fromEvent(worker, "message")
.pipe(
pluck<Event, T>("data"),
share()
pluck<Event, T>("data")
)
/* Send and receive messages, return hot observable */
return send$
return tx$
.pipe(
throttle(() => recv$, { leading: true, trailing: true }),
throttle(() => rx$, { leading: true, trailing: true }),
tap(message => worker.postMessage(message)),
switchMapTo(recv$),
switchMapTo(rx$),
share()
)
}

View File

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

View File

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

View File

@ -20,4 +20,10 @@
* IN THE SOFTWARE.
*/
export * from "./agent"
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.
*/
import { Observable, OperatorFunction, combineLatest, pipe } from "rxjs"
import { Observable, combineLatest } from "rxjs"
import {
distinctUntilChanged,
map,
pluck,
shareReplay,
switchMap
shareReplay
} from "rxjs/operators"
import { Agent } from "utilities"
import { HeaderState } from "../../header"
import { Viewport } from "../../agent"
import { Header } from "../../header"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Main area state
* Main area
*/
export interface MainState {
export interface Main {
offset: number /* Main area top offset */
height: number /* Main area visible height */
active: boolean /* Scrolled past top offset */
@ -54,7 +52,8 @@ export interface MainState {
* 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
*
* 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.
*
* @param el - Main area element
* @param agent - Agent
* @param options - Options
*
* @return Main area state observable
* @return Main area observable
*/
export function watchMain(
el: HTMLElement, { viewport }: Agent, { header$ }: Options
): Observable<MainState> {
el: HTMLElement, { header$, viewport$ }: Options
): Observable<Main> {
/* Compute necessary adjustment for header */
const adjust$ = header$
@ -85,13 +83,9 @@ export function watchMain(
)
/* Compute the main area's visible height */
const height$ = combineLatest([
viewport.offset$,
viewport.size$,
adjust$
])
const height$ = combineLatest([viewport$, adjust$])
.pipe(
map(([{ y }, { height }, adjust]) => {
map(([{ offset: { y }, size: { height } }, adjust]) => {
const top = el.offsetTop
const bottom = el.offsetHeight + top
return height
@ -102,9 +96,9 @@ export function watchMain(
)
/* Compute whether the viewport offset is past the main area's top */
const active$ = combineLatest([viewport.offset$, adjust$])
const active$ = combineLatest([viewport$, adjust$])
.pipe(
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
map(([{ offset: { y } }, adjust]) => y >= el.offsetTop - adjust),
distinctUntilChanged()
)
@ -115,25 +109,7 @@ export function watchMain(
offset: el.offsetTop - adjust,
height,
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 { ViewportOffset } from "../agent"
import { Viewport } from "../../agent"
/* ----------------------------------------------------------------------------
* Functions
@ -45,11 +45,11 @@ import { ViewportOffset } from "../agent"
*
* @return Operator function
*/
export function paintHidden(
export function paintHideable(
el: HTMLElement, offset: number = 0
): OperatorFunction<ViewportOffset, boolean> {
): OperatorFunction<Viewport, boolean> {
return pipe(
map(({ y }) => y >= offset),
map(({ offset: { y } }) => y >= offset),
distinctUntilChanged(),
/* Defer repaint to next animation frame */

View File

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

View File

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

View File

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

View File

@ -43,16 +43,17 @@ import {
resetOverflowScrolling,
setOverflowScrolling
} from "actions"
import { getElement } from "utilities"
import { getElement } from "../../agent"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Active layer
* Navigation layer
*/
export interface ActiveLayer {
export interface NavigationLayer {
prev?: HTMLElement /* Layer (previous) */
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
* 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.
* This implementation keeps track of the previous and currently active layer.
* only add it to the navigation layer or extremely weird cropping will occur.
* This implementation keeps track of the previous and current layer.
*
* @param els - Navigation elements
*
* @return Active layer observable
* @return Navigation layer observable
*/
export function watchActiveLayer(
export function watchNavigationLayer(
els: HTMLElement[]
): Observable<ActiveLayer> {
): Observable<NavigationLayer> {
const table = new Map<HTMLInputElement, HTMLElement>()
for (const el of els) {
const label = getElement<HTMLLabelElement>("label", el)
@ -86,7 +87,7 @@ export function watchActiveLayer(
}
/* Determine active layer */
const active$ = merge(
const layer$ = merge(
...[...table.keys()].map(input => fromEvent(input, "change"))
)
.pipe(
@ -96,7 +97,7 @@ export function watchActiveLayer(
)
/* Return previous and next layer */
return active$
return layer$
.pipe(
map(next => ({ 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
*
* @return Operator function
*/
export function paintActiveLayer(
export function paintNavigationLayer(
els: HTMLElement[]
): MonoTypeOperatorFunction<ActiveLayer> {
): MonoTypeOperatorFunction<NavigationLayer> {
return pipe(
/* Defer repaint to next animation frame */
@ -125,14 +126,6 @@ export function paintActiveLayer(
resetOverflowScrolling(prev)
}),
/* Reset on complete or error */
finalize(() => {
for (const el of els)
resetOverflowScrolling(
getElement(".md-nav__list", el)!
)
}),
/* Wait until transition has finished */
delay(250),
@ -140,6 +133,14 @@ export function paintActiveLayer(
observeOn(animationFrameScheduler),
tap(({ 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
} from "rxjs/operators"
import { watchElementFocus } from "utilities"
import { watchElementFocus } from "../../agent"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Search query state
* Search query
*/
export interface SearchQueryState {
export interface SearchQuery {
value: string /* Query value */
focus: boolean /* Query focus state */
focus: boolean /* Query focus */
}
/* ----------------------------------------------------------------------------
@ -50,7 +50,28 @@ export interface SearchQueryState {
* 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 options - Options
*
* @return Search query state observable
* @return Search query observable
*/
export function watchSearchQuery(
el: HTMLInputElement, { prepare }: Options
): Observable<SearchQueryState> {
el: HTMLInputElement, { transform = defaultTransform }: Options = {}
): Observable<SearchQuery> {
/* Intercept keyboard events */
const value$ = fromEvent(el, "keyup")
.pipe(
map(() => prepare(el.value)),
startWith(""),
map(() => transform(el.value)),
startWith(el.value),
distinctUntilChanged()
)

View File

@ -28,19 +28,25 @@ import {
} from "rxjs"
import {
finalize,
map,
mapTo,
observeOn,
scan,
switchMap
switchMap,
withLatestFrom
} from "rxjs/operators"
import {
addToSearchResultList,
resetSearchResultList
resetSearchResultList,
resetSearchResultMeta,
setSearchResultMeta
} from "actions"
import { SearchResult } from "modules"
import { renderSearchResult } from "templates"
import { getElement } from "utilities"
import { getElement } from "../../agent"
import { SearchQuery } from "../query"
/* ----------------------------------------------------------------------------
* Helper types
@ -50,7 +56,8 @@ import { getElement } from "utilities"
* 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 options - Options
*
* @return Operator function
*/
export function paintSearchResultList(
el: HTMLElement, { render$ }: Options
export function paintSearchResult(
el: HTMLElement, { query$, fetch$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> {
const container = el.parentElement!
const list = getElement(".md-search-result__list", el)!
const meta = getElement(".md-search-result__meta", el)!
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(
/* Defer repaint to next animation frame */
observeOn(animationFrameScheduler),
scan(index => {
const container = el.parentElement!
while (index < result.length) {
addToSearchResultList(list, renderSearchResult(result[index++]))
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
* 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.
*
* @param el - Toggle element

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { h } from "extensions"
import { h } from "utilities"
/* ----------------------------------------------------------------------------
* Data
@ -43,7 +43,7 @@ const css = {
*
* @param table - Table element
*
* @return HTML element
* @return Element
*/
export function renderTable(
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.
*/
export * from "./agent"
export * from "./hidden"
export * from "./jsx"
export * from "./rxjs"
export * from "./string"
export * from "./toggle"

View File

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

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { getElement } from "../agent"
import { getElement } from "observables"
/* ----------------------------------------------------------------------------
* Data
@ -35,23 +35,6 @@ let lang: Record<string, string>
* 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
*
@ -72,3 +55,20 @@ export function translate(key: string, value?: string): string {
? lang[key].replace("#", value)
: 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>
*
@ -14,78 +13,37 @@
*
* 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
* 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.
*/
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
import { Subject } from "rxjs"
/**
* Packer message type
*/
export const enum PackerMessageType {
STRING, /* String data */
BINARY /* Packed data */
}
import { WorkerHandler, watchWorker } from "observables"
/* ------------------------------------------------------------------------- */
/**
* 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
import { PackerMessage } from "../message"
/* ----------------------------------------------------------------------------
* 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(
message: PackerMessage
): message is PackerBinaryMessage {
return message.type === PackerMessageType.BINARY
}
export function setupPackerWorker(
worker: Worker
): WorkerHandler<PackerMessage> {
const tx$ = new Subject<PackerMessage>()
const rx$ = watchWorker(worker, { message$: tx$ })
/**
* Type guard for packer string messages
*
* @param message - Packer worker message
*
* @return Test result
*/
export function isPackerStringMessage(
message: PackerMessage
): message is PackerStringMessage {
return message.type === PackerMessageType.STRING
/* Return worker handler */
return { tx$, rx$ }
}

View File

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

View File

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

View File

@ -1,3 +1,4 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
@ -20,87 +21,71 @@
* 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
* ------------------------------------------------------------------------- */
/**
* Navigation state
* Packer message type
*/
export interface NavigationState {
sidebar: SidebarState /* Sidebar state */
export const enum PackerMessageType {
STRING, /* String data */
BINARY /* Packed data */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------- */
/**
* Options
* A message containing an unpacked string
*/
interface Options {
main$: Observable<MainState> /* Main area state observable */
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
* ------------------------------------------------------------------------- */
/**
* Watch navigation
* Type guard for packer binary messages
*
* @param el - Navigation element
* @param agent - Agent
* @param options - Options
* @param message - Packer worker message
*
* @return Navigation state observable
* @return Test result
*/
export function watchNavigation(
el: HTMLElement, agent: Agent, { main$ }: Options
): Observable<NavigationState> {
/* 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 }))
)
export function isPackerBinaryMessage(
message: PackerMessage
): message is PackerBinaryMessage {
return message.type === PackerMessageType.BINARY
}
/* ------------------------------------------------------------------------- */
/**
* Mount navigation from source observable
* Type guard for packer string messages
*
* @param agent - Agent
* @param options - Options
* @param message - Packer worker message
*
* @return Operator function
* @return Test result
*/
export function mountNavigation(
agent: Agent, options: Options
): OperatorFunction<HTMLElement, NavigationState> {
const { media } = agent
return pipe(
switchMapIf(media.screen$, el => watchNavigation(el, agent, options)),
shareReplay(1)
)
export function isPackerStringMessage(
message: PackerMessage
): message is PackerStringMessage {
return message.type === PackerMessageType.STRING
}

View File

@ -20,119 +20,97 @@
* 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 {
SETUP, /* Search index setup */
DUMP, /* Search index dump */
QUERY, /* Search query */
RESULT /* Search results */
interface Options {
base: string /* Base url */
query$: Observable<SearchQuery> /* Search query observable */
}
/* ------------------------------------------------------------------------- */
/**
* 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
* 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(
message: SearchMessage
): message is SearchSetupMessage {
return message.type === SearchMessageType.SETUP
}
export function setupSearchWorker(
url: string, { base, query$ }: Options
): WorkerHandler<SearchMessage> {
const worker = new Worker(url)
const prefix = new URL(base, location.href)
/**
* 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
}
/* Create communication channels and correct relative links */
const tx$ = new Subject<SearchMessage>()
const rx$ = watchWorker(worker, { tx$ })
.pipe(
map(message => {
if (isSearchResultMessage(message)) {
for (const { article, sections } of message.data) {
article.location = `${prefix}/${article.location}`
for (const section of sections)
section.location = `${prefix}/${section.location}`
}
}
return message
})
)
/**
* 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
}
/* Fetch index and setup search worker */
ajax({
url: `${base}/search/search_index.json`,
responseType: "json",
withCredentials: true
})
.pipe(
pluck("response"),
map<SearchIndexOptions, SearchSetupMessage>(data => ({
type: SearchMessageType.SETUP,
data
}))
)
.subscribe(tx$.next.bind(tx$))
/**
* 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
/* Subscribe to search query */
query$
.pipe(
distinctUntilKeyChanged("value"),
map<SearchQuery, SearchQueryMessage>(query => ({
type: SearchMessageType.QUERY,
data: query.value
}))
)
.subscribe(tx$.next.bind(tx$))
/* Return worker handler */
return { tx$, rx$ }
}

View File

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

View File

@ -22,7 +22,7 @@
import { SearchIndex, SearchIndexConfig } from "modules"
import { SearchMessage, SearchMessageType } from "../_"
import { SearchMessage, SearchMessageType } from "../message"
/* ----------------------------------------------------------------------------
* Data
@ -46,7 +46,7 @@ function setupLunrLanguages(config: SearchIndexConfig): void {
const base = "../lunr"
/* Add scripts for languages */
const scripts = [`${base}/min/lunr.stemmer.support.min.js`]
const scripts = []
for (const lang of config.lang) {
if (lang === "ja") scripts.push(`${base}/tinyseg.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`)
/* 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-radius: px2rem(2px);
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;
// Adjust for RTL languages
@ -109,7 +109,7 @@
&%#{nth($names, 1)},
&.#{nth($names, 1)} {
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
[dir="rtl"] & {

View File

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

View File

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