finder.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import $ from 'jquery';
  2. import Finder from '../utils/finder';
  3. import { getInitialRoute, getStore, setInitialRoute } from './index';
  4. // import getFilters from '../utils/get-filters';
  5. let XHRUUID = 0;
  6. const GRAV_CONFIG = typeof global.GravConfig !== 'undefined' ? global.GravConfig : global.GravAdmin.config;
  7. export const Instances = {};
  8. const isInViewport = (elem) => {
  9. const bounding = elem.getBoundingClientRect();
  10. const titlebar = document.querySelector('#titlebar');
  11. const offset = titlebar ? titlebar.getBoundingClientRect().height : 0;
  12. return (
  13. bounding.top >= offset &&
  14. bounding.left >= 0 &&
  15. bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  16. bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
  17. );
  18. };
  19. export class FlexPages {
  20. constructor(container, data) {
  21. this.container = $(container);
  22. this.data = data;
  23. const dataLoad = this.dataLoad;
  24. this.finder = new Finder(
  25. this.container,
  26. (parent, callback) => {
  27. return dataLoad.call(this, parent, callback);
  28. },
  29. {
  30. labelKey: 'title',
  31. defaultPath: getInitialRoute(),
  32. itemTrigger: '[data-flexpages-expand]',
  33. createItem: function(item) {
  34. return FlexPages.createItem(this.config, item, this);
  35. },
  36. createItemContent: function(item) {
  37. return FlexPages.createItemContent(this.config, item, this);
  38. }
  39. }
  40. );
  41. this.finder.$emitter.on('leaf-selected', (item) => {
  42. setInitialRoute({
  43. route: item.route.raw
  44. });
  45. });
  46. this.finder.$emitter.on('interior-selected', (item) => {
  47. setInitialRoute({
  48. route: item.route.raw
  49. });
  50. });
  51. /*
  52. this.finder.$emitter.on('leaf-selected', (item) => {
  53. console.log('selected', item);
  54. this.finder.emit('create-column', () => this.createSimpleColumn(item));
  55. });
  56. this.finder.$emitter.on('item-selected', (selected) => {
  57. console.log('selected', selected);
  58. // for future use only - create column-card creation for file with details like in macOS finder
  59. // this.finder.$emitter('create-column', () => this.createSimpleColumn(selected));
  60. }); */
  61. this.finder.$emitter.on('column-created', () => {
  62. this.container[0].scrollLeft = this.container[0].scrollWidth - this.container[0].clientWidth;
  63. });
  64. }
  65. static createItem(config, item, finder) {
  66. const listItem = $('<li />');
  67. const listItemClasses = [config.className.item];
  68. // const href = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/');
  69. const link = $('<div class="fjs-item-wrapper" />');
  70. const createItemContent = config.createItemContent || finder.createItemContent;
  71. const fragment = createItemContent.call(this, item);
  72. link.append(fragment)
  73. // .attr('href', href)
  74. .attr('tabindex', -1);
  75. if (item.url) {
  76. link.attr('href', item.url);
  77. listItemClasses.push(item.className);
  78. }
  79. if (item[config.childKey]) {
  80. listItemClasses.push(config.className[config.childKey]);
  81. }
  82. if (item.filters_hit) {
  83. listItemClasses.push('filters-hit');
  84. }
  85. listItem.addClass(listItemClasses.join(' '));
  86. listItem.append(link)
  87. .attr('data-fjs-item', item[config.itemKey]);
  88. listItem[0]._item = item;
  89. return listItem;
  90. }
  91. static createItemContent(config, item) {
  92. const frag = document.createDocumentFragment();
  93. const route = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/');
  94. const title = $('<div class="fjs-title" />');
  95. const link = $(`<a href="${route}" />`);
  96. const icon = $(`<span class="fjs-icon ${item.icon} badge-${item.extras && item.extras.published ? 'published' : 'unpublished'}" />`);
  97. if (item.extras && item.extras.lang) {
  98. let status = '';
  99. if (item.extras.translated) {
  100. status = 'translated';
  101. }
  102. if (item.extras.lang === 'n/a') {
  103. status = 'not-available';
  104. }
  105. const lang = $(`<span class="badge-lang ${status}">${item.extras.lang}</span>`);
  106. lang.appendTo(icon);
  107. }
  108. if (item.extras && item.extras && (item.extras.published_date || item.extras.unpublished_date)) {
  109. const clock = $('<span class="badge-clock" />');
  110. clock.appendTo(icon);
  111. }
  112. const info = $(`<span class="fjs-info"><b title="${item.title}">${item.title}</b> <em title="${item.route.display}">${item.route.display}</em></span>`);
  113. const actions = $('<span class="fjs-actions" />');
  114. let dotdotdot = null;
  115. if (item.extras) {
  116. const LANG_URL = $('[data-lang-url]').data('langUrl');
  117. dotdotdot = $('<div class="button-group" data-flexpages-dotx3 data-flexpages-prevent><button class="button dropdown-toggle" data-toggle="dropdown"><i class="fa fa-ellipsis-v fjs-action-toggle"></i></button></div>');
  118. dotdotdot.on('click', (event) => {
  119. if (!dotdotdot.find('.dropdown-menu').length) {
  120. let tags = '';
  121. let langs = '';
  122. item.extras.tags.forEach((tag) => {
  123. tags += `<span class="badge tag tag-${tag}">${tag}</span>`;
  124. });
  125. const translations = item.extras.langs || {};
  126. Object.keys(translations).forEach((lang) => {
  127. const translated = translations[lang];
  128. langs += `<a class="lang" href="${LANG_URL.replace(/%LANG%/g, lang).replace('//', '/')}${item.route.raw}"><span class="badge lang-${lang ? lang : 'default'} lang-${translated ? 'translated' : 'non-translated'}"><i class="fa fa-fw fa-circle"></i> ${lang ? lang : 'default'}</span></a>`;
  129. });
  130. const canPreview = item.extras.actions.includes('preview') && (!(item.extras.tags.includes('non-routable') || item.extras.tags.includes('unpublished')));
  131. const canEdit = item.extras.actions.includes('edit');
  132. const canCopy = item.extras.actions.includes('copy');
  133. const canMove = false; // item.extras.actions.includes('move');
  134. const canDelete = item.extras.actions.includes('delete');
  135. const ul = $(`<div class="dropdown-menu">
  136. <div class="action-bar">
  137. ${canPreview ? `<a href="${route}/:preview" class="dropdown-item" title="Preview"><i class="fa fa-fw fa-eye"></i></a>` : ''}
  138. ${canEdit ? `<a href="${route}" class="dropdown-item" title="Edit"><i class="fa fa-fw fa-pencil"></i></a>` : ''}
  139. ${canCopy ? `<a href="${route}/task:copy/admin-nonce:${GRAV_CONFIG.admin_nonce}" class="dropdown-item" title="Duplicate" href="#modal-page-copy" data-remodal-target="modal-page-copy" data-copy-flex-page data-title="${item.title}" data-folder="${item['item-key']}"><i class="fa fa-fw fa-copy"></i></a>` : ''}
  140. ${canMove ? '<a href="#" class="dropdown-item" title="Move (coming soon)"><i class="fa fa-fw fa-arrows"></i></a>' : ''}
  141. ${canDelete ? `<a href="#delete" data-remodal-target="delete" data-delete-url="${route}/task:delete/admin-nonce:${GRAV_CONFIG.admin_nonce}" class="dropdown-item danger" title="Delete"><i class="fa fa-fw fa-trash-o"></i></a>` : ''}
  142. </div>
  143. <div class="divider"></div>
  144. <div class="tags">${tags}</div>
  145. <div class="divider"></div>
  146. ${item.extras.lang || typeof item.extras.langs !== 'undefined' ? `<div class="langs">${langs}</div><div class="divider"></div>` : ''}
  147. <div class="details">
  148. <div class="infos">
  149. <table>
  150. <tr>
  151. <td><b>route</b></td>
  152. <td>${item.route.display}</td>
  153. </tr>
  154. <tr>
  155. <td><b>template</b></td>
  156. <td>${item.extras.template}</td>
  157. </tr>
  158. ${item.extras && item.extras.published_date ? `
  159. <tr>
  160. <td><b>publish</b></td>
  161. <td>${item.extras.published_date}</td>
  162. </tr>
  163. ` : ''}
  164. ${item.extras && item.extras.unpublished_date ? `
  165. <tr>
  166. <td><b>unpublish</b></td>
  167. <td>${item.extras.unpublished_date}</td>
  168. </tr>
  169. ` : ''}
  170. <tr>
  171. <td><b>modified</b></td>
  172. <td>${item.modified}</td>
  173. </tr>
  174. </table>
  175. </div>
  176. </div>
  177. </div>`);
  178. ul.appendTo(dotdotdot);
  179. }
  180. return true;
  181. });
  182. }
  183. if (item.child_count) {
  184. const button = $('<button class="fjs-children" data-flexpages-expand data-flexpages-prevent />');
  185. const count = $(`<span class="badge child-count">${typeof item.count !== 'undefined' ? `${item.count} / ` : ''}${item.child_count}</span>`);
  186. const arrow = $('<i class="fa fa-chevron-right"></i>');
  187. count.appendTo(button);
  188. arrow.appendTo(button);
  189. button.appendTo(actions);
  190. }
  191. icon.appendTo(title);
  192. dotdotdot.appendTo(title);
  193. link.appendTo(title);
  194. info.appendTo(link);
  195. title.appendTo(frag);
  196. actions.appendTo(frag);
  197. return frag;
  198. }
  199. static createLoadingColumn() {
  200. return $(`
  201. <div class="fjs-col leaf-col" style="overflow: hidden;">
  202. <div class="leaf-row">
  203. <div class="grav-loading"><div class="grav-loader">Loading...</div></div>
  204. </div>
  205. </div>
  206. `);
  207. }
  208. static createErrorColumn(error) {
  209. return $(`
  210. <div class="fjs-col leaf-col" style="overflow: hidden;">
  211. <div class="leaf-row error">
  212. <i class="fa fa-fw fa-warning"></i>
  213. <span>${error}</span>
  214. </div>
  215. </div>
  216. `);
  217. }
  218. createSimpleColumn(item) {}
  219. dataLoad(parent, callback, filters = getStore().filters || {}) {
  220. /* if (!parent && Object.keys(filters).length) {
  221. parent = { child_count: 1, route: { raw: '' } };
  222. }*/
  223. if (!parent) {
  224. return callback(this.data);
  225. }
  226. if (!parent.child_count) {
  227. return false;
  228. }
  229. const UUID = ++XHRUUID;
  230. this.startLoader();
  231. const withFilters = Object.keys(filters).length ? { ...filters } : {};
  232. $.ajax({
  233. url: `${GRAV_CONFIG.current_url}`,
  234. method: 'post',
  235. data: Object.assign({}, {
  236. route: b64_encode_unicode(parent.route.raw),
  237. action: 'listLevel'
  238. }, withFilters),
  239. success: (response) => {
  240. this.stopLoader();
  241. if (response.status === 'error') {
  242. this.finder.$emitter.emit('create-column', FlexPages.createErrorColumn(response.message)[0]);
  243. return false;
  244. }
  245. // stale request
  246. if (UUID !== XHRUUID) {
  247. return false;
  248. }
  249. if (response.data.length) {
  250. parent.children = response.data;
  251. }
  252. return callback(response.data);
  253. }
  254. });
  255. }
  256. startLoader() {
  257. if (!this.finder) {
  258. return null;
  259. }
  260. this.loadingIndicator = FlexPages.createLoadingColumn();
  261. this.finder.$emitter.emit('create-column', this.loadingIndicator[0]);
  262. return this.loadingIndicator;
  263. }
  264. stopLoader() {
  265. return this.loadingIndicator && this.loadingIndicator.remove();
  266. }
  267. }
  268. export const b64_encode_unicode = (str) => {
  269. return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
  270. function toSolidBytes(match, p1) {
  271. return String.fromCharCode('0x' + p1);
  272. }));
  273. };
  274. export const b64_decode_unicode = (str) => {
  275. return decodeURIComponent(atob(str).split('').map(function(c) {
  276. return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  277. }).join(''));
  278. };
  279. const updatePosition = (scrollingColumn, pageColumns) => {
  280. const group = document.querySelector('#pages-columns .button-group.open');
  281. if (group) {
  282. const button = group.querySelector('[data-toggle="dropdown"]');
  283. const dropdown = group.querySelector('.dropdown-menu');
  284. const buttonInView = isInViewport(button);
  285. if (button && dropdown) {
  286. if (!buttonInView) {
  287. $(dropdown).css({ display: 'none' });
  288. } else {
  289. $(dropdown).css({ display: 'inherit' });
  290. const buttonClientRect = button.getBoundingClientRect();
  291. const dropdownClientRect = dropdown.getBoundingClientRect();
  292. const scrollTop = (window.pageYOffset || document.documentElement.scrollTop);
  293. const scrollLeft = (window.pageXOffset || document.documentElement.scrollLeft);
  294. const top = buttonClientRect.height + buttonClientRect.top + scrollTop;
  295. let left = buttonClientRect.left + scrollLeft; // - dropdownClientRect.width
  296. if (left + dropdownClientRect.width > window.innerWidth) {
  297. left = window.innerWidth - dropdownClientRect.width - 5;
  298. }
  299. $(dropdown).css({ top, left });
  300. if (scrollingColumn) {
  301. const targetClientRect = event.target.getBoundingClientRect();
  302. if ((top < targetClientRect.top + scrollTop) || (top > targetClientRect.top + scrollTop + targetClientRect.height)) {
  303. $(dropdown).css({ display: 'none' });
  304. }
  305. }
  306. if (pageColumns) {
  307. const targetClientRect = event.target.getBoundingClientRect();
  308. if ((left < targetClientRect.left + scrollLeft) || (left > targetClientRect.left + scrollLeft + targetClientRect.width)) {
  309. $(dropdown).css({ display: 'none' });
  310. }
  311. }
  312. }
  313. }
  314. }
  315. };
  316. const closeGhostDropdowns = () => {
  317. const opened = document.querySelectorAll('#pages-columns .button-group:not(.open) .dropdown-menu') || [];
  318. opened.forEach((item) => { item.style.display = 'none'; });
  319. };
  320. document.addEventListener('scroll', (event) => {
  321. if (event.target && !event.target.classList) { return true; }
  322. const scrollingDocument = event.target.classList.contains('gm-scroll-view') || event.target.classList.contains('content-wrapper');
  323. const scrollingColumn = event.target.classList.contains('fjs-col');
  324. const pageColumns = event.target.id === 'pages-columns';
  325. if (scrollingDocument || scrollingColumn || pageColumns) {
  326. closeGhostDropdowns();
  327. updatePosition(scrollingColumn, pageColumns);
  328. }
  329. }, true);
  330. document.addEventListener('click', (event) => {
  331. closeGhostDropdowns();
  332. if (event.target.dataset.toggle || event.target.closest('[data-toggle="dropdown"]')) {
  333. const containerScroller = document.querySelectorAll('.gm-scroll-view');
  334. ((containerScroller.length ? containerScroller : document.querySelectorAll('.content-wrapper')) || []).forEach((scroll) => {
  335. const scrollEvent = new Event('scroll');
  336. scroll.dispatchEvent(scrollEvent);
  337. });
  338. }
  339. if ((event.target.classList && event.target.classList.contains('dropdown-menu')) || (event.target.closest('.dropdown-menu'))) {
  340. if (!$(event.target).closest('.dropdown-menu').find(event.target).length) {
  341. event.preventDefault();
  342. event.stopPropagation();
  343. }
  344. }
  345. if (event.target.dataset.copyFlexPage || event.target.closest('[data-copy-flex-page]')) {
  346. const target = event.target.dataset.copyFlexPage ? event.target : event.target.closest('[data-copy-flex-page]');
  347. const modal = document.querySelector('[data-remodal-id="modal-page-copy"]');
  348. const form = modal.querySelector('form');
  349. const titleField = modal.querySelector('[name="data[title]"]');
  350. const folderField = modal.querySelector('[name="data[folder]"]');
  351. titleField.value = `${target.dataset.title} (Copy)`;
  352. folderField.value = `${target.dataset.folder}-copy`;
  353. form.action = target.href;
  354. }
  355. });
  356. // Prevent dropdowns from closing when clicking within
  357. $(document).on('click.bs.dropdown.data-api', '.fjs-item-wrapper .dropdown-menu', (event) => {
  358. event.stopPropagation();
  359. });