4chan Image Viewer

Opens current thread Images in 4chan into a popup viewer, tested in Tampermonkey

2015-11-29 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name 4chan Image Viewer
  3. // @namespace IdontKnowWhatToDoWithThis
  4. // @description Opens current thread Images in 4chan into a popup viewer, tested in Tampermonkey
  5. // @match *://*.4chan.org/*/res/*
  6. // @match *://*.4chan.org/*/thread/*
  7. // @version 8.02
  8. // @copyright 2015+, Nicholas Perkins
  9. // @source https://github.com/nicholas-s-perkins/4chanImageViewer
  10. // ==/UserScript==
  11. "use strict";
  12. var Viewer;
  13. (function (Viewer) {
  14. /**
  15. * Didn't want to use any external libraries. This is my handy library for dealing with the DOM
  16. */
  17. var DomUtil = (function () {
  18. function DomUtil(obj) {
  19. this._elements = [];
  20. this._listeners = [];
  21. if (obj) {
  22. if (obj instanceof NodeList) {
  23. for (var i = 0; i < obj.length; ++i) {
  24. this._elements.push(obj[i]);
  25. }
  26. }
  27. else {
  28. this._elements.push(obj);
  29. }
  30. }
  31. }
  32. Object.defineProperty(DomUtil.prototype, "elementList", {
  33. get: function () {
  34. return this._elements;
  35. },
  36. enumerable: true,
  37. configurable: true
  38. });
  39. DomUtil.prototype.concat = function (collection) {
  40. if (collection instanceof DomUtil) {
  41. this._elements = this._elements.concat(collection._elements);
  42. }
  43. else {
  44. this._elements = this._elements.concat(DomUtil.formatNodeList(collection));
  45. }
  46. return this;
  47. };
  48. /** Adds a click handler */
  49. DomUtil.prototype.on = function (handler, func) {
  50. var _this = this;
  51. var handlers = handler.split(' ');
  52. this.each(function (element) {
  53. for (var _i = 0; _i < handlers.length; _i++) {
  54. var handler_1 = handlers[_i];
  55. _this._listeners.push(new Listener(element, handler_1, func));
  56. element.addEventListener(handler_1, func, false);
  57. }
  58. });
  59. return this;
  60. };
  61. DomUtil.prototype.appendTo = function (obj) {
  62. if (typeof obj === 'string') {
  63. DomUtil.get(obj).append(this);
  64. }
  65. else if (obj instanceof DomUtil) {
  66. obj.append(this);
  67. }
  68. else {
  69. new DomUtil(obj).append(this);
  70. }
  71. return this;
  72. };
  73. DomUtil.prototype.off = function (handlerType) {
  74. var remaining = [];
  75. for (var _i = 0, _a = this._listeners; _i < _a.length; _i++) {
  76. var listener = _a[_i];
  77. if (handlerType == null || listener.type === handlerType) {
  78. listener.element.removeEventListener(listener.type, listener.func);
  79. }
  80. else {
  81. remaining.push(listener);
  82. }
  83. }
  84. this._listeners = remaining;
  85. return this;
  86. };
  87. DomUtil.prototype.remove = function () {
  88. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  89. var element = _a[_i];
  90. if (element.parentElement) {
  91. element.parentElement.removeChild(element);
  92. }
  93. }
  94. return this;
  95. };
  96. DomUtil.prototype.prepend = function (obj) {
  97. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  98. var thisElement = _a[_i];
  99. for (var _b = 0, _c = obj._elements; _b < _c.length; _b++) {
  100. var objElement = _c[_b];
  101. if (thisElement.parentElement) {
  102. thisElement.parentElement.insertBefore(objElement, thisElement);
  103. }
  104. }
  105. }
  106. return this;
  107. };
  108. DomUtil.prototype.append = function (obj) {
  109. if (typeof obj === 'string') {
  110. this.each(function (element) {
  111. element.insertAdjacentHTML('beforeend', obj);
  112. });
  113. }
  114. else if (obj instanceof DomUtil) {
  115. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  116. var element = _a[_i];
  117. for (var _b = 0, _c = obj._elements; _b < _c.length; _b++) {
  118. var objEle = _c[_b];
  119. element.appendChild(objEle);
  120. }
  121. }
  122. }
  123. else {
  124. for (var _d = 0, _e = this._elements; _d < _e.length; _d++) {
  125. var element = _e[_d];
  126. element.appendChild(obj);
  127. }
  128. }
  129. return this;
  130. };
  131. DomUtil.prototype.empty = function () {
  132. this.each(function (element) {
  133. while (element.firstChild) {
  134. element.removeChild(element.firstChild);
  135. }
  136. });
  137. return this;
  138. };
  139. DomUtil.prototype.scrollToTop = function () {
  140. if (this._elements.length > 0) {
  141. this._elements[0].scrollTop = 0;
  142. }
  143. return this;
  144. };
  145. DomUtil.prototype.focus = function () {
  146. if (this._elements.length > 0) {
  147. this._elements[0].focus();
  148. }
  149. return this;
  150. };
  151. Object.defineProperty(DomUtil.prototype, "tabIndex", {
  152. set: function (index) {
  153. if (this._elements.length > 0) {
  154. this._elements[0].tabIndex = index;
  155. }
  156. },
  157. enumerable: true,
  158. configurable: true
  159. });
  160. DomUtil.prototype.setAttr = function (attr, value) {
  161. this.each(function (element) {
  162. element[attr] = value;
  163. });
  164. return this;
  165. };
  166. DomUtil.prototype.setStyle = function (styleConfig) {
  167. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  168. var element = _a[_i];
  169. for (var propName in styleConfig) {
  170. element.style[propName] = styleConfig[propName];
  171. }
  172. }
  173. return this;
  174. };
  175. DomUtil.prototype.setData = function (data) {
  176. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  177. var element = _a[_i];
  178. for (var propName in data) {
  179. element.dataset[propName] = data[propName];
  180. }
  181. }
  182. return this;
  183. };
  184. DomUtil.prototype.replaceWith = function (replacement) {
  185. var replaceEle = replacement._elements;
  186. this.each(function (element) {
  187. if (element.parentElement) {
  188. for (var i = replaceEle.length - 1; i >= 0; i--) {
  189. element.parentElement.insertBefore(replaceEle[i], element);
  190. }
  191. element.parentElement.removeChild(element);
  192. }
  193. });
  194. return this;
  195. };
  196. DomUtil.prototype.html = function (html) {
  197. if (typeof html === 'string') {
  198. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  199. var element = _a[_i];
  200. element.innerHTML = html;
  201. }
  202. }
  203. else {
  204. this.each(function (element) {
  205. DomUtil.get(element).remove();
  206. });
  207. this.append(html);
  208. }
  209. return this;
  210. };
  211. Object.defineProperty(DomUtil.prototype, "length", {
  212. get: function () {
  213. return this._elements.length;
  214. },
  215. enumerable: true,
  216. configurable: true
  217. });
  218. Object.defineProperty(DomUtil.prototype, "id", {
  219. get: function () {
  220. return this._elements.length > 0 ? this._elements[0].id : null;
  221. },
  222. enumerable: true,
  223. configurable: true
  224. });
  225. Object.defineProperty(DomUtil.prototype, "clientHeight", {
  226. get: function () {
  227. return this._elements.length > 0 ? this._elements[0].clientHeight : 0;
  228. },
  229. enumerable: true,
  230. configurable: true
  231. });
  232. Object.defineProperty(DomUtil.prototype, "clientWidth", {
  233. get: function () {
  234. return this._elements.length > 0 ? this._elements[0].clientWidth : 0;
  235. },
  236. enumerable: true,
  237. configurable: true
  238. });
  239. Object.defineProperty(DomUtil.prototype, "offsetHeight", {
  240. get: function () {
  241. return this._elements.length > 0 ? this._elements[0].offsetHeight : 0;
  242. },
  243. enumerable: true,
  244. configurable: true
  245. });
  246. Object.defineProperty(DomUtil.prototype, "offsetWidth", {
  247. get: function () {
  248. return this._elements.length > 0 ? this._elements[0].offsetWidth : 0;
  249. },
  250. enumerable: true,
  251. configurable: true
  252. });
  253. Object.defineProperty(DomUtil.prototype, "tagName", {
  254. get: function () {
  255. return this._elements.length > 0 ? this._elements[0].tagName : null;
  256. },
  257. enumerable: true,
  258. configurable: true
  259. });
  260. DomUtil.prototype.hasClass = function (className) {
  261. return this._elements.length > 0 ? this._elements[0].classList.contains(className) : false;
  262. };
  263. DomUtil.prototype.getAttr = function (attr) {
  264. if (this._elements.length > 0) {
  265. var ele = this._elements[0];
  266. return ele[attr];
  267. }
  268. else {
  269. return null;
  270. }
  271. };
  272. DomUtil.prototype.lightClone = function () {
  273. var newCollection = new DomUtil();
  274. this.each(function (element) {
  275. var newEle = document.createElement(element.tagName);
  276. newEle.className = element.className;
  277. newEle.innerHTML = element.innerHTML;
  278. newCollection._elements.push(newEle);
  279. });
  280. return newCollection;
  281. };
  282. DomUtil.prototype.addClass = function () {
  283. var classNames = [];
  284. for (var _i = 0; _i < arguments.length; _i++) {
  285. classNames[_i - 0] = arguments[_i];
  286. }
  287. this.each(function (element) {
  288. element.classList.add.apply(element.classList, classNames);
  289. });
  290. return this;
  291. };
  292. DomUtil.prototype.removeClass = function () {
  293. var classNames = [];
  294. for (var _i = 0; _i < arguments.length; _i++) {
  295. classNames[_i - 0] = arguments[_i];
  296. }
  297. this.each(function (element) {
  298. element.classList.remove.apply(element.classList, classNames);
  299. });
  300. return this;
  301. };
  302. DomUtil.prototype.each = function (func) {
  303. for (var i = 0; i < this._elements.length; ++i) {
  304. func(this._elements[i], i);
  305. }
  306. return this;
  307. };
  308. /** Finds all sub-elements matching the queryString */
  309. DomUtil.prototype.find = function (queryString) {
  310. var collection = new DomUtil();
  311. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  312. var element = _a[_i];
  313. collection.concat(element.querySelectorAll(queryString));
  314. }
  315. return collection;
  316. };
  317. Object.defineProperty(DomUtil.prototype, "exists", {
  318. get: function () {
  319. return this._elements.length > 0;
  320. },
  321. enumerable: true,
  322. configurable: true
  323. });
  324. /** because screw node lists */
  325. DomUtil.formatNodeList = function (nodes) {
  326. var arr = [];
  327. for (var i = 0; i < nodes.length; ++i) {
  328. arr.push(nodes[i]);
  329. }
  330. return arr;
  331. };
  332. DomUtil.get = function (query) {
  333. if (typeof query === 'string') {
  334. switch (query) {
  335. case 'body':
  336. return new DomUtil(document.body);
  337. case 'head':
  338. return new DomUtil(document.head);
  339. default:
  340. var nodes = document.querySelectorAll(query);
  341. return new DomUtil(nodes);
  342. }
  343. }
  344. else {
  345. return new DomUtil(query);
  346. }
  347. };
  348. DomUtil.getById = function (id) {
  349. var ele = document.getElementById(id);
  350. return new DomUtil(ele);
  351. };
  352. DomUtil.createElement = function (tagName, props) {
  353. var ele = document.createElement(tagName);
  354. if (props) {
  355. for (var propName in props) {
  356. if (props.hasOwnProperty(propName)) {
  357. if (propName === 'style') {
  358. for (var styleName in props[propName]) {
  359. if (props.style.hasOwnProperty(styleName)) {
  360. ele.style[styleName] = props.style[styleName];
  361. }
  362. }
  363. }
  364. else {
  365. ele[propName] = props[propName];
  366. }
  367. }
  368. }
  369. }
  370. return new DomUtil(ele);
  371. };
  372. return DomUtil;
  373. })();
  374. Viewer.DomUtil = DomUtil;
  375. var Listener = (function () {
  376. function Listener(element, type, func) {
  377. this.type = type;
  378. this.func = func;
  379. this.element = element;
  380. }
  381. return Listener;
  382. })();
  383. Viewer.Listener = Listener;
  384. })(Viewer || (Viewer = {}));
  385. var Viewer;
  386. (function (Viewer) {
  387. //IDs for important elements
  388. Viewer.VIEW_ID = "mainView";
  389. Viewer.IMG_ID = "mainImg";
  390. Viewer.CENTER_BOX_ID = "imageBox";
  391. Viewer.TOP_LAYER_ID = "viewerTopLayer";
  392. Viewer.IMG_WRAPPER_ID = 'mainImgWrapper';
  393. Viewer.TEXT_WRAPPER_ID = 'viewerTextWrapper';
  394. Viewer.STYLE_ID = 'viewerStyle';
  395. Viewer.MENU_ID = 'viewerBottomMenu';
  396. Viewer.LEFT_ARROW = 'previousImageButton';
  397. Viewer.RIGHT_ARROW = 'nextImageButton';
  398. Viewer.STYLE_TEXT = "\n div.reply.highlight,div.reply.highlight-anti{z-index:100 !important;position:fixed !important; top:1%;left:1%;}\n body{overflow:hidden !important;}\n #quote-preview{z-index:100;}\n a.quotelink, div.viewerBacklinks a.quotelink{color:#5c5cff !important;}\n a.quotelink:hover, div.viewerBacklinks a:hover{color:red !important;}\n #" + Viewer.IMG_ID + "{display:block !important; margin:auto;max-width:100%;height:auto;-webkit-user-select: none;cursor:pointer;}\n #" + Viewer.VIEW_ID + "{\n background-color:rgba(0,0,0,0.9);\n z-index:10;\n position:fixed;\n top:0;left:0;bottom:0;right:0;\n overflow:auto;\n text-align:center;\n -webkit-user-select: none;\n }\n #" + Viewer.CENTER_BOX_ID + " {display:flex;align-items:center;justify-content:center;flex-direction: column;min-height:100%;}\n #" + Viewer.IMG_WRAPPER_ID + " {width:100%;}\n #" + Viewer.TOP_LAYER_ID + "{position:fixed;top:0;bottom:0;left:0;right:0;z-index:20;opacity:0;visibility:hidden;transition:all .25s ease;}\n .viewerBlockQuote{color:white;}\n #" + Viewer.TEXT_WRAPPER_ID + "{max-width:60em;display:inline-block; color:gray;-webkit-user-select: all;}\n .bottomMenuShow{visibility:visible;}\n #" + Viewer.MENU_ID + "{box-shadow: -1px -1px 5px #888888;font-size:20px;padding:5px;background-color:white;position:fixed;bottom:0;right:0;z-index:200;}\n .hideCursor{cursor:none !important;}\n .hidden{visibility:hidden}\n .displayNone{display:none;}\n .pagingButtons{font-size:100px;color:white;text-shadow: 1px 1px 10px #27E3EB;z-index: 11;top: 50%;position: fixed;margin-top: -57px;width:100px;cursor:pointer;-webkit-user-select: none;}\n .pagingButtons:hover{color:#27E3EB;text-shadow: 1px 1px 10px #000}\n #" + Viewer.LEFT_ARROW + "{left:0;text-align:left;}\n #" + Viewer.RIGHT_ARROW + "{right:0;text-align:right;}\n @-webkit-keyframes flashAnimation{0%{ text-shadow: none;}100%{text-shadow: 0px 0px 5px blue;}}\n .flash{-webkit-animation: flashAnimation 1s alternate infinite linear;cursor:pointer;}\n .disableClick, .disableClick a{pointer-events: none;}\n ";
  399. })(Viewer || (Viewer = {}));
  400. var Viewer;
  401. (function (Viewer) {
  402. //cookieInfo
  403. var INDEX_KEY = "imageBrowserIndexCookie";
  404. var THREAD_KEY = "imageBrowserThreadCookie";
  405. var WIDTH_KEY = "imageBrowserWidthCookie";
  406. var HEIGHT_KEY = "imageBrowserHeightCookie";
  407. //keycode object. Better than remembering what each code does.
  408. var KEYS = { 38: 'up', 40: 'down', 37: 'left', 39: 'right', 27: 'esc', 86: 'v' };
  409. var BODY = Viewer.DomUtil.get(document.body);
  410. var WINDOW = Viewer.DomUtil.get(window);
  411. var UNSAFE_WINDOW = Viewer.DomUtil.get(typeof unsafeWindow === 'undefined' ? window : unsafeWindow);
  412. var MainView = (function () {
  413. function MainView(imagePostIndex) {
  414. var _this = this;
  415. this.postData = [];
  416. this.linkIndex = 0;
  417. //elements to keep track of
  418. this.mainView = null;
  419. this.mainImg = null;
  420. this.centerBox = null;
  421. this.topLayer = null;
  422. this.customStyle = null;
  423. this.textWrapper = null;
  424. this.leftArrow = null;
  425. this.rightArrow = null;
  426. this.bottomMenu = null;
  427. /** Determines if pre-loading can happen*/
  428. this.canPreload = false;
  429. /** determines if height of the image should be fit */
  430. this.shouldFitHeight = false;
  431. /** Keeps track of the process watching the mouse */
  432. this.mouseTimer = null;
  433. this.lastMousePos = { x: 0, y: 0 };
  434. console.log("Building 4chan Image Viewer");
  435. var currentThreadId = Viewer.DomUtil.get('.thread').id;
  436. if (imagePostIndex != undefined) {
  437. this.linkIndex = imagePostIndex;
  438. MainView.setPersistentValue(INDEX_KEY, imagePostIndex);
  439. }
  440. else if (MainView.getPersistentValue(THREAD_KEY) === currentThreadId) {
  441. this.linkIndex = parseInt(MainView.getPersistentValue(INDEX_KEY));
  442. }
  443. else {
  444. this.linkIndex = 0;
  445. MainView.setPersistentValue(INDEX_KEY, 0);
  446. }
  447. //set thread id
  448. MainView.setPersistentValue(THREAD_KEY, currentThreadId);
  449. //Create postData based on 4chan posts
  450. this.postData = Viewer.PostData.getImagePosts(true);
  451. if (this.linkIndex > (this.postData.length - 1)) {
  452. alert('Last saved image index is too large, a thread may have been deleted. Index will be reset. ');
  453. this.linkIndex = 0;
  454. MainView.setPersistentValue(INDEX_KEY, 0);
  455. }
  456. //set shouldFit Height so image can know about it if it loads before menuInit()
  457. var isHeight = MainView.getPersistentValue(HEIGHT_KEY);
  458. this.shouldFitHeight = isHeight ? true : false;
  459. var menuHtml = "\n <label><input id=\"" + WIDTH_KEY + "\" type=\"checkbox\" checked=\"checked\" />Fit Image to Width</label>\n <span>|</span>\n <label><input id=\"" + HEIGHT_KEY + "\" type=\"checkbox\" />Fit Image to Height</label>\n ";
  460. var viewFrag = "\n <style id=\"" + Viewer.STYLE_ID + "\">" + Viewer.STYLE_TEXT + "</style>\n <div id=\"" + Viewer.VIEW_ID + "\">\n <div id=\"" + Viewer.CENTER_BOX_ID + "\">\n <div id=\"" + Viewer.IMG_WRAPPER_ID + "\">\n <img id=\"" + Viewer.IMG_ID + "\" class=\"hideCursor\"/>\n </div>\n <div id=\"" + Viewer.TEXT_WRAPPER_ID + "\"></div>\n </div>\n <div id=\"" + Viewer.LEFT_ARROW + "\" class=\"pagingButtons hidden\"><span>&#9001;</span></div>\n <div id=\"" + Viewer.RIGHT_ARROW + "\" class=\"pagingButtons hidden\"><span>&#9002;</span></div>\n </div>\n <div id=\"" + Viewer.TOP_LAYER_ID + "\">&nbsp;</div>\n <form id=\"" + Viewer.MENU_ID + "\" class=\"hidden\">" + menuHtml + "</form>\n ";
  461. BODY.append(viewFrag);
  462. this.mainView = Viewer.DomUtil.getById(Viewer.VIEW_ID);
  463. this.centerBox = Viewer.DomUtil.getById(Viewer.CENTER_BOX_ID);
  464. this.mainImg = Viewer.DomUtil.getById(Viewer.IMG_ID);
  465. this.textWrapper = Viewer.DomUtil.getById(Viewer.TEXT_WRAPPER_ID);
  466. this.topLayer = Viewer.DomUtil.getById(Viewer.TOP_LAYER_ID);
  467. this.customStyle = Viewer.DomUtil.getById(Viewer.STYLE_ID);
  468. this.bottomMenu = Viewer.DomUtil.getById(Viewer.MENU_ID);
  469. this.leftArrow = Viewer.DomUtil.getById(Viewer.LEFT_ARROW);
  470. this.rightArrow = Viewer.DomUtil.getById(Viewer.RIGHT_ARROW);
  471. //add handlers
  472. this.centerBox.on('click', function () { _this.confirmExit(); });
  473. this.textWrapper.on('click', function (event) { _this.eventStopper(event); });
  474. this.bottomMenu.on('click', function () { _this.menuClickHandler(); });
  475. this.leftArrow.on('click', function (event) {
  476. event.stopImmediatePropagation();
  477. _this.previousImg();
  478. });
  479. this.rightArrow.on('click', function (event) {
  480. event.stopImmediatePropagation();
  481. _this.nextImg();
  482. });
  483. //build first image/video tag
  484. this.changeData(0);
  485. //initialize menu
  486. this.menuInit();
  487. //start preloading to next image index
  488. this.canPreload = true;
  489. window.setTimeout(function () {
  490. _this.runImagePreloading(_this.linkIndex);
  491. }, 100);
  492. //some fixes for weird browser behaviors
  493. this.centerBox.setStyle({ outline: '0' });
  494. this.centerBox.tabIndex = 1;
  495. this.centerBox.focus();
  496. //add keybinding listener, unsafeWindow is used here instead because at least in Tampermonkey
  497. //the safe window can fail to remove event listeners.
  498. UNSAFE_WINDOW
  499. .on('keydown', function (event) {
  500. _this.arrowKeyListener(event);
  501. })
  502. .on('mousemove', function (event) {
  503. _this.menuWatcher(event);
  504. });
  505. }
  506. MainView.prototype.menuInit = function () {
  507. var _this = this;
  508. var menuControls = this.bottomMenu.find('input');
  509. menuControls.each(function (input) {
  510. var cookieValue = MainView.getPersistentValue(input.id);
  511. if (cookieValue === 'true') {
  512. input.checked = true;
  513. }
  514. else if (cookieValue === 'false') {
  515. input.checked = false;
  516. }
  517. input.parentElement.classList.toggle('flash', input.checked);
  518. switch (input.id) {
  519. case WIDTH_KEY:
  520. _this.setFitToScreenWidth(input.checked);
  521. break;
  522. case HEIGHT_KEY:
  523. _this.setFitToScreenHeight(input.checked);
  524. break;
  525. }
  526. });
  527. };
  528. MainView.prototype.menuClickHandler = function () {
  529. var _this = this;
  530. var menuControls = this.bottomMenu.find('input');
  531. menuControls.each(function (input) {
  532. switch (input.id) {
  533. case WIDTH_KEY:
  534. _this.setFitToScreenWidth(input.checked);
  535. break;
  536. case HEIGHT_KEY:
  537. _this.setFitToScreenHeight(input.checked);
  538. break;
  539. }
  540. input.parentElement.classList.toggle('flash', input.checked);
  541. MainView.setPersistentValue(input.id, input.checked);
  542. });
  543. };
  544. MainView.prototype.windowClick = function (event) {
  545. if (!this) {
  546. return;
  547. }
  548. event.preventDefault();
  549. event.stopImmediatePropagation();
  550. this.nextImg();
  551. };
  552. /* Event function for determining behavior of viewer keypresses */
  553. MainView.prototype.arrowKeyListener = function (event) {
  554. switch (KEYS[event.keyCode]) {
  555. case 'right':
  556. this.nextImg();
  557. break;
  558. case 'left':
  559. this.previousImg();
  560. break;
  561. case 'esc':
  562. this.destroy();
  563. break;
  564. }
  565. };
  566. /* preloads images starting with the index provided */
  567. MainView.prototype.runImagePreloading = function (index) {
  568. var _this = this;
  569. if (this && index < this.postData.length) {
  570. if (this.canPreload) {
  571. //console.log('preloading: ' + index +' of '+(this.postData.length - 1) +' | '+ this.postData[index].imgSrc);
  572. var loadFunc = function () { _this.runImagePreloading(index + 1); };
  573. //have yet to figure out how to properly preload video, skip for now
  574. if (this.postData[index].tagType === Viewer.TagType.VIDEO) {
  575. window.setTimeout(loadFunc, 1);
  576. }
  577. else {
  578. var newImage = document.createElement(this.postData[index].tagTypeName);
  579. switch (this.postData[index].tagType) {
  580. case Viewer.TagType.VIDEO:
  581. newImage.oncanplaythrough = loadFunc;
  582. break;
  583. case Viewer.TagType.IMG:
  584. newImage.onload = loadFunc;
  585. break;
  586. }
  587. newImage.onerror = function () {
  588. console.log("imageError");
  589. _this.runImagePreloading(index + 1);
  590. };
  591. newImage.src = this.postData[index].imgSrc;
  592. }
  593. }
  594. }
  595. };
  596. /* Sets the img and message to the next one in the list*/
  597. MainView.prototype.nextImg = function () {
  598. var _this = this;
  599. if (this.linkIndex === this.postData.length - 1) {
  600. this.topLayer.setStyle({
  601. background: 'linear-gradient(to right,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)',
  602. opacity: '.5',
  603. visibility: 'visible'
  604. });
  605. window.setTimeout(function () {
  606. _this.topLayer.setStyle({
  607. opacity: '0',
  608. visibility: 'hidden'
  609. });
  610. }, 500);
  611. }
  612. else {
  613. this.changeData(1);
  614. }
  615. };
  616. /* Sets the img and message to the previous one in the list*/
  617. MainView.prototype.previousImg = function () {
  618. var _this = this;
  619. if (this.linkIndex === 0) {
  620. this.topLayer.setStyle({
  621. background: 'linear-gradient(to left,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)',
  622. opacity: '.5',
  623. visibility: 'visible'
  624. });
  625. window.setTimeout(function () {
  626. _this.topLayer.setStyle({ opacity: '0' });
  627. window.setTimeout(function () {
  628. _this.topLayer.setStyle({ visibility: 'hidden' });
  629. }, 200);
  630. }, 500);
  631. }
  632. else {
  633. this.changeData(-1);
  634. }
  635. };
  636. MainView.prototype.changeData = function (delta) {
  637. MainView.cleanLinks();
  638. //ignore out of bounds
  639. var newIndex = this.linkIndex + delta;
  640. if (newIndex > this.postData.length - 1 || newIndex < 0) {
  641. return;
  642. }
  643. if (this.postData[newIndex].tagTypeName !== this.mainImg.tagName || delta === 0) {
  644. this.mainImg = this.replaceElement(this.mainImg, this.postData[newIndex].tagTypeName);
  645. }
  646. //console.log('Opening: "' + this.postData[this.linkIndex].imgSrc +'" at index ' + this.linkIndex);
  647. this.mainImg.setAttr('src', this.postData[newIndex].imgSrc);
  648. var nextLinks = this.postData[newIndex].linksContainer;
  649. var nextQuote = this.postData[newIndex].quoteContainer;
  650. this.textWrapper.empty();
  651. this.textWrapper.append(nextLinks);
  652. this.textWrapper.append(nextQuote);
  653. this.linkIndex = newIndex;
  654. this.mainView.scrollToTop();
  655. MainView.setPersistentValue(INDEX_KEY, this.linkIndex);
  656. };
  657. MainView.cleanLinks = function () {
  658. var links = document.getElementsByClassName('quotelink');
  659. for (var i = 0; i < links.length; ++i) {
  660. links[i].dispatchEvent(new MouseEvent('mouseout'));
  661. }
  662. };
  663. MainView.prototype.replaceElement = function (element, newTagType) {
  664. var _this = this;
  665. var rawElement = element.elementList[0];
  666. var newElement = Viewer.DomUtil.createElement(newTagType, {
  667. id: element.id,
  668. className: rawElement.className,
  669. style: rawElement.style,
  670. autoplay: true,
  671. controls: false,
  672. loop: true
  673. });
  674. newElement
  675. .on('click', function (event) {
  676. event.stopPropagation();
  677. _this.nextImg();
  678. })
  679. .on('load', function () {
  680. _this.imageLoadHandler();
  681. })
  682. .on('progress', function (e) {
  683. //console.log(e);
  684. });
  685. element.prepend(newElement);
  686. element.remove();
  687. return newElement;
  688. };
  689. MainView.prototype.eventStopper = function (event) {
  690. event.stopPropagation();
  691. if (event.target.nodeName === 'A') {
  692. var confirmed = this.confirmExit('Exit Viewer to navigate to link?');
  693. if (!confirmed) {
  694. event.preventDefault();
  695. }
  696. }
  697. };
  698. MainView.prototype.confirmExit = function (message) {
  699. var confirmed = window.confirm(message || 'Exit Viewer?');
  700. if (confirmed) {
  701. this.destroy();
  702. }
  703. return confirmed;
  704. };
  705. /* Removes the view and cleans up handlers*/
  706. MainView.prototype.destroy = function () {
  707. MainView.cleanLinks();
  708. UNSAFE_WINDOW.off();
  709. WINDOW.off();
  710. BODY.off();
  711. this.topLayer.remove();
  712. this.mainView.remove();
  713. this.customStyle.remove();
  714. this.bottomMenu.remove();
  715. BODY.setStyle({ overflow: 'auto' });
  716. this.canPreload = false;
  717. };
  718. /*Mouse-move Handler that watches for when menus should appear and mouse behavior*/
  719. MainView.prototype.menuWatcher = function (event) {
  720. var _this = this;
  721. var height_offset = window.innerHeight - this.bottomMenu.offsetHeight;
  722. var width_offset = window.innerWidth - this.bottomMenu.offsetWidth;
  723. var center = window.innerHeight / 2;
  724. var halfArrow = this.leftArrow.offsetHeight / 2;
  725. if (event.clientX >= width_offset && event.clientY >= height_offset) {
  726. this.bottomMenu.removeClass('hidden').addClass('bottomMenuShow');
  727. }
  728. else if (this.bottomMenu.hasClass('bottomMenuShow')) {
  729. this.bottomMenu.removeClass('bottomMenuShow').addClass('hidden');
  730. }
  731. if ((event.clientX <= (100) || event.clientX >= (window.innerWidth - 100)) &&
  732. (event.clientY <= (center + halfArrow) && event.clientY >= (center - halfArrow))) {
  733. this.rightArrow.removeClass('hidden');
  734. this.leftArrow.removeClass('hidden');
  735. }
  736. else {
  737. this.rightArrow.addClass('hidden');
  738. this.leftArrow.addClass('hidden');
  739. }
  740. //avoids chrome treating mouseclicks as mousemoves
  741. if (event.clientX !== this.lastMousePos.x && event.clientY !== this.lastMousePos.y) {
  742. //mouse click moves to next image when invisible
  743. this.mainImg.removeClass('hideCursor');
  744. window.clearTimeout(this.mouseTimer);
  745. BODY.off('click');
  746. BODY.removeClass('hideCursor');
  747. this.textWrapper.removeClass('disableClick');
  748. this.mainImg.removeClass('disableClick');
  749. this.centerBox.removeClass('disableClick');
  750. if (event.target.id === this.mainImg.id) {
  751. //hide cursor if it stops, show if it moves
  752. this.mouseTimer = window.setTimeout(function () {
  753. _this.mainImg.addClass('hideCursor');
  754. _this.textWrapper.addClass('disableClick');
  755. _this.mainImg.addClass('disableClick');
  756. _this.centerBox.addClass('disableClick');
  757. BODY.addClass('hideCursor')
  758. .on('click', function (event) {
  759. _this.windowClick(event);
  760. });
  761. }, 200);
  762. }
  763. }
  764. this.lastMousePos.x = event.clientX;
  765. this.lastMousePos.y = event.clientY;
  766. };
  767. /*Stores a key value pair as a cookie*/
  768. MainView.setPersistentValue = function (key, value) {
  769. document.cookie = key + '=' + value + ';expires=Thu, 01 Jan 3000 00:00:00 UTC;domain=.4chan.org;path=/';
  770. };
  771. /* Retrieves a cookie value via its key*/
  772. MainView.getPersistentValue = function (key) {
  773. var cookieMatch = document.cookie.match(new RegExp(key + '\\s*=\\s*([^;]+)'));
  774. if (cookieMatch) {
  775. return cookieMatch[1];
  776. }
  777. else {
  778. return null;
  779. }
  780. };
  781. MainView.prototype.setFitToScreenHeight = function (shouldFitImage) {
  782. this.shouldFitHeight = shouldFitImage;
  783. //ignore if image has no height as it is likely not loaded.
  784. if (shouldFitImage && this.mainImg.getAttr('naturalHeight')) {
  785. this.fitHeightToScreen();
  786. }
  787. else {
  788. this.mainImg.setStyle({ maxHeight: '' });
  789. }
  790. };
  791. ;
  792. MainView.prototype.setFitToScreenWidth = function (shouldFitImage) {
  793. this.mainImg.setStyle({
  794. maxWidth: shouldFitImage ? '100%' : 'none'
  795. });
  796. };
  797. MainView.prototype.imageLoadHandler = function () {
  798. if (this.shouldFitHeight) {
  799. this.fitHeightToScreen();
  800. }
  801. };
  802. /* Fits image to screen height*/
  803. MainView.prototype.fitHeightToScreen = function () {
  804. //sets the changeable properties to the image's real size
  805. var height = this.mainImg.getAttr('naturalHeight');
  806. this.mainImg.setStyle({ maxHeight: (height + 'px') });
  807. //actually tests if it is too high including padding
  808. var heightDiff = (this.mainImg.clientHeight > height) ?
  809. this.mainImg.clientHeight - this.mainView.clientHeight :
  810. height - this.mainView.clientHeight;
  811. if (heightDiff > 0) {
  812. this.mainImg.setStyle({ maxHeight: (height - heightDiff) + 'px' });
  813. }
  814. else {
  815. this.mainImg.setStyle({ maxHeight: (height + 'px') });
  816. }
  817. };
  818. return MainView;
  819. })();
  820. Viewer.MainView = MainView;
  821. })(Viewer || (Viewer = {}));
  822. var Viewer;
  823. (function (Viewer) {
  824. (function (TagType) {
  825. TagType[TagType["IMG"] = 0] = "IMG";
  826. TagType[TagType["VIDEO"] = 1] = "VIDEO";
  827. })(Viewer.TagType || (Viewer.TagType = {}));
  828. var TagType = Viewer.TagType;
  829. var PostData = (function () {
  830. function PostData(imgSrc, quoteContainer, linksContainer, imageLink) {
  831. this.imgSrc = imgSrc;
  832. this.linksContainer = linksContainer;
  833. this.quoteContainer = quoteContainer;
  834. this.tagType = PostData.getElementType(imgSrc);
  835. this.imageLink = imageLink;
  836. }
  837. Object.defineProperty(PostData.prototype, "tagTypeName", {
  838. get: function () {
  839. return TagType[this.tagType];
  840. },
  841. enumerable: true,
  842. configurable: true
  843. });
  844. PostData.getElementType = function (src) {
  845. if (src.match(/\.(?:(?:webm)|(?:ogg)|(?:mp4))$/)) {
  846. return TagType.VIDEO;
  847. }
  848. else {
  849. return TagType.IMG;
  850. }
  851. };
  852. PostData.add4chanListenersToLinks = function (linkCollection) {
  853. linkCollection.find('.quotelink')
  854. .on('mouseover', Main.onThreadMouseOver)
  855. .on('mouseout', Main.onThreadMouseOut);
  856. };
  857. PostData.getImagePosts = function (asCopy) {
  858. var postData = [];
  859. var postFiles = Viewer.DomUtil.get('#delform').find('.postContainer');
  860. postFiles.each(function (post) {
  861. var _post = Viewer.DomUtil.get(post);
  862. var currentLinkTag = _post.find('.file .fileThumb');
  863. var currentLink = currentLinkTag.getAttr('href');
  864. if (!currentLink) {
  865. return;
  866. }
  867. var currentPostBlock = _post.find('.postMessage');
  868. var currentPostBacklinks = _post.find('.backlink');
  869. var newPostBlock = currentPostBlock;
  870. var newBackLinks = currentPostBacklinks;
  871. if (asCopy) {
  872. if (currentPostBlock.exists) {
  873. newPostBlock = currentPostBlock.lightClone();
  874. newPostBlock.addClass('viewerBlockQuote');
  875. PostData.add4chanListenersToLinks(newPostBlock);
  876. }
  877. if (currentPostBacklinks.exists) {
  878. newBackLinks = currentPostBacklinks.lightClone();
  879. newBackLinks.addClass('viewerBacklinks');
  880. PostData.add4chanListenersToLinks(newBackLinks);
  881. }
  882. }
  883. postData.push(new PostData(currentLink, newPostBlock, newBackLinks, currentLinkTag));
  884. });
  885. return postData;
  886. };
  887. return PostData;
  888. })();
  889. Viewer.PostData = PostData;
  890. })(Viewer || (Viewer = {}));
  891. /// <reference path="../MetaData.ts"/>
  892. /// <reference path="DomUtil.ts"/>
  893. /// <reference path="Css.ts"/>
  894. /// <reference path="MainView.ts"/>
  895. /// <reference path="PostData.ts"/>
  896. var Viewer;
  897. (function (Viewer) {
  898. function main() {
  899. // ========= Build the main Button ========= //
  900. Viewer.DomUtil.createElement('button')
  901. .setStyle({ position: 'fixed', bottom: '0', right: '0', })
  902. .html("Open Viewer")
  903. .on('click', function () {
  904. new Viewer.MainView();
  905. })
  906. .appendTo(document.body);
  907. // ========= Build buttons for each image thumbnail ========= //
  908. var posts = Viewer.PostData.getImagePosts(false);
  909. var imagePostCount = 0;
  910. for (var _i = 0; _i < posts.length; _i++) {
  911. var post = posts[_i];
  912. Viewer.DomUtil.createElement('button')
  913. .setStyle({
  914. display: 'inline',
  915. float: 'left',
  916. clear: 'both',
  917. fontSize: '11px',
  918. cursor: 'pointer'
  919. })
  920. .setData({
  921. postIndex: imagePostCount
  922. })
  923. .html('Open Viewer')
  924. .on('click', function (e) {
  925. e.preventDefault();
  926. e.stopPropagation();
  927. //make the viewer and put it on the window so we can clean it up later
  928. new Viewer.MainView(parseInt(this.dataset.postIndex));
  929. })
  930. .appendTo(post.imageLink);
  931. ++imagePostCount;
  932. }
  933. }
  934. Viewer.main = main;
  935. })(Viewer || (Viewer = {}));
  936. //run the module
  937. Viewer.main();
  938. //# sourceMappingURL=viewer.js.map