Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 
 
 

394 satır
12 KiB

  1. /**
  2. * (c) Trilby Media, LLC
  3. * Author Djamil Legato
  4. *
  5. * Based on Mark Matyas's Finderjs
  6. * MIT License
  7. */
  8. import $ from 'jquery';
  9. import EventEmitter from 'eventemitter3';
  10. export const DEFAULTS = {
  11. labelKey: 'name',
  12. valueKey: 'value', // new
  13. childKey: 'children',
  14. iconKey: 'icon', // new
  15. itemKey: 'item-key', // new
  16. itemTrigger: null,
  17. pathBar: true,
  18. className: {
  19. container: 'fjs-container',
  20. pathBar: 'fjs-path-bar',
  21. col: 'fjs-col',
  22. list: 'fjs-list',
  23. item: 'fjs-item',
  24. active: 'fjs-active',
  25. children: 'fjs-has-children',
  26. url: 'fjs-url',
  27. itemPrepend: 'fjs-item-prepend',
  28. itemContent: 'fjs-item-content',
  29. itemAppend: 'fjs-item-append'
  30. }
  31. };
  32. class Finder {
  33. constructor(container, data, options) {
  34. this.$emitter = new EventEmitter();
  35. this.container = $(container);
  36. this.data = data;
  37. this.config = $.extend(true, {}, DEFAULTS, options);
  38. this.container.off('click.finder keydown.finder');
  39. // dom events
  40. this.container.on('click.finder', this.clickEvent.bind(this));
  41. this.container.on('keydown.finder', this.keydownEvent.bind(this));
  42. // internal events
  43. this.$emitter.on('item-selected', this.itemSelected.bind(this));
  44. this.$emitter.on('create-column', this.addColumn.bind(this));
  45. this.$emitter.on('navigate', this.navigate.bind(this));
  46. this.$emitter.on('go-to', this.goTo.bind(this, this.data));
  47. this.container.addClass(this.config.className.container).attr('tabindex', 0);
  48. this.createColumn(this.data);
  49. if (this.config.pathBar) {
  50. this.pathBar = this.createPathBar();
  51. this.pathBar.on('click.finder', '[data-breadcrumb-node]', (event) => {
  52. event.preventDefault();
  53. const location = $(event.currentTarget).data('breadcrumbNode');
  54. this.goTo(this.data, location);
  55. });
  56. }
  57. // '' is <Root>
  58. if (this.config.defaultPath || this.config.defaultPath === '') {
  59. this.goTo(this.data, this.config.defaultPath);
  60. }
  61. }
  62. reload(data = this.data) {
  63. this.createColumn(data);
  64. // '' is <Root>
  65. if (this.config.defaultPath || this.config.defaultPath === '') {
  66. this.goTo(data, this.config.defaultPath);
  67. }
  68. }
  69. createColumn(data, parent) {
  70. const callback = (data) => this.createColumn(data, parent);
  71. if (typeof data === 'function') {
  72. data.call(this, parent, callback);
  73. } else if (Array.isArray(data) || typeof data === 'object') {
  74. if (typeof data === 'object') {
  75. data = Array.from(data);
  76. }
  77. const list = this.config.createList || this.createList;
  78. const div = $('<div />');
  79. div.append(list.call(this, data)).addClass(this.config.className.col);
  80. this.$emitter.emit('create-column', div);
  81. return div;
  82. } else {
  83. throw new Error('Unknown data type');
  84. }
  85. }
  86. createPathBar() {
  87. this.container.siblings(`.${this.config.className.pathBar}`).remove();
  88. const pathBar = $(`<div class="${this.config.className.pathBar}" />`);
  89. pathBar.insertAfter(this.container);
  90. return pathBar;
  91. }
  92. clickEvent(event) {
  93. const target = $(event.target);
  94. const column = target.closest(`.${this.config.className.col}`);
  95. const item = target.closest(`.${this.config.className.item}`);
  96. const prevent = target.is('[data-flexpages-prevent]') ? target : target.closest('[data-flexpages-prevent]');
  97. if (prevent.data('flexpagesPrevent') === undefined) {
  98. return true;
  99. }
  100. if (this.config.itemTrigger) {
  101. if (target.is(this.config.itemTrigger) || target.closest(this.config.itemTrigger).length) {
  102. event.stopPropagation();
  103. event.preventDefault();
  104. this.$emitter.emit('item-selected', {column, item});
  105. }
  106. return true;
  107. }
  108. event.stopPropagation();
  109. event.preventDefault();
  110. if (item.length) {
  111. this.$emitter.emit('item-selected', { column, item });
  112. }
  113. }
  114. keydownEvent(event) {
  115. const codes = { 37: 'left', 38: 'up', 39: 'right', 40: 'down', 13: 'enter' };
  116. if (event.keyCode in codes) {
  117. event.stopPropagation();
  118. event.preventDefault();
  119. this.$emitter.emit('navigate', {
  120. direction: codes[event.keyCode]
  121. });
  122. }
  123. }
  124. itemSelected(value) {
  125. const element = value.item;
  126. if (!element.length) { return false; }
  127. const item = element[0]._item;
  128. const column = value.column;
  129. const data = item[this.config.childKey] || this.data; // TODO: this.data for constant refresh
  130. const active = $(column).find(`.${this.config.className.active}`);
  131. if (active.length) {
  132. active.removeClass(this.config.className.active);
  133. }
  134. element.addClass(this.config.className.active);
  135. column.nextAll().remove(); // ?!?!?
  136. this.container[0].focus();
  137. window.scrollTo(window.pageXOffset, window.pageYOffset);
  138. this.updatePathBar();
  139. let newColumn;
  140. if (data) {
  141. newColumn = this.createColumn(data, item);
  142. this.$emitter.emit('interior-selected', item);
  143. } else {
  144. this.$emitter.emit('leaf-selected', item);
  145. }
  146. return newColumn;
  147. }
  148. addColumn(column) {
  149. this.container.append(column);
  150. this.$emitter.emit('column-created', column);
  151. }
  152. navigate(value) {
  153. const active = this.findLastActive();
  154. const direction = value.direction;
  155. let column;
  156. let item;
  157. let target;
  158. if (active) {
  159. item = active.item;
  160. column = active.column;
  161. if (direction === 'up' && item.prev().length) {
  162. target = item.prev();
  163. } else if (direction === 'down' && item.next().length) {
  164. target = item.next();
  165. } else if (direction === 'right' && column.next().length) {
  166. column = column.next();
  167. target = column.find(`.${this.config.className.item}`).first();
  168. } else if (direction === 'left' && column.prev().length) {
  169. column = column.prev();
  170. target = column.find(`.${this.config.className.active}`).first() || column.find(`.${this.config.className.item}`);
  171. }
  172. } else {
  173. column = this.container.find(`.${this.config.className.col}`).first();
  174. target = column.find(`.${this.config.className.item}`).first();
  175. }
  176. if (active && direction === 'enter') {
  177. const href = active.item.find('a').prop('href');
  178. if (href) {
  179. window.location = href;
  180. }
  181. }
  182. if (target) {
  183. this.$emitter.emit('item-selected', {
  184. column,
  185. item: target
  186. });
  187. if (!this.isInView(target, column, true)) {
  188. this.scrollToView(target[0], column[0]);
  189. }
  190. }
  191. }
  192. goTo(data, path) {
  193. path = Array.isArray(path) ? path : path.split('/').map(bit => bit.trim()).filter(Boolean);
  194. if (path.length) {
  195. this.container.children().remove();
  196. }
  197. if (typeof data === 'function') {
  198. data.call(this, null, (data) => this.selectPath(path, data));
  199. } else {
  200. this.selectPath(path, data);
  201. }
  202. }
  203. selectPath(path, data, column) {
  204. column = column || (path.length ? this.createColumn(data) : this.container.find(`> .${this.config.className.col}`));
  205. const current = path[0] || '';
  206. const children = data.find((item) => item[this.config.itemKey] === current);
  207. const item = column.find(`[data-fjs-item="${current}"]`).first();
  208. const newColumn = this.itemSelected({
  209. column,
  210. item
  211. });
  212. if (!this.isInView(item, column, true)) {
  213. this.scrollToView(item[0], column[0]);
  214. }
  215. path.shift();
  216. if (path.length && children) {
  217. this.selectPath(path, children[this.config.childKey], newColumn);
  218. }
  219. }
  220. findLastActive() {
  221. const active = this.container.find(`.${this.config.className.active}`);
  222. if (!active.length) {
  223. return null;
  224. }
  225. const item = active.last();
  226. const column = item.closest(`.${this.config.className.col}`);
  227. return { item, column };
  228. }
  229. createList(data) {
  230. const list = $('<ul />');
  231. const createItem = this.config.createItem || this.createItem;
  232. const items = data.map((item) => createItem.call(this, item));
  233. const fragments = items.reduce((fragment, current) => {
  234. fragment.appendChild(current[0] || current);
  235. return fragment;
  236. }, document.createDocumentFragment());
  237. list.append(fragments).addClass(this.config.className.list);
  238. return list;
  239. }
  240. createItem(item) {
  241. const listItem = $('<li />');
  242. const listItemClasses = [this.config.className.item];
  243. const link = $(`<a href="${item.href || ''}" />`);
  244. const createItemContent = this.config.createItemContent || this.createItemContent;
  245. const fragment = createItemContent.call(this, item);
  246. link.append(fragment)
  247. .attr('href', '')
  248. .attr('tabindex', -1);
  249. if (item.url) {
  250. link.attr('href', item.url);
  251. listItemClasses.push(item.className);
  252. }
  253. if (item[this.config.childKey]) {
  254. listItemClasses.push(this.config.className[this.config.childKey]);
  255. }
  256. listItem.addClass(listItemClasses.join(' '));
  257. listItem.append(link)
  258. .attr('data-fjs-item', item[this.config.itemKey]);
  259. listItem[0]._item = item;
  260. return listItem;
  261. }
  262. updatePathBar() {
  263. if (!this.config.pathBar) { return false; }
  264. const activeItems = this.container.find(`.${this.config.className.active}`);
  265. let itemKeys = '';
  266. this.pathBar.empty();
  267. activeItems.each((index, activeItem) => {
  268. const item = activeItem._item;
  269. const isLast = (index + 1) === activeItems.length;
  270. itemKeys += `/${item[this.config.itemKey]}`;
  271. this.pathBar.append(`
  272. <span class="breadcrumb-node ${item.icon}" ${item.type === 'dir' || item.child_count > 0 ? `data-breadcrumb-node="${itemKeys}"` : ''}>
  273. <i class="${item.icon}"></i>
  274. <span class="breadcrumb-node-name">${$('<div />').html(item[this.config.labelKey]).html()}</span>
  275. ${!isLast ? '<i class="fa fa-fw fa-chevron-right"></i>' : ''}
  276. </span>
  277. `);
  278. });
  279. }
  280. getIcon(type) {
  281. switch (type) {
  282. case 'root':
  283. return 'fa-sitemap';
  284. case 'file':
  285. return 'fa-file-o';
  286. case 'dir':
  287. default:
  288. return 'fa-folder';
  289. }
  290. }
  291. isInView(element, container, partial) {
  292. if (!element.length || !container.length) {
  293. return true;
  294. }
  295. const containerHeight = container.height();
  296. const elementTop = $(element).offset().top - container.offset().top;
  297. const elementBottom = elementTop + $(element).height();
  298. const isTotal = (elementTop >= 0 && elementBottom <= containerHeight);
  299. const isPartial = ((elementTop < 0 && elementBottom > 0) || (elementTop > 0 && elementTop <= container.height())) && partial;
  300. return isTotal || isPartial;
  301. }
  302. scrollToView(element, container) {
  303. const top = parseInt(container.getBoundingClientRect().top, 10);
  304. const bot = parseInt(container.getBoundingClientRect().bottom, 10);
  305. const now_top = parseInt(element.getBoundingClientRect().top, 10);
  306. const now_bot = parseInt(element.getBoundingClientRect().bottom, 10);
  307. let scroll_by = 0;
  308. if (now_top < top) {
  309. scroll_by = -(top - now_top);
  310. } else if (now_bot > bot) {
  311. scroll_by = now_bot - bot;
  312. }
  313. if (scroll_by !== 0) {
  314. container.scrollTop += scroll_by;
  315. }
  316. }
  317. }
  318. export default Finder;