Skip to content

find as you type search #799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions lib/resources/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,124 @@ function shiftWindow() {
scrollBy(0, -68);
}

function initSearch() {
var searchIndex; // the JSON data

var weights = {
'library' : 2,
'class' : 2,
'typedef' : 3,
'method' : 4,
'accessor' : 4,
'operator' : 4,
'property' : 4,
'constructor' : 4
};

function findMatches(q, cb) {
var allMatches = []; // ist of matches

function score(element, num) {
var weightFactor = weights[element.type] || 4;
return {e: element, score: (num / weightFactor) >> 0};
}

// iterate through the pool of strings and for any string that
// contains the substring `q`, add it to the `matches` array
$.each(searchIndex, function(i, element) {
// TODO: prefer matches in the current library
// TODO: help prefer a named constructor

var lowerName = element.name.toLowerCase();
var lowerQ = q.toLowerCase();

if (element.name === q) {
// exact match, maximum score
allMatches.push(score(element, 1000));
} else if (lowerName === lowerQ) {
// case-insensitive exact match
allMatches.push(score(element, 900));
} else if (element.name.indexOf(q) === 0) {
// starts with
allMatches.push(score(element, 750));
} else if (lowerName.indexOf(lowerQ) === 0) {
// case-insensitive starts with
allMatches.push(score(element, 650));
} else if (element.name.indexOf(q) >= 0) {
// contains
allMatches.push(score(element, 500));
} else if (lowerName.indexOf(lowerQ) >= 0) {
allMatches.push(score(element, 400));
}
});

allMatches.sort(function(a, b) {
var x = b.score - a.score;
if (x === 0) {
// tie-breaker: shorter name wins
return a.e.name.length - b.e.name.length;
} else {
return x;
}
});

var sortedMatches = [];
for (var i = 0; i < allMatches.length; i++) {
sortedMatches.push(allMatches[i].e);
}

cb(sortedMatches);
};

function initTypeahead() {
$('#search-box').prop('disabled', false);

$('#search-box.typeahead').typeahead({
hint: true,
highlight: true,
minLength: 3
},
{
name: 'elements',
limit: 10,
source: findMatches,
display: function(element) { return element.name; },
templates: {
suggestion: function(match) {
return [
'<div>',
match.name,
' ',
match.type.toLowerCase(),
(match.enclosedBy ? [
'<div class="search-from-lib">from ',
match.enclosedBy.name,
'</div>'].join('') : ''),
'</div>'
].join('');
}
}
});

$('#search-box.typeahead').bind('typeahead:select', function(ev, suggestion) {
window.location = suggestion.href;
});
}

var jsonReq = new XMLHttpRequest();
jsonReq.open('GET', 'index.json', true);
jsonReq.addEventListener('load', function() {
searchIndex = JSON.parse(jsonReq.responseText);
initTypeahead();
});
jsonReq.send();
}

