TUBB/h5-imageviewer

View on GitHub
src/imgListViewer.js

Summary

Maintainability
F
6 days
Test Coverage
import './main.less'
import imageLoaded from './utils/image_loaded'
import Transform from './utils/transform'
import To from './utils/to'
import ease from './utils/ease'
import imgAlloyFinger, {
  triggerDoubleTab,
  triggerPointEnd
} from './imgAlloyFinger'
import orit from './utils/orientation'
import scrollThrough from './utils/scrollThrough'
import IMG_EMPTY from './utils/error_plh'

const VIEWER_CONTAINER_ID = 'pobi_mobile_viewer_container_id'
const VIEWER_PANEL_ID = 'pobi_mobile_viewer_panel_id'
const VIEWER_SINGLE_IMAGE_ID = 'pobi_mobile_viewer_single_image_id'
const VIEWER_SINGLE_IMAGE_CONTAINER = 'pobi_mobile_viewer_single_image_container'

let containerDom = null
let panelDom = null
let currPage = 0
let orientation = orit.PORTRAIT
let viewerData = null
let viewerAlloyFinger = null

function noop () {}

/**
 * Display image list viewer
 * @param {Array} imgList image list
 * @param {Object} options config options
 * @param {Boolean} screenRotated device screen is rotated or not
 */
export const showImgListViewer = (imgList = [], options, screenRotated = false) => {
  if (!Array.isArray(imgList) || imgList.length <= 0) return
  const cachedCurrPage = currPage
  hideImgListViewer(false)
  scrollThrough(true)
  initParams(imgList, options, screenRotated, cachedCurrPage)
  appendViewerContainer()
  appendViewerPanel()
  scrollToFixedPage(viewerData.options.defaultPageIndex)
  handleImgDoms()
  handleRestDoms()
  handleOrientationChange()
}

/**
 * Hide image list viewer
 * @param {Boolean} notifyUser options.onViewerHideListener call or not
 */
export const hideImgListViewer = (notifyUser = true) => {
  if (viewerData) {
    if (notifyUser) {
      viewerData.options.onViewerHideListener()
    }
    scrollThrough(false)
    removeViewerContainer()
  }
}

/**
 * Set viewer current page
 * @param {Number} pageIndex page index, start with 0
 */
export const setCurrentPage = pageIndex => {
  if (viewerData === null 
    || pageIndex < 0 
    || pageIndex > viewerData.imgList.length - 1) {
    return
  }
  const { imgList, options: { limit } } = viewerData
  const lastIndex = imgList.length - 1
  const updateDom = page => {
    const currNode = panelDom.childNodes[page]
    if (currNode && !currNode.hasAttribute('class')) {
      panelDom.replaceChild(appendSingleViewer(imgList[page], page), currNode)
    }
  }
  if (pageIndex === 0) {
    let page = pageIndex
    while (page < limit) {
      updateDom(page)
      page++
    }
  } else if (pageIndex === lastIndex) {
    let page = pageIndex
    const endedIndex = pageIndex - limit
    while (page > endedIndex && page >= 0) {
      updateDom(page)
      page--
    }
  } else {
    const halfCount = Math.floor(limit / 2)
    let firstHalf = pageIndex
    const firstHalfEndedIndex = firstHalf - halfCount
    while (firstHalf >= firstHalfEndedIndex && firstHalf >= 0) {
      updateDom(firstHalf)
      firstHalf--
    }
    let secondHalf = pageIndex
    const secondHalfEndedIndex = secondHalf + halfCount
    while (secondHalf <= secondHalfEndedIndex && secondHalf <= lastIndex) {
      updateDom(secondHalf)
      secondHalf++
    }
  }
  const currImgDom = getCurrImgDom()
  if (currImgDom) {
    const { scaleX, scaleY, translateX, translateY } = currImgDom
    const { imgMinScale } = viewerData.options
    if (scaleX !== imgMinScale || scaleY !== imgMinScale) {
      new To(currImgDom, 'scaleX', imgMinScale, 200, ease)
      new To(currImgDom, 'scaleY', imgMinScale, 200, ease)
    }
    if (translateX !== 0) {
      new To(currImgDom, 'translateX', 0, 200, ease)
    }
    if (translateY !== 0) {
      new To(currImgDom, 'translateY', 0, 200, ease)
    }
  }
  scrollToFixedPage(pageIndex)
}

