finderjs.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. pathBar: true,
  17. className: {
  18. container: 'fjs-container',
  19. pathBar: 'fjs-path-bar',
  20. col: 'fjs-col',
  21. list: 'fjs-list',
  22. item: 'fjs-item',
  23. active: 'fjs-active',
  24. children: 'fjs-has-children',
  25. url: 'fjs-url',
  26. itemPrepend: 'fjs-item-prepend',
  27. itemContent: 'fjs-item-content',
  28. itemAppend: 'fjs-item-append'
  29. }
  30. };
  31. class Finder {
  32. constructor(container, data, options) {
  33. this.$emitter = new EventEmitter();
  34. this.container = $(container);
  35. this.data = data;
  36. this.config = $.extend({}, DEFAULTS, options);
  37. // dom events
  38. this.container.on('click', this.clickEvent.bind(this));
  39. this.container.on('keydown', this.keydownEvent.bind(this));
  40. // internal events
  41. this.$emitter.on('item-selected', this.itemSelected.bind(this));
  42. this.$emitter.on('create-column', this.addColumn.bind(this));
  43. this.$emitter.on('navigate', this.navigate.bind(this));
  44. this.$emitter.on('go-to', this.goTo.bind(this, this.data));
  45. this.container.addClass(this.config.className.container).attr('tabindex', 0);
  46. this.createColumn(this.data);
  47. if (this.config.pathBar) {
  48. this.pathBar = this.createPathBar();
  49. this.pathBar.on('click', '[data-breadcrumb-node]', (event) => {
  50. event.preventDefault();
  51. const location = $(event.currentTarget).data('breadcrumbNode');
  52. this.goTo(this.data, location);
  53. });
  54. }
  55. // '' is <Root>
  56. if (this.config.defaultPath || this.config.defaultPath === '') {
  57. this.goTo(this.data, this.config.defaultPath);
  58. }
  59. }
  60. reload(data = this.data) {
  61. this.createColumn(data);
  62. // '' is <Root>
  63. if (this.config.defaultPath || this.config.defaultPath === '') {
  64. this.goTo(data, this.config.defaultPath);
  65. }
  66. }
  67. createColumn(data, parent) {
  68. const callback = (data) => this.createColumn(data, parent);
  69. if (typeof data === 'function') {
  70. data.call(this, parent, callback);
  71. } else if (Array.isArray(data) || typeof data === 'object') {
  72. if (typeof data === 'object') {
  73. data = Array.from(data);
  74. }
  75. const list = this.createList(data);
  76. const div = $('<div />');
  77. div.append(list).addClass(this.config.className.col);
  78. this.$emitter.emit('create-column', div);
  79. return div;
  80. } else {
  81. throw new Error('Unknown data type');
  82. }
  83. }
  84. createPathBar() {
  85. this.container.siblings(`.${this.config.className.pathBar}`).remove();
  86. const pathBar = $(`<div class="${this.config.className.pathBar}" />`);
  87. pathBar.insertAfter(this.container);
  88. return pathBar;
  89. }
  90. clickEvent(event) {
  91. event.stopPropagation();
  92. event.preventDefault();
  93. const target = $(event.target);
  94. const column = target.closest(`.${this.config.className.col}`);
  95. const item = target.closest(`.${this.config.className.item}`);
  96. if (item.length) {
  97. this.$emitter.emit('item-selected', { column, item });
  98. }
  99. }
  100. keydownEvent(event) {
  101. const codes = { 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
  102. if (event.keyCode in codes) {
  103. event.stopPropagation();
  104. event.preventDefault();
  105. this.$emitter.emit('navigate', {
  106. direction: codes[event.keyCode]
  107. });
  108. }
  109. }
  110. itemSelected(value) {
  111. const element = value.item;
  112. if (!element.length) { return false; }
  113. const item = element[0]._item;
  114. const column = value.column;
  115. const data = item[this.config.childKey] || this.data;
  116. const active = $(column).find(`.${this.config.className.active}`);
  117. if (active.length) {
  118. active.removeClass(this.config.className.active);
  119. }
  120. element.addClass(this.config.className.active);
  121. column.nextAll().remove(); // ?!?!?
  122. this.container[0].focus();
  123. window.scrollTo(window.pageXOffset, window.pageYOffset);
  124. this.updatePathBar();
  125. let newColumn;
  126. if (data) {
  127. newColumn = this.createColumn(data, item);
  128. this.$emitter.emit('interior-selected', item);
  129. } else {
  130. this.$emitter.emit('leaf-selected', item);
  131. }
  132. return newColumn;
  133. }
  134. addColumn(column) {
  135. this.container.append(column);
  136. this.$emitter.emit('column-created', column);
  137. }
  138. navigate(value) {
  139. const active = this.findLastActive();
  140. const direction = value.direction;
  141. let column;
  142. let item;
  143. let target;
  144. if (active) {
  145. item = active.item;
  146. column = active.column;
  147. if (direction === 'up' && item.prev().length) {
  148. target = item.prev();
  149. } else if (direction === 'down' && item.next().length) {
  150. target = item.next();
  151. } else if (direction === 'right' && column.next().length) {
  152. column = column.next();
  153. target = column.find(`.${this.config.className.item}`).first();
  154. } else if (direction === 'left' && column.prev().length) {
  155. column = column.prev();
  156. target = column.find(`.${this.config.className.active}`).first() || column.find(`.${this.config.className.item}`);
  157. }
  158. } else {
  159. column = this.container.find(`.${this.config.className.col}`).first();
  160. target = column.find(`.${this.config.className.item}`).first();
  161. }
  162. if (target) {
  163. this.$emitter.emit('item-selected', {
  164. column,
  165. item: target
  166. });
  167. }
  168. }
  169. goTo(data, path) {
  170. path = Array.isArray(path) ? path : path.split('/').map(bit => bit.trim()).filter(Boolean);
  171. if (path.length) {
  172. this.container.children().remove();
  173. }
  174. if (typeof data === 'function') {
  175. data.call(this, null, (data) => this.selectPath(path, data));
  176. } else {
  177. this.selectPath(path, data);
  178. }
  179. }
  180. selectPath(path, data, column) {
  181. column = column || (path.length ? this.createColumn(data) : this.container.find(`> .${this.config.className.col}`));
  182. const current = path[0] || '';
  183. const children = data.find((item) => item[this.config.itemKey] === current);
  184. const newColumn = this.itemSelected({
  185. column,
  186. item: column.find(`[data-fjs-item="${current}"]`).first()
  187. });
  188. path.shift();
  189. if (path.length && children) {
  190. this.selectPath(path, children[this.config.childKey], newColumn);
  191. }
  192. }
  193. findLastActive() {
  194. const active = this.container.find(`.${this.config.className.active}`);
  195. if (!active.length) {
  196. return null;
  197. }
  198. const item = active.last();
  199. const column = item.closest(`.${this.config.className.col}`);
  200. return { item, column };
  201. }
  202. createList(data) {
  203. const list = $('<ul />');
  204. const items = data.map((item) => this.createItem(item));
  205. const fragments = items.reduce((fragment, current) => {
  206. fragment.appendChild(current[0] || current);
  207. return fragment;
  208. }, document.createDocumentFragment());
  209. list.append(fragments).addClass(this.config.className.list);
  210. return list;
  211. }
  212. createItem(item) {
  213. const listItem = $('<li />');
  214. const listItemClasses = [this.config.className.item];
  215. const link = $('<a />');
  216. const createItemContent = this.config.createItemContent || this.createItemContent;
  217. const fragment = createItemContent.call(this, item);
  218. link.append(fragment)
  219. .attr('href', '')
  220. .attr('tabindex', -1);
  221. if (item.url) {
  222. link.attr('href', item.url);
  223. listItemClasses.push(item.className);
  224. }
  225. if (item[this.config.childKey]) {
  226. listItemClasses.push(this.config.className[this.config.childKey]);
  227. }
  228. listItemClasses.push(`fjs-item-${item.type}`);
  229. listItem.addClass(listItemClasses.join(' '));
  230. listItem.append(link)
  231. .attr('data-fjs-item', item[this.config.itemKey]);
  232. listItem[0]._item = item;
  233. return listItem;
  234. }
  235. updatePathBar() {
  236. if (!this.config.pathBar) { return false; }
  237. const activeItems = this.container.find(`.${this.config.className.active}`);
  238. let itemKeys = '';
  239. this.pathBar.children().empty();
  240. activeItems.each((index, activeItem) => {
  241. const item = activeItem._item;
  242. const isLast = (index + 1) === activeItems.length;
  243. itemKeys += `/${item[this.config.itemKey]}`;
  244. this.pathBar.append(`
  245. <span class="breadcrumb-node breadcrumb-node-${item.type}" ${item.type === 'dir' ? `data-breadcrumb-node="${itemKeys}"` : ''}>
  246. <i class="fa fa-fw ${this.getIcon(item.type)}"></i>
  247. <span class="breadcrumb-node-name">${$('<div />').html(item[this.config.labelKey]).html()}</span>
  248. ${!isLast ? '<i class="fa fa-fw fa-chevron-right"></i>' : ''}
  249. </span>
  250. `);
  251. });
  252. }
  253. getIcon(type) {
  254. switch (type) {
  255. case 'root':
  256. return 'fa-sitemap';
  257. case 'file':
  258. return 'fa-file-o';
  259. case 'dir':
  260. default:
  261. return 'fa-folder';
  262. }
  263. }
  264. }
  265. export default Finder;