SpeciesFileGroup/taxonworks

View on GitHub
app/javascript/vue/components/ui/ImageViewer/ImageViewer.vue

Summary

Maintainability
Test Coverage
<template>
  <div class="depiction-thumb-container">
    <v-modal
      v-if="isModalVisible"
      @close="isModalVisible = false"
      :container-style="{
        width: `${imageObject.width}px`,
        minWidth: '700px'
      }"
    >
      <template #header>
        <h3>View</h3>
      </template>
      <template #body>
        <div class="image-container">
          <SvgViewer
            v-if="svgClip"
            class="img-maxsize full_width"
            :height="depiction.image.height"
            :groups="svgClip"
            :image="{
              url: depiction.image.image_file_url,
              width: depiction.image.width,
              height: depiction.image.height
            }"
          />
          <img
            v-else
            :class="[
              'img-maxsize',
              state.fullSizeImage ? 'img-fullsize' : 'img-normalsize'
            ]"
            @click="state.fullSizeImage = !state.fullSizeImage"
            :src="urlSrc"
          />
        </div>

        <template v-if="edit">
          <div v-if="depiction">
            <div class="field separate-top">
              <input
                v-model="depiction.figure_label"
                type="text"
                placeholder="Label"
              />
            </div>
            <div class="field separate-bottom">
              <textarea
                v-model="depiction.caption"
                rows="5"
                placeholder="Caption"
              />
            </div>
          </div>
          <div class="flex-separate">
            <div>
              <button
                v-if="depiction"
                type="button"
                class="button normal-input button-submit"
                @click="updateDepiction"
              >
                Update
              </button>
            </div>
            <div class="horizontal-left-content">
              <div class="horizontal-left-content">
                <span class="margin-small-right">Image</span>
                <div class="square-brackets">
                  <ul class="context-menu no_bullets">
                    <li>
                      <v-btn
                        circle
                        color="primary"
                        @click="openFullsize"
                      >
                        <v-icon
                          x-small
                          name="expand"
                          color="white"
                        />
                      </v-btn>
                    </li>
                    <li>
                      <v-btn
                        circle
                        :href="image.image_file_url"
                        :download="image.image_original_filename"
                        color="primary"
                      >
                        <v-icon
                          x-small
                          name="download"
                          color="white"
                        />
                      </v-btn>
                    </li>
                    <li>
                      <radial-annotator
                        type="annotations"
                        :global-id="imageObject.global_id"
                      />
                    </li>
                    <li>
                      <radial-navigation :global-id="imageObject.global_id" />
                    </li>
                  </ul>
                </div>
              </div>

              <div
                v-if="depiction"
                class="horizontal-left-content margin-large-left"
              >
                <span class="margin-small-right">Depiction</span>
                <div class="square-brackets">
                  <ul class="context-menu no_bullets">
                    <li>
                      <radial-annotator
                        type="annotations"
                        :global-id="depiction.global_id"
                      />
                    </li>
                    <li>
                      <radial-navigation :global-id="depiction.global_id" />
                    </li>
                  </ul>
                </div>
              </div>
            </div>
          </div>
        </template>
        <hr />

        <div class="flex-separate">
          <slot name="infoColumn" />
          <div
            v-if="depiction && !edit"
            class="full_width panel content"
          >
            <h3>Depiction</h3>
            <ul class="no_bullets">
              <li v-if="depiction.figure_label">
                <span>Label:</span>
                <b v-html="depiction.figure_label" />
              </li>
              <li v-if="depiction.caption">
                <span>Caption:</span>
                <b v-html="depiction.caption" />
              </li>
            </ul>
          </div>

          <ImageViewerAttributions :attributions="state.attributions" />
          <ImageViewerCitations :citations="state.citations" />
        </div>
      </template>
    </v-modal>
    <div>
      <div
        class="cursor-pointer"
        @click="isModalVisible = true"
      >
        <slot>
          <div :class="[`depiction-${thumbSize}-image`]">
            <img
              class="img-thumb"
              :src="thumbUrlSrc"
              :height="imageObject.alternatives[thumbSize].height"
              :width="imageObject.alternatives[thumbSize].width"
            />
          </div>
        </slot>
      </div>
      <slot name="thumbfooter" />
    </div>
  </div>
