viewer.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import DEFAULTS from './defaults';
  2. import TEMPLATE from './template';
  3. import render from './render';
  4. import events from './events';
  5. import handlers from './handlers';
  6. import methods from './methods';
  7. import others from './others';
  8. import {
  9. BUTTONS,
  10. CLASS_CLOSE,
  11. CLASS_FADE,
  12. CLASS_FIXED,
  13. CLASS_FULLSCREEN,
  14. CLASS_HIDE,
  15. CLASS_INVISIBLE,
  16. DATA_ACTION,
  17. EVENT_CLICK,
  18. EVENT_LOAD,
  19. EVENT_READY,
  20. NAMESPACE,
  21. REGEXP_SPACES,
  22. WINDOW,
  23. } from './constants';
  24. import {
  25. addClass,
  26. addListener,
  27. assign,
  28. dispatchEvent,
  29. forEach,
  30. getResponsiveClass,
  31. hyphenate,
  32. isFunction,
  33. isNumber,
  34. isPlainObject,
  35. isString,
  36. isUndefined,
  37. removeListener,
  38. setData,
  39. setStyle,
  40. toggleClass,
  41. } from './utilities';
  42. const AnotherViewer = WINDOW.Viewer;
  43. class Viewer {
  44. /**
  45. * Create a new Viewer.
  46. * @param {Element} element - The target element for viewing.
  47. * @param {Object} [options={}] - The configuration options.
  48. */
  49. constructor(element, options = {}) {
  50. if (!element || element.nodeType !== 1) {
  51. throw new Error('The first argument is required and must be an element.');
  52. }
  53. this.element = element;
  54. this.options = assign({}, DEFAULTS, isPlainObject(options) && options);
  55. this.action = false;
  56. this.fading = false;
  57. this.fulled = false;
  58. this.hiding = false;
  59. this.imageClicked = false;
  60. this.imageData = {};
  61. this.index = this.options.initialViewIndex;
  62. this.isImg = false;
  63. this.isShown = false;
  64. this.length = 0;
  65. this.played = false;
  66. this.playing = false;
  67. this.pointers = {};
  68. this.ready = false;
  69. this.showing = false;
  70. this.timeout = false;
  71. this.tooltipping = false;
  72. this.viewed = false;
  73. this.viewing = false;
  74. this.wheeling = false;
  75. this.zooming = false;
  76. this.init();
  77. }
  78. init() {
  79. const { element, options } = this;
  80. if (element[NAMESPACE]) {
  81. return;
  82. }
  83. element[NAMESPACE] = this;
  84. const isImg = element.tagName.toLowerCase() === 'img';
  85. const images = [];
  86. forEach(isImg ? [element] : element.querySelectorAll('img'), (image) => {
  87. if (isFunction(options.filter)) {
  88. if (options.filter.call(this, image)) {
  89. images.push(image);
  90. }
  91. } else if (this.getImageURL(image)) {
  92. images.push(image);
  93. }
  94. });
  95. this.isImg = isImg;
  96. this.length = images.length;
  97. this.images = images;
  98. this.initBody();
  99. // Override `transition` option if it is not supported
  100. if (isUndefined(document.createElement(NAMESPACE).style.transition)) {
  101. options.transition = false;
  102. }
  103. if (options.inline) {
  104. let count = 0;
  105. const progress = () => {
  106. count += 1;
  107. if (count === this.length) {
  108. let timeout;
  109. this.initializing = false;
  110. this.delaying = {
  111. abort: () => {
  112. clearTimeout(timeout);
  113. },
  114. };
  115. // build asynchronously to keep `this.viewer` is accessible in `ready` event handler.
  116. timeout = setTimeout(() => {
  117. this.delaying = false;
  118. this.build();
  119. }, 0);
  120. }
  121. };
  122. this.initializing = {
  123. abort: () => {
  124. forEach(images, (image) => {
  125. if (!image.complete) {
  126. removeListener(image, EVENT_LOAD, progress);
  127. }
  128. });
  129. },
  130. };
  131. forEach(images, (image) => {
  132. if (image.complete) {
  133. progress();
  134. } else {
  135. addListener(image, EVENT_LOAD, progress, {
  136. once: true,
  137. });
  138. }
  139. });
  140. } else {
  141. addListener(element, EVENT_CLICK, (this.onStart = ({ target }) => {
  142. if (target.tagName.toLowerCase() === 'img'
  143. && (!isFunction(options.filter) || options.filter.call(this, target))) {
  144. this.view(this.images.indexOf(target));
  145. }
  146. }));
  147. }
  148. }
  149. build() {
  150. if (this.ready) {
  151. return;
  152. }
  153. const { element, options } = this;
  154. const parent = element.parentNode;
  155. const template = document.createElement('div');
  156. template.innerHTML = TEMPLATE;
  157. const viewer = template.querySelector(`.${NAMESPACE}-container`);
  158. const title = viewer.querySelector(`.${NAMESPACE}-title`);
  159. const toolbar = viewer.querySelector(`.${NAMESPACE}-toolbar`);
  160. const navbar = viewer.querySelector(`.${NAMESPACE}-navbar`);
  161. const button = viewer.querySelector(`.${NAMESPACE}-button`);
  162. const canvas = viewer.querySelector(`.${NAMESPACE}-canvas`);
  163. this.parent = parent;
  164. this.viewer = viewer;
  165. this.title = title;
  166. this.toolbar = toolbar;
  167. this.navbar = navbar;
  168. this.button = button;
  169. this.canvas = canvas;
  170. this.footer = viewer.querySelector(`.${NAMESPACE}-footer`);
  171. this.tooltipBox = viewer.querySelector(`.${NAMESPACE}-tooltip`);
  172. this.player = viewer.querySelector(`.${NAMESPACE}-player`);
  173. this.list = viewer.querySelector(`.${NAMESPACE}-list`);
  174. addClass(title, !options.title ? CLASS_HIDE : getResponsiveClass(Array.isArray(options.title)
  175. ? options.title[0]
  176. : options.title));
  177. addClass(navbar, !options.navbar ? CLASS_HIDE : getResponsiveClass(options.navbar));
  178. toggleClass(button, CLASS_HIDE, !options.button);
  179. if (options.backdrop) {
  180. addClass(viewer, `${NAMESPACE}-backdrop`);
  181. if (!options.inline && options.backdrop !== 'static') {
  182. setData(canvas, DATA_ACTION, 'hide');
  183. }
  184. }
  185. if (isString(options.className) && options.className) {
  186. // In case there are multiple class names
  187. options.className.split(REGEXP_SPACES).forEach((className) => {
  188. addClass(viewer, className);
  189. });
  190. }
  191. if (options.toolbar) {
  192. const list = document.createElement('ul');
  193. const custom = isPlainObject(options.toolbar);
  194. const zoomButtons = BUTTONS.slice(0, 3);
  195. const rotateButtons = BUTTONS.slice(7, 9);
  196. const scaleButtons = BUTTONS.slice(9);
  197. if (!custom) {
  198. addClass(toolbar, getResponsiveClass(options.toolbar));
  199. }
  200. forEach(custom ? options.toolbar : BUTTONS, (value, index) => {
  201. const deep = custom && isPlainObject(value);
  202. const name = custom ? hyphenate(index) : value;
  203. const show = deep && !isUndefined(value.show) ? value.show : value;
  204. if (
  205. !show
  206. || (!options.zoomable && zoomButtons.indexOf(name) !== -1)
  207. || (!options.rotatable && rotateButtons.indexOf(name) !== -1)
  208. || (!options.scalable && scaleButtons.indexOf(name) !== -1)
  209. ) {
  210. return;
  211. }
  212. const size = deep && !isUndefined(value.size) ? value.size : value;
  213. const click = deep && !isUndefined(value.click) ? value.click : value;
  214. const item = document.createElement('li');
  215. item.setAttribute('role', 'button');
  216. addClass(item, `${NAMESPACE}-${name}`);
  217. if (!isFunction(click)) {
  218. setData(item, DATA_ACTION, name);
  219. }
  220. if (isNumber(show)) {
  221. addClass(item, getResponsiveClass(show));
  222. }
  223. if (['small', 'large'].indexOf(size) !== -1) {
  224. addClass(item, `${NAMESPACE}-${size}`);
  225. } else if (name === 'play') {
  226. addClass(item, `${NAMESPACE}-large`);
  227. }
  228. if (isFunction(click)) {
  229. addListener(item, EVENT_CLICK, click);
  230. }
  231. list.appendChild(item);
  232. });
  233. toolbar.appendChild(list);
  234. } else {
  235. addClass(toolbar, CLASS_HIDE);
  236. }
  237. if (!options.rotatable) {
  238. const rotates = toolbar.querySelectorAll('li[class*="rotate"]');
  239. addClass(rotates, CLASS_INVISIBLE);
  240. forEach(rotates, (rotate) => {
  241. toolbar.appendChild(rotate);
  242. });
  243. }
  244. if (options.inline) {
  245. addClass(button, CLASS_FULLSCREEN);
  246. setStyle(viewer, {
  247. zIndex: options.zIndexInline,
  248. });
  249. if (window.getComputedStyle(parent).position === 'static') {
  250. setStyle(parent, {
  251. position: 'relative',
  252. });
  253. }
  254. parent.insertBefore(viewer, element.nextSibling);
  255. } else {
  256. addClass(button, CLASS_CLOSE);
  257. addClass(viewer, CLASS_FIXED);
  258. addClass(viewer, CLASS_FADE);
  259. addClass(viewer, CLASS_HIDE);
  260. setStyle(viewer, {
  261. zIndex: options.zIndex,
  262. });
  263. let { container } = options;
  264. if (isString(container)) {
  265. container = element.ownerDocument.querySelector(container);
  266. }
  267. if (!container) {
  268. container = this.body;
  269. }
  270. container.appendChild(viewer);
  271. }
  272. if (options.inline) {
  273. this.render();
  274. this.bind();
  275. this.isShown = true;
  276. }
  277. this.ready = true;
  278. if (isFunction(options.ready)) {
  279. addListener(element, EVENT_READY, options.ready, {
  280. once: true,
  281. });
  282. }
  283. if (dispatchEvent(element, EVENT_READY) === false) {
  284. this.ready = false;
  285. return;
  286. }
  287. if (this.ready && options.inline) {
  288. this.view(this.index);
  289. }
  290. }
  291. /**
  292. * Get the no conflict viewer class.
  293. * @returns {Viewer} The viewer class.
  294. */
  295. static noConflict() {
  296. window.Viewer = AnotherViewer;
  297. return Viewer;
  298. }
  299. /**
  300. * Change the default options.
  301. * @param {Object} options - The new default options.
  302. */
  303. static setDefaults(options) {
  304. assign(DEFAULTS, isPlainObject(options) && options);
  305. }
  306. }
  307. assign(Viewer.prototype, render, events, handlers, methods, others);
  308. export default Viewer;