@teipublisher/pb-components 1.24.19 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ import { Registry } from "./registry.js";
2
+
3
+ /**
4
+ * Connector for the corporate archive of Georgfischer AG.
5
+ */
6
+ export class GF extends Registry {
7
+
8
+ constructor(configElem) {
9
+ super(configElem);
10
+ this._api = configElem.getAttribute('api');
11
+ }
12
+
13
+ async query(key) {
14
+ const results = [];
15
+
16
+ const register = this.getRegister();
17
+ const url = `https://archives.georgfischer.com/api/${register}?search=${encodeURIComponent(key)}`;
18
+ const label = this.getLabelField();
19
+ return new Promise((resolve) => {
20
+ fetch(url)
21
+ .then(response => response.json())
22
+ .then(json => {
23
+ if (!json.data) {
24
+ resolve({
25
+ totalItems: 0,
26
+ items: []
27
+ });
28
+ return;
29
+ }
30
+ json.data.forEach(item => {
31
+ if ((this._register === 'organization' && item.authority_type === 'Person') ||
32
+ (this._register === 'person' && item.authority_type !== 'Person')) {
33
+ return;
34
+ }
35
+ const result = {
36
+ register: this._register,
37
+ id: (this._prefix ? `${this._prefix}-${item.id}` : item.id),
38
+ label: item[label],
39
+ details: `${item.id}`,
40
+ link: `https://archives.georgfischer.com/api/${register}/${item.id}`,
41
+ strings: [item[label]],
42
+ provider: 'GF'
43
+ };
44
+ results.push(result);
45
+ });
46
+ resolve({
47
+ totalItems: json.meta.total,
48
+ items: results,
49
+ });
50
+ })
51
+ .catch((reason) => Promise.reject(reason));
52
+ })
53
+ }
54
+
55
+ info(key, container) {
56
+ if (!key) {
57
+ return Promise.resolve({});
58
+ }
59
+ const id = this._prefix ? key.substring(this._prefix.length + 1) : key;
60
+ const label = this.getLabelField();
61
+ return new Promise((resolve) => {
62
+ this.getRecord(id)
63
+ .then(json => {
64
+ const died = json.data.death ? `† ${json.data.death}` : '';
65
+ const dates = json.data.birth ? `<p>* ${json.data.birth} ${died}</p>` : '';
66
+ const note = json.data.note_bio ? `<p>${json.data.note_bio}</p>` : '';
67
+ const output = `
68
+ <h3 class="label"><a href="https://${json.wikipediaURL}" target="_blank">${json.data[label]}</a></h3>
69
+ ${dates}
70
+ ${note}
71
+ `;
72
+ container.innerHTML = output;
73
+ resolve({
74
+ id: (this._prefix ? `${this._prefix}-${json.data.id}` : json.data.id),
75
+ strings: [json.data[label]]
76
+ });
77
+ });
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Retrieve a raw JSON record for the given key as returned by the endpoint.
83
+ *
84
+ * @param {string} key the key to look up
85
+ * @returns {Promise<any>} promise resolving to the JSON record returned by the endpoint
86
+ */
87
+ async getRecord(key) {
88
+ const id = key.replace(/^.*-([^-]+)$/, '$1');
89
+ const url = `https://archives.georgfischer.com/api/${this.getRegister()}/${id}`;
90
+ return fetch(url)
91
+ .then(response => response.json())
92
+ .then(json => {
93
+ const output = Object.assign({}, json);
94
+ output.name = json.data[this.getLabelField()];
95
+ switch (this._register) {
96
+ case 'place':
97
+ output.country = json.data.country;
98
+ output.location = json.data.location.coordinates;
99
+ output.links = json.data.links.map((link) => link.url);
100
+ break;
101
+ case 'person':
102
+ output.birth = json.data.birth;
103
+ output.death = json.data.death;
104
+ output.note = json.data.note_bio;
105
+ output.links = [`https://${json.wikipediaURL}`];
106
+ break;
107
+ default:
108
+ break;
109
+ }
110
+ return output;
111
+ })
112
+ .catch((reason) => Promise.reject(reason));
113
+ }
114
+
115
+ getLabelField() {
116
+ let label;
117
+ switch (this._register) {
118
+ case 'term':
119
+ label = 'label';
120
+ break;
121
+ default:
122
+ label = 'fullname';
123
+ break;
124
+ }
125
+ return label;
126
+ }
127
+
128
+ getRegister() {
129
+ if (this._api) {
130
+ return this._api;
131
+ }
132
+ let register;
133
+ switch(this._register) {
134
+ case 'person':
135
+ case 'organization':
136
+ register = 'actors';
137
+ break;
138
+ case 'place':
139
+ register = 'places';
140
+ break;
141
+ case 'term':
142
+ register = 'keywords';
143
+ break;
144
+ case 'abbreviation':
145
+ register = 'abbreviations';
146
+ break;
147
+ default:
148
+ register = this._register;
149
+ }
150
+ return register;
151
+ }
152
+ }
package/src/dts-client.js CHANGED
@@ -4,6 +4,7 @@ import { translate } from "./pb-i18n.js";
4
4
  import '@polymer/iron-ajax';
5
5
  import '@polymer/iron-icon';
6
6
  import '@polymer/iron-icons';
7
+ import '@polymer/paper-button';
7
8
 
8
9
  /**
9
10
  * A client for the Distributed Text Services (DTS) protocol. This defines an API
@@ -13,6 +14,13 @@ import '@polymer/iron-icons';
13
14
  * @slot toolbar - toolbar area
14
15
  * @slot pagination - pagination area
15
16
  *
17
+ * @csspart parent-link - Link to parent collection
18
+ * @csspart collection-title - Collection title
19
+ * @csspart title - Member title
20
+ * @csspart author - Author
21
+ * @csspart license - License information
22
+ * @csspart link - Links
23
+ *
16
24
  * @fires pb-start-update - Fired before the element updates its content
17
25
  * @fires pb-results-received - Fired when results are received from the server
18
26
  * @fires pb-end-update - Fired after the element has finished updating its content
@@ -44,6 +52,12 @@ export class DtsClient extends pbMixin(LitElement) {
44
52
  };
45
53
  }
46
54
 
55
+ constructor() {
56
+ super();
57
+ this._parentCollections = [];
58
+ this.collection = 'default';
59
+ }
60
+
47
61
  connectedCallback() {
48
62
  super.connectedCallback();
49
63
 
@@ -51,7 +65,7 @@ export class DtsClient extends pbMixin(LitElement) {
51
65
  this.page = this.getParameter('page');
52
66
 
53
67
  this.subscribeTo('dts-endpoint', (ev) => {
54
- this._setEndpoint(ev.detail.endpoint, ev.detail.reload);
68
+ this._setEndpoint(ev.detail.endpoint, ev.detail.collection, ev.detail.reload);
55
69
  });
56
70
  this.subscribeTo('pb-load', (ev) => {
57
71
  this.page = ev.detail.params.page;
@@ -68,11 +82,11 @@ export class DtsClient extends pbMixin(LitElement) {
68
82
  this.signalReady();
69
83
  }
70
84
 
71
- _setEndpoint(endpoint, reload) {
85
+ _setEndpoint(endpoint, collection, reload) {
72
86
  if (!reload) {
73
87
  this.page = null;
74
- this.collection = null;
75
88
  }
89
+ this.collection = collection;
76
90
  this._configureEndpoint(endpoint);
77
91
  this.baseUri = endpoint;
78
92
  }
@@ -86,14 +100,24 @@ export class DtsClient extends pbMixin(LitElement) {
86
100
  }
87
101
  }
88
102
 
89
- _navigate(ev, member) {
103
+ _navigate(ev, member, downwards = true) {
90
104
  ev.preventDefault();
91
- this.collection = member['@id'];
105
+ if (downwards) {
106
+ this._parentCollections.push(this.collection);
107
+ }
108
+ this.collection = member && typeof member === 'object' ? member['@id'] : member;
92
109
  this.page = null;
93
110
  console.log('<dts-client> navigating to collection %s', this.collection);
94
111
  this._update();
95
112
  }
96
113
 
114
+ _navigateUp(ev) {
115
+ if (this._parentCollections.length === 0) {
116
+ return;
117
+ }
118
+ this._navigate(ev, this._parentCollections.pop(), false);
119
+ }
120
+
97
121
  _preview(ev, member) {
98
122
  ev.preventDefault();
99
123
  this.emitTo('pb-start-update');
@@ -236,8 +260,17 @@ export class DtsClient extends pbMixin(LitElement) {
236
260
  _renderClient() {
237
261
  return html`
238
262
  <div class="uri">${this.baseUri}</div>
239
- <h3>${this.data ? this.data.title : 'Loading ...'}</h3>
263
+ <h3 part="collection-title">${this.data ? this.data.title : 'Loading ...'}</h3>
240
264
  <slot name="pagination"></slot>
265
+ ${
266
+ this._parentCollections.length > 0 || this.collection ?
267
+ html`
268
+ <paper-button part="parent-link" @click="${this._navigateUp}">
269
+ <iron-icon icon="icons:arrow-upward"></iron-icon>
270
+ ${translate('browse.up')}
271
+ </paper-button>`
272
+ : null
273
+ }
241
274
  ${this.data ? this._renderMembers() : ''}
242
275
  `;
243
276
  }
@@ -255,9 +288,11 @@ export class DtsClient extends pbMixin(LitElement) {
255
288
  return html`
256
289
  <iron-icon icon="icons:folder-open"></iron-icon>
257
290
  <div class="details">
258
- <h4 class="collection">
259
- <a href="#" @click="${(ev) => this._navigate(ev, member)}">${member.title}</a>
260
- </h4>
291
+ <a href="#" @click="${(ev) => this._navigate(ev, member)}" part="link">
292
+ <h4 class="collection" part="collection-title">
293
+ ${member.title}
294
+ </h4>
295
+ </a>
261
296
  </div>
262
297
  `;
263
298
  }
@@ -266,11 +301,13 @@ export class DtsClient extends pbMixin(LitElement) {
266
301
  <iron-icon icon="icons:code"></iron-icon>
267
302
  <div class="details">
268
303
  <div>
269
- <h4>
270
- <a href="#" @click="${(ev) => this._preview(ev, member)}">${member.title}</a>
271
- </h4>
272
- <p class="creator">${DtsClient._getCreator(member)}</p>
273
- ${license ? html`<p class="license"><a href="${license}">${translate('dts.licence')}</a></p>` : ''}
304
+ <a href="#" @click="${(ev) => this._preview(ev, member)}" part="link">
305
+ <h4 part="title">
306
+ ${member.title}
307
+ </h4>
308
+ </a>
309
+ <p part="creator" class="creator">${DtsClient._getCreator(member)}</p>
310
+ ${license ? html`<p part="license" class="license"><a href="${license}">${translate('dts.licence')}</a></p>` : ''}
274
311
  </div>
275
312
  <iron-icon title="${translate('dts.import')}" icon="icons:file-download"
276
313
  @click="${(ev) => this._download(ev, member)}">
@@ -95,14 +95,16 @@ export class DtsSelectEndpoint extends pbMixin(LitElement) {
95
95
  if (!newEndpoint) {
96
96
  return;
97
97
  }
98
- this.setParameter('endpoint', newEndpoint);
98
+ const endpoint = this.endpoints.find((endp) => endp.url === newEndpoint);
99
+ this.setParameter('endpoint', endpoint.url);
99
100
  this.pushHistory('dts-endpoint');
100
101
  console.log('<dts-select-endpoint> Setting endpoint to %s', newEndpoint);
101
102
  this.emitTo('dts-endpoint', {
102
- endpoint: newEndpoint,
103
+ endpoint: endpoint.url,
104
+ collection: endpoint.collection,
103
105
  reload: !this.endpoint
104
106
  });
105
- this.endpoint = newEndpoint;
107
+ this.endpoint = endpoint.url;
106
108
  }
107
109
  }
108
110
  customElements.define('dts-select-endpoint', DtsSelectEndpoint);
@@ -56,6 +56,7 @@ import './pb-authority-lookup.js';
56
56
  import './pb-message.js';
57
57
  import './pb-blacklab-results.js';
58
58
  import './pb-blacklab-highlight.js';
59
+ import './pb-table-grid.js';
59
60
 
60
61
  import '@polymer/iron-icons/editor-icons';
61
62
  import '@polymer/iron-icons/social-icons';
package/src/pb-load.js CHANGED
@@ -160,6 +160,12 @@ export class PbLoad extends pbMixin(LitElement) {
160
160
  }
161
161
  this.wait(() => this.load());
162
162
  });
163
+ } else {
164
+ PbLoad.waitOnce('pb-page-ready', (data) => {
165
+ if (data && data.language) {
166
+ this.language = data.language;
167
+ }
168
+ });
163
169
  }
164
170
  }
165
171
 
@@ -0,0 +1,66 @@
1
+ import { LitElement } from 'lit-element';
2
+ import { html } from "gridjs";
3
+ import './pb-popover.js';
4
+
5
+ /**
6
+ * Defines a column within `pb-table-grid`.
7
+ */
8
+ export class PbTableColumn extends LitElement {
9
+ static get properties() {
10
+ return {
11
+ /**
12
+ * Column heading to display
13
+ */
14
+ label: {
15
+ type: String
16
+ },
17
+ /**
18
+ * Name of the JSON property containing the data
19
+ */
20
+ property: {
21
+ type: String
22
+ },
23
+ /**
24
+ * Should the column support sorting?
25
+ */
26
+ sort: {
27
+ type: Boolean
28
+ },
29
+ /**
30
+ * Optional fixed width of the column (e.g. '200px' or '30%')
31
+ */
32
+ width: {
33
+ type: String
34
+ },
35
+ ...super.properties
36
+ };
37
+ }
38
+
39
+ constructor() {
40
+ super();
41
+ this.label = 'no-label';
42
+ this.property = null;
43
+ this.sort = false;
44
+ this.width = null;
45
+ }
46
+
47
+ connectedCallback() {
48
+ super.connectedCallback();
49
+ }
50
+
51
+ data() {
52
+ const config = {
53
+ name: this.label,
54
+ sort: { enabled: this.sort },
55
+ formatter: (cell) => html(cell)
56
+ };
57
+ if (this.property) {
58
+ config.id = this.property;
59
+ }
60
+ if (this.width) {
61
+ config.width = this.width;
62
+ }
63
+ return config;
64
+ }
65
+ }
66
+ customElements.define('pb-table-column', PbTableColumn);
@@ -0,0 +1,190 @@
1
+ import { LitElement, html, css } from 'lit-element';
2
+ import { Grid } from "gridjs";
3
+ import { pbMixin } from './pb-mixin.js';
4
+ import { resolveURL } from './utils.js';
5
+ import '@polymer/paper-input/paper-input';
6
+ import '@polymer/iron-icons';
7
+ import '@polymer/iron-form';
8
+ import '@polymer/paper-icon-button';
9
+ import './pb-table-column.js';
10
+
11
+ /**
12
+ * A table grid based on [gridjs](https://gridjs.io/), which loads its data from a server endpoint
13
+ * specified in `source`. If `source` is a relative URI, it will be resolved relative to the
14
+ * TEI Publisher endpoint.
15
+ *
16
+ * The JSON data returned by the endpoint should be an object with two properties:
17
+ *
18
+ * * `count`: the overall number of rows available on the server
19
+ * * `results`: an array containing each record as an object
20
+ *
21
+ * The parameters send to the server are as follows:
22
+ *
23
+ *
24
+ * Parameter | Description
25
+ * ---------|----------
26
+ * limit | number of records to return for each page
27
+ * start | start offset from which to return records
28
+ * order | the id of the column to sort by
29
+ * dir | sort direction: either 'asc' or 'desc'
30
+ * search | an optional search string entered by the user
31
+ *
32
+ * Table columns are configured via nested `<pb-table-column>` elements:
33
+ *
34
+ * ```html
35
+ * <pb-table-column label="Name" property="name" sort width="33%"></pb-table-column>
36
+ * <pb-table-column label="Born" property="birth"></pb-table-column>
37
+ * <pb-table-column label="Died" property="death"></pb-table-column>
38
+ * ```
39
+ */
40
+ export class PbTableGrid extends pbMixin(LitElement) {
41
+ static get properties() {
42
+ return {
43
+ /**
44
+ * URI of the server-side endpoint to retrieve data from.
45
+ * Relative URIs are resolved relative to the configured TEI Publisher endpoint.
46
+ */
47
+ source: {
48
+ type: String
49
+ },
50
+ /**
51
+ * Path to the gridjs theme CSS files.
52
+ */
53
+ cssPath: {
54
+ type: String,
55
+ attribute: 'css-path'
56
+ },
57
+ /**
58
+ * If specified, columns (without a fixed width) will be resizable.
59
+ */
60
+ resizable: {
61
+ type: Boolean
62
+ },
63
+ /**
64
+ * If specified, enable server-side search.
65
+ */
66
+ search: {
67
+ type: Boolean
68
+ },
69
+ _params: {
70
+ type: Object
71
+ },
72
+ ...super.properties
73
+ };
74
+ }
75
+
76
+ constructor() {
77
+ super();
78
+ this.cssPath = '../css/gridjs';
79
+ this._params = {};
80
+ this.resizable = false;
81
+ this.search = false;
82
+ }
83
+
84
+ connectedCallback() {
85
+ super.connectedCallback();
86
+
87
+ this.subscribeTo('pb-search-resubmit', (ev) => {
88
+ this._params = Object.assign({}, ev.detail.params);
89
+ this._submit();
90
+ });
91
+
92
+ window.addEventListener('popstate', (ev) => {
93
+ this._params = ev.state;
94
+ this._submit();
95
+ });
96
+ }
97
+
98
+ firstUpdated() {
99
+ const table = this.shadowRoot.getElementById('table');
100
+
101
+ const pbColumns = this.querySelectorAll('pb-table-column');
102
+ const columns = [];
103
+ pbColumns.forEach((column) => columns.push(column.data()));
104
+
105
+ PbTableGrid.waitOnce('pb-page-ready', () => {
106
+ this._params = this.getParameters();
107
+ const url = this.toAbsoluteURL(this.source);
108
+ const config = {
109
+ columns,
110
+ resizable: this.resizable,
111
+ server: {
112
+ url,
113
+ then: data => data.results,
114
+ total: data => data.count
115
+ },
116
+ sort: {
117
+ multiColumn: false,
118
+ server: {
119
+ url: (prev, cols) => {
120
+ if (!cols.length) return prev;
121
+ const col = cols[0];
122
+ return `${prev}${prev.indexOf('?') > -1 ? '&' : '?'}order=${columns[col.index].id}&dir=${col.direction === 1 ? 'asc' : 'desc'}`;
123
+ }
124
+ }
125
+ },
126
+ pagination: {
127
+ enabled: true,
128
+ limit: 10,
129
+ server: {
130
+ url: (prev, page, limit) => {
131
+ const form = this.shadowRoot.getElementById('form');
132
+ if (form) {
133
+ Object.assign(this._params, form.serializeForm());
134
+ }
135
+ this._params.limit = limit;
136
+ this._params.start = page * limit;
137
+ this.setParameters(this._params);
138
+ this.pushHistory('grid', this._params);
139
+
140
+ return `${prev}${prev.indexOf('?') > -1 ? '&' : '?'}${new URLSearchParams(this._params).toString()}`;
141
+ }
142
+ }
143
+ }
144
+ };
145
+ this.grid = new Grid(config);
146
+ this.grid.on('load', () => {
147
+ this.emitTo('pb-results-received', {
148
+ "params": this._params
149
+ });
150
+ });
151
+
152
+ this.grid.render(table);
153
+ });
154
+ }
155
+
156
+ _submit() {
157
+ this.grid.forceRender();
158
+ }
159
+
160
+ render() {
161
+ const themes = resolveURL(this.cssPath);
162
+ return html`
163
+ <link href="${themes}/mermaid.min.css" rel="stylesheet">
164
+ ${
165
+ this.search ? html`
166
+ <iron-form id="form">
167
+ <form action="">
168
+ <paper-input id="search" name="search" label="Search" @keyup="${(e) => e.keyCode == 13 ? this._submit() : null}">
169
+ <paper-icon-button icon="search" @click="${this._submit}" slot="suffix"></paper-icon-button>
170
+ </paper-input>
171
+ </form>
172
+ </iron-form>
173
+ ` : null
174
+ }
175
+ <div id="table"></div>
176
+ `;
177
+ }
178
+
179
+ static get styles() {
180
+ return css`
181
+ :host {
182
+ display: block;
183
+ }
184
+ button {
185
+ border: 0;
186
+ }
187
+ `;
188
+ }
189
+ }
190
+ customElements.define('pb-table-grid', PbTableGrid);