const initParams = (imgList, options, screenRotation, cachedCurrPage) => {
  let wrapOptions = {}
  if (options) wrapOptions = { ...options }
  const {
    defaultPageIndex = 0,
    errorPlh,
    onPageChanged = noop,
    onViewerHideListener = noop,
    restDoms = [],
    pageThreshold = 0.1,
    pageDampingFactor = 0.9,
    imgMoveFactor = 1.5,
    imgMinScale = 1,
    imgMaxScale = 2,
    limit = 5,
    zIndex = null,
    viewerBg = null,
    clickClosable = true
  } = wrapOptions
  if (!/^[0-9]+$/.test(limit) || limit < 3 || limit % 2 !== 1) {
    throw Error('limit must be odd number and greater than 3')
  }
  viewerData = {
    imgList: [...imgList],
    options: {
      limit,
      defaultPageIndex: screenRotation ? cachedCurrPage : defaultPageIndex,
      errorPlh,
      onPageChanged,
      onViewerHideListener,
      restDoms,
      pageThreshold,
      pageDampingFactor,
      imgMoveFactor,
      imgMinScale,
      imgMaxScale,
      zIndex,
      viewerBg,
      clickClosable
    }
  }
  if (viewerData.options.defaultPageIndex < 0 ||
    viewerData.options.defaultPageIndex > imgList.length - 1) {
    viewerData.options.defaultPageIndex = 0
  }
}

