diff --git a/Gemfile b/Gemfile
index 442ee9f533..9b92aa4bbc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,7 +38,6 @@ gem "graphiql-rails", "1.4.10", group: :development
gem "graphql", "1.8.2"
gem "honeybadger"
gem "hydra-access-controls"
-gem "hydra-editor", "~> 6.0"
gem "hydra-head"
gem "hydra-role-management"
gem "iiif_manifest", git: "https://github.com/samvera-labs/iiif_manifest"
diff --git a/Gemfile.lock b/Gemfile.lock
index 6be3654304..0aef8b194d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -155,8 +155,6 @@ GEM
afm (0.2.2)
airbrussh (1.4.0)
sshkit (>= 1.6.1, != 1.7.0)
- almond-rails (0.3.0)
- rails (>= 4.2)
amazing_print (1.4.0)
amq-protocol (2.3.2)
arabic-letter-connector (0.1.1)
@@ -185,10 +183,6 @@ GEM
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
- babel-source (5.8.35)
- babel-transpiler (0.7.0)
- babel-source (>= 4.0, < 6)
- execjs (~> 2.0)
backport (1.2.0)
bagit (0.4.4)
docopt (~> 0.5.0)
@@ -495,15 +489,6 @@ GEM
deprecation
mime-types (> 2.0, < 4.0)
mini_magick (>= 3.2, < 5)
- hydra-editor (6.0.0)
- active-fedora (>= 9.0.0)
- activerecord (>= 5.2, < 6.1)
- almond-rails (~> 0.1)
- cancancan (~> 1.8)
- rails (>= 5.2, < 6.1)
- simple_form (>= 4.1.0, < 6.0)
- sprockets (>= 3.7)
- sprockets-es6
hydra-head (12.0.1)
hydra-access-controls (= 12.0.1)
hydra-core (= 12.0.1)
@@ -899,10 +884,6 @@ GEM
sprockets (4.1.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-es6 (0.9.2)
- babel-source (>= 5.8.11)
- babel-transpiler
- sprockets (>= 3.0.0)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
@@ -1046,7 +1027,6 @@ DEPENDENCIES
graphql (= 1.8.2)
honeybadger
hydra-access-controls
- hydra-editor (~> 6.0)
hydra-head
hydra-role-management
iiif_manifest!
@@ -1120,4 +1100,4 @@ DEPENDENCIES
whenever (~> 0.10)
BUNDLED WITH
- 2.2.16
+ 2.3.11
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 707641cf07..d25a9f72a3 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -34,12 +34,6 @@
//= require openseadragon/openseadragon
//= require openseadragon/jquery
//= require bootstrap_select_dropdown
-//= //require bootstrap/affix
-//= require babel/polyfill
-//= require hydra-editor/hydra-editor
//= require cocoon
//= require blacklight_range_limit
//= require_tree .
-$(document).ready(function() {
- $('.multi_value.form-group').manage_fields();
-});
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index b453f7e2d4..bc9f96ddb9 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -63,9 +63,9 @@
@import 'components/thumbnail';
@import 'components/vue_select';
@import 'components/facets';
+@import 'components/inputs';
@import 'blacklight/blacklight';
-@import "hydra-editor/hydra-editor";
@import "openseadragon/openseadragon";
@import 'components/pagination';
diff --git a/app/assets/stylesheets/components/inputs.scss b/app/assets/stylesheets/components/inputs.scss
new file mode 100644
index 0000000000..a315c670a1
--- /dev/null
+++ b/app/assets/stylesheets/components/inputs.scss
@@ -0,0 +1,79 @@
+.multi_value {
+ .btn.remove {
+ color: $brand-danger;
+ }
+
+ .btn.remove, .btn.add {
+ text-decoration: underline;
+ }
+
+ .btn.add {
+ padding-left: 0;
+ }
+
+ .field-wrapper {
+ list-style-type:none;
+ }
+
+ .listing {
+ margin-left: 0;
+ max-width: 40em;
+ padding-left: 0px;
+ margin-bottom: 0;
+ }
+
+ .field-controls {
+ margin-left: 2em;
+ }
+
+ .remove .glyphicon-remove,
+ .add .glyphicon-plus {
+ margin-right: 0.3rem
+ }
+
+ .message {
+ background-size: 40px 40px;
+ background-image: linear-gradient(135deg, rgba(255, 255, 255, .05) 25%, transparent 25%,
+ transparent 50%, rgba(255, 255, 255, .05) 50%, rgba(255, 255, 255, .05) 75%,
+ transparent 75%, transparent);
+ box-shadow: inset 0 -1px 0 rgba(255,255,255,.4);
+ width: 100%;
+ border: 1px solid;
+ color: #fff;
+ padding: 10px;
+ text-shadow: 0 1px 0 rgba(0,0,0,.5);
+ animation: animate-bg 5s linear infinite;
+ border-radius: $border-radius-base;
+ }
+
+ .has-error {
+ background-color: #de4343;
+ border-color: #c43d3d;
+ }
+
+ .has-warning{
+ background-color: #eaaf51;
+ border-color: #d99a36;
+ }
+
+ .listing li:not(:last-child) {
+ margin-bottom: 0.5rem;
+ }
+
+ .listing li:first-of-type {
+ width: 83.3333333333%;
+
+ input {
+ border-radius: 4px;
+ }
+
+ .remove {
+ display: none;
+ }
+ }
+}
+
+// The contributor listing needs some normalization
+#contributors .listing {
+ max-width:20em;
+}
diff --git a/app/helpers/edit_field_helper.rb b/app/helpers/edit_field_helper.rb
index 8c6f37c2c5..91f673cfb1 100644
--- a/app/helpers/edit_field_helper.rb
+++ b/app/helpers/edit_field_helper.rb
@@ -4,6 +4,11 @@ def reorder_languages(languages, top_languages)
pull_to_front(languages) { |term| top_languages.include? term }
end
+ def render_edit_field_partial(field_name, locals)
+ collection = locals[:f].object.model_name.collection
+ render_edit_field_partial_with_action(collection, field_name, locals)
+ end
+
private
def pull_to_front(array, &block)
@@ -11,4 +16,23 @@ def pull_to_front(array, &block)
array.reject!(&block)
temp + array
end
+
+ # This finds a partial based on the record_type and field_name
+ # if no partial exists for the record_type it tries using "records" as a default
+ def render_edit_field_partial_with_action(record_type, field_name, locals)
+ partial = find_edit_field_partial(record_type, field_name)
+ render partial, locals.merge(key: field_name)
+ end
+
+ def find_edit_field_partial(record_type, field_name)
+ ["#{record_type}/edit_fields/_#{field_name}", "records/edit_fields/_#{field_name}",
+ "#{record_type}/edit_fields/_default", "records/edit_fields/_default"].find do |partial|
+ logger.debug "Looking for edit field partial #{partial}"
+ return partial.sub(/\/_/, "/") if partial_exists?(partial)
+ end
+ end
+
+ def partial_exists?(partial)
+ lookup_context.find_all(partial).any?
+ end
end
diff --git a/app/inputs/multi_value_input.rb b/app/inputs/multi_value_input.rb
new file mode 100644
index 0000000000..db5bb5a328
--- /dev/null
+++ b/app/inputs/multi_value_input.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+class MultiValueInput < SimpleForm::Inputs::CollectionInput
+ def input(_wrapper_options)
+ @rendered_first_element = false
+ input_html_classes.unshift("string")
+ input_html_options[:name] ||= "#{object_name}[#{attribute_name}][]"
+
+ outer_wrapper do
+ buffer_each(collection) do |value, index|
+ inner_wrapper do
+ build_field(value, index)
+ end
+ end
+ end
+ end
+
+ protected
+
+ def buffer_each(collection)
+ collection.each_with_object("".dup).with_index do |(value, buffer), index|
+ buffer << yield(value, index)
+ end
+ end
+
+ def outer_wrapper
+ "
\n"
+ end
+
+ def inner_wrapper
+ <<-HTML
+
+ #{yield}
+
+ HTML
+ end
+
+ private
+
+ # Although the 'index' parameter is not used in this implementation it is useful in an
+ # an overridden version of this method, especially when the field is a complex object and
+ # the override defines nested fields.
+ def build_field_options(value, _index)
+ options = input_html_options.dup
+
+ options[:value] = value
+ if @rendered_first_element
+ options[:id] = nil
+ options[:required] = nil
+ else
+ options[:id] ||= input_dom_id
+ end
+ options[:class] ||= []
+ options[:class] += ["#{input_dom_id} form-control multi-text-field"]
+ options[:'aria-labelledby'] = label_id
+ @rendered_first_element = true
+
+ options
+ end
+
+ def build_field(value, index)
+ options = build_field_options(value, index)
+ if options.delete(:type) == "textarea"
+ @builder.text_area(attribute_name, options)
+ else
+ @builder.text_field(attribute_name, options)
+ end
+ end
+
+ def label_id
+ input_dom_id + "_label"
+ end
+
+ def input_dom_id
+ input_html_options[:id] || "#{object_name}_#{attribute_name}"
+ end
+
+ def collection
+ @collection ||=
+ Array(object.send(attribute_name)).reject { |v| v.to_s.strip.blank? } + [""]
+ end
+
+ def multiple?
+ true
+ end
+end
diff --git a/app/javascript/figgy/field_manager.js b/app/javascript/figgy/field_manager.js
new file mode 100644
index 0000000000..7ecfeeebd6
--- /dev/null
+++ b/app/javascript/figgy/field_manager.js
@@ -0,0 +1,176 @@
+export default class FieldManager {
+ constructor(element, options) {
+ this.element = $(element);
+
+ this.options = options;
+
+ this.options.label = this.getFieldLabel(this.element, options)
+
+ this.addSelector = '.add'
+ this.removeSelector = '.remove'
+
+ this.adder = this.createAddHtml(this.options)
+ this.remover = this.createRemoveHtml(this.options)
+
+ this.controls = $(options.controlsHtml);
+
+ this.inputTypeClass = options.inputTypeClass;
+ this.fieldWrapperClass = options.fieldWrapperClass;
+ this.warningClass = options.warningClass;
+ this.listClass = options.listClass;
+
+ this.init();
+ }
+
+ init() {
+ this._addInitialClasses();
+ this._addAriaLiveRegions()
+ this._appendControls();
+ this._attachEvents();
+ this._addCallbacks();
+ }
+
+ _addInitialClasses() {
+ this.element.addClass("managed");
+ $(this.fieldWrapperClass, this.element).addClass("input-group input-append");
+ }
+
+ _addAriaLiveRegions() {
+ $(this.element).find('.listing').attr('aria-live', 'polite')
+ }
+
+ // Add the "Add another" and "Remove" controls to the DOM
+ _appendControls() {
+ // We want to make these DOM additions idempotently, so exit if it's
+ // already set up.
+ if (!this._hasRemoveControl()) {
+ this._createRemoveWrapper()
+ this._createRemoveControl()
+ }
+
+ if (!this._hasAddControl()) {
+ this._createAddControl()
+ }
+ }
+
+ _createRemoveWrapper() {
+ $(this.fieldWrapperClass, this.element).append(this.controls);
+ }
+
+ _createRemoveControl() {
+ $(this.fieldWrapperClass + ' .field-controls', this.element).append(this.remover)
+ }
+
+ _createAddControl() {
+ this.element.find(this.listClass).after(this.adder)
+ }
+
+ _hasRemoveControl() {
+ return this.element.find(this.removeSelector).length > 0
+ }
+
+ _hasAddControl() {
+ return this.element.find(this.addSelector).length > 0
+ }
+
+ _attachEvents() {
+ this.element.on('click', this.removeSelector, (e) => this.removeFromList(e))
+ this.element.on('click', this.addSelector, (e) => this.addToList(e))
+ }
+
+ _addCallbacks() {
+ this.element.bind('managed_field:add', this.options.add);
+ this.element.bind('managed_field:remove', this.options.remove);
+ }
+
+ _manageFocus() {
+ $(this.element).find(this.listClass).children('li').last().find('.form-control').focus();
+ }
+
+ addToList( event ) {
+ event.preventDefault();
+ let $listing = $(event.target).closest(this.inputTypeClass).find(this.listClass)
+ let $activeField = $listing.children('li').last()
+
+ if (this.inputIsEmpty($activeField)) {
+ this.displayEmptyWarning();
+ } else {
+ this.clearEmptyWarning();
+ $listing.append(this._newField($activeField));
+ }
+
+ this._manageFocus()
+ }
+
+ inputIsEmpty($activeField) {
+ return $activeField.children('input.multi-text-field').val() === '';
+ }
+
+ _newField ($activeField) {
+ var $newField = this.createNewField($activeField);
+ return $newField;
+ }
+
+ createNewField($activeField) {
+ let $newField = $activeField.clone();
+ let $newChildren = this.createNewChildren($newField);
+ this.element.trigger("managed_field:add", $newChildren);
+ return $newField;
+ }
+
+ clearEmptyWarning() {
+ let $listing = $(this.listClass, this.element)
+ $listing.children(this.warningClass).remove();
+ }
+
+ displayEmptyWarning() {
+ let $listing = $(this.listClass, this.element)
+ var $warningMessage = $("cannot add another with empty field
");
+ $listing.children(this.warningClass).remove();
+ $listing.append($warningMessage);
+ }
+
+ removeFromList( event ) {
+ event.preventDefault();
+ var $field = $(event.target).parents(this.fieldWrapperClass).remove();
+ this.element.trigger("managed_field:remove", $field);
+
+ this._manageFocus();
+ }
+
+ destroy() {
+ $(this.fieldWrapperClass, this.element).removeClass("input-append");
+ this.element.removeClass("managed");
+ }
+
+ getFieldLabel($element, options) {
+ var label = '';
+ var $label = $element.find("label").first();
+ if ($label.length && options.labelControls) {
+ var label = $label.data('label') || $.trim($label.contents().filter(function() { return this.nodeType === 3; }).text());
+ label = ' ' + label;
+ }
+
+ return label;
+ }
+
+ createAddHtml(options) {
+ var $addHtml = $(options.addHtml);
+ $addHtml.find('.controls-add-text').html(options.addText + options.label);
+ return $addHtml;
+ }
+
+ createRemoveHtml(options) {
+ var $removeHtml = $(options.removeHtml);
+ $removeHtml.find('.controls-remove-text').html(options.removeText);
+ $removeHtml.find('.controls-field-name-text').html(options.label);
+ return $removeHtml;
+ }
+
+ createNewChildren(clone) {
+ let $newChildren = $(clone).children(this.inputTypeClass);
+ $newChildren.val('').removeAttr('required');
+ $newChildren.first().focus();
+ return $newChildren.first();
+ }
+}
diff --git a/app/javascript/figgy/figgy_boot.js b/app/javascript/figgy/figgy_boot.js
index bc0ad50ef3..2c4b413c47 100644
--- a/app/javascript/figgy/figgy_boot.js
+++ b/app/javascript/figgy/figgy_boot.js
@@ -14,6 +14,7 @@ import MemberResourcesTables from "figgy/relationships/member_resources_table"
import ParentResourcesTables from "figgy/relationships/parent_resources_table"
import BulkLabeler from "figgy/bulk_labeler/bulk_label"
import BoundingBoxSelector from "figgy/bounding_box_selector"
+import FieldManager from "figgy/field_manager"
export default class Initializer {
constructor() {
@@ -31,6 +32,7 @@ export default class Initializer {
this.auto_ingest_handler = new AutoIngestHandler
this.bulk_labeler = new BulkLabeler
this.sortable_placeholder()
+ this.initialize_multi_fields()
// Incompatibility in Blacklight with newer versions of jQuery seem to be
// causing this to not run. Manually calling it so facet more links work.
@@ -161,4 +163,38 @@ export default class Initializer {
ui.placeholder.height(found_element.height())
})
}
+
+ initialize_multi_fields() {
+ const DEFAULTS = {
+ /* callback to run after add is called */
+ add: null,
+ /* callback to run after remove is called */
+ remove: null,
+
+ controlsHtml: '',
+ fieldWrapperClass: '.field-wrapper',
+ warningClass: '.has-warning',
+ listClass: '.listing',
+ inputTypeClass: '.multi_value',
+
+ addHtml: '',
+ addText: 'Add another',
+
+ removeHtml: '',
+ removeText: 'Remove',
+
+ labelControls: true,
+ }
+
+ $.fn.manage_fields = function(option) {
+ return this.each(function() {
+ var $this = $(this);
+ var data = $this.data('manage_fields');
+ var options = $.extend({}, DEFAULTS, $this.data(), typeof option == 'object' && option);
+
+ if (!data) $this.data('manage_fields', (data = new FieldManager(this, options)));
+ })
+ }
+ $('.multi_value.form-group').manage_fields();
+ }
}
diff --git a/app/views/layouts/blacklight/base.html.erb b/app/views/layouts/blacklight/base.html.erb
index cb73520dae..fbd8d6dd04 100644
--- a/app/views/layouts/blacklight/base.html.erb
+++ b/app/views/layouts/blacklight/base.html.erb
@@ -17,19 +17,11 @@
<%# There were errors when we tried to include this webpack %>
<%# see comments culminating with https://github.com/pulibrary/figgy/pull/3754#issuecomment-598510375 %>
- <%= javascript_tag do %>
- oldDefine = define;
- define = null;
- <% end %>
-
- <%= javascript_tag do %>
- define = oldDefine;
- <% end %>
<%= stylesheet_pack_tag 'application' %>
<%= javascript_pack_tag "application" %>
<%= content_for(:head) %>
diff --git a/app/views/records/edit_fields/_default.html.erb b/app/views/records/edit_fields/_default.html.erb
new file mode 100644
index 0000000000..12fb6b9618
--- /dev/null
+++ b/app/views/records/edit_fields/_default.html.erb
@@ -0,0 +1,5 @@
+<% if f.object.multiple? key %>
+ <%= f.input key, as: :multi_value, input_html: { class: 'form-control' }, required: f.object.required?(key) %>
+<% else %>
+ <%= f.input key, required: f.object.required?(key) %>
+<% end %>