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..8d3e745758 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,81 @@ 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
+1. run `npm install` to download dependencies
+1. run `npm run-script 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
+Install Webpacker
+Add blacklight-frontend as a dependency by doing:
+```
+yarn add blacklight-frontend
+```
+
+Then add these lines to `config/webpack/environment.js` as per https://getbootstrap.com/docs/4.0/getting-started/webpack/
+and https://github.com/rails/webpacker/blob/master/docs/webpack.md#plugins
+
+```js
+const webpack = require('webpack')
+
+environment.plugins.set(
+ 'Provide',
+ new webpack.ProvidePlugin({
+ $: 'jquery',
+ jQuery: 'jquery',
+ jquery: 'jquery',
+ 'window.jQuery': 'jquery',
+ Popper: ['popper.js', 'default'],
+ })
+)
+
+module.exports = environment
+```
+
+In you pack file (`app/javascript/packs/application.js`), require blacklight:
+```
+require('blacklight-frontend/app/assets/javascripts/blacklight/blacklight')
+```
+Then remove these requires from `app/assets/javascripts/application.js`:
+
+```
+//= require jquery
+//= require popper
+//= require twitter/typeahead
+//= require bootstrap
+```
+
+Add the following to the app/views/layouts/blacklight/base.html.erb (maybe this can be simpler)
+```
+<%= javascript_pack_tag 'application' %>
+```
+You can probably remove the `<%= javascript_include_tag %>`
+
+### Using sprockets (not webpacker)
+
+If you want to use sprockets rather than webpacker, you must ensure these dependencies are in your Gemfile (done automatically by the install generator):
+
+```
+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..c138612539 100644
--- a/app/assets/javascripts/blacklight/blacklight.js
+++ b/app/assets/javascripts/blacklight/blacklight.js
@@ -1,61 +1,489 @@
-// 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.do_bookmark_toggle_behavior = function () {
+ $(Blacklight.do_bookmark_toggle_behavior.selector).bl_checkbox_submit({
+ // css_class is added to elements added, plus used for id base
+ css_class: "toggle-bookmark",
+ success: function (checked, response) {
+ if (response.bookmarks) {
+ $('[data-role=bookmark-counter]').text(response.bookmarks.count);
+ }
+ }
+ });
+ };
+ Blacklight.do_bookmark_toggle_behavior.selector = "form.bookmark-toggle";
+
+ Blacklight.onLoad(function () {
+ Blacklight.do_bookmark_toggle_behavior();
+ });
+})(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").bl_checkbox_submit({
+ checked_label: "Selected",
+ unchecked_label: "Select",
+ progress_label: "Saving...",
+ //css_class is added to elements added, plus used for id base
+ css_class: "toggle_my_kinda_form",
+ success: function(after_success_check_state) {
+ #optional callback
+ }
+ });
+*/
+(function ($) {
+ $.fn.bl_checkbox_submit = function (arg_opts) {
+
+ this.each(function () {
+ var options = $.extend({}, $.fn.bl_checkbox_submit.defaults, arg_opts);
+
+ 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 unique_id = 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.css_class).attr("id", options.css_class + "_" + unique_id);
+ var label = $('