aboutsummaryrefslogtreecommitdiff
path: root/haddock-api/resources/html/index.js
diff options
context:
space:
mode:
authoralexbiehl <alex.biehl@gmail.com>2017-08-27 18:50:16 +0200
committeralexbiehl <alex.biehl@gmail.com>2017-08-27 18:50:16 +0200
commit705dc006595db165e1cb244485cf47bcae02e2d0 (patch)
treee65650d436af87bfd84b4ae65668d14490b98476 /haddock-api/resources/html/index.js
parent3a09040a16fb574254d4dc095047ed7b0b7beb19 (diff)
Content search for haddock html doc
Diffstat (limited to 'haddock-api/resources/html/index.js')
-rw-r--r--haddock-api/resources/html/index.js381
1 files changed, 381 insertions, 0 deletions
diff --git a/haddock-api/resources/html/index.js b/haddock-api/resources/html/index.js
new file mode 100644
index 00000000..9123f8ae
--- /dev/null
+++ b/haddock-api/resources/html/index.js
@@ -0,0 +1,381 @@
+// alias preact's hyperscript reviver since it's referenced a lot:
+var h = preact.h;
+
+function createClass(obj) {
+ // sub-class Component:
+ function F(){ preact.Component.call(this); }
+ var p = F.prototype = new preact.Component;
+ // copy our skeleton into the prototype:
+ for (var i in obj) {
+ if (i === 'getDefaultProps' && typeof obj.getDefaultProps === 'function') {
+ F.defaultProps = obj.getDefaultProps() || {};
+ } else {
+ p[i] = obj[i];
+ }
+ }
+ // restore constructor:
+ return p.constructor = F;
+}
+
+function loadJSON(path, success, error) {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function()
+ {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ if (success)
+ success(JSON.parse(xhr.responseText));
+ } else {
+ if (error)
+ error(xhr);
+ }
+ }
+ };
+ xhr.open("GET", path, true);
+ xhr.send();
+}
+
+// -------------------------------------------------------------------------- //
+
+var PageMenuButton = createClass({
+
+ render: function(props) {
+ function onClick(e) {
+ e.preventDefault();
+ props.onClick();
+ }
+
+ return h('li', null,
+ h('a', { href: '#', onClick: onClick }, props.title)
+ );
+ }
+
+});
+
+function addSearchPageMenuButton(action) {
+ var pageMenu = document.querySelector('#package-header ul.links');
+ var dummy = document.createElement('li');
+ pageMenu.insertBefore(dummy, pageMenu.firstChild);
+ preact.render(h(PageMenuButton, { onClick: action, title: "Search" }), pageMenu, dummy);
+}
+
+// -------------------------------------------------------------------------- //
+
+function take(n, arr) {
+ if (arr.length <= n) { return arr; }
+ return arr.slice(0, n);
+}
+
+var App = createClass({
+ componentWillMount: function() {
+ var self = this;
+ self.setState({
+ searchString: '',
+ isVisible: false,
+ expanded: {},
+ activeLinkIndex: -1,
+ moduleResults: []
+ });
+ loadJSON("doc-index.json", function(data) {
+ self.setState({
+ fuse: new Fuse(data, {
+ threshold: 0.4,
+ caseSensitive: true,
+ includeScore: true,
+ keys: ["name"]
+ }),
+ moduleResults: []
+ });
+ }, function (err) {
+ if (console) {
+ console.error("could not load 'doc-index.json' for searching", err);
+ }
+ self.setState({ failedLoading: true });
+ });
+
+ document.addEventListener('mousedown', this.hide.bind(this));
+
+ document.addEventListener('keydown', function(e) {
+ if (self.state.isVisible) {
+ if (e.key === 'Escape') {
+ self.hide();
+ } else if (e.key === 'ArrowUp' || (e.key === 'k' && e.ctrlKey)) {
+ e.preventDefault();
+ self.navigateLinks(-1);
+ } else if (e.key === 'ArrowDown' || (e.key === 'j' && e.ctrlKey)) {
+ e.preventDefault();
+ self.navigateLinks(+1);
+ } else if (e.key === 'Enter' && self.state.activeLinkIndex >= 0) {
+ self.followActiveLink();
+ }
+ }
+
+ if (e.key === 's' && e.target.tagName.toLowerCase() !== 'input') {
+ e.preventDefault();
+ self.show();
+ }
+ })
+ },
+
+ hide: function() {
+ this.setState({ isVisible: false });
+ },
+
+ show: function() {
+ if (!this.state.isVisible) {
+ this.focusPlease = true;
+ this.setState({ isVisible: true, activeLinkIndex: -1 });
+ }
+ },
+
+ toggleVisibility: function() {
+ if (this.state.isVisible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ },
+
+ navigateLinks: function(change) {
+ var newActiveLinkIndex = Math.max(-1, Math.min(this.linkIndex-1, this.state.activeLinkIndex + change));
+ this.navigatedByKeyboard = true;
+ this.setState({ activeLinkIndex: newActiveLinkIndex });
+ },
+
+ followActiveLink: function() {
+ if (!this.activeLinkAction) { return; }
+ this.activeLinkAction();
+ },
+
+ updateResults: function() {
+ var searchString = this.input.value;
+ var results = this.state.fuse.search(searchString)
+
+ var resultsByModule = {};
+
+ results.forEach(function(result) {
+ var moduleName = result.item.module;
+ var resultsInModule = resultsByModule[moduleName] || (resultsByModule[moduleName] = []);
+ resultsInModule.push(result);
+ });
+
+ var moduleResults = [];
+ for (var moduleName in resultsByModule) {
+ var items = resultsByModule[moduleName];
+ var sumOfInverseScores = 0;
+ items.forEach(function(item) { sumOfInverseScores += 1/item.score; });
+ moduleResults.push({ module: moduleName, totalScore: 1/sumOfInverseScores, items: items });
+ }
+
+ moduleResults.sort(function(a, b) { return a.totalScore - b.totalScore; });
+
+ this.setState({ searchString: searchString, isVisible: true, moduleResults: moduleResults });
+ },
+
+ componentDidUpdate: function() {
+ if (this.state.isVisible && this.activeLink && this.navigatedByKeyboard) {
+ var rect = this.activeLink.getClientRects()[0];
+ var searchResultsTop = this.searchResults.getClientRects()[0].top;
+ if (rect.bottom > window.innerHeight) {
+ this.searchResults.scrollTop += rect.bottom - window.innerHeight + 80;
+ } else if (rect.top < searchResultsTop) {
+ this.searchResults.scrollTop -= searchResultsTop - rect.top + 80;
+ }
+ }
+ if (this.focusPlease && this.input) {
+ this.input.focus();
+ }
+ this.navigatedByKeyboard = false;
+ this.focusPlease = false;
+ },
+
+ componentDidMount: function() {
+ addSearchPageMenuButton(this.toggleVisibility.bind(this));
+ },
+
+ render: function(props, state) {
+ if (state.failedLoading) { return null; }
+
+ var self = this;
+ this.linkIndex = 0;
+
+ var stopPropagation = function(e) { e.stopPropagation(); };
+
+ var onMouseOver = function(e) {
+ var target = e.target;
+ while (target) {
+ if (typeof target.hasAttribute == 'function' && target.hasAttribute('data-link-index')) {
+ var linkIndex = parseInt(target.getAttribute('data-link-index'), 10);
+ this.setState({ activeLinkIndex: linkIndex });
+ break;
+ }
+ target = target.parentNode;
+ }
+ }.bind(this);
+
+ var items = take(10, state.moduleResults).map(this.renderResultsInModule.bind(this));
+
+ return (
+ h('div', { id: 'search', class: state.isVisible ? '' : 'hidden' },
+ h('div', { id: 'search-form', onMouseDown: stopPropagation },
+ h('input', {
+ placeholder: "Search in package by name",
+ ref: function(input) { self.input = input; },
+ onFocus: this.show.bind(this),
+ onClick: this.show.bind(this),
+ onInput: this.updateResults.bind(this)
+ }),
+ ),
+ h('div', {
+ id: 'search-results',
+ ref: function(el) { self.searchResults = el; },
+ onMouseDown: stopPropagation, onMouseOver: onMouseOver
+ },
+ state.searchString === ''
+ ? [h(IntroMsg), h(KeyboardShortcuts)]
+ : items.length == 0
+ ? h(NoResultsMsg, { searchString: state.searchString })
+ : h('ul', null, items)
+ )
+ )
+ );
+ },
+
+ renderResultsInModule: function(resultsInModule) {
+ var items = resultsInModule.items;
+ var moduleName = resultsInModule.module;
+ var showAll = this.state.expanded[moduleName] || items.length <= 10;
+ var visibleItems = showAll ? items : take(8, items);
+
+ var expand = function() {
+ var newExpanded = Object.assign({}, this.state.expanded);
+ newExpanded[moduleName] = true;
+ this.setState({ expanded: newExpanded });
+ }.bind(this);
+
+ var renderItem = function(item) {
+ return h('li', { class: 'search-result' },
+ this.navigationLink(item.link, {},
+ h(DocHtml, { html: item.display_html })
+ )
+ );
+ }.bind(this);
+
+ return h('li', { class: 'search-module' },
+ h('h4', null, moduleName),
+ h('ul', null,
+ visibleItems.map(function(item) { return renderItem(item.item); }),
+ showAll
+ ? null
+ : h('li', { class: 'more-results' },
+ this.actionLink(expand, {}, "show " + (items.length - visibleItems.length) + " more results from this module")
+ )
+ ),
+ )
+ },
+
+ navigationLink: function(href, attrs) {
+ var fullAttrs = Object.assign({ href: href, onClick: this.hide.bind(this) }, attrs);
+ var action = function() { window.location.href = href; this.hide(); }.bind(this);
+ var args = [fullAttrs, action].concat(Array.prototype.slice.call(arguments, 2));
+ return this.menuLink.apply(this, args);
+ },
+
+ actionLink: function(callback, attrs) {
+ var onClick = function(e) { e.preventDefault(); callback(); }
+ var fullAttrs = Object.assign({ href: '#', onClick: onClick }, attrs);
+ var args = [fullAttrs, callback].concat(Array.prototype.slice.call(arguments, 2));
+ return this.menuLink.apply(this, args);
+ },
+
+ menuLink: function(attrs, action) {
+ var children = Array.prototype.slice.call(arguments, 2);
+ var linkIndex = this.linkIndex;
+ if (linkIndex === this.state.activeLinkIndex) {
+ attrs['class'] = (attrs['class'] ? attrs['class'] + ' ' : '') + 'active-link';
+ attrs.ref = function (link) { if (link) this.activeLink = link; }.bind(this);
+ this.activeLinkAction = action;
+ }
+ var newAttrs = Object.assign({ 'data-link-index': linkIndex }, attrs);
+ var args = ['a', newAttrs].concat(children);
+ this.linkIndex += 1;
+ return h.apply(null, args);
+ }
+
+});
+
+var DocHtml = createClass({
+
+ shouldComponentUpdate: function(newProps) {
+ return this.props.html !== newProps.html;
+ },
+
+ render: function(props) {
+ return h('div', {dangerouslySetInnerHTML: {__html: props.html}});
+ }
+
+});
+
+var KeyboardShortcuts = function() {
+ return h('table', { class: 'keyboard-shortcuts' },
+ h('tr', null,
+ h('th', null, "Key"),
+ h('th', null, "Shortcut")
+ ),
+ h('tr', null,
+ h('td', null, h('span', { class: 'key' }, "s")),
+ h('td', null, "Open this search box")
+ ),
+ h('tr', null,
+ h('td', null, h('span', { class: 'key' }, "esc")),
+ h('td', null, "Close this search box")
+ ),
+ h('tr', null,
+ h('td', null,
+ h('span', { class: 'key' }, "↓"), ", ",
+ h('span', { class: 'key' }, "ctrl"), "+",
+ h('span', { class: 'key' }, "j")
+ ),
+ h('td', null, "Move down in search results")
+ ),
+ h('tr', null,
+ h('td', null,
+ h('span', { class: 'key' }, "↑"), ", ",
+ h('span', { class: 'key' }, "ctrl"), "+",
+ h('span', { class: 'key' }, "k")
+ ),
+ h('td', null, "Move up in search results")
+ ),
+ h('tr', null,
+ h('td', null, h('span', { class: 'key' }, "↵")),
+ h('td', null, "Go to active search result")
+ ),
+ );
+};
+
+var IntroMsg = function() {
+ return h('p', null,
+ "You can find any exported type, constructor, class, function or pattern defined in this package by (approximate) name.",
+ );
+};
+
+var NoResultsMsg = function(props) {
+ var messages = [
+ h('p', null,
+ "Your search for '" + props.searchString + "' produced the following list of results: ",
+ h('code', null, '[]'),
+ "."
+ ),
+ h('p', null,
+ h('code', null, 'Nothing'),
+ " matches your query for '" + props.searchString + "'.",
+ ),
+ h('p', null,
+ h('code', null, 'Left "no matches for \'' + props.searchString + '\'" :: Either String (NonEmpty SearchResult)'),
+ )
+ ];
+
+ return messages[(props.searchString || 'a').charCodeAt(0) % messages.length];
+};
+
+preact.render(h(App), document.body); \ No newline at end of file