diff --git a/aio/.angular-cli.json b/aio/.angular-cli.json
index d08d31e3d5..57a6fab8d2 100644
--- a/aio/.angular-cli.json
+++ b/aio/.angular-cli.json
@@ -10,6 +10,7 @@
"assets": [
"assets",
"content",
+ "app/search/search-worker.js",
"favicon.ico"
],
"index": "index.html",
diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html
index d64217206c..416a22b935 100644
--- a/aio/src/app/app.component.html
+++ b/aio/src/app/app.component.html
@@ -2,7 +2,7 @@
-
+
@@ -10,11 +10,15 @@
-
diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts
index 1668793ecd..0e33a3ea75 100644
--- a/aio/src/app/app.component.ts
+++ b/aio/src/app/app.component.ts
@@ -2,6 +2,7 @@ import { Component, ViewChild, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { DocumentService, DocumentContents } from 'app/documents/document.service';
import { NavigationService, NavigationViews } from 'app/navigation/navigation.service';
+import { SearchService, QueryResults } from 'app/search/search.service';
@Component({
selector: 'aio-shell',
@@ -91,10 +92,12 @@ export class AppComponent implements OnInit {
currentDocument: Observable;
navigationViews: Observable;
+ searchResults: Observable;
- constructor(documentService: DocumentService, navigationService: NavigationService) {
+ constructor(documentService: DocumentService, navigationService: NavigationService, private searchService: SearchService) {
this.currentDocument = documentService.currentDocument;
this.navigationViews = navigationService.navigationViews;
+ this.searchResults = searchService.searchResults;
}
ngOnInit() {
@@ -104,4 +107,10 @@ export class AppComponent implements OnInit {
onResize(width) {
this.isSideBySide = width > this.sideBySideWidth;
}
+
+ onSearch(event: KeyboardEvent) {
+ const query = (event.target as HTMLInputElement).value;
+ console.log(query);
+ this.searchService.search(query);
+ }
}
diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts
index 875a8f3708..d536f5a17b 100644
--- a/aio/src/app/app.module.ts
+++ b/aio/src/app/app.module.ts
@@ -22,6 +22,7 @@ import { Logger } from 'app/shared/logger.service';
import { LocationService } from 'app/shared/location.service';
import { NavigationService } from 'app/navigation/navigation.service';
import { DocumentService } from 'app/documents/document.service';
+import { SearchService } from 'app/search/search.service';
import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component';
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
@@ -54,6 +55,7 @@ import { LinkDirective } from 'app/shared/link.directive';
LocationService,
NavigationService,
DocumentService,
+ SearchService,
Platform
],
entryComponents: [ embeddedComponents ],
diff --git a/aio/src/app/search/search-worker-client.ts b/aio/src/app/search/search-worker-client.ts
deleted file mode 100644
index b4115371ea..0000000000
--- a/aio/src/app/search/search-worker-client.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-Copyright 2016 Google Inc. All Rights Reserved.
-Use of this source code is governed by an MIT-style license that
-can be found in the LICENSE file at http://angular.io/license
-*/
-
-import {NgZone} from '@angular/core';
-import {Observable} from 'rxjs/Observable';
-import {Subscriber} from 'rxjs/Subscriber';
-import 'rxjs/add/observable/fromPromise';
-import 'rxjs/add/observable/of';
-import 'rxjs/add/operator/switchMap';
-
-
-/**
- * We will use this client from a component with something like...
- *
- * ngOnInit() {
- * const searchWorker = new SearchWorkerClient('app/search-worker.js', this.zone);
- * this.indexReady = searchWorker.ready;
- * this.searchInput = new FormControl();
- * this.searchResult$ = this.searchInput.valueChanges
- * .switchMap((searchText: string) => searchWorker.search(searchText));
- * }
- *
- * TODO(petebd): do we need a fallback for browsers that do not support service workers?
- */
-
-type QueryResults = Object[];
-
-export interface ResultsReadyMessage {
- type: 'query-results';
- id: number;
- query: string;
- results: QueryResults;
-}
-
-export class SearchWorkerClient {
- ready: Promise;
- worker: Worker;
- private _queryId = 0;
-
- constructor(url: string, private zone: NgZone) {
- this.worker = new Worker(url);
- this.ready = this._waitForIndex(this.worker);
- }
-
- search(query: string) {
- return Observable.fromPromise(this.ready)
- .switchMap(() => this._createQuery(query));
- }
-
- private _waitForIndex(worker: Worker) {
- return new Promise((resolve, reject) => {
-
- worker.onmessage = (e) => {
- if (e.data.type === 'index-ready') {
- resolve(true);
- cleanup();
- }
- };
-
- worker.onerror = (e) => {
- reject(e);
- cleanup();
- };
- });
-
- function cleanup() {
- worker.onmessage = null;
- worker.onerror = null;
- }
- }
-
- private _createQuery(query: string) {
- return new Observable((subscriber: Subscriber) => {
-
- // get a new identifier for this query that we can match to results
- const id = this._queryId++;
-
- const handleMessage = (message: MessageEvent) => {
- const {type, id: queryId, results} = message.data as ResultsReadyMessage;
- if (type === 'query-results' && id === queryId) {
- this.zone.run(() => {
- subscriber.next(results);
- subscriber.complete();
- });
- }
- };
-
- const handleError = (error: ErrorEvent) => {
- this.zone.run(() => {
- subscriber.error(error);
- });
- };
-
- // Wire up the event listeners for this query
- this.worker.addEventListener('message', handleMessage);
- this.worker.addEventListener('error', handleError);
-
- // Post the query to the web worker
- this.worker.postMessage({query, id});
-
- // At completion/error unwire the event listeners
- return () => {
- this.worker.removeEventListener('message', handleMessage);
- this.worker.removeEventListener('error', handleError);
- };
- });
- }
-}
diff --git a/aio/src/app/search/search-worker.js b/aio/src/app/search/search-worker.js
index 8528060e18..1e5c5d3b04 100644
--- a/aio/src/app/search/search-worker.js
+++ b/aio/src/app/search/search-worker.js
@@ -3,13 +3,13 @@
/* eslint-env worker */
/* global importScripts, lunr */
+var SEARCH_TERMS_URL = '/content/docs/app/search-data.json';
+
importScripts('https://unpkg.com/lunr@0.7.2');
var index = createIndex();
var pages = {};
-makeRequest('search-data.json', loadIndex);
-
self.onmessage = handleMessage;
// Create the lunr index - the docs should be an array of objects, each object containing
@@ -23,9 +23,38 @@ function createIndex() {
});
}
+// The worker receives a message to load the index and to query the index
+function handleMessage(message) {
+ var type = message.data.type;
+ var id = message.data.id;
+ var payload = message.data.payload;
+ switch(type) {
+ case 'load-index':
+ makeRequest(SEARCH_TERMS_URL, function(searchInfo) {
+ loadIndex(searchInfo);
+ self.postMessage({type: type, id: id, payload: true});
+ });
+ break;
+ case 'query-index':
+ self.postMessage({type: type, id: id, payload: {query: payload, results: queryIndex(payload)}});
+ break;
+ default:
+ self.postMessage({type: type, id: id, payload: {error: 'invalid message type'}})
+ }
+}
// Use XHR to make a request to the server
function makeRequest(url, callback) {
+
+ // The JSON file that is loaded should be an array of SearchTerms:
+ //
+ // export interface SearchTerms {
+ // path: string;
+ // type: string,
+ // titleWords: string;
+ // keyWords: string;
+ // }
+
var searchDataRequest = new XMLHttpRequest();
searchDataRequest.onload = function() {
callback(JSON.parse(this.responseText));
@@ -43,19 +72,8 @@ function loadIndex(searchInfo) {
index.add(page);
pages[page.path] = page;
});
- self.postMessage({type: 'index-ready'});
}
-
-// The worker receives a message everytime the web app wants to query the index
-function handleMessage(message) {
- var id = message.data.id;
- var query = message.data.query;
- var results = queryIndex(query);
- self.postMessage({type: 'query-results', id: id, query: query, results: results});
-}
-
-
// Query the index and return the processed results
function queryIndex(query) {
// Only return the array of paths to pages
diff --git a/aio/src/app/search/search.service.ts b/aio/src/app/search/search.service.ts
new file mode 100644
index 0000000000..d84e7af67e
--- /dev/null
+++ b/aio/src/app/search/search.service.ts
@@ -0,0 +1,40 @@
+/*
+Copyright 2016 Google Inc. All Rights Reserved.
+Use of this source code is governed by an MIT-style license that
+can be found in the LICENSE file at http://angular.io/license
+*/
+
+import { NgZone, Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+import 'rxjs/add/operator/publishLast';
+import 'rxjs/add/operator/concatMap';
+import { WebWorkerClient } from 'app/shared/web-worker';
+
+export interface QueryResults {
+ query: string;
+ results: Object[];
+}
+
+const SEARCH_WORKER_URL = 'app/search/search-worker.js';
+
+@Injectable()
+export class SearchService {
+ private worker: WebWorkerClient;
+ private ready: Observable;
+ private resultsSubject = new Subject();
+ get searchResults() { return this.resultsSubject.asObservable(); }
+
+ constructor(private zone: NgZone) {
+ this.worker = new WebWorkerClient(SEARCH_WORKER_URL, zone);
+ const ready = this.ready = this.worker.sendMessage('load-index').publishLast();
+ // trigger the index to be loaded immediately
+ ready.connect();
+ }
+
+ search(query: string) {
+ this.ready.concatMap(ready => {
+ return this.worker.sendMessage('query-index', query) as Observable;
+ }).subscribe(results => this.resultsSubject.next(results));
+ }
+}
diff --git a/aio/src/app/shared/web-worker.ts b/aio/src/app/shared/web-worker.ts
new file mode 100644
index 0000000000..c5fe6b27f0
--- /dev/null
+++ b/aio/src/app/shared/web-worker.ts
@@ -0,0 +1,59 @@
+/*
+Copyright 2016 Google Inc. All Rights Reserved.
+Use of this source code is governed by an MIT-style license that
+can be found in the LICENSE file at http://angular.io/license
+*/
+
+import {NgZone, Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+
+export interface WebWorkerMessage {
+ type: string;
+ payload: any;
+ id?: number;
+}
+
+export class WebWorkerClient {
+ worker: Worker;
+ private messageId = 0;
+
+ constructor(url: string, private zone: NgZone) {
+ this.worker = new Worker(url);
+ }
+
+ sendMessage(type: string, payload?: any): Observable {
+
+ return new Observable(subscriber => {
+
+ const id = this.messageId++;
+
+ const handleMessage = (response: MessageEvent) => {
+ const {type: responseType, id: responseId, payload: responsePayload} = response.data as WebWorkerMessage;
+ if (type === responseType && id === responseId) {
+ this.zone.run(() => {
+ subscriber.next(responsePayload);
+ subscriber.complete();
+ });
+ }
+ };
+
+ const handleError = (error: ErrorEvent) => {
+ // Since we do not check type and id any error from the webworker will kill all subscribers
+ this.zone.run(() => subscriber.error(error));
+ };
+
+ // Wire up the event listeners for this message
+ this.worker.addEventListener('message', handleMessage);
+ this.worker.addEventListener('error', handleError);
+
+ // Post the message to the web worker
+ this.worker.postMessage({type, id, payload});
+
+ // At completion/error unwire the event listeners
+ return () => {
+ this.worker.removeEventListener('message', handleMessage);
+ this.worker.removeEventListener('error', handleError);
+ };
+ });
+ }
+}