diff --git a/.gitignore b/.gitignore
index 664e6d7042..cfb5a7fb23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ pkg/*
.internal_test_app
.vagrant
/spec/examples.txt
+node_modules/*
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000000..1bba312420
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,23 @@
+.*
+Gemfile
+Rakefile
+Vagrantfile
+app/assets/images
+app/controllers
+app/helpers
+app/models
+app/presenters
+app/services
+app/views
+blacklight.gemspec
+config/locales
+config/routes.rb
+coverage
+db
+lib
+pkg
+provision.sh
+solr
+spec
+tasks
+template.demo.rb
diff --git a/README.md b/README.md
index 999af6819f..3bebec1bf8 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ You can use Blacklight to enable searching and browsing of your collections.
Blacklight uses the [Apache Solr](http://lucene.apache.org/solr) search engine
to search full text and/or metadata. Blacklight has a highly
configurable Ruby on Rails front-end. Blacklight was originally developed at
-the University of Virginia Library and is made public under an Apache 2.0 license.
+the University of Virginia Library and is made public under an Apache 2.0 license.
## Installation
@@ -38,5 +38,43 @@ rails generate blacklight:install
* Bundler
* Rails 5.0+
-## Configuring Apache Solr
+## Configuring Apache Solr
You'll also want some information about how Blacklight expects [Apache Solr](http://lucene.apache.org/solr ) to run, which you can find in [README_SOLR](https://github.com/projectblacklight/blacklight/wiki/README_SOLR)
+
+## Building the javascript
+The javascript is built by npm from sources in `app/javascript` into a bundle
+in `app/assets/javascripts/blacklight/blacklight.js`. This file should not be edited
+by hand as any changes would be overwritten.
+
+This is accomplished with the following steps:
+1. [Install npm](https://www.npmjs.com/get-npm)
+1. run `npm install` to download dependencies
+1. run `npm run js-compile-bundle` to build the bundle
+1. run `npm publish` to push the javascript package to https://npmjs.org/package/blacklight-frontend
+
+## Using the javascript
+Blacklight ships with Javascript that can be compiled either by webpacker or by
+sprockets. To use Webpacker see the directions at https://github.com/projectblacklight/blacklight/wiki/Using-Webpacker-to-compile-javascript-assets
+
+
+### Using sprockets (not Webpacker)
+
+If you want to use sprockets rather than Webpacker, you must ensure these
+dependencies are in your Gemfile. The Blacklight install generator does this for
+you:
+
+```
+gem 'bootstrap', '~> 4.0'
+gem 'popper_js'
+gem 'twitter-typeahead-rails', '0.11.1.pre.corejavascript'
+```
+
+Then insure these requires are in `app/assets/javascripts/application.js` (done
+automatically by the install generator):
+
+```
+//= require jquery
+//= require popper
+//= require twitter/typeahead
+//= require bootstrap
+```
diff --git a/app/assets/javascripts/blacklight/blacklight.js b/app/assets/javascripts/blacklight/blacklight.js
index 7a25620038..585cbe85f9 100644
--- a/app/assets/javascripts/blacklight/blacklight.js
+++ b/app/assets/javascripts/blacklight/blacklight.js
@@ -1,61 +1,499 @@
-// This file is generated by Blacklight. You probably don't want to edit
-// this file directly, or you'll have to manually merge your changes if later
-// versions of Blacklight change this file. Instead, use your own JS file
-// which over-rides things in this JS file, as described below.
-//
-// These javascript files are compiled in via the Rails asset pipeline:
+Blacklight = function () {
+ var buffer = new Array();
+ return {
+ onLoad: function (func) {
+ buffer.push(func);
+ },
+
+ activate: function () {
+ for (var i = 0; i < buffer.length; i++) {
+ buffer[i].call();
+ }
+ },
+
+ listeners: function () {
+ var listeners = [];
+ if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) {
+ // Turbolinks 5
+ if (Turbolinks.BrowserAdapter) {
+ listeners.push('turbolinks:load');
+ } else {
+ // Turbolinks < 5
+ listeners.push('page:load', 'ready');
+ }
+ } else {
+ listeners.push('ready');
+ }
+
+ return listeners.join(' ');
+ }
+ };
+}();
+
+// turbolinks triggers page:load events on page transition
+// If app isn't using turbolinks, this event will never be triggered, no prob.
+$(document).on(Blacklight.listeners(), function () {
+ Blacklight.activate();
+});
+
+$('.no-js').removeClass('no-js').addClass('js');
+/*global Bloodhound */
+
+Blacklight.onLoad(function () {
+ 'use strict';
+
+ $('[data-autocomplete-enabled="true"]').each(function () {
+ var $el = $(this);
+ if ($el.hasClass('tt-hint')) {
+ return;
+ }
+ var suggestUrl = $el.data().autocompletePath;
+
+ var terms = new Bloodhound({
+ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
+ queryTokenizer: Bloodhound.tokenizers.whitespace,
+ remote: {
+ url: suggestUrl + '?q=%QUERY',
+ wildcard: '%QUERY'
+ }
+ });
+
+ terms.initialize();
+
+ $el.typeahead({
+ hint: true,
+ highlight: true,
+ minLength: 2
+ }, {
+ name: 'terms',
+ displayKey: 'term',
+ source: terms.ttAdapter()
+ });
+ });
+});
+(function ($) {
+ //change form submit toggle to checkbox
+ Blacklight.doBookmarkToggleBehavior = function () {
+ if (typeof Blacklight.do_bookmark_toggle_behavior == 'function') {
+ console.warn("do_bookmark_toggle_behavior is deprecated. Use doBookmarkToggleBehavior instead.");
+ return Blacklight.do_bookmark_toggle_behavior();
+ }
+ $(Blacklight.doBookmarkToggleBehavior.selector).blCheckboxSubmit({
+ // cssClass is added to elements added, plus used for id base
+ cssClass: 'toggle-bookmark',
+ success: function (checked, response) {
+ if (response.bookmarks) {
+ $('[data-role=bookmark-counter]').text(response.bookmarks.count);
+ }
+ }
+ });
+ };
+ Blacklight.doBookmarkToggleBehavior.selector = 'form.bookmark-toggle';
+
+ Blacklight.onLoad(function () {
+ Blacklight.doBookmarkToggleBehavior();
+ });
+})(jQuery);
+/* A JQuery plugin (should this be implemented as a widget instead? not sure)
+ that will convert a "toggle" form, with single submit button to add/remove
+ something, like used for Bookmarks, into an AJAXy checkbox instead.
+
+ Apply to a form. Does require certain assumption about the form:
+ 1) The same form 'action' href must be used for both ADD and REMOVE
+ actions, with the different being the hidden input name="_method"
+ being set to "put" or "delete" -- that's the Rails method to pretend
+ to be doing a certain HTTP verb. So same URL, PUT to add, DELETE
+ to remove. This plugin assumes that.
+
+ Plus, the form this is applied to should provide a data-doc-id
+ attribute (HTML5-style doc-*) that contains the id/primary key
+ of the object in question -- used by plugin for a unique value for
+ DOM id's.
+
+ Uses HTML for a checkbox compatible with Bootstrap 3.
+
+ Pass in options for your class name and labels:
+ $("form.something").blCheckboxSubmit({
+ checked_label: "Selected",
+ unchecked_label: "Select",
+ progress_label: "Saving...",
+ //cssClass is added to elements added, plus used for id base
+ cssClass: "toggle_my_kinda_form",
+ success: function(after_success_check_state) {
+ #optional callback
+ }
+ });
+*/
+(function ($) {
+ $.fn.blCheckboxSubmit = function (argOpts) {
+ this.each(function () {
+ var options = $.extend({}, $.fn.blCheckboxSubmit.defaults, argOpts);
+
+ var form = $(this);
+ form.children().hide();
+ //We're going to use the existing form to actually send our add/removes
+ //This works conveneintly because the exact same action href is used
+ //for both bookmarks/$doc_id. But let's take out the irrelevant parts
+ //of the form to avoid any future confusion.
+ form.find('input[type=submit]').remove();
+
+ //View needs to set data-doc-id so we know a unique value
+ //for making DOM id
+ var uniqueId = form.attr('data-doc-id') || Math.random();
+ // if form is currently using method delete to change state,
+ // then checkbox is currently checked
+ var checked = form.find('input[name=_method][value=delete]').size() != 0;
+
+ var checkbox = $('').addClass(options.cssClass).attr('id', options.cssClass + '_' + uniqueId);
+ var label = $('