Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

finder.js 17 KiB

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. });