text: /**
   * 工具类
   * 此模块灵感源于viewer.js源码，有时间还是要多阅读优秀的源码，提升自己
   * @author hzm 
   */
  import {
    CLASS_HIDE_MD_DOWN,
    CLASS_HIDE_SM_DOWN,
    CLASS_HIDE_XS_DOWN,
    IS_BROWSER,
    REGEXP_SPACES,
    WINDOW,
  } from './constants';
  
  /**
   * Check if the given value is a string.
   * @param {*} value - The value to check.
   * @returns {boolean} Returns `true` if the given value is a string, else `false`.
   */
  export function isString(value) {
    return typeof value === 'string';
  }
  
  /**
   * Check if the given value is not a number.
   */
  export const isNaN = Number.isNaN || WINDOW.isNaN;
  
  /**
   * Check if the given value is a number.
   * @param {*} value - The value to check.
   * @returns {boolean} Returns `true` if the given value is a number, else `false`.
   */
  export function isNumber(value) {
    return typeof value === 'number' && !isNaN(value);
  }
  
  /**
   * Check if the given value is undefined.
   * @param {*} value - The value to check.
   * @returns {boolean} Returns `true` if the given value is undefined, else `false`.
   */
  export function isUndefined(value) {
    return typeof value === 'undefined';
  }
  
  /**
   * Check if the given value is an object.
   * @param {*} value - The value to check.
   * @returns {boolean} Returns `true` if the given value is an object, else `false`.
   */
  export function isObject(value) {
    return typeof value === 'object' && value !== null;
  }
  
  const { hasOwnProperty } = Object.prototype;
  
  /**
   * Check if the given value is a plain object.
   * @param {*} value - The value to check.
   * @returns {boolean} Returns `true` if the given value is a plain object, else `false`.
   */
  export function isPlainObject(value) {
    if (!isObject(value)) {
      return false;
    }
  
    try {
      const { constructor } = value;
      const { prototype } = constructor;
  
      return constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf');
    } catch (error) {
      return false;
    }
  }
  
  /**
   * Check if the given value is a function.
   * @param {*} value - The value to check.
   * @returns {boolean} Returns `true` if the given value is a function, else `false`.
   */
  export function isFunction(value) {
    return typeof value === 'function';
  }
  
  /**
   * Iterate the given data.
   * @param {*} data - The data to iterate.
   * @param {Function} callback - The process function for each element.
   * @returns {*} The original data.
   */
  export function forEach(data, callback) {
    if (data && isFunction(callback)) {
      if (Array.isArray(data) || isNumber(data.length)/* array-like */) {
        const { length } = data;
        let i;
  
        for (i = 0; i < length; i += 1) {
          if (callback.call(data, data[i], i, data) === false) {
            break;
          }
        }
      } else if (isObject(data)) {
        Object.keys(data).forEach((key) => {
          callback.call(data, data[key], key, data);
        });
      }
    }
  
    return data;
  }
  
  /**
   * Extend the given object.
   * @param {*} obj - The object to be extended.
   * @param {*} args - The rest objects which will be merged to the first object.
   * @returns {Object} The extended object.
   */
  export const assign = Object.assign || function assign(obj, ...args) {
    if (isObject(obj) && args.length > 0) {
      args.forEach((arg) => {
        if (isObject(arg)) {
          Object.keys(arg).forEach((key) => {
            obj[key] = arg[key];
          });
        }
      });
    }
  
    return obj;
  };
  
  const REGEXP_SUFFIX = /^(?:width|height|left|top|marginLeft|marginTop)$/;
  
  /**
   * Apply styles to the given element.
   * @param {Element} element - The target element.
   * @param {Object} styles - The styles for applying.
   */
  export function setStyle(element, styles) {
    const { style } = element;
  
    forEach(styles, (value, property) => {
      if (REGEXP_SUFFIX.test(property) && isNumber(value)) {
        value += 'px';
      }
  
      style[property] = value;
    });
  }
  
  /**
   * Escape a string for using in HTML.
   * @param {String} value - The string to escape.
   * @returns {String} Returns the escaped string.
   */
  export function escapeHTMLEntities(value) {
    return isString(value) ? value
      .replace(/&(?!amp;|quot;|#39;|lt;|gt;)/g, '&amp;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;') : value;
  }
  
  /**
   * Check if the given element has a special class.
   * @param {Element} element - The element to check.
   * @param {string} value - The class to search.
   * @returns {boolean} Returns `true` if the special class was found.
   */
  export function hasClass(element, value) {
    return element.classList
      ? element.classList.contains(value)
      : element.className.indexOf(value) > -1;
  }
  
  /**
   * Add classes to the given element.
   * @param {Element} element - The target element.
   * @param {string} value - The classes to be added.
   */
  export function addClass(element, value) {
    if (!value) {
      return;
    }
  
    if (isNumber(element.length)) {
      forEach(element, (elem) => {
        addClass(elem, value);
      });
      return;
    }
  
    if (element.classList) {
      element.classList.add(value);
      return;
    }
  
    const className = element.className.trim();
  
    if (!className) {
      element.className = value;
    } else if (className.indexOf(value) < 0) {
      element.className = `${className} ${value}`;
    }
  }
  
  /**
   * Remove classes from the given element.
   * @param {Element} element - The target element.
   * @param {string} value - The classes to be removed.
   */
  export function removeClass(element, value) {
    if (!value) {
      return;
    }
  
    if (isNumber(element.length)) {
      forEach(element, (elem) => {
        removeClass(elem, value);
      });
      return;
    }
  
    if (element.classList) {
      element.classList.remove(value);
      return;
    }
  
    if (element.className.indexOf(value) >= 0) {
      element.className = element.className.replace(value, '');
    }
  }
  
  /**
   * Add or remove classes from the given element.
   * @param {Element} element - The target element.
   * @param {string} value - The classes to be toggled.
   * @param {boolean} added - Add only.
   */
  export function toggleClass(element, value, added) {
    if (!value) {
      return;
    }
  
    if (isNumber(element.length)) {
      forEach(element, (elem) => {
        toggleClass(elem, value, added);
      });
      return;
    }
  
    // IE10-11 doesn't support the second parameter of `classList.toggle`
    if (added) {
      addClass(element, value);
    } else {
      removeClass(element, value);
    }
  }
  
  const REGEXP_HYPHENATE = /([a-z\d])([A-Z])/g;
  
  /**
   * Transform the given string from camelCase to kebab-case
   * @param {string} value - The value to transform.
   * @returns {string} The transformed value.
   */
  export function hyphenate(value) {
    return value.replace(REGEXP_HYPHENATE, '$1-$2').toLowerCase();
  }
  
  /**
   * Get data from the given element.
   * @param {Element} element - The target element.
   * @param {string} name - The data key to get.
   * @returns {string} The data value.
   */
  export function getData(element, name) {
    if (isObject(element[name])) {
      return element[name];
    }
  
    if (element.dataset) {
      return element.dataset[name];
    }
  
    return element.getAttribute(`data-${hyphenate(name)}`);
  }
  
  /**
   * Set data to the given element.
   * @param {Element} element - The target element.
   * @param {string} name - The data key to set.
   * @param {string} data - The data value.
   */
  export function setData(element, name, data) {
    if (isObject(data)) {
      element[name] = data;
    } else if (element.dataset) {
      element.dataset[name] = data;
    } else {
      element.setAttribute(`data-${hyphenate(name)}`, data);
    }
  }
  
  /**
   * Remove data from the given element.
   * @param {Element} element - The target element.
   * @param {string} name - The data key to remove.
   */
  export function removeData(element, name) {
    if (isObject(element[name])) {
      try {
        delete element[name];
      } catch (error) {
        element[name] = undefined;
      }
    } else if (element.dataset) {
      // #128 Safari not allows to delete dataset property
      try {
        delete element.dataset[name];
      } catch (error) {
        element.dataset[name] = undefined;
      }
    } else {
      element.removeAttribute(`data-${hyphenate(name)}`);
    }
  }
  
  const onceSupported = (() => {
    let supported = false;
  
    if (IS_BROWSER) {
      let once = false;
      const listener = () => {};
      const options = Object.defineProperty({}, 'once', {
        get() {
          supported = true;
          return once;
        },
  
        /**
         * This setter can fix a `TypeError` in strict mode
         * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only}
         * @param {boolean} value - The value to set
         */
        set(value) {
          once = value;
        },
      });
  
      WINDOW.addEventListener('test', listener, options);
      WINDOW.removeEventListener('test', listener, options);
    }
  
    return supported;
  })();
  
  /**
   * Remove event listener from the target element.
   * @param {Element} element - The event target.
   * @param {string} type - The event type(s).
   * @param {Function} listener - The event listener.
   * @param {Object} options - The event options.
   */
  export function removeListener(element, type, listener, options = {}) {
    let handler = listener;
  
    type.trim().split(REGEXP_SPACES).forEach((event) => {
      if (!onceSupported) {
        const { listeners } = element;
  
        if (listeners && listeners[event] && listeners[event][listener]) {
          handler = listeners[event][listener];
          delete listeners[event][listener];
  
          if (Object.keys(listeners[event]).length === 0) {
            delete listeners[event];
          }
  
          if (Object.keys(listeners).length === 0) {
            delete element.listeners;
          }
        }
      }
  
      element.removeEventListener(event, handler, options);
    });
  }
  
  /**
   * Add event listener to the target element.
   * @param {Element} element - The event target.
   * @param {string} type - The event type(s).
   * @param {Function} listener - The event listener.
   * @param {Object} options - The event options.
   */
  export function addListener(element, type, listener, options = {}) {
    let handler = listener;
  
    type.trim().split(REGEXP_SPACES).forEach((event) => {
      if (options.once && !onceSupported) {
        const { listeners = {} } = element;
  
        handler = (...args) => {
          delete listeners[event][listener];
          element.removeEventListener(event, handler, options);
          listener.apply(element, args);
        };
  
        if (!listeners[event]) {
          listeners[event] = {};
        }
  
        if (listeners[event][listener]) {
          element.removeEventListener(event, listeners[event][listener], options);
        }
  
        listeners[event][listener] = handler;
        element.listeners = listeners;
      }
  
      element.addEventListener(event, handler, options);
    });
  }
  
  /**
   * Dispatch event on the target element.
   * @param {Element} element - The event target.
   * @param {string} type - The event type(s).
   * @param {Object} data - The additional event data.
   * @returns {boolean} Indicate if the event is default prevented or not.
   */
  export function dispatchEvent(element, type, data) {
    let event;
  
    // Event and CustomEvent on IE9-11 are global objects, not constructors
    if (isFunction(Event) && isFunction(CustomEvent)) {
      event = new CustomEvent(type, {
        detail: data,
        bubbles: true,
        cancelable: true,
      });
    } else {
      event = document.createEvent('CustomEvent');
      event.initCustomEvent(type, true, true, data);
    }
  
    return element.dispatchEvent(event);
  }
  
  /**
   * Get the offset base on the document.
   * @param {Element} element - The target element.
   * @returns {Object} The offset data.
   */
  export function getOffset(element) {
    const box = element.getBoundingClientRect();
  
    return {
      left: box.left + (window.pageXOffset - document.documentElement.clientLeft),
      top: box.top + (window.pageYOffset - document.documentElement.clientTop),
    };
  }
  
  /**
   * Get transforms base on the given object.
   * @param {Object} obj - The target object.
   * @returns {string} A string contains transform values.
   */
  export function getTransforms({
    rotate,
    scaleX,
    scaleY,
    translateX,
    translateY,
  }) {
    const values = [];
  
    if (isNumber(translateX) && translateX !== 0) {
      values.push(`translateX(${translateX}px)`);
    }
  
    if (isNumber(translateY) && translateY !== 0) {
      values.push(`translateY(${translateY}px)`);
    }
  
    // Rotate should come first before scale to match orientation transform
    if (isNumber(rotate) && rotate !== 0) {
      values.push(`rotate(${rotate}deg)`);
    }
  
    if (isNumber(scaleX) && scaleX !== 1) {
      values.push(`scaleX(${scaleX})`);
    }
  
    if (isNumber(scaleY) && scaleY !== 1) {
      values.push(`scaleY(${scaleY})`);
    }
  
    const transform = values.length ? values.join(' ') : 'none';
  
    return {
      WebkitTransform: transform,
      msTransform: transform,
      transform,
    };
  }
  
  /**
   * Get an image name from an image url.
   * @param {string} url - The target url.
   * @example
   * // picture.jpg
   * getImageNameFromURL('http://domain.com/path/to/picture.jpg?size=1280×960')
   * @returns {string} A string contains the image name.
   */
  export function getImageNameFromURL(url) {
    return isString(url) ? decodeURIComponent(url.replace(/^.*\//, '').replace(/[?&#].*$/, '')) : '';
  }
  
  const IS_SAFARI = WINDOW.navigator && /(Macintosh|iPhone|iPod|iPad).*AppleWebKit/i.test(WINDOW.navigator.userAgent);
  
  /**
   * Get an image's natural sizes.
   * @param {string} image - The target image.
   * @param {Function} callback - The callback function.
   * @returns {HTMLImageElement} The new image.
   */
  export function getImageNaturalSizes(image, callback) {
    const newImage = document.createElement('img');
  
    // Modern browsers (except Safari)
    if (image.naturalWidth && !IS_SAFARI) {
      callback(image.naturalWidth, image.naturalHeight);
      return newImage;
    }
  
    const body = document.body || document.documentElement;
  
    newImage.onload = () => {
      callback(newImage.width, newImage.height);
  
      if (!IS_SAFARI) {
        body.removeChild(newImage);
      }
    };
  
    newImage.src = image.src;
  
    // iOS Safari will convert the image automatically
    // with its orientation once append it into DOM
    if (!IS_SAFARI) {
      newImage.style.cssText = (
        'left:0;'
        + 'max-height:none!important;'
        + 'max-width:none!important;'
        + 'min-height:0!important;'
        + 'min-width:0!important;'
        + 'opacity:0;'
        + 'position:absolute;'
        + 'top:0;'
        + 'z-index:-1;'
      );
      body.appendChild(newImage);
    }
  
    return newImage;
  }
  
  /**
   * Get the related class name of a responsive type number.
   * @param {string} type - The responsive type.
   * @returns {string} The related class name.
   */
  export function getResponsiveClass(type) {
    switch (type) {
      case 2:
        return CLASS_HIDE_XS_DOWN;
  
      case 3:
        return CLASS_HIDE_SM_DOWN;
  
      case 4:
        return CLASS_HIDE_MD_DOWN;
  
      default:
        return '';
    }
  }
  
  /**
   * Get the max ratio of a group of pointers.
   * @param {string} pointers - The target pointers.
   * @returns {number} The result ratio.
   */
  export function getMaxZoomRatio(pointers) {
    const pointers2 = assign({}, pointers);
    const ratios = [];
  
    forEach(pointers, (pointer, pointerId) => {
      delete pointers2[pointerId];
  
      forEach(pointers2, (pointer2) => {
        const x1 = Math.abs(pointer.startX - pointer2.startX);
        const y1 = Math.abs(pointer.startY - pointer2.startY);
        const x2 = Math.abs(pointer.endX - pointer2.endX);
        const y2 = Math.abs(pointer.endY - pointer2.endY);
        const z1 = Math.sqrt((x1 * x1) + (y1 * y1));
        const z2 = Math.sqrt((x2 * x2) + (y2 * y2));
        const ratio = (z2 - z1) / z1;
  
        ratios.push(ratio);
      });
    });
  
    ratios.sort((a, b) => Math.abs(a) < Math.abs(b));
  
    return ratios[0];
  }
  
  /**
   * Get a pointer from an event object.
   * @param {Object} event - The target event object.
   * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not.
   * @returns {Object} The result pointer contains start and/or end point coordinates.
   */
  export function getPointer({ pageX, pageY }, endOnly) {
    const end = {
      endX: pageX,
      endY: pageY,
    };
  
    return endOnly ? end : assign({
      timeStamp: Date.now(),
      startX: pageX,
      startY: pageY,
    }, end);
  }
  
  /**
   * Get the center point coordinate of a group of pointers.
   * @param {Object} pointers - The target pointers.
   * @returns {Object} The center point coordinate.
   */
  export function getPointersCenter(pointers) {
    let pageX = 0;
    let pageY = 0;
    let count = 0;
  
    forEach(pointers, ({ startX, startY }) => {
      pageX += startX;
      pageY += startY;
      count += 1;
    });
  
    pageX /= count;
    pageY /= count;
  
    return {
      pageX,
      pageY,
    };
  }
  