document.addEventListener("DOMContentLoaded", function() {
prettyPrint();
initScroller();
initSideNav();
initSearch();

// Make sure the anchors scroll past the fixed page header (#648).
if (location.hash) shiftWindow();
Expand Down
139 changes: 125 additions & 14 deletions lib/resources/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,24 @@ nav {
/* some of this is to reset bootstrap */
nav.navbar {
background-color: inherit;
min-height: 56px;
min-height: 48px;
border: 0;
display: flex;
flex-direction: column;
justify-content: center;
display: -webkit-flex;
-webkit-flex-direction: column;
-webkit-justify-content: center;
}

nav.navbar .row {
padding-top: 8px;
}

nav .container {
white-space: nowrap;
}

@media screen and (min-width: 500px) and (max-width: 768px) {
.navbar-right {
float: right!important;
}
}

header {
background-color: #2196F3;
color: white;
Expand All @@ -64,6 +68,17 @@ header .masthead {
padding-top: 64px;
}

header .contents {
padding: 0;
}

@media screen and (max-width:768px) {
header .contents {
padding-left: 15px;
padding-right: 15px;
}
}

.body {
margin-top: 24px;
}
Expand Down Expand Up @@ -99,6 +114,10 @@ h5 {
font-size: 16px;
}

strong {
font-weight: 500;
}

.subtitle {
font-size: 17px;
min-height: 1.4em;
Expand Down Expand Up @@ -373,6 +392,12 @@ footer .container-fluid {
line-height: 1;
}

@media screen and (min-width: 768px) {
nav ol.breadcrumbs {
float: left;
}
}

@media screen and (max-width: 768px) {
.breadcrumbs {
margin: 0 0 24px 0;
Expand Down Expand Up @@ -541,11 +566,6 @@ button {

/* left-nav disappears, and can transition in from the left */
@media screen and (max-width:768px) {
.main-content {
padding-left: 0;
padding-right: 0;
}

#sidenav-left-toggle {
display: inline;
background: no-repeat url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path fill='#FFFFFF' d='M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z'/></svg>");
Expand Down Expand Up @@ -610,8 +630,99 @@ button {
}
}

/* source code method bodies */

#overlay-under-drawer {
display: none;
}

/* find-as-you-type search box */

/* override bootstrap defaults */
.form-control {
border-radius: 0;
border: 0;
}

form.search {
display: inline;
}

@media screen and (max-width: 500px) {
form.search {
display: none;
}
}

.typeahead,
.tt-query,
.tt-hint {
width: 200px;
height: 30px;
padding: 8px 12px;
line-height: 30px;
outline: none;
}

.typeahead {
background-color: #fff;
border-radius: 2px;
}

.tt-query {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}

.tt-hint {
color: #999
}

.tt-menu {
right:0;
left: inherit !important;
width: 422px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear from the screenshots how this works on mobile. My sense is that smaller then some screen width, the search menu should display as wide as the screen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't taken a look at mobile yet. That will certainly need another batch of fixes/changes.

max-height: 250px;
overflow-y: auto;
font-size: 14px;
margin: 0;
padding: 8px 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
}

.tt-suggestion {
padding: 3px 20px;
color: #212121;
}

.tt-suggestion:hover {
cursor: pointer;
color: #fff;
background-color: #0097cf;
}

.tt-suggestion:hover .search-from-lib {
color: #ddd;
}

.tt-suggestion.tt-cursor {
color: #fff;
background-color: #0097cf;
}

.tt-suggestion.tt-cursor .search-from-lib {
color: #ddd;
}

.tt-suggestion p {
margin: 0;
}

.search-from-lib {
font-style: italic;
color: gray;
}
8 changes: 8 additions & 0 deletions lib/resources/typeahead.bundle.min.js

Large diffs are not rendered by default.

37 changes: 25 additions & 12 deletions lib/src/html_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
library dartdoc.html_generator;

import 'dart:async' show Future;
import 'dart:io';
import 'dart:io' show Directory, File;
import 'dart:convert' show JSON;
import 'dart:typed_data' show Uint8List;

import 'package:mustache4dart/mustache4dart.dart';
Expand All @@ -17,6 +18,7 @@ import 'resources.g.dart' as resources;
import '../generator.dart';
import '../markdown_processor.dart';
import '../resource_loader.dart' as loader;
import 'io_utils.dart' show createOutputFile;

String dartdocVersion = 'unknown';

Expand Down Expand Up @@ -156,7 +158,7 @@ class HtmlGeneratorInstance {
final Package package;
final Directory out;

final List<String> _htmlFiles = [];
final List<ModelElement> documentedElements = [];

HtmlGeneratorInstance(this.url, this._templates, this.package, this.out);

Expand All @@ -166,13 +168,30 @@ class HtmlGeneratorInstance {

if (package != null) {
_generateDocs();
_generateSearchIndex();
// TODO: generate sitemap
}

//if (url != null) generateSiteMap();

await _copyResources();
}

void _generateSearchIndex() {
File jsonFile = createOutputFile(out, 'index.json');
String json = JSON.encode(documentedElements.map((ModelElement e) {
// TODO: find a better string for type
Map data = {'name': e.name, 'href': e.href, 'type': e.kind};
if (e is EnclosedElement) {
EnclosedElement ee = e as EnclosedElement;
data['enclosedBy'] = {
'name': ee.enclosingElement.name,
'type': ee.enclosingElement.kind
};
}
return data;
}).toList());
jsonFile.writeAsStringSync(json);
}

void _generateDocs() {
if (package == null) return;

Expand Down Expand Up @@ -354,22 +373,16 @@ class HtmlGeneratorInstance {
}
}

File _createOutputFile(String filename) {
File f = new File(path.join(out.path, filename));
if (!f.existsSync()) f.createSync(recursive: true);
_htmlFiles.add(filename);
return f;
}

void _build(String filename, TemplateRenderer template, TemplateData data) {
String content = template(data,
assumeNullNonExistingProperty: false, errorOnMissingProperty: true);

_writeFile(filename, content);
if (data.self is ModelElement) documentedElements.add(data.self);
}

void _writeFile(String filename, String content) {
File f = _createOutputFile(filename);
File f = createOutputFile(out, filename);
f.writeAsStringSync(content);
}
}
Expand Down
Loading