const registerViewerAlloyFinger = () => {
  const {
    pageDampingFactor,
    imgMoveFactor,
    pageThreshold,
    imgMinScale,
    imgMaxScale,
    clickClosable
  } = viewerData.options
  const pageCount = viewerData.imgList.length
  viewerAlloyFinger = imgAlloyFinger(containerDom, {
    swipeListener: evt => {
      const { translateX } = panelDom
      const scrollFactor = Math.abs(translateX) / window.innerWidth
      const page = Math.floor(scrollFactor)
      const factor = scrollFactor - page
      const fixedCurrPage = currPage
      if (evt.direction === 'Left' &&
        currPage < pageCount - 1 &&
        (page >= currPage && factor >= pageThreshold)) {
        currPage += 1
        scrollToPage(getCurrImgDom(fixedCurrPage), currPage, fixedCurrPage)
      } else if (evt.direction === 'Right' &&
        currPage > 0 &&
        (page < currPage && factor <= (1 - pageThreshold))) {
        currPage -= 1
        scrollToPage(getCurrImgDom(fixedCurrPage), currPage, fixedCurrPage)
      } else {
        scrollToPage(getCurrImgDom(), currPage, fixedCurrPage)
      }
    },
    pressMoveListener: evt => {
      const currImgDom = getCurrImgDom()
      if (!evt || !currImgDom) return
      const currPageTranslateStart = currPage * window.innerWidth
      const { scaleX, width, translateX } = currImgDom
      const { deltaX, deltaY } = evt
      const panelTranslateX = pageDampingFactor * deltaX + panelDom.translateX
      const realWidth = scaleX * width
      const scaledWidth = Math.abs(realWidth - width) / 2
      const imgTranslateX = translateX + deltaX * imgMoveFactor
      if (Math.abs(deltaX) > Math.abs(deltaY)) {
        if (realWidth <= width) { // img shrinked
          panelDom.translateX = panelTranslateX
        } else { // img enlarged
          if (Math.abs(imgTranslateX) - scaledWidth <= 0) {
            if (deltaX > 0) { // move to right
              if (Math.abs(panelDom.translateX) > currPageTranslateStart) {
                const panelReturnDis = panelTranslateX
                if (Math.abs(panelReturnDis) < currPageTranslateStart) {
                  panelDom.translateX = -currPageTranslateStart
                } else {
                  panelDom.translateX = panelReturnDis
                }
              } else {
                currImgDom.translateX = imgTranslateX
              }
            } else { // move to left
              if (Math.abs(panelDom.translateX) < currPageTranslateStart) {
                const panelReturnDis = panelTranslateX
                if (Math.abs(panelReturnDis) > currPageTranslateStart) {
                  panelDom.translateX = -currPageTranslateStart
                } else {
                  panelDom.translateX = panelReturnDis
                }
              } else {
                currImgDom.translateX = imgTranslateX
              }
            }
          } else {
            panelDom.translateX = panelTranslateX
          }
        }
      } else {
        currImgDom.translateY += deltaY * imgMoveFactor
      }
    },
    touchEndListener: () => {
      const { translateX } = panelDom
      const scrollFactor = Math.abs(translateX) / window.innerWidth
      const page = Math.floor(scrollFactor)
      const factor = scrollFactor - page
      const fixedCurrPage = currPage
      let newPage = currPage
      if (page < currPage) { // to prev page
        if (factor <= (1 - pageThreshold)) {
          newPage -= 1
        }
      } else { // to next page
        if (factor >= pageThreshold) {
          if (currPage + 1 !== pageCount && translateX < 0) {
            newPage += 1
          }
        }
      }
      if (newPage === fixedCurrPage) {
        scrollToPage(getCurrImgDom(fixedCurrPage), fixedCurrPage, fixedCurrPage)
      }
    },
    singleTapListener: () => {
      if(clickClosable) hideImgListViewer()
    },
    doubleTapListener: evt => {
      triggerDoubleTab(getCurrImgDom(), evt, imgMinScale, imgMaxScale)
    },
    multipointEndListener: () => {
      if (viewerData) {
        triggerPointEnd(getCurrImgDom(), imgMinScale, imgMaxScale)
      }
    },
    multipointStartListener: () => {
      const dom = getCurrImgDom()
      if (!dom) return 0
      else return dom.scaleX
    },
    pinchListener: (evt, initScale) => {
      const dom = getCurrImgDom()
      if (!evt || !dom) return
      dom.scaleX = dom.scaleY = initScale * evt.zoom 
    }
  })
  containerDom.style.transform = 'none'
}

const handleOrientationChange = () => {
  orientation = orit.phoneOrientation()
  orit.removeOrientationChangeListener(userOrientationListener)
  orit.addOrientationChangeListener(userOrientationListener)
}

const userOrientationListener = () => {
  const newOrientation = orit.phoneOrientation()
  if (newOrientation !== orientation && viewerData) { // orientation changed
    // window.innerWidth and window.innerHeight changed will delay
    setTimeout(() => {
      if (viewerData) {
        showImgListViewer(viewerData.imgList, viewerData.options, true)
      }
    }, 300)
  }
}

const scrollToFixedPage = (page) => {
  currPage = page
  panelDom.translateX = -currPage * window.innerWidth
  viewerData.options.onPageChanged(currPage)
}

const handleImgDoms = () => {
  let docfrag = document.createDocumentFragment()
  const { imgList } = viewerData
  const lastIndex = imgList.length - 1
  const { limit } = viewerData.options
  if (limit >= imgList.length) {
    imgList.forEach((imgObj, index) => {
      docfrag.appendChild(appendSingleViewer(imgObj, index))
    })
  } else {
    const plDom = createImgPl()
    imgList.forEach((imgObj, index) => {
      if (currPage === 0) {
        if (index < limit) {
          docfrag.appendChild(appendSingleViewer(imgObj, index))
        } else {
          docfrag.appendChild(plDom.cloneNode())
        }
      } else if (currPage === lastIndex) {
        if (index > lastIndex - limit) {
          docfrag.appendChild(appendSingleViewer(imgObj, index))
        } else {
          docfrag.appendChild(plDom.cloneNode())
        }
      } else {
        const halfCount = Math.floor(limit / 2)
        if (index === currPage || (index >= currPage - halfCount && index <= currPage + halfCount)) {
          docfrag.appendChild(appendSingleViewer(imgObj, index))
        } else {
          docfrag.appendChild(plDom.cloneNode())
        }
      }
    })
  }
  panelDom.appendChild(docfrag)
  docfrag = null
}

