function()

in kahuna/public/js/components/gr-image-metadata/gr-image-metadata.js [42:648]


  function ($rootScope,
    $scope,
    $window,
    $document,
    editsService,
    mediaApi,
    editsApi,
    collections,
    imageList,
    imageAccessor,
    inject$,
    labelService,
    storage,
    searchWithModifiers) {

    let ctrl = this;

    ctrl.displayMetadataTemplates = window._clientConfig.metadataTemplates !== undefined && window._clientConfig.metadataTemplates.length > 0;
    // Deep copying window._clientConfig.domainMetadataModels
    ctrl.domainMetadataSpecs = JSON.parse(JSON.stringify(window._clientConfig.domainMetadataSpecs));
    ctrl.showUsageRights = false;
    ctrl.usageRightsSummary = window._clientConfig.usageRightsSummary;
    ctrl.metadataUpdatedByTemplate = [];

    ctrl.validImageTypes = window._clientConfig.imageTypes || [];

    ctrl.$onInit = () => {
      $scope.$watchCollection('ctrl.selectedImages', function () {
        ctrl.singleImage = singleImage();
        ctrl.selectedLabels = selectedLabels();
        ctrl.usageRights = selectedUsageRights();
        inject$($scope, Rx.Observable.fromPromise(selectedUsageCategory(ctrl.usageRights)), ctrl, 'usageCategory');
        ctrl.rawMetadata = rawMetadata();
        ctrl.metadata = displayMetadata();
        ctrl.metadata.dateTaken = ctrl.displayDateTakenMetadata();
        ctrl.newPeopleInImage = "";
        ctrl.newKeywords = "";
        ctrl.extraInfo = extraInfo();
        if (ctrl.singleImage) {
          updateSingleImage();
        }

      });

      $document.on('keydown', textareaKeydownListener);

      function textareaKeydownListener(evt) {
        if (evt.isDefaultPrevented()
            || evt.target.type !== 'textarea'
            || !evt.target.form) {
          return evt;
        }

        const effectiveForms = ["specialInstructionsEditForm", "descriptionEditForm"];
        switch (evt.key) {
          case 'Escape':
            return formButtonClick(evt, 'button-cancel', effectiveForms);
          case 'Enter':
            if (evt.ctrlKey || evt.metaKey) {
              return formButtonClick(evt, 'button-save', effectiveForms);
            } else {
              return evt;
            }
          default:
            return evt;
        }
      };

      function formButtonClick(evt, buttonClass, forms) {
        const form = evt.target.form;
        if (!forms.includes(form.name)) {
          return evt;
        }
        const buttons = Array.from(form.elements).filter(function(element) {
          return element.classList.contains(buttonClass);
        });
        if (buttons.length > 0) {
          buttons[0].click();
        }
        return;
      };

      ctrl.checkSpecialInstructionsLength = async function() {
        if (ctrl.hasMultipleSpecialInstructions() || !ctrl.metadata.specialInstructions) {
          return;
        }
        await updateTextareaCursor("specialInstructionsEditForm", ctrl.metadata.specialInstructions);
      };

      ctrl.checkDescriptionLength = async function() {
        if (ctrl.hasMultipleValues(ctrl.rawMetadata.description) || !ctrl.metadata.description) {
          return;
        }
        await updateTextareaCursor("descriptionEditForm", ctrl.metadata.description);
      };

      async function updateTextareaCursor(formName, formText) {
        try {
          const txtLen = formText.length;
          const form = Array.from($document[0].forms).filter(f => f.name == formName)[0];
          const txtAreas = Array.from(form.elements).filter(function(element) {
            return element.classList.contains('editable-input');
          });
          if (txtAreas.length > 0) {
            const t = txtAreas[0];
            const elapseLimit = 500; /* how many milliseconds we'll wait for content to load before aborting */
            const startTime = Date.now();
            let elapsedTime = 0;
            do {
              await delay(20); /* pause for 20ms to let content load into text area */
              elapsedTime = Date.now() - startTime;
            } while (t.value.length < txtLen && elapsedTime < elapseLimit);
            t.setSelectionRange(t.value.length, t.value.length);
          }
        } catch {
          console.log("ERROR setting textarea cursor");
        } finally {
          $scope.$digest();
        }
      };

      function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }

      const freeUpdateListener = $rootScope.$on('images-updated',
        (e, updatedImages) => updateHandler(updatedImages));

      const updateHandler = (updatedImages) => {
        ctrl.selectedImages = new List(updatedImages);
      };

      ctrl.hasMultipleSpecialInstructions = function () {
        const val = ctrl.rawMetadata.specialInstructions;
        const val2 = ctrl.rawMetadata.usageInstructions;
        return ((Array.isArray(val) && val.length > 1) || (Array.isArray(val2) && val2.length > 1));
      };

      ctrl.hasMultipleValues = (val) => Array.isArray(val) && val.length > 1;

      ctrl.displayDateTakenMetadata = function () {
        let dateTaken = ctrl.metadata.dateTaken ? new Date(ctrl.metadata.dateTaken) : undefined;
        if (dateTaken) {
          dateTaken.setSeconds(0, 0);
        }
        return dateTaken;
      };

      ctrl.credits = function (searchText) {
        return ctrl.metadataSearch('credit', searchText);
      };

      ctrl.metadataSearch = (field, q) => {
        return mediaApi.metadataSearch(field, {q}).then(resource => {
          return resource.data.map(d => d.key);
        });
      };

      ctrl.descriptionOption = overwrite.key;

      ctrl.descriptionOptions = editOptions;

      ctrl.updateDescriptionField = function () {
        ctrl.updateMetadataField('description', ctrl.metadata.description);
      };

      ctrl.updateSpecialInstructionsField = function () {
        ctrl.updateMetadataField('specialInstructions', ctrl.metadata.specialInstructions);
      };

      ctrl.updateLocationField = function (data, value) {
        Object.keys(value).forEach(key => {
          if (value[key] === undefined) {
            delete value[key];
          }
        });
        ctrl.updateMetadataField('location', value);
      };

      ctrl.updateMetadataField = function (field, value) {
        var imageArray = Array.from(ctrl.selectedImages);
        if (field === 'dateTaken') {
          value = value.toISOString();
        }
        if (field === 'peopleInImage') {
          ctrl.addPersonToImages(imageArray, value);
          return;
        }
        if (field === 'keywords') {
          ctrl.addKeywordToImages(imageArray, value);
          return;
        }
        return editsService.batchUpdateMetadataField(
          imageArray,
          field,
          value,
          ctrl.descriptionOption
        );
      };

      ctrl.updateDomainMetadataField = function (name, field, value) {
        return editsService.updateDomainMetadataField(ctrl.singleImage, name, field, value)
          .then((updatedImage) => {
            if (updatedImage) {
              ctrl.singleImage = updatedImage;
              $rootScope.$emit(
                'track:event',
                'Metadata',
                'Edit',
                'Success',
                null,
                {
                  field: field,
                  value: value
                }
              );
            }
          })
          .catch(() => {
            $rootScope.$emit(
              'track:event',
              'Metadata',
              'Edit',
              'Failure',
              null,
              {
                field: field,
                value: value
              }
            );
            /*
            Save failed.
            Per the angular-xeditable docs, returning a string indicates an error and will
            not update the local model, nor will the form close (so the edit is not lost).
            Instead, a message is shown and the field keeps focus for user to edit again.
            http://vitalets.github.io/angular-xeditable/#onbeforesave
            */
            return 'failed to save (press esc to cancel)';
          });
      };

      ctrl.addLabelToImages = labelService.batchAdd;
      ctrl.removeLabelFromImages = labelService.batchRemove;
      ctrl.labelAccessor = (image) => imageAccessor.readLabels(image).map(label => label.data);

      const updateImages = (images, metadataFieldName, valueFn) => {
        images.map((image) => {
          editsService.batchUpdateMetadataField(
            [image],
            metadataFieldName,
            valueFn(image),
            ctrl.descriptionOption
          );
        });
        return Promise.resolve(ctrl.selectedImages);
      };
      const removeXFromImages = (metadataFieldName, accessor) => (images, removedX) =>
        updateImages(
          images,
          metadataFieldName,
          (image) => accessor(image)?.filter((x) => x !== removedX) || []
        );
      const addXToImages = (metadataFieldName, accessor) => (images, addedX) =>
        updateImages(
          images,
          metadataFieldName,
          (image) => {
            const currentXInImage = accessor(image);
            return currentXInImage ? [...currentXInImage, addedX] : [addedX];
          }
        );

      ctrl.peopleAccessor = (image) => imageAccessor.readPeopleInImage(image);
      ctrl.removePersonFromImages = removeXFromImages('peopleInImage', ctrl.peopleAccessor);
      ctrl.addPersonToImages = addXToImages('peopleInImage', ctrl.peopleAccessor);

      ctrl.keywordAccessor = (image) => imageAccessor.readMetadata(image).keywords;
      ctrl.removeKeywordFromImages = removeXFromImages('keywords', ctrl.keywordAccessor);
      ctrl.addKeywordToImages = addXToImages('keywords', ctrl.keywordAccessor);

      ctrl.subjectsAccessor = (image) => imageAccessor.readMetadata(image).subjects;

      ctrl.selectedImagesHasAny = (accessor) => ctrl.selectedImages.find(
        (image) => Object.keys(accessor(image)).length > 0
      );

      const ignoredMetadata = [
        'title', 'description', 'copyright', 'keywords', 'byline',
        'credit', 'subLocation', 'city', 'state', 'country',
        'dateTaken', 'specialInstructions', 'subjects', 'peopleInImage',
        'domainMetadata', 'usageInstructions', 'imageType'
      ];

      function updateSingleImage() {
        // Alias for convenience in view
        ctrl.identifiers = ctrl.singleImage.data.identifiers;

        ctrl.additionalMetadata = Object.fromEntries(
          Object.entries(ctrl.singleImage.data.aliases)
            .map(([key, value]) => {
              let fieldAlias = ctrl.fieldAliases.find(_ => _.alias === key);
              if (fieldAlias && fieldAlias.displayInAdditionalMetadata === true) {
                return [fieldAlias.label, {value, alias: fieldAlias.alias}];
              }
            })
            .filter(_ => _ !== undefined));

        registerSectionStore('additionalMetadata');

        ctrl.domainMetadata = ctrl.domainMetadataSpecs
          .filter(domainMetadataSpec => domainMetadataSpec.fields.length > 0)
          .reduce((acc, domainMetadataSpec) => {
            let domainMetadata = {...domainMetadataSpec};

            if (ctrl.singleImage.data.metadata) {
              const imageDomainMetadata = ctrl.singleImage.data.metadata.domainMetadata ? ctrl.singleImage.data.metadata.domainMetadata : {};
              domainMetadata.fields = domainMetadataSpec.fields.map(field => setDomainMetadataFieldValueOrDefault(imageDomainMetadata, field, domainMetadataSpec));
            }

            acc.push(domainMetadata);

            return acc;
          }, []);

        ctrl.domainMetadata.forEach(domainMetadata => registerSectionStore(domainMetadata.name));
      }

      const registerSectionStore = (key) => {
        const storeName = generateStoreName(key);
        const state = storage.getJs(storeName) || {hidden: true};
        storage.setJs(storeName, state);
      };

      const generateStoreName = (key) => `${key}MetadataSection`;

      function setDomainMetadataFieldValueOrDefault(domainMetadata, field, spec) {
        let fieldValue = undefined;

        if (field.fieldType === 'datetime' && fieldValue) {
          fieldValue = new Date(fieldValue);
        }

        if (field.fieldType === 'select' && field.options.length > 0) {
          field.selectOptions = field.options
            .filter(option => option)
            .map(option => {
              return {value: option, text: option};
            });
        }

        if (domainMetadata.hasOwnProperty(spec.name)) {
          fieldValue = domainMetadata[spec.name][field.name];

          if (field.fieldType === 'select' && fieldValue) {
            field.selectOptions = [{value: "", text: ""}].concat(field.selectOptions);
          }
        }

        return {
          ...field,
          value: fieldValue
        };
      }

      ctrl.showMetadataSection = (key) => {
        const storeName = generateStoreName(key);
        const state = storage.getJs(storeName);
        storage.setJs(storeName, {hidden: !state.hidden});
      };

      ctrl.isMetadataSectionHidden = (key) => {
        return storage.getJs(generateStoreName(key)).hidden;
      };

      ctrl.srefNonfree = () => storage.getJs("isNonFree", true) ? true : undefined;

      function isUsefulMetadata(metadataKey) {
        return ignoredMetadata.indexOf(metadataKey) === -1;
      }

      ctrl.isUsefulMetadata = isUsefulMetadata;

      function hasLocationInformation() {
        return ctrl.metadata && (
          ctrl.metadata.subLocation ||
          ctrl.metadata.city ||
          ctrl.metadata.state ||
          ctrl.metadata.country
        );
      }

      ctrl.hasLocationInformation = hasLocationInformation;

      function singleImage() {
        if (ctrl.selectedImages.size === 1) {
          return ctrl.selectedImages.first();
        }
      }

      function selectedLabels() {
        const labels = imageList.getLabels(ctrl.selectedImages);
        return imageList.getOccurrences(labels);
      }

      function selectedUsageRights() {
        //--image selection change event--
        const customEvent = new CustomEvent('imageSelectionChange', {
          detail: {images: ctrl.selectedImages},
          bubbles: true
        });
        window.dispatchEvent(customEvent);

        return ctrl.selectedImages.map(image => {
          return {
            image: image,
            data: imageAccessor.readUsageRights(image)
          };
        });
      }

      function selectedUsageCategory(usageRights) {
        const categoriesPromise = editsApi.getUsageRightsCategories();
        return categoriesPromise.then(categories => {
          const categoryCode = usageRights.reduce((m, o) => {
            return (m == o.data.category) ? o.data.category : 'multiple categories';
          }, usageRights && usageRights.first().data.category);

          const usageCategory = categories.find(cat => cat.value === categoryCode);
          return usageCategory ? usageCategory.name : categoryCode;
        });
      }

      function selectedMetadata() {
        const metadata = imageList.getMetadata(ctrl.selectedImages);
        return imageList.getSetOfProperties(metadata);
      }

      function rawMetadata() {
        return selectedMetadata().map((values) => {
          switch (values.size) {
            case 0:
              return undefined;
            case 1:
              return values.first();
            default:
              return Array.from(values);
          }
        }).toObject();
      }

      function displayMetadata() {
        return selectedMetadata().map((values) => {
          switch (values.size) {
            case 1:
              return values.first();
            default:
              return undefined;
          }
        }).toObject();
      }

      function extraInfo() {
        const info = imageList.getExtraInfo(ctrl.selectedImages);
        const properties = imageList.getSetOfProperties(info);
        return properties.map((values) => {
          switch (values.size) {
            case 0:
              return undefined;
            case 1:
              return values.first();
            default:
              return Array.from(values);
          }
        }).toObject();
      }

      ctrl.displayLeases = () => ctrl.userCanEdit || (ctrl.singleImage && imageAccessor.readLeases(ctrl.singleImage).leases.length > 0);

      // Map of metadata location field to query filter name
      ctrl.locationFieldMap = {
        'subLocation': 'location',
        'city': 'city',
        'state': 'state',
        'country': 'country'
      };

      ctrl.locationFieldPluralMap = {
        'subLocation': 'subLocations',
        'city': 'cities',
        'state': 'states',
        'country': 'countries'
      };

      ctrl.fieldAliases = $window._clientConfig.fieldAliases;

      ctrl.removeImageFromCollection = (collection) => {
        ctrl.removingCollection = collection;
        collections.removeImageFromCollection(collection, ctrl.singleImage)
          .then(() => ctrl.removingCollection = false);
      };

      ctrl.callbackUsageCategory = (images) => {
          let usageRights = images.map(image => {
              return {
                  image: image,
                  data: imageAccessor.readUsageRights(image)
              };
          });
          const categoryCode = usageRights.reduce((m, o) => {
              return (m == o.data.category) ? o.data.category : 'multiple categories';
          }, usageRights && usageRights.first().data.category);
          let categoryPromise = editsApi.getUsageRightsCategories().then(categories => {
              const usageCategory = categories.find(cat => cat.value === categoryCode);
              return usageCategory ? usageCategory.name : categoryCode;
          });
          return categoryPromise;
      };

      $scope.$on('$destroy', function () {
        freeUpdateListener();
        $document.off('keydown', textareaKeydownListener);
      });

      ctrl.onMetadataTemplateSelected = (metadata, usageRights, collection, leasesWithConfig) => {
        ctrl.collectionUpdatedByTemplate = false;
        ctrl.leasesUpdatedByTemplate = false;
        ctrl.showUsageRights = false;
        ctrl.usageRightsUpdatedByTemplate = false;
        ctrl.usageRights.first().data = usageRights;

        if (angular.isDefined(metadata)) {
          ctrl.metadataUpdatedByTemplate = Object.keys(metadata).filter(key => ctrl.rawMetadata[key] !== metadata[key]);
          ctrl.metadata = metadata;
        }

        if (angular.isDefined(leasesWithConfig)) {
          const leasesFromTemplate = leasesWithConfig.leases.map(lease => {
            return {...lease, fromTemplate: true};
          });
          ctrl.updatedLeases = [...leasesFromTemplate, ...(leasesWithConfig.replace ? [] : ctrl.singleImage.data.leases.data.leases)];
          ctrl.leasesUpdatedByTemplate = true;
        }

        if (angular.isDefined(collection)) {
          if (ctrl.singleImage.data.collections.filter(r => r.data.path.toString() === collection.data.fullPath.toString()).length === 0) {
            ctrl.updatedCollections = [
              {description: collection.data.data.description, fromTemplate: true},
              ...ctrl.singleImage.data.collections.map(resource => resource.data)
            ];

            ctrl.collectionUpdatedByTemplate = true;
          }
        }

        if (usageRights.category !== undefined) {
          if ((ctrl.singleImage.data.usageRights === undefined) ||
            (ctrl.singleImage.data.usageRights.category !== usageRights.category)) {
            ctrl.showUsageRights = true;
          }
        }

        const originalUsageRights = ctrl.singleImage.data.usageRights ? ctrl.singleImage.data.usageRights : {};
        if (angular.equals(usageRights, originalUsageRights) === false) {
          ctrl.usageRightsUpdatedByTemplate = true;
        }
      };

      ctrl.onMetadataTemplateApplying = (leases) => {
        if (angular.isDefined(leases)) {
          ctrl.leasesUpdatingByTemplate = true;
        }
      };

      ctrl.onMetadataTemplateApplied = () => {
        ctrl.collectionUpdatedByTemplate = false;
        ctrl.leasesUpdatedByTemplate = false;
        ctrl.leasesUpdatingByTemplate = false;
        ctrl.showUsageRights = false;
        ctrl.usageRightsUpdatedByTemplate = false;
        ctrl.metadataUpdatedByTemplate = [];
      };

      ctrl.onMetadataTemplateCancelled = (metadata, usageRights) => {
        ctrl.collectionUpdatedByTemplate = false;
        ctrl.leasesUpdatedByTemplate = false;
        ctrl.metadataUpdatedByTemplate = [];
        ctrl.showUsageRights = false;
        ctrl.usageRightsUpdatedByTemplate = false;
        ctrl.metadata = metadata;
        ctrl.usageRights.first().data = usageRights;
      };

      ctrl.isDomainMetadataEmpty = (key) => {
        return ctrl.domainMetadata.find(obj => obj.name === key).fields.every(field => field.value === undefined);
      };

      ctrl.isAdditionalMetadataEmpty = () => {
        const totalAdditionalMetadataCount = Object.keys(ctrl.metadata).filter(key => ctrl.isUsefulMetadata(key)).length +
          Object.keys(ctrl.additionalMetadata).length +
          Object.keys(ctrl.identifiers).length;

        return totalAdditionalMetadataCount == 0;
      };

      ctrl.searchWithModifiers = searchWithModifiers;
    };
  }