init
This commit is contained in:
425
user/plugins/flex-objects/app/columns/finder.js
Normal file
425
user/plugins/flex-objects/app/columns/finder.js
Normal file
@@ -0,0 +1,425 @@
|
||||
import $ from 'jquery';
|
||||
import Finder from '../utils/finder';
|
||||
import { getInitialRoute, getStore, setInitialRoute } from './index';
|
||||
// import getFilters from '../utils/get-filters';
|
||||
|
||||
let XHRUUID = 0;
|
||||
const GRAV_CONFIG = typeof global.GravConfig !== 'undefined' ? global.GravConfig : global.GravAdmin.config;
|
||||
|
||||
export const Instances = {};
|
||||
|
||||
const isInViewport = (elem) => {
|
||||
const bounding = elem.getBoundingClientRect();
|
||||
const titlebar = document.querySelector('#titlebar');
|
||||
const offset = titlebar ? titlebar.getBoundingClientRect().height : 0;
|
||||
return (
|
||||
bounding.top >= offset &&
|
||||
bounding.left >= 0 &&
|
||||
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
};
|
||||
|
||||
export class FlexPages {
|
||||
constructor(container, data) {
|
||||
this.container = $(container);
|
||||
this.data = data;
|
||||
const dataLoad = this.dataLoad;
|
||||
|
||||
this.finder = new Finder(
|
||||
this.container,
|
||||
(parent, callback) => {
|
||||
return dataLoad.call(this, parent, callback);
|
||||
},
|
||||
{
|
||||
labelKey: 'title',
|
||||
defaultPath: getInitialRoute(),
|
||||
itemTrigger: '[data-flexpages-expand]',
|
||||
createItem: function(item) {
|
||||
return FlexPages.createItem(this.config, item, this);
|
||||
},
|
||||
createItemContent: function(item) {
|
||||
return FlexPages.createItemContent(this.config, item, this);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.finder.$emitter.on('leaf-selected', (item) => {
|
||||
setInitialRoute({
|
||||
route: item.route.raw
|
||||
});
|
||||
});
|
||||
|
||||
this.finder.$emitter.on('interior-selected', (item) => {
|
||||
setInitialRoute({
|
||||
route: item.route.raw
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
this.finder.$emitter.on('leaf-selected', (item) => {
|
||||
console.log('selected', item);
|
||||
this.finder.emit('create-column', () => this.createSimpleColumn(item));
|
||||
});
|
||||
|
||||
this.finder.$emitter.on('item-selected', (selected) => {
|
||||
console.log('selected', selected);
|
||||
// for future use only - create column-card creation for file with details like in macOS finder
|
||||
// this.finder.$emitter('create-column', () => this.createSimpleColumn(selected));
|
||||
}); */
|
||||
|
||||
this.finder.$emitter.on('column-created', () => {
|
||||
this.container[0].scrollLeft = this.container[0].scrollWidth - this.container[0].clientWidth;
|
||||
});
|
||||
}
|
||||
|
||||
static createItem(config, item, finder) {
|
||||
const listItem = $('<li />');
|
||||
const listItemClasses = [config.className.item];
|
||||
// const href = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/');
|
||||
const link = $('<div class="fjs-item-wrapper" />');
|
||||
const createItemContent = config.createItemContent || finder.createItemContent;
|
||||
const fragment = createItemContent.call(this, item);
|
||||
link.append(fragment)
|
||||
// .attr('href', href)
|
||||
.attr('tabindex', -1);
|
||||
|
||||
if (item.url) {
|
||||
link.attr('href', item.url);
|
||||
listItemClasses.push(item.className);
|
||||
}
|
||||
|
||||
if (item[config.childKey]) {
|
||||
listItemClasses.push(config.className[config.childKey]);
|
||||
}
|
||||
|
||||
if (item.filters_hit) {
|
||||
listItemClasses.push('filters-hit');
|
||||
}
|
||||
|
||||
listItem.addClass(listItemClasses.join(' '));
|
||||
listItem.append(link)
|
||||
.attr('data-fjs-item', item[config.itemKey]);
|
||||
|
||||
listItem[0]._item = item;
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
static createItemContent(config, item) {
|
||||
const frag = document.createDocumentFragment();
|
||||
const route = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/');
|
||||
const title = $('<div class="fjs-title" />');
|
||||
const link = $(`<a href="${route}" />`);
|
||||
const icon = $(`<span class="fjs-icon ${item.icon} badge-${item.extras && item.extras.published ? 'published' : 'unpublished'}" />`);
|
||||
|
||||
if (item.extras && item.extras.lang) {
|
||||
let status = '';
|
||||
if (item.extras.translated) {
|
||||
status = 'translated';
|
||||
}
|
||||
|
||||
if (item.extras.lang === 'n/a') {
|
||||
status = 'not-available';
|
||||
}
|
||||
|
||||
const lang = $(`<span class="badge-lang ${status}">${item.extras.lang}</span>`);
|
||||
lang.appendTo(icon);
|
||||
}
|
||||
|
||||
if (item.extras && item.extras && (item.extras.published_date || item.extras.unpublished_date)) {
|
||||
const clock = $('<span class="badge-clock" />');
|
||||
clock.appendTo(icon);
|
||||
}
|
||||
|
||||
const info = $(`<span class="fjs-info"><b title="${item.title}">${item.title}</b> <em title="${item.route.display}">${item.route.display}</em></span>`);
|
||||
const actions = $('<span class="fjs-actions" />');
|
||||
|
||||
let dotdotdot = null;
|
||||
if (item.extras) {
|
||||
const LANG_URL = $('[data-lang-url]').data('langUrl');
|
||||
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>');
|
||||
dotdotdot.on('click', (event) => {
|
||||
if (!dotdotdot.find('.dropdown-menu').length) {
|
||||
let tags = '';
|
||||
let langs = '';
|
||||
|
||||
item.extras.tags.forEach((tag) => {
|
||||
tags += `<span class="badge tag tag-${tag}">${tag}</span>`;
|
||||
});
|
||||
|
||||
const translations = item.extras.langs || {};
|
||||
Object.keys(translations).forEach((lang) => {
|
||||
const translated = translations[lang];
|
||||
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>`;
|
||||
});
|
||||
|
||||
const canPreview = item.extras.actions.includes('preview') && (!(item.extras.tags.includes('non-routable') || item.extras.tags.includes('unpublished')));
|
||||
const canEdit = item.extras.actions.includes('edit');
|
||||
const canCopy = item.extras.actions.includes('copy');
|
||||
const canMove = false; // item.extras.actions.includes('move');
|
||||
const canDelete = item.extras.actions.includes('delete');
|
||||
const ul = $(`<div class="dropdown-menu">
|
||||
<div class="action-bar">
|
||||
${canPreview ? `<a href="${route}/:preview" class="dropdown-item" title="Preview"><i class="fa fa-fw fa-eye"></i></a>` : ''}
|
||||
${canEdit ? `<a href="${route}" class="dropdown-item" title="Edit"><i class="fa fa-fw fa-pencil"></i></a>` : ''}
|
||||
${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>` : ''}
|
||||
${canMove ? '<a href="#" class="dropdown-item" title="Move (coming soon)"><i class="fa fa-fw fa-arrows"></i></a>' : ''}
|
||||
${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>` : ''}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="tags">${tags}</div>
|
||||
<div class="divider"></div>
|
||||
${item.extras.lang || typeof item.extras.langs !== 'undefined' ? `<div class="langs">${langs}</div><div class="divider"></div>` : ''}
|
||||
<div class="details">
|
||||
<div class="infos">
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>route</b></td>
|
||||
<td>${item.route.display}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>template</b></td>
|
||||
<td>${item.extras.template}</td>
|
||||
</tr>
|
||||
${item.extras && item.extras.published_date ? `
|
||||
<tr>
|
||||
<td><b>publish</b></td>
|
||||
<td>${item.extras.published_date}</td>
|
||||
</tr>
|
||||
` : ''}
|
||||
${item.extras && item.extras.unpublished_date ? `
|
||||
<tr>
|
||||
<td><b>unpublish</b></td>
|
||||
<td>${item.extras.unpublished_date}</td>
|
||||
</tr>
|
||||
` : ''}
|
||||
<tr>
|
||||
<td><b>modified</b></td>
|
||||
<td>${item.modified}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
ul.appendTo(dotdotdot);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (item.child_count) {
|
||||
const button = $('<button class="fjs-children" data-flexpages-expand data-flexpages-prevent />');
|
||||
const count = $(`<span class="badge child-count">${typeof item.count !== 'undefined' ? `${item.count} / ` : ''}${item.child_count}</span>`);
|
||||
const arrow = $('<i class="fa fa-chevron-right"></i>');
|
||||
count.appendTo(button);
|
||||
arrow.appendTo(button);
|
||||
button.appendTo(actions);
|
||||
}
|
||||
|
||||
icon.appendTo(title);
|
||||
dotdotdot.appendTo(title);
|
||||
link.appendTo(title);
|
||||
info.appendTo(link);
|
||||
|
||||
title.appendTo(frag);
|
||||
actions.appendTo(frag);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
static createLoadingColumn() {
|
||||
return $(`
|
||||
<div class="fjs-col leaf-col" style="overflow: hidden;">
|
||||
<div class="leaf-row">
|
||||
<div class="grav-loading"><div class="grav-loader">Loading...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
static createErrorColumn(error) {
|
||||
return $(`
|
||||
<div class="fjs-col leaf-col" style="overflow: hidden;">
|
||||
<div class="leaf-row error">
|
||||
<i class="fa fa-fw fa-warning"></i>
|
||||
<span>${error}</span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
createSimpleColumn(item) {}
|
||||
|
||||
dataLoad(parent, callback, filters = getStore().filters || {}) {
|
||||
/* if (!parent && Object.keys(filters).length) {
|
||||
parent = { child_count: 1, route: { raw: '' } };
|
||||
}*/
|
||||
|
||||
if (!parent) {
|
||||
return callback(this.data);
|
||||
}
|
||||
|
||||
if (!parent.child_count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UUID = ++XHRUUID;
|
||||
this.startLoader();
|
||||
|
||||
const withFilters = Object.keys(filters).length ? { ...filters } : {};
|
||||
|
||||
$.ajax({
|
||||
url: `${GRAV_CONFIG.current_url}`,
|
||||
method: 'post',
|
||||
data: Object.assign({}, {
|
||||
route: b64_encode_unicode(parent.route.raw),
|
||||
action: 'listLevel'
|
||||
}, withFilters),
|
||||
success: (response) => {
|
||||
this.stopLoader();
|
||||
|
||||
if (response.status === 'error') {
|
||||
this.finder.$emitter.emit('create-column', FlexPages.createErrorColumn(response.message)[0]);
|
||||
return false;
|
||||
}
|
||||
// stale request
|
||||
if (UUID !== XHRUUID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.data.length) {
|
||||
parent.children = response.data;
|
||||
}
|
||||
|
||||
return callback(response.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startLoader() {
|
||||
if (!this.finder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.loadingIndicator = FlexPages.createLoadingColumn();
|
||||
this.finder.$emitter.emit('create-column', this.loadingIndicator[0]);
|
||||
|
||||
return this.loadingIndicator;
|
||||
}
|
||||
|
||||
stopLoader() {
|
||||
return this.loadingIndicator && this.loadingIndicator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export const b64_encode_unicode = (str) => {
|
||||
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
||||
function toSolidBytes(match, p1) {
|
||||
return String.fromCharCode('0x' + p1);
|
||||
}));
|
||||
};
|
||||
|
||||
export const b64_decode_unicode = (str) => {
|
||||
return decodeURIComponent(atob(str).split('').map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
};
|
||||
|
||||
const updatePosition = (scrollingColumn, pageColumns) => {
|
||||
const group = document.querySelector('#pages-columns .button-group.open');
|
||||
if (group) {
|
||||
const button = group.querySelector('[data-toggle="dropdown"]');
|
||||
const dropdown = group.querySelector('.dropdown-menu');
|
||||
const buttonInView = isInViewport(button);
|
||||
|
||||
if (button && dropdown) {
|
||||
if (!buttonInView) {
|
||||
$(dropdown).css({ display: 'none' });
|
||||
} else {
|
||||
$(dropdown).css({ display: 'inherit' });
|
||||
|
||||
const buttonClientRect = button.getBoundingClientRect();
|
||||
const dropdownClientRect = dropdown.getBoundingClientRect();
|
||||
const scrollTop = (window.pageYOffset || document.documentElement.scrollTop);
|
||||
const scrollLeft = (window.pageXOffset || document.documentElement.scrollLeft);
|
||||
const top = buttonClientRect.height + buttonClientRect.top + scrollTop;
|
||||
let left = buttonClientRect.left + scrollLeft; // - dropdownClientRect.width
|
||||
|
||||
if (left + dropdownClientRect.width > window.innerWidth) {
|
||||
left = window.innerWidth - dropdownClientRect.width - 5;
|
||||
}
|
||||
|
||||
$(dropdown).css({ top, left });
|
||||
|
||||
if (scrollingColumn) {
|
||||
const targetClientRect = event.target.getBoundingClientRect();
|
||||
if ((top < targetClientRect.top + scrollTop) || (top > targetClientRect.top + scrollTop + targetClientRect.height)) {
|
||||
$(dropdown).css({ display: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
if (pageColumns) {
|
||||
const targetClientRect = event.target.getBoundingClientRect();
|
||||
if ((left < targetClientRect.left + scrollLeft) || (left > targetClientRect.left + scrollLeft + targetClientRect.width)) {
|
||||
$(dropdown).css({ display: 'none' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeGhostDropdowns = () => {
|
||||
const opened = document.querySelectorAll('#pages-columns .button-group:not(.open) .dropdown-menu') || [];
|
||||
opened.forEach((item) => { item.style.display = 'none'; });
|
||||
};
|
||||
|
||||
document.addEventListener('scroll', (event) => {
|
||||
if (event.target && !event.target.classList) { return true; }
|
||||
const scrollingDocument = event.target.classList.contains('gm-scroll-view') || event.target.classList.contains('content-wrapper');
|
||||
const scrollingColumn = event.target.classList.contains('fjs-col');
|
||||
const pageColumns = event.target.id === 'pages-columns';
|
||||
|
||||
if (scrollingDocument || scrollingColumn || pageColumns) {
|
||||
closeGhostDropdowns();
|
||||
updatePosition(scrollingColumn, pageColumns);
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
closeGhostDropdowns();
|
||||
if (event.target.dataset.toggle || event.target.closest('[data-toggle="dropdown"]')) {
|
||||
const containerScroller = document.querySelectorAll('.gm-scroll-view');
|
||||
|
||||
((containerScroller.length ? containerScroller : document.querySelectorAll('.content-wrapper')) || []).forEach((scroll) => {
|
||||
const scrollEvent = new Event('scroll');
|
||||
scroll.dispatchEvent(scrollEvent);
|
||||
});
|
||||
}
|
||||
|
||||
if ((event.target.classList && event.target.classList.contains('dropdown-menu')) || (event.target.closest('.dropdown-menu'))) {
|
||||
if (!$(event.target).closest('.dropdown-menu').find(event.target).length) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
if (event.target.dataset.copyFlexPage || event.target.closest('[data-copy-flex-page]')) {
|
||||
const target = event.target.dataset.copyFlexPage ? event.target : event.target.closest('[data-copy-flex-page]');
|
||||
const modal = document.querySelector('[data-remodal-id="modal-page-copy"]');
|
||||
const form = modal.querySelector('form');
|
||||
const titleField = modal.querySelector('[name="data[title]"]');
|
||||
const folderField = modal.querySelector('[name="data[folder]"]');
|
||||
|
||||
titleField.value = `${target.dataset.title} (Copy)`;
|
||||
folderField.value = `${target.dataset.folder}-copy`;
|
||||
form.action = target.href;
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent dropdowns from closing when clicking within
|
||||
$(document).on('click.bs.dropdown.data-api', '.fjs-item-wrapper .dropdown-menu', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
102
user/plugins/flex-objects/app/columns/index.js
Normal file
102
user/plugins/flex-objects/app/columns/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import $ from 'jquery';
|
||||
import { b64_decode_unicode, b64_encode_unicode, FlexPages } from './finder';
|
||||
import { isEnabled, getCookie, setCookie } from 'tiny-cookie';
|
||||
import getFilters from '../utils/get-filters';
|
||||
|
||||
const container = document.querySelector('#pages-content-wrapper');
|
||||
|
||||
export const getStore = () => {
|
||||
if (!isEnabled) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return JSON.parse(b64_decode_unicode(getCookie('grav-admin-flexpages') || 'e30='));
|
||||
};
|
||||
|
||||
export const setStore = (store = {}, options = { expires: '1Y', samesite: 'Lax' }) => {
|
||||
if (!isEnabled) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return setCookie('grav-admin-flexpages', b64_encode_unicode(JSON.stringify(store)), options);
|
||||
};
|
||||
|
||||
export const getInitialRoute = () => {
|
||||
const parsed = getStore();
|
||||
return parsed.route || '';
|
||||
};
|
||||
|
||||
export const setInitialRoute = ({ route = '', filters = getStore().filters || {}, options = { expires: '1Y' }} = {}) => {
|
||||
return setStore({ route, filters }, options);
|
||||
};
|
||||
|
||||
export let FlexPagesInstance = null;
|
||||
|
||||
export const ReLoad = (fresh = false) => {
|
||||
const search = document.querySelector('#pages-filters [name="filters[search]"]');
|
||||
const loader = container.querySelector('.grav-loading');
|
||||
const content = container.querySelector('#pages-columns');
|
||||
const gravConfig = typeof global.GravConfig !== 'undefined' ? global.GravConfig : global.GravAdmin.config;
|
||||
|
||||
if (fresh && search) {
|
||||
search.focus();
|
||||
}
|
||||
|
||||
if (loader && content) {
|
||||
loader.style.display = 'block';
|
||||
content.innerHTML = '';
|
||||
|
||||
const filters = fresh ? getStore().filters || {} : getFilters();
|
||||
const withFilters = Object.keys(filters).length ? { ...filters, initial: true } : {};
|
||||
|
||||
const store = getStore();
|
||||
store.filters = filters;
|
||||
setStore(store);
|
||||
|
||||
let isSearchFocused = false;
|
||||
if (search) {
|
||||
isSearchFocused = search === document.activeElement;
|
||||
}
|
||||
|
||||
const contentWrapper = document.querySelector('.content-wrapper .gm-scroll-view');
|
||||
const scrollPosition = {
|
||||
top: contentWrapper ? contentWrapper.scrollTop : 0,
|
||||
left: contentWrapper ? contentWrapper.scrollLeft : 0
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: `${gravConfig.current_url}`,
|
||||
method: 'post',
|
||||
data: Object.assign({}, {
|
||||
route: b64_encode_unicode(getInitialRoute()),
|
||||
initial: true,
|
||||
action: 'listLevel'
|
||||
}, withFilters),
|
||||
success(response) {
|
||||
loader.style.display = 'none';
|
||||
|
||||
if (response.status === 'error') {
|
||||
content.innerHTML = response.message;
|
||||
return true;
|
||||
}
|
||||
|
||||
FlexPagesInstance = null;
|
||||
FlexPagesInstance = new FlexPages(content, response.data);
|
||||
|
||||
if (search && isSearchFocused) {
|
||||
search.focus();
|
||||
}
|
||||
|
||||
if (contentWrapper) {
|
||||
contentWrapper.scrollTo(scrollPosition);
|
||||
}
|
||||
|
||||
return FlexPagesInstance;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (container) {
|
||||
ReLoad(true);
|
||||
}
|
||||
46
user/plugins/flex-objects/app/filters/index.js
Normal file
46
user/plugins/flex-objects/app/filters/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import '../utils/indeterminate';
|
||||
import './panel';
|
||||
import { ReLoad } from '../columns';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const filterType = event.target && event.target.dataset.filters;
|
||||
|
||||
if (filterType === 'reset') {
|
||||
const filters = event.target.closest('#pages-filters');
|
||||
(filters.querySelectorAll('input[type="text"]') || []).forEach((input) => {
|
||||
input.value = '';
|
||||
});
|
||||
|
||||
(filters.querySelectorAll('input[type="checkbox"]') || []).forEach((input) => {
|
||||
const wrapper = input.closest('.checkboxes');
|
||||
if (wrapper) {
|
||||
wrapper.classList.remove('status-checked', 'status-unchecked', 'status-indeterminate');
|
||||
wrapper.dataset._checkStatus = '0';
|
||||
wrapper.classList.add('status-unchecked');
|
||||
}
|
||||
|
||||
input.indeterminate = false;
|
||||
input.checked = false;
|
||||
input.value = '';
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterType === 'apply') {
|
||||
ReLoad();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const throttledReload = throttle(() => {
|
||||
ReLoad();
|
||||
}, 350, { leading: false });
|
||||
|
||||
document.addEventListener('input', (event) => {
|
||||
if (event.target.getAttribute && event.target.getAttribute('name') === 'filters[search]') {
|
||||
throttledReload.cancel();
|
||||
throttledReload();
|
||||
}
|
||||
});
|
||||
15
user/plugins/flex-objects/app/filters/panel.js
Normal file
15
user/plugins/flex-objects/app/filters/panel.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const toggle = document.querySelector('.filters-bar .adv-options');
|
||||
const panel = document.querySelector('.filters-advanced');
|
||||
|
||||
if (toggle && panel) {
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('adv-options') || event.target.closest('.adv-options')) {
|
||||
event.preventDefault();
|
||||
const isOpen = toggle.classList.contains('open');
|
||||
|
||||
panel.classList.toggle('hide');
|
||||
toggle.classList.remove(isOpen ? 'open' : 'close');
|
||||
toggle.classList.add(isOpen ? 'close' : 'open');
|
||||
}
|
||||
});
|
||||
}
|
||||
27
user/plugins/flex-objects/app/list/App.vue
Normal file
27
user/plugins/flex-objects/app/list/App.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<flex-filter-bar :store="store" />
|
||||
<flex-content-loader :store="store" v-show="loading" />
|
||||
<flex-table :store="store" v-model="loading" v-show="!loading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FlexTable from './components/Table.vue';
|
||||
import FlexFilterBar from './components/FilterBar.vue';
|
||||
import FlexContentLoader from './components/ContentLoader.vue';
|
||||
|
||||
export default {
|
||||
props: ['initialStore'],
|
||||
components: {FlexTable, FlexFilterBar, FlexContentLoader},
|
||||
data: () => ({
|
||||
perPage: 10,
|
||||
loading: true
|
||||
}),
|
||||
computed: {
|
||||
store() {
|
||||
return JSON.parse(this.initialStore || '{}');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
29
user/plugins/flex-objects/app/list/VuetableCssConfig.js
Normal file
29
user/plugins/flex-objects/app/list/VuetableCssConfig.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export default {
|
||||
table: {
|
||||
tableClass: 'table',
|
||||
loadingClass: 'loading',
|
||||
sortableIcon: '',
|
||||
ascendingIcon: 'fa fa-fw fa-chevron-up',
|
||||
descendingIcon: 'fa fa-fw fa-chevron-down',
|
||||
ascendingClass: '',
|
||||
descendingClass: '',
|
||||
handleIcon: 'fa fa-fw fa-bars',
|
||||
renderIcon: (classes, options) => `<i class="${classes.join(' ')}"></i>`
|
||||
},
|
||||
pagination: {
|
||||
wrapperClass: 'flex-objects-pagination',
|
||||
activeClass: 'button active',
|
||||
disabledClass: 'button disabled',
|
||||
pageClass: 'button page',
|
||||
linkClass: 'button link',
|
||||
icons: {
|
||||
first: 'fa fa-fw fa-angle-double-left',
|
||||
prev: 'fa fa-fw fa-chevron-left',
|
||||
next: 'fa fa-fw fa-chevron-right',
|
||||
last: 'fa fa-fw fa-angle-double-right'
|
||||
}
|
||||
},
|
||||
paginationInfo: {
|
||||
infoClass: ''
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :style="{ height: 300, width: '100%' }"></div>
|
||||
<content-loader
|
||||
:height="fixedAmount * count"
|
||||
:width="1060"
|
||||
:speed="2"
|
||||
primaryColor="#d9d9d9"
|
||||
secondaryColor="#ecebeb"
|
||||
>
|
||||
<template v-for="index in count">
|
||||
<rect x="13" :y="fixedAmount * index + offset" rx="6" ry="6" :width="200 * random()" height="12" />
|
||||
<rect x="533" :y="fixedAmount * index + offset" rx="6" ry="6" :width="63 * random()" height="12" />
|
||||
<rect x="653" :y="fixedAmount * index + offset" rx="6" ry="6" :width="78 * random()" height="12" />
|
||||
<rect x="755" :y="fixedAmount * index + offset" rx="6" ry="6" :width="117 * random()" height="12" />
|
||||
<rect x="938" :y="fixedAmount * index + offset" rx="6" ry="6" :width="83 * random()" height="12" />
|
||||
|
||||
<rect x="0" :y="fixedAmount * index" rx="6" ry="6" width="1060" height=".3" />
|
||||
</template>
|
||||
</content-loader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ContentLoader } from 'vue-content-loader';
|
||||
|
||||
export default {
|
||||
props: ['store'],
|
||||
data: () => ({
|
||||
fixedAmount: 31,
|
||||
offset: 10,
|
||||
steps: [0.7, 0.8, 0.9, 1]
|
||||
}),
|
||||
computed: {
|
||||
count() {
|
||||
return this.store.perPage;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
random() {
|
||||
return this.steps[Math.floor(Math.random() * this.steps.length)];
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ContentLoader
|
||||
}
|
||||
}
|
||||
</script>
|
||||
66
user/plugins/flex-objects/app/list/components/FilterBar.vue
Normal file
66
user/plugins/flex-objects/app/list/components/FilterBar.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="search-wrapper">
|
||||
<input type="text" class="search" :placeholder="store.searchPlaceholder" v-model.trim="filterText" @input="doFilter">
|
||||
<select class="filter-perPage" v-model="store.perPage" @change="changePerPage">
|
||||
<option v-for="(value, title) in this.perPageOptions"
|
||||
:value="value"
|
||||
:selected="store.perPage === value">{{ title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export default {
|
||||
props: ['store'],
|
||||
data: () => ({
|
||||
filterText: '',
|
||||
searchPlaceholder: 'Filter...',
|
||||
selected: ''
|
||||
}),
|
||||
computed: {
|
||||
perPageOptions() {
|
||||
const options = {
|
||||
'25': 25,
|
||||
'50': 50,
|
||||
'100': 100,
|
||||
'200': 200,
|
||||
'All': ''
|
||||
};
|
||||
|
||||
if (!options[this.store.perPage]) {
|
||||
options[this.store.perPage] = this.store.perPage;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.doFilter = debounce(() => {
|
||||
this.$events.fire('filter-set', this.filterText);
|
||||
}, 250, { leading: false });
|
||||
|
||||
this.changePerPage = () => {
|
||||
this.$events.fire('filter-perPage', this.store.perPage);
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
resetFilter() {
|
||||
this.filterText = '';
|
||||
this.$events.fire('filter-reset');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-wrapper select {
|
||||
margin-bottom: 0;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
94
user/plugins/flex-objects/app/list/components/Table.vue
Normal file
94
user/plugins/flex-objects/app/list/components/Table.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<vuetable ref="vuetable"
|
||||
:css="css.table"
|
||||
:fields="store.fields || []"
|
||||
:searchFields="store.searchFields || []"
|
||||
:sortOrder="store.sortOrder"
|
||||
:multi-sort="true"
|
||||
|
||||
:api-mode="true"
|
||||
:api-url="store.api"
|
||||
:per-page="perPage"
|
||||
:append-params="extraParams"
|
||||
pagination-path="links.pagination"
|
||||
:show-sort-icons="true"
|
||||
@vuetable:pagination-data="onPaginationData"
|
||||
@vuetable:loading="onVuetableLoading"
|
||||
@vuetable:load-success="onVueTableLoadSuccess"
|
||||
/>
|
||||
|
||||
<div class="flex-list-pagination">
|
||||
<vuetable-pagination-info ref="paginationInfo"
|
||||
:info-template="store.paginationInfo"
|
||||
:info-no-data-template="store.emptyResult"
|
||||
:css="css.paginationInfo"
|
||||
/>
|
||||
<vuetable-pagination ref="pagination"
|
||||
:css="css.pagination"
|
||||
@vuetable-pagination:change-page="onChangePage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import Vuetable from 'vuetable-2/src/components/Vuetable.vue';
|
||||
import VuetablePagination from "vuetable-2/src/components/VuetablePagination.vue";
|
||||
import VuetablePaginationInfo from 'vuetable-2/src/components/VuetablePaginationInfo.vue';
|
||||
import VuetableCssConfig from "../VuetableCssConfig.js";
|
||||
|
||||
import set from 'lodash/set';
|
||||
import unset from 'lodash/unset';
|
||||
|
||||
export default {
|
||||
props: ['store', 'value'],
|
||||
components: {Vuetable, VuetablePagination, VuetablePaginationInfo},
|
||||
data: () => ({
|
||||
css: VuetableCssConfig,
|
||||
perPage: 10,
|
||||
data: [],
|
||||
extraParams: {}
|
||||
}),
|
||||
created() {
|
||||
this.perPage = this.store.perPage;
|
||||
this.data = Object.values(this.store.data);
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.vuetable.setData(this.store.data);
|
||||
this.$events.$on('filter-set', event => this.onFilterSet(event));
|
||||
this.$events.$on('filter-reset', event => this.onFilterReset());
|
||||
this.$events.$on('filter-perPage', event => this.onFilterPerPage(event));
|
||||
},
|
||||
methods: {
|
||||
onPaginationData(paginationData) {
|
||||
this.$refs.pagination.setPaginationData(paginationData);
|
||||
this.$refs.paginationInfo.setPaginationData(paginationData);
|
||||
},
|
||||
onFilterSet (filterText) {
|
||||
set(this.extraParams, 'filter', filterText);
|
||||
Vue.nextTick(() => this.$refs.vuetable.refresh());
|
||||
},
|
||||
onFilterReset () {
|
||||
unset(this.extraParams, 'filter');
|
||||
Vue.nextTick(() => this.$refs.vuetable.refresh());
|
||||
},
|
||||
onFilterPerPage (limit) {
|
||||
// console.log('onFilterPerPage', limit, this.store.data);
|
||||
this.perPage = limit || this.$refs.paginationInfo.tablePagination.total;
|
||||
// this.$refs.vuetable.perPage = limit;
|
||||
Vue.nextTick(() => this.$refs.vuetable.refresh());
|
||||
},
|
||||
onChangePage(page) {
|
||||
this.$refs.vuetable.changePage(page);
|
||||
},
|
||||
onVuetableLoading() {
|
||||
this.$emit('input', true);
|
||||
},
|
||||
onVueTableLoadSuccess() {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
19
user/plugins/flex-objects/app/list/index.js
Normal file
19
user/plugins/flex-objects/app/list/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue';
|
||||
import VueEvents from 'vue-events';
|
||||
import App from './App.vue';
|
||||
|
||||
Vue.use(VueEvents);
|
||||
|
||||
const ID = '#flex-objects-list';
|
||||
const element = document.querySelector(ID);
|
||||
|
||||
if (element) {
|
||||
const initialStore = element.dataset.initialStore;
|
||||
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
el: ID,
|
||||
render: h => h(App, {
|
||||
props: {initialStore}
|
||||
})
|
||||
});
|
||||
}
|
||||
3
user/plugins/flex-objects/app/main.js
Normal file
3
user/plugins/flex-objects/app/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import './list';
|
||||
import './columns';
|
||||
import './filters';
|
||||
393
user/plugins/flex-objects/app/utils/finder.js
Normal file
393
user/plugins/flex-objects/app/utils/finder.js
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* (c) Trilby Media, LLC
|
||||
* Author Djamil Legato
|
||||
*
|
||||
* Based on Mark Matyas's Finderjs
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
export const DEFAULTS = {
|
||||
labelKey: 'name',
|
||||
valueKey: 'value', // new
|
||||
childKey: 'children',
|
||||
iconKey: 'icon', // new
|
||||
itemKey: 'item-key', // new
|
||||
itemTrigger: null,
|
||||
pathBar: true,
|
||||
className: {
|
||||
container: 'fjs-container',
|
||||
pathBar: 'fjs-path-bar',
|
||||
col: 'fjs-col',
|
||||
list: 'fjs-list',
|
||||
item: 'fjs-item',
|
||||
active: 'fjs-active',
|
||||
children: 'fjs-has-children',
|
||||
url: 'fjs-url',
|
||||
itemPrepend: 'fjs-item-prepend',
|
||||
itemContent: 'fjs-item-content',
|
||||
itemAppend: 'fjs-item-append'
|
||||
}
|
||||
};
|
||||
|
||||
class Finder {
|
||||
constructor(container, data, options) {
|
||||
this.$emitter = new EventEmitter();
|
||||
this.container = $(container);
|
||||
this.data = data;
|
||||
|
||||
this.config = $.extend(true, {}, DEFAULTS, options);
|
||||
this.container.off('click.finder keydown.finder');
|
||||
|
||||
// dom events
|
||||
this.container.on('click.finder', this.clickEvent.bind(this));
|
||||
this.container.on('keydown.finder', this.keydownEvent.bind(this));
|
||||
|
||||
// internal events
|
||||
this.$emitter.on('item-selected', this.itemSelected.bind(this));
|
||||
this.$emitter.on('create-column', this.addColumn.bind(this));
|
||||
this.$emitter.on('navigate', this.navigate.bind(this));
|
||||
this.$emitter.on('go-to', this.goTo.bind(this, this.data));
|
||||
|
||||
this.container.addClass(this.config.className.container).attr('tabindex', 0);
|
||||
|
||||
this.createColumn(this.data);
|
||||
|
||||
if (this.config.pathBar) {
|
||||
this.pathBar = this.createPathBar();
|
||||
this.pathBar.on('click.finder', '[data-breadcrumb-node]', (event) => {
|
||||
event.preventDefault();
|
||||
const location = $(event.currentTarget).data('breadcrumbNode');
|
||||
this.goTo(this.data, location);
|
||||
});
|
||||
}
|
||||
|
||||
// '' is <Root>
|
||||
if (this.config.defaultPath || this.config.defaultPath === '') {
|
||||
this.goTo(this.data, this.config.defaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
reload(data = this.data) {
|
||||
this.createColumn(data);
|
||||
|
||||
// '' is <Root>
|
||||
if (this.config.defaultPath || this.config.defaultPath === '') {
|
||||
this.goTo(data, this.config.defaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
createColumn(data, parent) {
|
||||
const callback = (data) => this.createColumn(data, parent);
|
||||
|
||||
if (typeof data === 'function') {
|
||||
data.call(this, parent, callback);
|
||||
} else if (Array.isArray(data) || typeof data === 'object') {
|
||||
if (typeof data === 'object') {
|
||||
data = Array.from(data);
|
||||
}
|
||||
const list = this.config.createList || this.createList;
|
||||
const div = $('<div />');
|
||||
div.append(list.call(this, data)).addClass(this.config.className.col);
|
||||
this.$emitter.emit('create-column', div);
|
||||
|
||||
return div;
|
||||
} else {
|
||||
throw new Error('Unknown data type');
|
||||
}
|
||||
}
|
||||
|
||||
createPathBar() {
|
||||
this.container.siblings(`.${this.config.className.pathBar}`).remove();
|
||||
const pathBar = $(`<div class="${this.config.className.pathBar}" />`);
|
||||
pathBar.insertAfter(this.container);
|
||||
|
||||
return pathBar;
|
||||
}
|
||||
|
||||
clickEvent(event) {
|
||||
const target = $(event.target);
|
||||
const column = target.closest(`.${this.config.className.col}`);
|
||||
const item = target.closest(`.${this.config.className.item}`);
|
||||
const prevent = target.is('[data-flexpages-prevent]') ? target : target.closest('[data-flexpages-prevent]');
|
||||
|
||||
if (prevent.data('flexpagesPrevent') === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.config.itemTrigger) {
|
||||
if (target.is(this.config.itemTrigger) || target.closest(this.config.itemTrigger).length) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
this.$emitter.emit('item-selected', {column, item});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (item.length) {
|
||||
this.$emitter.emit('item-selected', { column, item });
|
||||
}
|
||||
}
|
||||
|
||||
keydownEvent(event) {
|
||||
const codes = { 37: 'left', 38: 'up', 39: 'right', 40: 'down', 13: 'enter' };
|
||||
|
||||
if (event.keyCode in codes) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
this.$emitter.emit('navigate', {
|
||||
direction: codes[event.keyCode]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
itemSelected(value) {
|
||||
const element = value.item;
|
||||
if (!element.length) { return false; }
|
||||
const item = element[0]._item;
|
||||
const column = value.column;
|
||||
const data = item[this.config.childKey] || this.data; // TODO: this.data for constant refresh
|
||||
const active = $(column).find(`.${this.config.className.active}`);
|
||||
|
||||
if (active.length) {
|
||||
active.removeClass(this.config.className.active);
|
||||
}
|
||||
|
||||
element.addClass(this.config.className.active);
|
||||
column.nextAll().remove(); // ?!?!?
|
||||
|
||||
this.container[0].focus();
|
||||
window.scrollTo(window.pageXOffset, window.pageYOffset);
|
||||
|
||||
this.updatePathBar();
|
||||
|
||||
let newColumn;
|
||||
if (data) {
|
||||
newColumn = this.createColumn(data, item);
|
||||
this.$emitter.emit('interior-selected', item);
|
||||
} else {
|
||||
this.$emitter.emit('leaf-selected', item);
|
||||
}
|
||||
|
||||
return newColumn;
|
||||
}
|
||||
|
||||
addColumn(column) {
|
||||
this.container.append(column);
|
||||
this.$emitter.emit('column-created', column);
|
||||
}
|
||||
|
||||
navigate(value) {
|
||||
const active = this.findLastActive();
|
||||
const direction = value.direction;
|
||||
let column;
|
||||
let item;
|
||||
let target;
|
||||
|
||||
if (active) {
|
||||
item = active.item;
|
||||
column = active.column;
|
||||
|
||||
if (direction === 'up' && item.prev().length) {
|
||||
target = item.prev();
|
||||
} else if (direction === 'down' && item.next().length) {
|
||||
target = item.next();
|
||||
} else if (direction === 'right' && column.next().length) {
|
||||
column = column.next();
|
||||
target = column.find(`.${this.config.className.item}`).first();
|
||||
} else if (direction === 'left' && column.prev().length) {
|
||||
column = column.prev();
|
||||
target = column.find(`.${this.config.className.active}`).first() || column.find(`.${this.config.className.item}`);
|
||||
}
|
||||
} else {
|
||||
column = this.container.find(`.${this.config.className.col}`).first();
|
||||
target = column.find(`.${this.config.className.item}`).first();
|
||||
}
|
||||
|
||||
if (active && direction === 'enter') {
|
||||
const href = active.item.find('a').prop('href');
|
||||
if (href) {
|
||||
window.location = href;
|
||||
}
|
||||
}
|
||||
|
||||
if (target) {
|
||||
this.$emitter.emit('item-selected', {
|
||||
column,
|
||||
item: target
|
||||
});
|
||||
|
||||
if (!this.isInView(target, column, true)) {
|
||||
this.scrollToView(target[0], column[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goTo(data, path) {
|
||||
path = Array.isArray(path) ? path : path.split('/').map(bit => bit.trim()).filter(Boolean);
|
||||
|
||||
if (path.length) {
|
||||
this.container.children().remove();
|
||||
}
|
||||
|
||||
if (typeof data === 'function') {
|
||||
data.call(this, null, (data) => this.selectPath(path, data));
|
||||
} else {
|
||||
this.selectPath(path, data);
|
||||
}
|
||||
}
|
||||
|
||||
selectPath(path, data, column) {
|
||||
column = column || (path.length ? this.createColumn(data) : this.container.find(`> .${this.config.className.col}`));
|
||||
|
||||
const current = path[0] || '';
|
||||
const children = data.find((item) => item[this.config.itemKey] === current);
|
||||
const item = column.find(`[data-fjs-item="${current}"]`).first();
|
||||
const newColumn = this.itemSelected({
|
||||
column,
|
||||
item
|
||||
});
|
||||
|
||||
if (!this.isInView(item, column, true)) {
|
||||
this.scrollToView(item[0], column[0]);
|
||||
}
|
||||
|
||||
path.shift();
|
||||
|
||||
if (path.length && children) {
|
||||
this.selectPath(path, children[this.config.childKey], newColumn);
|
||||
}
|
||||
}
|
||||
|
||||
findLastActive() {
|
||||
const active = this.container.find(`.${this.config.className.active}`);
|
||||
if (!active.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = active.last();
|
||||
const column = item.closest(`.${this.config.className.col}`);
|
||||
|
||||
return { item, column };
|
||||
}
|
||||
|
||||
createList(data) {
|
||||
const list = $('<ul />');
|
||||
const createItem = this.config.createItem || this.createItem;
|
||||
const items = data.map((item) => createItem.call(this, item));
|
||||
|
||||
const fragments = items.reduce((fragment, current) => {
|
||||
fragment.appendChild(current[0] || current);
|
||||
|
||||
return fragment;
|
||||
}, document.createDocumentFragment());
|
||||
|
||||
list.append(fragments).addClass(this.config.className.list);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
createItem(item) {
|
||||
const listItem = $('<li />');
|
||||
const listItemClasses = [this.config.className.item];
|
||||
const link = $(`<a href="${item.href || ''}" />`);
|
||||
const createItemContent = this.config.createItemContent || this.createItemContent;
|
||||
const fragment = createItemContent.call(this, item);
|
||||
link.append(fragment)
|
||||
.attr('href', '')
|
||||
.attr('tabindex', -1);
|
||||
|
||||
if (item.url) {
|
||||
link.attr('href', item.url);
|
||||
listItemClasses.push(item.className);
|
||||
}
|
||||
|
||||
if (item[this.config.childKey]) {
|
||||
listItemClasses.push(this.config.className[this.config.childKey]);
|
||||
}
|
||||
|
||||
listItem.addClass(listItemClasses.join(' '));
|
||||
listItem.append(link)
|
||||
.attr('data-fjs-item', item[this.config.itemKey]);
|
||||
|
||||
listItem[0]._item = item;
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
updatePathBar() {
|
||||
if (!this.config.pathBar) { return false; }
|
||||
|
||||
const activeItems = this.container.find(`.${this.config.className.active}`);
|
||||
let itemKeys = '';
|
||||
this.pathBar.empty();
|
||||
activeItems.each((index, activeItem) => {
|
||||
const item = activeItem._item;
|
||||
const isLast = (index + 1) === activeItems.length;
|
||||
itemKeys += `/${item[this.config.itemKey]}`;
|
||||
this.pathBar.append(`
|
||||
<span class="breadcrumb-node ${item.icon}" ${item.type === 'dir' || item.child_count > 0 ? `data-breadcrumb-node="${itemKeys}"` : ''}>
|
||||
<i class="${item.icon}"></i>
|
||||
<span class="breadcrumb-node-name">${$('<div />').html(item[this.config.labelKey]).html()}</span>
|
||||
${!isLast ? '<i class="fa fa-fw fa-chevron-right"></i>' : ''}
|
||||
</span>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
getIcon(type) {
|
||||
switch (type) {
|
||||
case 'root':
|
||||
return 'fa-sitemap';
|
||||
case 'file':
|
||||
return 'fa-file-o';
|
||||
case 'dir':
|
||||
default:
|
||||
return 'fa-folder';
|
||||
}
|
||||
}
|
||||
|
||||
isInView(element, container, partial) {
|
||||
if (!element.length || !container.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const containerHeight = container.height();
|
||||
const elementTop = $(element).offset().top - container.offset().top;
|
||||
const elementBottom = elementTop + $(element).height();
|
||||
|
||||
const isTotal = (elementTop >= 0 && elementBottom <= containerHeight);
|
||||
const isPartial = ((elementTop < 0 && elementBottom > 0) || (elementTop > 0 && elementTop <= container.height())) && partial;
|
||||
|
||||
return isTotal || isPartial;
|
||||
}
|
||||
|
||||
scrollToView(element, container) {
|
||||
const top = parseInt(container.getBoundingClientRect().top, 10);
|
||||
const bot = parseInt(container.getBoundingClientRect().bottom, 10);
|
||||
|
||||
const now_top = parseInt(element.getBoundingClientRect().top, 10);
|
||||
const now_bot = parseInt(element.getBoundingClientRect().bottom, 10);
|
||||
|
||||
let scroll_by = 0;
|
||||
if (now_top < top) {
|
||||
scroll_by = -(top - now_top);
|
||||
} else if (now_bot > bot) {
|
||||
scroll_by = now_bot - bot;
|
||||
}
|
||||
|
||||
if (scroll_by !== 0) {
|
||||
container.scrollTop += scroll_by;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Finder;
|
||||
34
user/plugins/flex-objects/app/utils/get-filters.js
Normal file
34
user/plugins/flex-objects/app/utils/get-filters.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default () => {
|
||||
const inputs = document.querySelectorAll('#pages-filters input[name]');
|
||||
const filters = {};
|
||||
const trackMulti = [];
|
||||
|
||||
inputs.forEach((filter) => {
|
||||
if (filter.type === 'checkbox') {
|
||||
if (filter.indeterminate || filter.checked) {
|
||||
if (filter.name.match(/\[]$/)) {
|
||||
const name = filter.name.replace(/\[]$/, '');
|
||||
if (!filters[name]) {
|
||||
filters[name] = [];
|
||||
}
|
||||
|
||||
if (!trackMulti.includes(name)) {
|
||||
trackMulti.push(name);
|
||||
}
|
||||
|
||||
filters[name].push(filter.value);
|
||||
} else {
|
||||
filters[filter.name] = filter.value;
|
||||
}
|
||||
}
|
||||
} else if (filter.value) {
|
||||
filters[filter.name] = filter.value;
|
||||
}
|
||||
});
|
||||
|
||||
trackMulti.forEach((multi) => {
|
||||
filters[multi] = filters[multi].join(',');
|
||||
});
|
||||
|
||||
return filters;
|
||||
};
|
||||
44
user/plugins/flex-objects/app/utils/indeterminate.js
Normal file
44
user/plugins/flex-objects/app/utils/indeterminate.js
Normal file
@@ -0,0 +1,44 @@
|
||||
document.addEventListener('click', (event) => {
|
||||
const wrapper = event.target.closest('.checkboxes.indeterminate');
|
||||
if (wrapper) {
|
||||
event.preventDefault();
|
||||
const checkbox = wrapper.querySelector('input[type="checkbox"]:not([disabled])');
|
||||
const checkStatus = wrapper.dataset._checkStatus;
|
||||
wrapper.classList.remove('status-checked', 'status-unchecked', 'status-indeterminate');
|
||||
|
||||
switch (checkStatus) {
|
||||
// checked, going indeterminate
|
||||
case '1':
|
||||
wrapper.dataset._checkStatus = '2';
|
||||
checkbox.indeterminate = true;
|
||||
checkbox.checked = false;
|
||||
checkbox.value = 0;
|
||||
wrapper.classList.add('status-indeterminate');
|
||||
break;
|
||||
|
||||
// indeterminate, going unchecked
|
||||
case '2':
|
||||
wrapper.dataset._checkStatus = '0';
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = false;
|
||||
checkbox.value = '';
|
||||
wrapper.classList.add('status-unchecked');
|
||||
break;
|
||||
|
||||
// unchecked, going checked
|
||||
case '0':
|
||||
default:
|
||||
wrapper.dataset._checkStatus = '1';
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = true;
|
||||
checkbox.value = 1;
|
||||
wrapper.classList.add('status-checked');
|
||||
break;
|
||||
}
|
||||
|
||||
const input = new CustomEvent('input', { detail: { target: checkbox }});
|
||||
document.dispatchEvent(input);
|
||||
}
|
||||
});
|
||||
|
||||
(document.querySelectorAll('input[type="checkbox"][indeterminate="true"]') || []).forEach((input) => { input.indeterminate = true; });
|
||||
Reference in New Issue
Block a user