const replaceImgDom = (prevPage) => {
  if (viewerData === null) return 
  const { imgList, options: { limit } } = viewerData
  const lastIndex = imgList.length - 1
  if (currPage === 0 ||
    currPage === lastIndex ||
    currPage === prevPage ||
    limit >= imgList.length) {
    return
  }
  setTimeout(() => {
    const imgContainerDoms = panelDom.childNodes
    const currNode = imgContainerDoms[currPage]
    if (currNode && !currNode.hasAttribute('class')) {
      panelDom.replaceChild(appendSingleViewer(imgList[currPage], currPage), currNode)
    }
    const halfCount = Math.floor(limit / 2)
    if (currPage > prevPage) { // scrolled to next page
      const nextIndex = currPage + halfCount
      const nextNode = imgContainerDoms[nextIndex]
      if (nextNode && !nextNode.hasAttribute('class')) {
        panelDom.replaceChild(appendSingleViewer(imgList[nextIndex], nextIndex), nextNode)
      }
      const ppIndex = currPage - halfCount - 1
      if (ppIndex >= 0) {
        const ppNode = imgContainerDoms[ppIndex]
        panelDom.replaceChild(createImgPl(), ppNode)
      }
    } else if (currPage < prevPage) { // scrolled to prev page
      const prevIndex = currPage - halfCount
      const prevNode = imgContainerDoms[prevIndex]
      if (prevNode && !prevNode.hasAttribute('class')) {
        panelDom.replaceChild(appendSingleViewer(imgList[prevIndex], prevIndex), prevNode)
      }
      const nnIndex = prevPage + halfCount
      if (nnIndex <= lastIndex) {
        const nnNode = imgContainerDoms[nnIndex]
        panelDom.replaceChild(createImgPl(), nnNode)
      }
    }
  }, 0)
}

const createImgPl = () => {
  const pl = document.createElement('div')
  pl.style.width = window.innerWidth + 'px'
  pl.style.visibility = 'hidden'
  return pl
}

const handleRestDoms = () => {
  const { restDoms } = viewerData.options
  if (restDoms.length > 0) {
    let docfrag = document.createDocumentFragment()
    restDoms.forEach(additionDom => {
      // element
      if (additionDom.nodeType === 1) {
        docfrag.appendChild(additionDom)
      } else {
        console.warn('Ignore invalid dom', additionDom)
      }
    })
    containerDom.appendChild(docfrag)
    docfrag = null
  }
}

const genImgId = index => `${VIEWER_SINGLE_IMAGE_ID}_${index}`

const scrollToPage = (dom, targetPage, prevPage) => {
  // page changed, so we reset current page's translateX、scaleX、scaleY
  if (targetPage !== prevPage) {
    viewerData.options.onPageChanged(targetPage)
    const { translateX, translateY, scaleX, scaleY } = dom
    if (translateX !== 0) new To(dom, 'translateX', 0, 300, ease)
    if (translateY !== 0) new To(dom, 'translateY', 0, 300, ease)
    if (scaleX > 1) new To(dom, 'scaleX', 1, 300, ease)
    if (scaleY > 1) new To(dom, 'scaleY', 1, 300, ease)
  }
  panelToX(targetPage, prevPage)
}

const panelToX = (targetPage, prevPage) => {
  const toX = -targetPage * window.innerWidth
  if (Math.abs(panelDom.translateX) !== Math.abs(toX)) {
    new To(panelDom, 'translateX', toX, 300, ease, replaceImgDom.bind(this, prevPage))
  }
}

