This commit is contained in:
Loïc Guibert
2022-09-30 20:02:02 +01:00
commit 66dafc36c3
2561 changed files with 454489 additions and 0 deletions

View 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();
});

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

View 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();
}
});

View 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');
}
});
}

View 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>

View 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: ''
}
};

View File

@@ -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>

View 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>

View 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>

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

View File

@@ -0,0 +1,3 @@
import './list';
import './columns';
import './filters';

View 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;

View 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;
};

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