archivesspace/archivesspace

View on GitHub
frontend/app/assets/javascripts/linker.js

Summary

Maintainability
F
1 wk
Test Coverage
//= require jquery.tokeninput

$(function () {
  let resource_edit_path_regex = new RegExp(
    '^' + APP_PATH + 'resources/\\d+/edit$'
  );
  let on_resource_edit_path = window.location.pathname.match(
    resource_edit_path_regex
  );

  $.fn.linker = function () {
    $(this).each(function () {
      var $this = $(this);
      var $linkerWrapper = $this.parents('.linker-wrapper:first');

      if ($this.hasClass('initialised')) {
        return;
      }

      $this.addClass('initialised');

      // this is a bit hacky, but we need to have some input fields present in
      // the form so we don't have to rely on the linker to make sure data
      // persists. we can remove those after the linker does its thing.
      $('.prelinker', $linkerWrapper).remove();

      var config = {
        url: decodeURIComponent($this.data('url')),
        browse_url: decodeURIComponent($this.data('browse-url')),
        span_class: $this.data('span-class'),
        format_template: $this.data('format_template'),
        format_template_id: $this.data('format_template_id'),
        format_property: $this.data('format_property'),
        path: $this.data('path'),
        name: $this.data('name'),
        multiplicity: $this.data('multiplicity') || 'many',
        label_create: $this.data('label_create'),
        label_browse: $this.data('label_browse'),
        label_link: $this.data('label_link'),
        label_create_and_link: $this.data('label_create_and_link'),
        modal_id: $this.data('modal_id') || $this.attr('id') + '_modal',
        sortable: $this.data('sortable') === true,
        types: $this.data('types'),
        exclude_ids: $this.data('exclude') || [],
      };

      config.allow_multiple = config.multiplicity === 'many';

      if (
        config.format_template &&
        config.format_template.substring(0, 2) != '${'
      ) {
        config.format_template = '${' + config.format_template + '}';
      }

      var renderCreateFormForObject = function (form_uri) {
        var $modal = $('#' + config.modal_id);

        var initCreateForm = function (formEl) {
          $('.linker-container', $modal).html(formEl);
          $('#createAndLinkButton', $modal).removeAttr('disabled');
          $('form', $modal).ajaxForm({
            data: {
              inline: true,
            },
            beforeSubmit: function () {
              $('#createAndLinkButton', $modal).attr('disabled', 'disabled');
            },
            success: function (response, status, xhr) {
              if ($(response).is('form')) {
                initCreateForm(response);
              } else {
                if (config.multiplicity === 'one') {
                  clearTokens();
                }

                $this.tokenInput('add', {
                  id: response.uri,
                  name: tokenName(response),
                  json: response,
                });
                $this.triggerHandler('change');
                $modal.modal('hide');
              }
            },
            error: function (obj, errorText, errorDesc) {
              $('#createAndLinkButton', $modal).removeAttr('disabled');
            },
          });

          $modal.scrollTo('.alert');

          $modal.trigger('resize');
          $(document).triggerHandler('loadedrecordform.aspace', [$modal]);
        };

        $.ajax({
          url: form_uri,
          success: initCreateForm,
        });
        $('#createAndLinkButton', $modal).click(function () {
          $('form', $modal).triggerHandler('submit');
        });
      };

      var showLinkerCreateModal = function () {
        // Ensure all typeahead dropdowns are hidden (sometimes blur leaves them visible)
        $('.token-input-dropdown').hide();

        AS.openCustomModal(
          config.modal_id,
          config.label_create,
          AS.renderTemplate('linker_createmodal_template', config),
          'large',
          {},
          this
        );
        if ($(this).hasClass('linker-create-btn')) {
          renderCreateFormForObject($(this).data('target'));
        } else {
          renderCreateFormForObject(
            $('.linker-create-btn:first', $linkerWrapper).data('target')
          );
        }
        return false; // IE8 patch
      };

      var initAndShowLinkerBrowseModal = function () {
        var currentlySelected = {};

        var renderItemsInModal = function (page) {
          $.each($this.tokenInput('get'), function () {
            currentlySelected[this.id] = this.json;
          });

          $.ajax({
            url: config.browse_url,
            data: {
              page: 1,
              type: config.types,
              linker: true,
              exclude: config.exclude_ids,
              multiplicity: config.multiplicity,
            },
            type: 'GET',
            dataType: 'html',
            success: function (html) {
              var $modal = $('#' + config.modal_id);

              var $linkerBrowseContainer = $('.linker-container', $modal);

              var initBrowseFormInputs = function () {
                // add some click handlers to allow clicking of the row
                $(':input[name=linker-item]', $linkerBrowseContainer).each(
                  function () {
                    var $input = $(this);
                    $input.click(function (event) {
                      event.stopPropagation();

                      // If one-to-one, currentlySelected should only ever
                      // contain one record
                      if (!config.allow_multiple) {
                        currentlySelected = {};
                        $('tr.selected', $input.closest('table')).removeClass(
                          'selected'
                        );
                      }

                      if (
                        Object.prototype.hasOwnProperty.call(
                          currentlySelected,
                          $input.val()
                        )
                      ) {
                        // remove from the list
                        delete currentlySelected[$input.val()];
                        $input.closest('tr').removeClass('selected');
                      } else {
                        // add to the selected list
                        currentlySelected[$input.val()] = $input.data('object');
                        $input.closest('tr').addClass('selected');
                      }
                    });

                    $('td', $input.closest('tr')).click(function (event) {
                      event.preventDefault();

                      $input.trigger('click');
                    });
                  }
                );

                // select a result if it's currently a selected record
                $.each(currentlySelected, function (uri) {
                  $(":input[value='" + uri + "']", $linkerBrowseContainer)
                    .attr('checked', 'checked')
                    .closest('tr')
                    .addClass('selected');
                });

                $modal.trigger('resize');
              };

              $linkerBrowseContainer.html(html);
              $($linkerBrowseContainer).on(
                'click',
                'a:not(.dropdown-toggle):not(.record-toolbar .btn)',
                function (event) {
                  event.preventDefault();

                  $linkerBrowseContainer.load(
                    event.currentTarget.href,
                    initBrowseFormInputs
                  );
                }
              );

              $($linkerBrowseContainer).on('submit', 'form', function (event) {
                event.preventDefault();

                var $form = $(event.target);
                var method = ($form.attr('method') || 'get').toUpperCase();

                if (method == 'POST') {
                  jQuery.post(
                    $form.attr('action') + '.js',
                    $form.serializeArray(),
                    function (html) {
                      $linkerBrowseContainer.html(html);
                      initBrowseFormInputs();
                    }
                  );
                } else {
                  $linkerBrowseContainer.load(
                    $form.attr('action') + '.js?' + $form.serialize(),
                    initBrowseFormInputs
                  );
                }
              });

              initBrowseFormInputs();
            },
          });
        };

        var addSelected = function () {
          selectedItems = [];
          $('.token-input-delete-token', $linkerWrapper).each(function () {
            $(this).triggerHandler('click');
          });
          $.each(currentlySelected, function (uri, object) {
            $this.tokenInput('add', {
              id: uri,
              name: tokenName(object),
              json: object,
            });
          });
          $('#' + config.modal_id).modal('hide');
          $this.triggerHandler('change');
        };

        // Ensure all typeahead dropdowns are hidden (sometimes blur leaves them visible)
        $('.token-input-dropdown').hide();

        AS.openCustomModal(
          config.modal_id,
          config.label_browse,
          AS.renderTemplate('linker_browsemodal_template', config),
          'large',
          {},
          this
        );
        renderItemsInModal();
        $('#' + config.modal_id).on('click', '#addSelectedButton', addSelected);
        $('#' + config.modal_id).on(
          'click',
          '.linker-list .pagination .navigation a',
          function () {
            renderItemsInModal($(this).attr('rel'));
          }
        );
        return false; // IE patch
      };

      var formatResults = function (searchData) {
        var formattedResults = [];

        var currentlySelectedIds = [];
        $.each($this.tokenInput('get'), function (obj) {
          currentlySelectedIds.push(obj.id);
        });

        $.each(searchData.search_data.results, function (index, obj) {
          // only allow selection of unselected items
          if ($.inArray(obj.uri, currentlySelectedIds) === -1) {
            formattedResults.push({
              name: tokenName(obj),
              id: obj.id,
              json: obj,
            });
          }
        });
        return formattedResults;
      };

      var addEventBindings = function () {
        $('.linker-browse-btn', $linkerWrapper).on(
          'click',
          initAndShowLinkerBrowseModal
        );
        $('.linker-create-btn', $linkerWrapper).on(
          'click',
          showLinkerCreateModal
        );

        // Initialise popover on demand to improve performance
        $linkerWrapper.one('mouseenter focus', '.has-popover', function () {
          $(document).triggerHandler('init.popovers', [$this.parent()]);
        });
      };

      var clearTokens = function () {
        // as tokenInput plugin won't clear a token
        // if it has an input.. remove all inputs first!
        var $tokenList = $('.token-input-list', $this.parent());
        for (var i = 0; i < $this.tokenInput('get').length; i++) {
          var id_to_remove = $this.tokenInput('get')[i].id.replace(/\//g, '_');
          $('#' + id_to_remove + ' :input', $tokenList).remove();
        }
        $this.tokenInput('clear');
      };

      var enableSorting = function () {
        if ($('.token-input-list', $linkerWrapper).data('sortable')) {
          $('.token-input-list', $linkerWrapper).sortable('destroy');
        }
        $('.token-input-list', $linkerWrapper).sortable({
          items: 'li.token-input-token',
        });
        $('.token-input-list', $linkerWrapper)
          .off('sortupdate')
          .on('sortupdate', function () {
            $this.parents('form:first').triggerHandler('formchanged.aspace');
          });
      };

      var tokensForPrepopulation = function () {
        if ($this.data('multiplicity') === 'one') {
          // If we are on a resource or archival object edit page, and open a top_container modal with a
          // collection_resource linker then we prepopulate the collection_resource field with resource
          // data necessary to perform the search
          let onResource = $('.label.label-info').text() === 'Resource';
          let onArchivalObject =
            $('.label.label-info').text() === 'Archival Object';
          let modalHasResource =
            $('.modal-dialog').find('#collection_resource').length > 0;
          let idMatches = $this[0].id === 'collection_resource';

          if (
            on_resource_edit_path &&
            modalHasResource &&
            idMatches &&
            (onResource || onArchivalObject)
          ) {
            let currentForm = $('#object_container').find('form').first();
            if (onResource) {
              return [
                {
                  id: currentForm.attr('data-update-monitor-record-uri'),
                  name: $('#resource_title_').text(),
                  json: {
                    id: currentForm.attr('data-update-monitor-record-uri'),
                    uri: currentForm.attr('data-update-monitor-record-uri'),
                    title: $('#resource_title_').text(),
                    jsonmodel_type: 'resource',
                  },
                },
              ];
            } else if (onArchivalObject) {
              return [
                {
                  id: $('#archival_object_resource_').attr('value'),
                  name: $('.record-title').first().text(),
                  json: {
                    id: $('#archival_object_resource_').attr('value'),
                    uri: $('#archival_object_resource_').attr('value'),
                    title: $('.record-title').first().text(),
                    jsonmodel_type: 'resource',
                  },
                },
              ];
            }
          }

          if ($.isEmptyObject($this.data('selected'))) {
            return [];
          }
          return [
            {
              id: $this.data('selected').uri,
              name: tokenName($this.data('selected')),
              json: $this.data('selected'),
            },
          ];
        } else {
          if (!$this.data('selected') || $this.data('selected').length === 0) {
            return [];
          }

          return $this.data('selected').map(function (item) {
            if (typeof item == 'string') {
              item = JSON.parse(item);
            }
            return {
              id: item.uri,
              name: tokenName(item),
              json: item,
            };
          });
        }
      };

      // ANW-521: For subjects, we want to have specialized icons based on the subjects' term type.
      var tag_subjects_by_term_type = function (obj) {
        if (obj.json.jsonmodel_type == 'subject') {
          switch (obj.json.first_term_type) {
            case 'cultural_context':
              return 'subject_type_cultural_context';
            case 'function':
              return 'subject_type_function';
            case 'genre_form':
              return 'subject_type_genre_form';
            case 'geographic':
              return 'subject_type_geographic';
            case 'occupation':
              return 'subject_type_occupation';
            case 'style_period':
              return 'subject_type_style_period';
            case 'technique':
              return 'subject_type_technique';
            case 'temporal':
              return 'subject_type_temporal';
            case 'topical':
              return 'subject_type_topical';
            case 'uniform_title':
              return 'subject_type_uniform_title';
            default:
              return '';
          }
        } else {
          return '';
        }
      };

      // ANW-631, ANW-700: Add four_part_id to token name via data source
      function tokenName(object) {
        var title = object.display_string || object.title;

        function output(id) {
          return id + ': ' + title;
        }

        if (object.four_part_id !== undefined) {
          // Data comes from Solr index
          return output(object.four_part_id.split(' ').join('-'));
        } else if (object.digital_object_id !== undefined) {
          // Data comes from Solr index
          return output(object.digital_object_id);
        } else {
          // Data comes from JSON property on data from Solr index
          var idProperties = ['id_0', 'id_1', 'id_2', 'id_3'];
          var fourPartIdArr = idProperties.reduce(function (acc, id) {
            if (object[id] !== undefined) {
              acc.push(object[id]);
            }
            return acc;
          }, []);

          return fourPartIdArr.length > 0
            ? output(fourPartIdArr.join('-'))
            : title;
        }
      }

      var init = function () {
        var tokenInputConfig = $.extend({}, AS.linker_locales, {
          animateDropdown: false,
          preventDuplicates: true,
          allowFreeTagging: false,
          tokenLimit: config.multiplicity === 'one' ? 1 : null,
          caching: false,
          onCachedResult: formatResults,
          onResult: formatResults,
          zindex: 1100,
          tokenFormatter: function (item) {
            var tokenEl = $(
              AS.renderTemplate('linker_selectedtoken_template', {
                item: item,
                config: config,
              })
            );
            tokenEl
              .children('div')
              .children('.icon-token')
              .addClass(config.span_class);
            $('input[name*=resolved]', tokenEl).val(JSON.stringify(item.json));
            return tokenEl;
          },
          resultsFormatter: function (item) {
            var string = item.name;
            var $resultSpan = $(
              "<span class='" +
                item.json.jsonmodel_type +
                "' aria-label='" +
                string +
                "'>"
            );
            var extra_class = tag_subjects_by_term_type(item);
            $resultSpan.text(string);
            $resultSpan.prepend(
              "<span class='icon-token " + extra_class + "'></span>"
            );
            var $resultLi = $("<li role='option'>");
            $resultLi.append($resultSpan);
            return $resultLi[0].outerHTML;
          },
          prePopulate: tokensForPrepopulation(),
          onDelete: function () {
            $this.triggerHandler('change');
          },
          onAdd: function (item) {
            // ANW-521: After adding a subject, find the added node and apply the special class for that node.
            var extra_class = tag_subjects_by_term_type(item);
            var added_node_id = '#' + item.id.replace(/\//g, '_');

            added_node = $(added_node_id);
            added_node
              .children('div')
              .children('.icon-token')
              .addClass(extra_class);

            if (config.sortable && config.allow_multiple) {
              enableSorting();
            }

            //            $this.triggerHandler("change");
            $(document).triggerHandler('init.popovers', [$this.parent()]);
          },
          formatQueryParam: function (q, ajax_params) {
            if (
              $this.tokenInput('get').length > 0 ||
              config.exclude_ids.length > 0
            ) {
              var currentlySelectedIds = $.merge([], config.exclude_ids);
              $.each($this.tokenInput('get'), function (i, obj) {
                currentlySelectedIds.push(obj.id);
              });

              ajax_params.data['exclude[]'] = currentlySelectedIds;
            }
            if (config.types && config.types.length > 0) {
              ajax_params.data['type'] = config.types;
            }

            return (q + '*').toLowerCase();
          },
        });

        setTimeout(function () {
          $this.tokenInput(config.url, tokenInputConfig);
          $(
            '> :input[type=text]',
            $('.token-input-input-token', $this.parent())
          )
            .attr('placeholder', AS.linker_locales.hintText)
            .attr('aria-label', config.label)
            .attr('role', 'searchbox')
            .attr('aria-multiline', 'false');
          $(
            '> :input[type=text]',
            $('.token-input-input-token', $this.parent())
          ).addClass('form-control');

          $this.parent().addClass('multiplicity-' + config.multiplicity);

          if (config.sortable && config.allow_multiple) {
            enableSorting();
            $linkerWrapper.addClass('sortable');
          }

          // This is part of automatically executing a search for the current resource on the browse top
          // containers modal when opened from the edit resource or archival object pages.
          // If this setTimeout is for the last linker in the modal, only then is it safe to execute the search
          let lastLinker = $('.modal-dialog').find('.linker').last();
          let isLastLinker = lastLinker.attr('id') === $this.context.id;
          let onResource = $('.label.label-info').text() === 'Resource';
          let onArchivalObject =
            $('.label.label-info').text() === 'Archival Object';
          let modalHasResource =
            $('.modal-dialog').find('#collection_resource').length > 0;
          let resultsEmpty =
            $('.modal-dialog').find('.table-search-results').length < 1;

          if (
            on_resource_edit_path &&
            modalHasResource &&
            resultsEmpty &&
            isLastLinker &&
            (onResource || onArchivalObject)
          ) {
            $('.modal-dialog').find("input[type='submit']").click();
          }
        });

        addEventBindings();
      };

      init();
    });
  };
});

$(document).ready(function () {
  $(document).bind('loadedrecordsubforms.aspace', function (event, $container) {
    $(
      '.linker-wrapper:visible > .linker:not(.initialised)',
      $container
    ).linker();
    // we can go ahead and init dropdowns ( such as those in the toolbars )
    $('#archives_tree_toolbar .linker:not(.initialised)').linker();
  });

  $(document).bind(
    'subrecordcreated.aspace',
    function (event, object_name, subform) {
      $('.linker:not(.initialised)', subform).linker();
    }
  );
});