const resetImgDom = (imgDom, w) => {
  let imgWidth = 0
  if (w > window.innerWidth) {
    imgWidth = window.innerWidth
  } else {
    imgWidth = w
  }
  imgDom.style.width = imgWidth + 'px'
  imgDom.style.height = 'auto'
}

const imgOnloadListener = (imgDom, w, h, url) => {
  imgDom.src = url
  resetImgDom(imgDom, w, h)
}

const imgOnerrorListener = (imgContainerDom, imgDom, loadingDom, error) => {
  if (error) {
    const {
      errorPlh
    } = viewerData.options
    if (errorPlh) {
      const successListener = function (w, h) {
        imgOnloadListener(imgDom, w, h, errorPlh)
      }
      const errorListener = function () {
        imgContainerDom.removeChild(loadingDom)
      }
      imageLoaded(errorPlh, successListener, errorListener)
      imgDom.src = errorPlh
    } else {
      imgContainerDom.removeChild(loadingDom)
    }
  } else {
    imgContainerDom.removeChild(loadingDom)
  }
}

const appendSingleViewer = (imgObj, index) => {
  const imgContainerDom = document.createElement('div')
  imgContainerDom.setAttribute('class', VIEWER_SINGLE_IMAGE_CONTAINER)
  imgContainerDom.style.width = window.innerWidth + 'px'

  const loadingDom = document.createElement('div')
  loadingDom.setAttribute('class', 'pobi_mobile_viewer_loading')
  imgContainerDom.appendChild(loadingDom)

  const imgDom = document.createElement('img')
  imgDom.setAttribute('id', genImgId(index))
  imgDom.setAttribute('src', IMG_EMPTY)
  imgDom.setAttribute('alt', imgObj.alt || '')
  imgDom.style.width = window.innerWidth + 'px'
  imgDom.style.height = window.innerWidth + 'px'
  imgContainerDom.appendChild(imgDom)

  imageLoaded(imgObj.src, function (w, h) {
    imgOnloadListener(imgDom, w, h, imgObj.src)
  }, function (error) {
    imgOnerrorListener(imgContainerDom, imgDom, loadingDom, error)
  })

  Transform(imgDom)
  return imgContainerDom
}

const appendViewerContainer = () => {
  containerDom = document.getElementById(VIEWER_CONTAINER_ID)
  if (!containerDom) {
    containerDom = document.createElement('div')
    containerDom.setAttribute('id', VIEWER_CONTAINER_ID)
    containerDom.style.width = window.innerWidth + 'px'
    containerDom.style.height = window.innerHeight + 'px'
    const { zIndex, viewerBg } = viewerData.options
    if (zIndex !== null) {
      containerDom.style['z-index'] = zIndex
    }
    if (viewerBg !== null) {
      containerDom.style.background = viewerBg
    }
    document.body.appendChild(containerDom)
    registerViewerAlloyFinger()
  }
}

const getCurrImgDom = (page = currPage) => {
  return panelDom.childNodes[page].childNodes[0]
}

const appendViewerPanel = () => {
  panelDom = document.getElementById(VIEWER_PANEL_ID)
  if (!panelDom) {
    panelDom = document.createElement('div')
    panelDom.setAttribute('id', VIEWER_PANEL_ID)
    panelDom.style.width = (window.innerWidth * viewerData.imgList.length) + 'px'
    panelDom.style.height = window.innerHeight + 'px'
    containerDom.appendChild(panelDom)
    Transform(panelDom)
  }
}

const removeViewerContainer = () => {
  containerDom && document.body.removeChild(containerDom)
  orit.removeOrientationChangeListener(userOrientationListener)
  orientation = orit.PORTRAIT
  containerDom = null
  panelDom = null
  currPage = 0
  viewerData = null
  if (viewerAlloyFinger) {
    viewerAlloyFinger.destroy()
    viewerAlloyFinger.pressMoveListener = null
    viewerAlloyFinger = null
  }
}