@teipublisher/pb-components
Version:
Collection of webcomponents underlying TEI Publisher
606 lines (562 loc) • 17.3 kB
JavaScript
import { html, css } from 'lit-element';
import { PbLoad } from './pb-load.js';
import { waitOnce } from './pb-mixin.js';
import { translate } from './pb-i18n.js';
import { themableMixin } from './theming.js';
import { cmpVersion } from './utils.js';
import { registry } from './urls.js';
import '@polymer/paper-input/paper-input.js';
import '@polymer/paper-button';
import '@polymer/paper-dropdown-menu/paper-dropdown-menu.js';
import '@polymer/paper-listbox';
import '@polymer/paper-dialog';
import '@polymer/paper-dialog-scrollable';
import '@polymer/iron-ajax';
import '@cwmr/paper-autocomplete/paper-autocomplete-suggestions.js';
/**
* Component to browse through a collection of documents with sorting, filtering and facets.
*
* @slot toolbar - toolbar area
* @slot - unnamed default slot
* @slot footer - footer area
*
* @fires pb-collection - Sent to inform e.g. pb-upload about current collection
* @fires pb-search-resubmit - When received, set facet values as received from the event
* @fires pb-login - When received, refresh the view if the user changed
*
* @cssprop --pb-search-suggestions-background - Background for the autocomplete suggestions for the filter field
* @cssprop --pb-search-suggestions-color - Text color for the autocomplete suggestion for the filter field
* @cssprop --pb-search-label-color - Determines the color of small label above the sort by/filter by/filter fields
* @cssprop --pb-search-input-color - Determines the color of the text in the sort by/filter by/filter fields
* @cssprop --pb-search-focus-color - Color of the field labels and underline when in focus
* @cssprop --pb-browse-toolbar-justify-content - How to justify the browse toolbar content, following flexbox justify-content property e.g. center, space-evenly, start...
*
* @csspart delete-button - the delete button
* @csspart sort-dropdown - dropdown for sorting
* @csspart filter-dropdown - dropdown for filtering
* @csspart filter-input - input for filtering
*/
export class PbBrowseDocs extends themableMixin(PbLoad) {
static get properties() {
return {
...super.properties,
sortBy: {
type: String,
attribute: 'sort-by',
},
sortOptions: {
type: Array,
attribute: 'sort-options',
},
sortLabel: {
type: String,
},
filter: {
type: String,
},
filterBy: {
type: String,
attribute: 'filter-by',
},
filterOptions: {
type: Array,
attribute: 'filter-options',
},
filterByLabel: {
type: String,
},
filterPlaceholderLabel: {
type: String,
},
collection: {
type: String,
},
facets: {
type: Object,
},
/** Id of the pb-login element to connect to */
login: {
type: String,
},
/**
* If set, requires the logged in user to be member of
* the given group.
*/
group: {
type: String,
},
subforms: {
type: String,
},
/**
* If set, rewrite URLs to load pages as static HTML files,
* so no TEI Publisher instance is required
*/
static: {
type: Boolean,
},
_file: {
type: String,
},
_selected: {
type: Array,
},
_allowModification: {
type: Boolean,
},
_suggestions: {
type: Array,
},
};
}
constructor() {
super();
this.sortOptions = [];
this.sortLabel = 'browse.sort';
this.sortBy = 'default';
this.filter = '';
this.filterOptions = [
{
label: 'Title',
value: 'title',
},
];
this.filterByLabel = 'browse.filter';
this.filterPlaceholderLabel = 'browse.filterPlaceholder';
this.filterBy = 'title';
this._allowModification = false;
this._suggestions = [];
this.static = false;
}
connectedCallback() {
super.connectedCallback();
waitOnce('pb-page-ready', () => {
if (registry.state.sort) {
this.sortBy = registry.state.sort;
}
if (registry.state.filter) {
this.filter = registry.state.filter;
this.filterBy = registry.state.filterBy || this.filterBy;
}
this.facets = {};
// registry get state by regex
// this.facets = registry.getParametersMatching(this, /^facet-.*$/)
Object.keys(registry.state).forEach(key => {
if (/^facet-.*$/.test(key)) {
const param = registry.state[key];
if (this.facets[key]) {
this.facets[key].push(param);
} else if (Array.isArray(param)) {
this.facets[key] = param;
} else {
this.facets[key] = [param];
}
}
});
this.collection = registry.state.collection;
if (this.collection) {
registry.replace(this, {
collection: this.collection,
});
}
registry.subscribe(this, state => {
this.collection = state.collection;
this.load();
});
});
this.subscribeTo('pb-search-resubmit', this._facets.bind(this));
this.subscribeTo(
'pb-login',
ev => {
if (ev.detail.userChanged) {
this._facets(ev);
}
},
[],
);
document.addEventListener('pb-i18n-update', () => {
// clear paper-listbox selection after language updates
const lb = this.shadowRoot.getElementById('sort-list');
let old = lb.selected;
lb.selected = undefined;
lb.selected = old;
const fl = this.shadowRoot.getElementById('filter-list');
old = fl.selected;
fl.selected = undefined;
fl.selected = old;
});
}
firstUpdated() {
waitOnce('pb-page-ready', options => {
const loader = this.shadowRoot.getElementById('autocompleteLoader');
if (cmpVersion(options.apiVersion, '1.0.0') >= 0) {
loader.url = `${options.endpoint}/api/search/autocomplete`;
if (!this.url) {
this.url = 'api/collection';
}
} else {
loader.url = `${options.endpoint}/modules/autocomplete.xql`;
if (!this.url) {
this.url = 'collection/';
}
}
});
this.shadowRoot
.getElementById('autocomplete')
.addEventListener('autocomplete-change', this._autocomplete.bind(this));
if (this.login) {
const login = document.getElementById(this.login);
if (!login) {
console.error('<pb-browse-docs> connected pb-login element not found!');
} else {
this.subscribeTo(
'pb-login',
ev => {
this._allowModification = this._loggedIn(ev.detail.user, ev.detail.group);
},
[],
);
this._allowModification = login.loggedIn && this._loggedIn(login.user, login.groups);
}
}
this.shadowRoot
.getElementById('sort-list')
.addEventListener('selected-item-changed', this._sort.bind(this));
this.shadowRoot
.getElementById('delete')
.addEventListener('click', this._handleDelete.bind(this));
super.firstUpdated();
}
render() {
return html`
<custom-style>
<style>
:host {
--suggestions-item: {
color: var(--pb-search-suggestions-color, black);
}
--suggestions-wrapper: {
background: var(--pb-search-suggestions-background, white);
}
}
</style>
</custom-style>
<slot name="header"></slot>
<div class="toolbar">
<paper-dropdown-menu id="sort" label="${translate(this.sortLabel)}" part="sort-dropdown">
<paper-listbox
id="sort-list"
selected="${this.sortBy}"
slot="dropdown-content"
class="dropdown-content"
attr-for-selected="value"
>
${this.sortOptions.map(
option =>
html`<paper-item value="${option.value}">${translate(option.label)}</paper-item>`,
)}
</paper-listbox>
</paper-dropdown-menu>
<div>
<paper-dropdown-menu
id="filterSelect"
label="${translate(this.filterByLabel)}"
part="filter-dropdown"
>
<paper-listbox
id="filter-list"
selected="${this.filterBy}"
slot="dropdown-content"
class="dropdown-content"
attr-for-selected="value"
-item-changed="${this._filterChanged}"
>
${this.filterOptions.map(
option =>
html`<paper-item value="${option.value}">${translate(option.label)}</paper-item>`,
)}
</paper-listbox>
</paper-dropdown-menu>
<paper-input
id="filterString"
type="search"
name="filter"
label="${translate(this.filterPlaceholderLabel)}"
value="${this.filter}"
="${this._handleEnter}"
part="filter-input"
>
<iron-icon icon="search" ="${this._filter}" slot="prefix"></iron-icon>
</paper-input>
<paper-autocomplete-suggestions
id="autocomplete"
for="filterString"
source="${this._suggestions}"
remote-source
></paper-autocomplete-suggestions>
</div>
</div>
<div class="toolbar">
<slot name="toolbar"></slot>
<paper-button
id="delete"
part="delete-button"
title="${translate('browse.delete')}"
class="${this._canModify(this._allowModification)}"
>
<iron-icon icon="delete"></iron-icon>
<span class="label">${translate('browse.delete')}</span>
</paper-button>
</div>
<slot></slot>
<slot name="footer"></slot>
<iron-ajax
id="loadContent"
verbose
handle-as="text"
method="get"
with-credentials
="${this._handleContent}"
="${this._handleError}"
></iron-ajax>
<iron-ajax
id="autocompleteLoader"
verbose
handle-as="json"
method="get"
with-credentials
="${this._updateSuggestions}"
></iron-ajax>
<paper-dialog id="deleteDialog">
<h2>${translate('browse.delete')}</h2>
<paper-dialog-scrollable>
<p>
${translate('browse.confirmDeletion', {
count: this._selected ? this._selected.length : 0,
})}
</p>
</paper-dialog-scrollable>
<div class="buttons">
<paper-button dialog-confirm="dialog-confirm" autofocus ="${this._confirmDelete}"
>${translate('dialogs.yes')}</paper-button
>
<paper-button dialog-confirm="dialog-cancel">${translate('dialogs.no')}</paper-button>
</div>
</paper-dialog>
<paper-dialog id="errorDialog">
<h2>${translate('dialogs.error')}</h2>
<paper-dialog-scrollable></paper-dialog-scrollable>
<div class="buttons">
<paper-button dialog-confirm="dialog-confirm" autofocus="autofocus">
${translate('dialogs.close')}
</paper-button>
</div>
</paper-dialog>
`;
}
static get styles() {
return css`
:host {
display: block;
--paper-input-container-color: var(--pb-search-label-color, var(--paper-grey-500, #303030));
--paper-input-container-input-color: var(
--pb-search-input-color,
var(--pb-color-primary, #000000)
);
--paper-input-container-focus-color: var(
--pb-search-focus-color,
var(--paper-grey-500, #303030)
);
}
.toolbar {
display: flex;
justify-content: var(--pb-browse-toolbar-justify-content);
}
[name='toolbar'] {
flex: 1 0;
}
#sort {
display: block;
}
#filterString {
position: relative;
display: inline-block;
vertical-align: bottom;
}
.hidden {
display: none;
}
`;
}
getURL(params) {
if (this.static) {
// use a static URL
return `collections/${this.collection ? `${this.collection}/` : ''}${
params.start || '1'
}.html`;
}
const url = super.getURL(params);
return this.collection ? `${url}/${this.collection}` : url;
}
prepareParameters(params) {
params = this._paramsFromSubforms(params);
params.sort = this.sortBy;
if (this.filter) {
params.filter = this.filter;
params.browse = this.filterBy;
}
if (this.facets) {
params = Object.assign(params, this.facets);
}
return params;
}
_paramsFromSubforms(params) {
if (this.subforms) {
document.querySelectorAll(this.subforms).forEach(form => {
if (form.serializeForm) {
Object.assign(params, form.serializeForm());
}
});
}
return params;
}
/**
* returns selected documents.
*
* @returns {Array}
*/
getSelected() {
const selected = [];
if (this.container) {
document.querySelectorAll(this.container).forEach(container =>
container.querySelectorAll('.document-select paper-checkbox[checked]').forEach(checkbox => {
selected.push(checkbox.value);
}),
);
} else {
this.querySelectorAll('.document-select paper-checkbox[checked]').forEach(checkbox => {
selected.push(checkbox.value);
});
}
return selected;
}
_filter() {
const filter = this.shadowRoot.getElementById('filterString').value;
const filterBy = this.shadowRoot.getElementById('filter-list').selected;
if (typeof filter !== 'undefined') {
console.log('<pb-browse-docs> Filter by %s', filter);
this.filter = filter;
registry.commit(this, { filter, filterBy });
this.load();
}
}
_filterChanged() {
const filterBy = this.shadowRoot.getElementById('filter-list').selected;
if (filterBy && filterBy !== this.filterBy) {
console.log('<pb-browse-docs> Filtering on %s', filterBy);
this.filterBy = filterBy;
}
}
_sort() {
const sortBy = this.shadowRoot.getElementById('sort-list').selected;
if (sortBy && sortBy !== this.sortBy) {
console.log('<pb-browse-docs> Sorting by %s', sortBy);
this.sortBy = sortBy;
registry.commit(this, { sort: sortBy });
this.load();
}
}
_facets(ev) {
if (ev.detail && ev.detail.params) {
registry.clearParametersMatching(this, /^(all-|facet-).*/);
this.facets = ev.detail.params;
this.start = 1;
registry.commit(this, ev.detail.params);
}
this.load();
}
_onLoad(content) {
window.scrollTo(0, 0);
const div = content.querySelector('[data-root]');
const collection = div && div.getAttribute('data-root');
const writable = div && div.classList.contains('writable');
this.emitTo('pb-collection', {
writable,
collection,
});
document.querySelectorAll('[can-write]').forEach(elem => {
elem.disabled = !writable;
});
content.querySelectorAll('[data-collection]').forEach(link => {
link.addEventListener('click', ev => {
ev.preventDefault();
this.collection = link.getAttribute('data-collection');
this.start = 1;
registry.commit(this, { collection: this.collection });
console.log('<pb-browse-docs> loading collection %s', this.collection);
this.load();
});
});
}
_handleDelete(target, ev) {
const deleteDialog = this.shadowRoot.getElementById('deleteDialog');
const selected = this.getSelected();
if (selected.length > 0) {
this._selected = selected;
deleteDialog.open();
}
}
_confirmDelete() {
if (!(this._file || this._selected)) {
return;
}
let files;
if (this._selected) {
files = this._selected;
} else {
files = [this._file];
}
console.log('<pb-browse-docs> Deleting %o', this._file);
const params = {
action: 'delete',
'docs[]': files,
};
this._file = null;
this._selected = null;
this.load(params);
}
_loggedIn(user, groups) {
if (user == null) {
return false;
}
if (this.group) {
if (!groups) {
return false;
}
return groups.indexOf(this.group) > -1;
}
return true;
}
_canModify(allowModification) {
return allowModification ? '' : 'hidden';
}
_autocomplete(ev) {
const autocompleteLoader = this.shadowRoot.getElementById('autocompleteLoader');
autocompleteLoader.params = {
query: ev.detail.option.text,
field: this.filterBy,
};
autocompleteLoader.generateRequest();
}
_updateSuggestions() {
const autocomplete = this.shadowRoot.getElementById('autocomplete');
const autocompleteLoader = this.shadowRoot.getElementById('autocompleteLoader');
autocomplete.suggestions(autocompleteLoader.lastResponse);
}
_handleEnter(e) {
if (e.keyCode === 13) {
this._filter();
}
}
}
customElements.define('pb-browse-docs', PbBrowseDocs);