</template>
<script setup>
import VModal from '@/components/ui/Modal.vue'
import VBtn from '@/components/ui/VBtn/index.vue'
import VIcon from '@/components/ui/VIcon/index.vue'
import RadialAnnotator from '@/components/radials/annotator/annotator'
import RadialNavigation from '@/components/radials/navigation/radial.vue'
import ImageViewerAttributions from './ImageViewerAttributions.vue'
import ImageViewerCitations from './ImageViewerCitations.vue'
import SvgViewer from '@/components/Svg/SvgViewer.vue'
import { Depiction, Image, Citation, Attribution } from '@/routes/endpoints'
import { imageSVGViewBox, imageScale } from '@/helpers/images'
import { computed, reactive, ref, watch } from 'vue'
import { IMAGE } from '@/constants'

const CONVERT_IMAGE_TYPES = ['image/tiff']
const IMG_MAX_SIZES = {
  thumb: 100,
  medium: 300
}

const props = defineProps({
  depiction: {
    type: Object,
    default: undefined
  },

  edit: {
    type: Boolean,
    default: false
  },

  image: {
    type: Object,
    default: undefined
  },

  thumbSize: {
    type: String,
    default: 'thumb'
  }
})

const isModalVisible = ref(false)
const state = reactive({
  fullSizeImage: true,
  alreadyLoaded: false,
  attributions: [],
  citations: []
})

const image = computed(() =>
  state.fullSizeImage
    ? props.depiction?.image || props.image
    : props.depiction?.image.alternatives.medium ||
      props.image.alternatives.medium
)

const svgClip = computed(() => {
  return props.depiction?.svg_clip
    ? [
        {
          g: props.depiction.svg_clip,
          attributes: { fill: '#FFA500', 'fill-opacity': 0.25 }
        }
      ]
    : null
})

const imageObject = computed(() => props.depiction?.image || props.image)

const urlSrc = computed(() => {
  const depiction = props.depiction
  const { width, height } = image.value

  if (hasSVGBox.value) {
    return imageSVGViewBox(
      imageObject.value.id,
      depiction.svg_view_box,
      width,
      height
    )
  }

  if (CONVERT_IMAGE_TYPES.includes(image.value.content_type)) {
    return imageScale(
      imageObject.value.id,
      `0 0 ${width} ${height}`,
      width,
      height
    )
  }

  return image.value.image_file_url
})

const hasSVGBox = computed(() => props.depiction?.svg_view_box != null)

const thumbUrlSrc = computed(() => {
  const depiction = props.depiction

  return props.hasSVGBox
    ? imageSVGViewBox(
        imageObject.value.id,
        depiction.svg_view_box,
        IMG_MAX_SIZES[props.thumbSize],
        IMG_MAX_SIZES[props.thumbSize]
      )
    : imageObject.value.alternatives[props.thumbSize].image_file_url
})

const loadAttributions = async () => {
  state.citations = (
    await Citation.where({
      citation_object_id: imageObject.value.id,
      citation_object_type: IMAGE,
      extend: ['source']
    })
  ).body
  state.attributions = (
    await Attribution.where({
      attribution_object_id: imageObject.value.id,
      attribution_object_type: IMAGE,
      extend: ['roles']
    })
  ).body
}

const updateDepiction = () => {
  const depiction = {
    caption: props.depiction.caption,
    figure_label: props.depiction.figure_label
  }

  Depiction.update(props.depiction.id, { depiction }).then(() => {
    TW.workbench.alert.create('Depiction was successfully updated.', 'notice')
  })
}

const openFullsize = () => {
  window.open(imageObject.value.image_file_url, '_blank')
}

watch(isModalVisible, (newVal) => {
  if (newVal && !state.alreadyLoaded) {
    loadAttributions()
    state.alreadyLoaded = true
  }
})
</script>

<style lang="scss">
.depiction-thumb-image {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100px;
  height: 100px;
  border: 1px solid black;
  overflow: hidden;
}

.depiction-medium-image {
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: 300px;
  height: 300px;
  border: 1px solid black;
}

.depiction-thumb-container {
  margin: 4px;

  .modal-container {
    max-width: 90vw;
    max-height: 90vh;
    overflow: auto;
  }

  .img-thumb {
    cursor: pointer;
  }

  .img-maxsize {
    transition: all 0.5s ease;
    max-width: 100%;
    max-height: 60vh;
  }

  .img-fullsize {
    cursor: zoom-out;
  }

  .img-normalsize {
    cursor: zoom-in;
  }

  .field {
    input,
    textarea {
      width: 100%;
    }
  }

  .image-container {
    display: flex;
    justify-content: center;
    img {
      border: 1px solid black;
    }
  }
  hr {
    height: 1px;
    color: #f5f5f5;
    background: #f5f5f5;
    font-size: 0;
    margin: 15px;
    border: 0;
  }
}
</style>