@teipublisher/pb-components 1.43.5 → 1.44.1

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.
package/src/pb-mixin.js CHANGED
@@ -1,542 +1,542 @@
1
- import { cmpVersion } from './utils.js';
2
-
3
- if (!window.TeiPublisher) {
4
- window.TeiPublisher = {};
5
-
6
- TeiPublisher.url = new URL(window.location.href);
7
- }
8
-
9
- /**
10
- * Global set to record the names of the channels for which a
11
- * `pb-ready` event was fired.
12
- */
13
- const readyEventsFired = new Set();
14
-
15
- /**
16
- * Gobal map to record the initialization events which have
17
- * been received.
18
- */
19
- const initEventsFired = new Map();
20
-
21
- export function clearPageEvents() {
22
- initEventsFired.clear();
23
- }
24
-
25
- /**
26
- * Wait until the global event identified by name
27
- * has been fired once. This is mainly used to wait for initialization
28
- * events like `pb-page-ready`.
29
- *
30
- * @param {string} name
31
- * @param {Function} callback
32
- */
33
- export function waitOnce(name, callback) {
34
- if (initEventsFired.has(name)) {
35
- callback(initEventsFired.get(name));
36
- } else {
37
- document.addEventListener(name, (ev) => {
38
- initEventsFired.set(name, ev.detail);
39
- callback(ev.detail);
40
- }, {
41
- once: true
42
- });
43
- }
44
- }
45
-
46
- /**
47
- * Implements the core channel/event mechanism used by components in TEI Publisher
48
- * to communicate.
49
- *
50
- * As there might be several documents/fragments being displayed on a page at the same time,
51
- * a simple event mechanism is not enough for components to exchange messages. They need to
52
- * be able to target a specific view. The mechanism implemented by this mixin thus combines
53
- * events and channels. Components may emit an event into a named channel to which other
54
- * components might subscribe. For example, there might be a view which subscribes to the
55
- * channel *transcription* and another one subscribing to *translation*. By using distinct
56
- * channels, other components can address only one of the two.
57
- *
58
- * @polymer
59
- * @mixinFunction
60
- */
61
- export const pbMixin = (superclass) => class PbMixin extends superclass {
62
-
63
- static get properties() {
64
- return {
65
- /**
66
- * The name of the channel to subscribe to. Only events on a channel corresponding
67
- * to this property are listened to.
68
- */
69
- subscribe: {
70
- type: String
71
- },
72
- /**
73
- * Configuration object to define a channel/event mapping. Every property
74
- * in the object is interpreted as the name of a channel and its value should
75
- * be an array of event names to listen to.
76
- */
77
- subscribeConfig: {
78
- type: Object,
79
- attribute: 'subscribe-config'
80
- },
81
- /**
82
- * The name of the channel to send events to.
83
- */
84
- emit: {
85
- type: String
86
- },
87
- /**
88
- * Configuration object to define a channel/event mapping. Every property
89
- * in the object is interpreted as the name of a channel and its value should
90
- * be an array of event names to be dispatched.
91
- */
92
- emitConfig: {
93
- type: Object,
94
- attribute: 'emit-config'
95
- },
96
- /**
97
- * A selector pointing to other components this component depends on.
98
- * When method `wait` is called, it will wait until all referenced
99
- * components signal with a `pb-ready` event that they are ready and listening
100
- * to events.
101
- */
102
- waitFor: {
103
- type: String,
104
- attribute: 'wait-for'
105
- },
106
- _isReady: {
107
- type: Boolean
108
- },
109
- /**
110
- * Common property to disable the functionality associated with a component.
111
- * `pb-highlight` and `pb-popover` react to this.
112
- */
113
- disabled: {
114
- type: Boolean,
115
- reflect: true
116
- },
117
- _endpoint: {
118
- type: String
119
- },
120
- _apiVersion: {
121
- type: String
122
- }
123
- }
124
- }
125
-
126
- constructor() {
127
- super();
128
- this._isReady = false;
129
- this.disabled = false;
130
- this._subscriptions = new Map();
131
- }
132
-
133
- connectedCallback() {
134
- super.connectedCallback();
135
- waitOnce('pb-page-ready', (options) => {
136
- this._endpoint = options.endpoint;
137
- this._apiVersion = options.apiVersion;
138
- });
139
- }
140
-
141
- disconnectedCallback() {
142
- super.disconnectedCallback();
143
- this._subscriptions.forEach((handlers, type) => {
144
- handlers.forEach((handler) => {
145
- document.removeEventListener(type, handler);
146
- });
147
- });
148
- }
149
-
150
- /**
151
- * Enable or disable certain features of a component. Called by `pb-toggle-feature`
152
- * and `pb-select-feature` to change the components behaviour.
153
- *
154
- * By default only one command is known: `disable` will disable any interactive feature
155
- * of the component.
156
- *
157
- * @param {string} command name of an action to take or setting to be toggled
158
- * @param {Boolean} state the state to set the setting to
159
- */
160
- command(command, state) {
161
- if (command === 'disable') {
162
- this.disabled = state;
163
- }
164
- }
165
-
166
- /**
167
- * Wait for the components referenced by the selector given in property `waitFor`
168
- * to signal that they are ready to respond to events. Only wait for elements which
169
- * emit to one of the channels this component subscribes to.
170
- *
171
- * @param callback function to be called when all components are ready
172
- */
173
- wait(callback) {
174
- if (this.waitFor) {
175
- const targetNodes = Array.from(document.querySelectorAll(this.waitFor));
176
- const targets = targetNodes.filter(target => this.emitsOnSameChannel(target));
177
- const targetCount = targets.length;
178
- if (targetCount === 0) {
179
- // selector did not return any targets
180
- return callback();
181
- }
182
- let count = 0;
183
- targets.forEach((target) => {
184
- if (target._isReady) {
185
- count++;
186
- if (targetCount === count) {
187
- callback();
188
- }
189
- } else {
190
- const handler = target.addEventListener('pb-ready', (ev) => {
191
- if (ev.detail.source == this) {
192
- // same source: ignore
193
- return;
194
- }
195
- count++;
196
- if (targetCount === count) {
197
- target.removeEventListener('pb-ready', handler);
198
- callback();
199
- }
200
- });
201
- }
202
- });
203
- } else {
204
- callback();
205
- }
206
- }
207
-
208
- /**
209
- * Wait until a `pb-ready` event is received from one of the channels
210
- * this component subscribes to. Used internally by components which depend
211
- * on a particular `pb-view` to be ready and listening to events.
212
- *
213
- * @param callback function to be called when `pb-ready` is received
214
- */
215
- waitForChannel(callback) {
216
- // check first if a `pb-ready` event has already been received on one of the channels
217
- if (this.subscribeConfig) {
218
- for (const key in this.subscribeConfig) {
219
- this.subscribeConfig[key].forEach(t => {
220
- if (t === 'pb-ready' && readyEventsFired.has(key)) {
221
- return callback();
222
- }
223
- });
224
- }
225
- } else if (
226
- (this.subscribe && readyEventsFired.has(this.subscribe)) ||
227
- (!this.subscribe && readyEventsFired.has('__default__'))
228
- ) {
229
- return callback();
230
- }
231
-
232
- const listeners = this.subscribeTo('pb-ready', (ev) => {
233
- if (ev.detail._source == this) {
234
- return;
235
- }
236
- listeners.forEach(listener => document.removeEventListener('pb-ready', listener));
237
- callback();
238
- });
239
- }
240
-
241
- /**
242
- * Wait until the global event identified by name
243
- * has been fired once. This is mainly used to wait for initialization
244
- * events like `pb-page-ready`.
245
- *
246
- * @param {string} name
247
- * @param {Function} callback
248
- * @deprecated Use exported `waitOnce` function
249
- */
250
- static waitOnce(name, callback) {
251
- waitOnce(name, callback);
252
- }
253
-
254
- /**
255
- * Signal that the component is ready to respond to events.
256
- * Emits an event to all channels the component is registered with.
257
- */
258
- signalReady(name = 'pb-ready', data) {
259
- this._isReady = true;
260
- initEventsFired.set(name, data);
261
- this.dispatchEvent(new CustomEvent(name, { detail: { data, source: this } }));
262
- this.emitTo(name, data);
263
- }
264
-
265
- /**
266
- * Get the list of channels this element subscribes to.
267
- *
268
- * @returns an array of channel names
269
- */
270
- getSubscribedChannels() {
271
- const chs = [];
272
- if (this.subscribeConfig) {
273
- Object.keys(this.subscribeConfig).forEach((key) => {
274
- chs.push(key);
275
- });
276
- } else if (this.subscribe) {
277
- chs.push(this.subscribe);
278
- }
279
- return chs;
280
- }
281
-
282
- /**
283
- * Check if the other element emits to one of the channels this
284
- * element subscribes to.
285
- *
286
- * @param {Element} other the other element to compare with
287
- */
288
- emitsOnSameChannel(other) {
289
- const myChannels = this.getSubscribedChannels();
290
- const otherChannels = PbMixin.getEmittedChannels(other);
291
- if (myChannels.length === 0 && otherChannels.length === 0) {
292
- // both emit to the default channel
293
- return true;
294
- }
295
- return myChannels.some((channel) => otherChannels.includes(channel));
296
- }
297
-
298
- /**
299
- * Get the list of channels this element emits to.
300
- *
301
- * @returns an array of channel names
302
- */
303
- static getEmittedChannels(elem) {
304
- const chs = [];
305
- const emitConfig = elem.getAttribute('emit-config');
306
- if (emitConfig) {
307
- const json = JSON.parse(emitConfig);
308
- Object.keys(json).forEach(key => {
309
- chs.push(key);
310
- });
311
- } else {
312
- const emitAttr = elem.getAttribute('emit');
313
- if (emitAttr) {
314
- chs.push(emitAttr);
315
- }
316
- }
317
- return chs;
318
- }
319
-
320
- /**
321
- * Listen to the event defined by type. If property `subscribe` or `subscribe-config`
322
- * is defined, this method will trigger the listener only if the event has a key
323
- * equal to the key defined in `subscribe` or `subscribe-config`.
324
- *
325
- * @param {String} type Name of the event, usually starting with `pb-`
326
- * @param {Function} listener Callback function
327
- * @param {Array} [channels] Optional: explicitely specify the channels to emit to. This overwrites
328
- * the emit property. Pass empty array to target the default channel.
329
- */
330
- subscribeTo(type, listener, channels) {
331
- let handlers;
332
- const chs = channels || this.getSubscribedChannels();
333
- if (chs.length === 0) {
334
- // no channel defined: listen for all events not targetted at a channel
335
- const handle = (ev) => {
336
- if (ev.detail && ev.detail.key) {
337
- return;
338
- }
339
- listener(ev);
340
- };
341
- document.addEventListener(type, handle);
342
- handlers = [handle];
343
- } else {
344
- handlers = chs.map(key => {
345
- const handle = ev => {
346
- if (ev.detail && ev.detail.key && ev.detail.key === key) {
347
- listener(ev);
348
- }
349
- };
350
- document.addEventListener(type, handle);
351
- return handle;
352
- });
353
- }
354
- // add new handlers to list of active subscriptions
355
- this._subscriptions.set(type, handlers);
356
- return handlers;
357
- }
358
-
359
- /**
360
- * Dispatch an event of the given type. If the properties `emit` or `emit-config`
361
- * are defined, the event will be limited to the channel specified there.
362
- *
363
- * @param {String} type Name of the event, usually starting with `pb-`
364
- * @param {Object} [options] Options to be passed in ev.detail
365
- * @param {Array} [channels] Optional: explicitely specify the channels to emit to. This overwrites
366
- * the 'emit' property setting. Pass empty array to target the default channel.
367
- */
368
- emitTo(type, options, channels) {
369
- const chs = channels || PbMixin.getEmittedChannels(this);
370
- if (chs.length == 0) {
371
- if (type === 'pb-ready') {
372
- readyEventsFired.add('__default__');
373
- }
374
- if (options) {
375
- options = Object.assign({ _source: this }, options);
376
- } else {
377
- options = { _source: this };
378
- }
379
- const ev = new CustomEvent(type, {
380
- detail: options,
381
- composed: true,
382
- bubbles: true
383
- });
384
- this.dispatchEvent(ev);
385
- } else {
386
- chs.forEach(key => {
387
- const detail = {
388
- key: key,
389
- _source: this
390
- };
391
- if (options) {
392
- for (const opt in options) {
393
- if (options.hasOwnProperty(opt)) {
394
- detail[opt] = options[opt];
395
- }
396
- }
397
- }
398
- if (type === 'pb-ready') {
399
- readyEventsFired.add(key);
400
- }
401
- const ev = new CustomEvent(type, {
402
- detail: detail,
403
- composed: true,
404
- bubbles: true
405
- });
406
- this.dispatchEvent(ev);
407
- });
408
- }
409
- }
410
-
411
- /**
412
- * Returns the `pb-document` element this component is connected to.
413
- *
414
- * @returns the document component or undefined if not set/found
415
- */
416
- getDocument() {
417
- if (this.src) {
418
- const doc = document.getElementById(this.src);
419
- if (doc) {
420
- return doc;
421
- }
422
- }
423
- return null;
424
- }
425
-
426
- getParameter(name, fallback) {
427
- const params = TeiPublisher.url.searchParams && TeiPublisher.url.searchParams.getAll(name);
428
- if (params && params.length == 1) {
429
- return params[0];
430
- }else if (params && params.length > 1) {
431
- return params
432
- }
433
- return fallback;
434
- }
435
-
436
- getParameterValues(name) {
437
- return TeiPublisher.url.searchParams.getAll(name);
438
- }
439
-
440
- setParameter(name, value) {
441
- if (typeof value === 'undefined') {
442
- TeiPublisher.url.searchParams.delete(name);
443
- } else if (Array.isArray(value)) {
444
- TeiPublisher.url.searchParams.delete(name);
445
- value.forEach(function (val) {
446
- TeiPublisher.url.searchParams.append(name, val);
447
- });
448
- } else {
449
- TeiPublisher.url.searchParams.set(name, value);
450
- }
451
- }
452
-
453
- setParameters(obj) {
454
- TeiPublisher.url.search = '';
455
- for (let key in obj) {
456
- if (obj.hasOwnProperty(key)) {
457
- this.setParameter(key, obj[key]);
458
- }
459
- }
460
- }
461
-
462
- getParameters() {
463
- const params = {};
464
- for (let key of TeiPublisher.url.searchParams.keys()) {
465
- params[key] = this.getParameter(key);
466
- }
467
- return params;
468
- }
469
-
470
- getParametersMatching(regex) {
471
- const params = {};
472
- for (let pair of TeiPublisher.url.searchParams.entries()) {
473
- if (regex.test(pair[0])) {
474
- const param = params[pair[0]];
475
- if (param) {
476
- param.push(pair[1]);
477
- } else {
478
- params[pair[0]] = [pair[1]];
479
- }
480
- }
481
- }
482
- return params;
483
- }
484
-
485
- clearParametersMatching(regex) {
486
- for (let pair of TeiPublisher.url.searchParams.entries()) {
487
- if (regex.test(pair[0])) {
488
- TeiPublisher.url.searchParams.delete(pair[0]);
489
- }
490
- }
491
- }
492
-
493
- setPath(path) {
494
- const page = document.querySelector('pb-page');
495
- if (page) {
496
- const appRoot = page.appRoot;
497
-
498
- this.getUrl().pathname = appRoot + '/' + path;
499
- }
500
- }
501
-
502
- getUrl() {
503
- return TeiPublisher.url;
504
- }
505
-
506
- pushHistory(msg, state) {
507
- history.pushState(state, msg, TeiPublisher.url.toString());
508
- }
509
-
510
- getEndpoint() {
511
- return this._endpoint;
512
- }
513
-
514
- toAbsoluteURL(relative, server) {
515
- if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(relative)) {
516
- return relative;
517
- }
518
- const endpoint = server || this.getEndpoint();
519
- let base;
520
- if (endpoint === '.') {
521
- base = new URL(window.location.href);
522
- // loaded in iframe
523
- } else if (window.location.protocol === 'about:') {
524
- base = document.baseURI
525
- } else {
526
- base = new URL(`${endpoint}/`, `${window.location.protocol}//${window.location.host}`);
527
- }
528
- return new URL(relative, base).href;
529
- }
530
-
531
- minApiVersion(requiredVersion) {
532
- return cmpVersion(this._apiVersion, requiredVersion) >= 0;
533
- }
534
-
535
- lessThanApiVersion(requiredVersion) {
536
- return cmpVersion(this._apiVersion, requiredVersion) < 0;
537
- }
538
-
539
- compareApiVersion(requiredVersion) {
540
- return cmpVersion(this._apiVersion, requiredVersion);
541
- }
542
- }
1
+ import { cmpVersion } from './utils.js';
2
+
3
+ if (!window.TeiPublisher) {
4
+ window.TeiPublisher = {};
5
+
6
+ TeiPublisher.url = new URL(window.location.href);
7
+ }
8
+
9
+ /**
10
+ * Global set to record the names of the channels for which a
11
+ * `pb-ready` event was fired.
12
+ */
13
+ const readyEventsFired = new Set();
14
+
15
+ /**
16
+ * Gobal map to record the initialization events which have
17
+ * been received.
18
+ */
19
+ const initEventsFired = new Map();
20
+
21
+ export function clearPageEvents() {
22
+ initEventsFired.clear();
23
+ }
24
+
25
+ /**
26
+ * Wait until the global event identified by name
27
+ * has been fired once. This is mainly used to wait for initialization
28
+ * events like `pb-page-ready`.
29
+ *
30
+ * @param {string} name
31
+ * @param {Function} callback
32
+ */
33
+ export function waitOnce(name, callback) {
34
+ if (initEventsFired.has(name)) {
35
+ callback(initEventsFired.get(name));
36
+ } else {
37
+ document.addEventListener(name, (ev) => {
38
+ initEventsFired.set(name, ev.detail);
39
+ callback(ev.detail);
40
+ }, {
41
+ once: true
42
+ });
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Implements the core channel/event mechanism used by components in TEI Publisher
48
+ * to communicate.
49
+ *
50
+ * As there might be several documents/fragments being displayed on a page at the same time,
51
+ * a simple event mechanism is not enough for components to exchange messages. They need to
52
+ * be able to target a specific view. The mechanism implemented by this mixin thus combines
53
+ * events and channels. Components may emit an event into a named channel to which other
54
+ * components might subscribe. For example, there might be a view which subscribes to the
55
+ * channel *transcription* and another one subscribing to *translation*. By using distinct
56
+ * channels, other components can address only one of the two.
57
+ *
58
+ * @polymer
59
+ * @mixinFunction
60
+ */
61
+ export const pbMixin = (superclass) => class PbMixin extends superclass {
62
+
63
+ static get properties() {
64
+ return {
65
+ /**
66
+ * The name of the channel to subscribe to. Only events on a channel corresponding
67
+ * to this property are listened to.
68
+ */
69
+ subscribe: {
70
+ type: String
71
+ },
72
+ /**
73
+ * Configuration object to define a channel/event mapping. Every property
74
+ * in the object is interpreted as the name of a channel and its value should
75
+ * be an array of event names to listen to.
76
+ */
77
+ subscribeConfig: {
78
+ type: Object,
79
+ attribute: 'subscribe-config'
80
+ },
81
+ /**
82
+ * The name of the channel to send events to.
83
+ */
84
+ emit: {
85
+ type: String
86
+ },
87
+ /**
88
+ * Configuration object to define a channel/event mapping. Every property
89
+ * in the object is interpreted as the name of a channel and its value should
90
+ * be an array of event names to be dispatched.
91
+ */
92
+ emitConfig: {
93
+ type: Object,
94
+ attribute: 'emit-config'
95
+ },
96
+ /**
97
+ * A selector pointing to other components this component depends on.
98
+ * When method `wait` is called, it will wait until all referenced
99
+ * components signal with a `pb-ready` event that they are ready and listening
100
+ * to events.
101
+ */
102
+ waitFor: {
103
+ type: String,
104
+ attribute: 'wait-for'
105
+ },
106
+ _isReady: {
107
+ type: Boolean
108
+ },
109
+ /**
110
+ * Common property to disable the functionality associated with a component.
111
+ * `pb-highlight` and `pb-popover` react to this.
112
+ */
113
+ disabled: {
114
+ type: Boolean,
115
+ reflect: true
116
+ },
117
+ _endpoint: {
118
+ type: String
119
+ },
120
+ _apiVersion: {
121
+ type: String
122
+ }
123
+ }
124
+ }
125
+
126
+ constructor() {
127
+ super();
128
+ this._isReady = false;
129
+ this.disabled = false;
130
+ this._subscriptions = new Map();
131
+ }
132
+
133
+ connectedCallback() {
134
+ super.connectedCallback();
135
+ waitOnce('pb-page-ready', (options) => {
136
+ this._endpoint = options.endpoint;
137
+ this._apiVersion = options.apiVersion;
138
+ });
139
+ }
140
+
141
+ disconnectedCallback() {
142
+ super.disconnectedCallback();
143
+ this._subscriptions.forEach((handlers, type) => {
144
+ handlers.forEach((handler) => {
145
+ document.removeEventListener(type, handler);
146
+ });
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Enable or disable certain features of a component. Called by `pb-toggle-feature`
152
+ * and `pb-select-feature` to change the components behaviour.
153
+ *
154
+ * By default only one command is known: `disable` will disable any interactive feature
155
+ * of the component.
156
+ *
157
+ * @param {string} command name of an action to take or setting to be toggled
158
+ * @param {Boolean} state the state to set the setting to
159
+ */
160
+ command(command, state) {
161
+ if (command === 'disable') {
162
+ this.disabled = state;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Wait for the components referenced by the selector given in property `waitFor`
168
+ * to signal that they are ready to respond to events. Only wait for elements which
169
+ * emit to one of the channels this component subscribes to.
170
+ *
171
+ * @param callback function to be called when all components are ready
172
+ */
173
+ wait(callback) {
174
+ if (this.waitFor) {
175
+ const targetNodes = Array.from(document.querySelectorAll(this.waitFor));
176
+ const targets = targetNodes.filter(target => this.emitsOnSameChannel(target));
177
+ const targetCount = targets.length;
178
+ if (targetCount === 0) {
179
+ // selector did not return any targets
180
+ return callback();
181
+ }
182
+ let count = 0;
183
+ targets.forEach((target) => {
184
+ if (target._isReady) {
185
+ count++;
186
+ if (targetCount === count) {
187
+ callback();
188
+ }
189
+ } else {
190
+ const handler = target.addEventListener('pb-ready', (ev) => {
191
+ if (ev.detail.source == this) {
192
+ // same source: ignore
193
+ return;
194
+ }
195
+ count++;
196
+ if (targetCount === count) {
197
+ target.removeEventListener('pb-ready', handler);
198
+ callback();
199
+ }
200
+ });
201
+ }
202
+ });
203
+ } else {
204
+ callback();
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Wait until a `pb-ready` event is received from one of the channels
210
+ * this component subscribes to. Used internally by components which depend
211
+ * on a particular `pb-view` to be ready and listening to events.
212
+ *
213
+ * @param callback function to be called when `pb-ready` is received
214
+ */
215
+ waitForChannel(callback) {
216
+ // check first if a `pb-ready` event has already been received on one of the channels
217
+ if (this.subscribeConfig) {
218
+ for (const key in this.subscribeConfig) {
219
+ this.subscribeConfig[key].forEach(t => {
220
+ if (t === 'pb-ready' && readyEventsFired.has(key)) {
221
+ return callback();
222
+ }
223
+ });
224
+ }
225
+ } else if (
226
+ (this.subscribe && readyEventsFired.has(this.subscribe)) ||
227
+ (!this.subscribe && readyEventsFired.has('__default__'))
228
+ ) {
229
+ return callback();
230
+ }
231
+
232
+ const listeners = this.subscribeTo('pb-ready', (ev) => {
233
+ if (ev.detail._source == this) {
234
+ return;
235
+ }
236
+ listeners.forEach(listener => document.removeEventListener('pb-ready', listener));
237
+ callback();
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Wait until the global event identified by name
243
+ * has been fired once. This is mainly used to wait for initialization
244
+ * events like `pb-page-ready`.
245
+ *
246
+ * @param {string} name
247
+ * @param {Function} callback
248
+ * @deprecated Use exported `waitOnce` function
249
+ */
250
+ static waitOnce(name, callback) {
251
+ waitOnce(name, callback);
252
+ }
253
+
254
+ /**
255
+ * Signal that the component is ready to respond to events.
256
+ * Emits an event to all channels the component is registered with.
257
+ */
258
+ signalReady(name = 'pb-ready', data) {
259
+ this._isReady = true;
260
+ initEventsFired.set(name, data);
261
+ this.dispatchEvent(new CustomEvent(name, { detail: { data, source: this } }));
262
+ this.emitTo(name, data);
263
+ }
264
+
265
+ /**
266
+ * Get the list of channels this element subscribes to.
267
+ *
268
+ * @returns an array of channel names
269
+ */
270
+ getSubscribedChannels() {
271
+ const chs = [];
272
+ if (this.subscribeConfig) {
273
+ Object.keys(this.subscribeConfig).forEach((key) => {
274
+ chs.push(key);
275
+ });
276
+ } else if (this.subscribe) {
277
+ chs.push(this.subscribe);
278
+ }
279
+ return chs;
280
+ }
281
+
282
+ /**
283
+ * Check if the other element emits to one of the channels this
284
+ * element subscribes to.
285
+ *
286
+ * @param {Element} other the other element to compare with
287
+ */
288
+ emitsOnSameChannel(other) {
289
+ const myChannels = this.getSubscribedChannels();
290
+ const otherChannels = PbMixin.getEmittedChannels(other);
291
+ if (myChannels.length === 0 && otherChannels.length === 0) {
292
+ // both emit to the default channel
293
+ return true;
294
+ }
295
+ return myChannels.some((channel) => otherChannels.includes(channel));
296
+ }
297
+
298
+ /**
299
+ * Get the list of channels this element emits to.
300
+ *
301
+ * @returns an array of channel names
302
+ */
303
+ static getEmittedChannels(elem) {
304
+ const chs = [];
305
+ const emitConfig = elem.getAttribute('emit-config');
306
+ if (emitConfig) {
307
+ const json = JSON.parse(emitConfig);
308
+ Object.keys(json).forEach(key => {
309
+ chs.push(key);
310
+ });
311
+ } else {
312
+ const emitAttr = elem.getAttribute('emit');
313
+ if (emitAttr) {
314
+ chs.push(emitAttr);
315
+ }
316
+ }
317
+ return chs;
318
+ }
319
+
320
+ /**
321
+ * Listen to the event defined by type. If property `subscribe` or `subscribe-config`
322
+ * is defined, this method will trigger the listener only if the event has a key
323
+ * equal to the key defined in `subscribe` or `subscribe-config`.
324
+ *
325
+ * @param {String} type Name of the event, usually starting with `pb-`
326
+ * @param {Function} listener Callback function
327
+ * @param {Array} [channels] Optional: explicitely specify the channels to emit to. This overwrites
328
+ * the emit property. Pass empty array to target the default channel.
329
+ */
330
+ subscribeTo(type, listener, channels) {
331
+ let handlers;
332
+ const chs = channels || this.getSubscribedChannels();
333
+ if (chs.length === 0) {
334
+ // no channel defined: listen for all events not targetted at a channel
335
+ const handle = (ev) => {
336
+ if (ev.detail && ev.detail.key) {
337
+ return;
338
+ }
339
+ listener(ev);
340
+ };
341
+ document.addEventListener(type, handle);
342
+ handlers = [handle];
343
+ } else {
344
+ handlers = chs.map(key => {
345
+ const handle = ev => {
346
+ if (ev.detail && ev.detail.key && ev.detail.key === key) {
347
+ listener(ev);
348
+ }
349
+ };
350
+ document.addEventListener(type, handle);
351
+ return handle;
352
+ });
353
+ }
354
+ // add new handlers to list of active subscriptions
355
+ this._subscriptions.set(type, handlers);
356
+ return handlers;
357
+ }
358
+
359
+ /**
360
+ * Dispatch an event of the given type. If the properties `emit` or `emit-config`
361
+ * are defined, the event will be limited to the channel specified there.
362
+ *
363
+ * @param {String} type Name of the event, usually starting with `pb-`
364
+ * @param {Object} [options] Options to be passed in ev.detail
365
+ * @param {Array} [channels] Optional: explicitely specify the channels to emit to. This overwrites
366
+ * the 'emit' property setting. Pass empty array to target the default channel.
367
+ */
368
+ emitTo(type, options, channels) {
369
+ const chs = channels || PbMixin.getEmittedChannels(this);
370
+ if (chs.length == 0) {
371
+ if (type === 'pb-ready') {
372
+ readyEventsFired.add('__default__');
373
+ }
374
+ if (options) {
375
+ options = Object.assign({ _source: this }, options);
376
+ } else {
377
+ options = { _source: this };
378
+ }
379
+ const ev = new CustomEvent(type, {
380
+ detail: options,
381
+ composed: true,
382
+ bubbles: true
383
+ });
384
+ this.dispatchEvent(ev);
385
+ } else {
386
+ chs.forEach(key => {
387
+ const detail = {
388
+ key: key,
389
+ _source: this
390
+ };
391
+ if (options) {
392
+ for (const opt in options) {
393
+ if (options.hasOwnProperty(opt)) {
394
+ detail[opt] = options[opt];
395
+ }
396
+ }
397
+ }
398
+ if (type === 'pb-ready') {
399
+ readyEventsFired.add(key);
400
+ }
401
+ const ev = new CustomEvent(type, {
402
+ detail: detail,
403
+ composed: true,
404
+ bubbles: true
405
+ });
406
+ this.dispatchEvent(ev);
407
+ });
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Returns the `pb-document` element this component is connected to.
413
+ *
414
+ * @returns the document component or undefined if not set/found
415
+ */
416
+ getDocument() {
417
+ if (this.src) {
418
+ const doc = document.getElementById(this.src);
419
+ if (doc) {
420
+ return doc;
421
+ }
422
+ }
423
+ return null;
424
+ }
425
+
426
+ getParameter(name, fallback) {
427
+ const params = TeiPublisher.url.searchParams && TeiPublisher.url.searchParams.getAll(name);
428
+ if (params && params.length == 1) {
429
+ return params[0];
430
+ }else if (params && params.length > 1) {
431
+ return params
432
+ }
433
+ return fallback;
434
+ }
435
+
436
+ getParameterValues(name) {
437
+ return TeiPublisher.url.searchParams.getAll(name);
438
+ }
439
+
440
+ setParameter(name, value) {
441
+ if (typeof value === 'undefined') {
442
+ TeiPublisher.url.searchParams.delete(name);
443
+ } else if (Array.isArray(value)) {
444
+ TeiPublisher.url.searchParams.delete(name);
445
+ value.forEach(function (val) {
446
+ TeiPublisher.url.searchParams.append(name, val);
447
+ });
448
+ } else {
449
+ TeiPublisher.url.searchParams.set(name, value);
450
+ }
451
+ }
452
+
453
+ setParameters(obj) {
454
+ TeiPublisher.url.search = '';
455
+ for (let key in obj) {
456
+ if (obj.hasOwnProperty(key)) {
457
+ this.setParameter(key, obj[key]);
458
+ }
459
+ }
460
+ }
461
+
462
+ getParameters() {
463
+ const params = {};
464
+ for (let key of TeiPublisher.url.searchParams.keys()) {
465
+ params[key] = this.getParameter(key);
466
+ }
467
+ return params;
468
+ }
469
+
470
+ getParametersMatching(regex) {
471
+ const params = {};
472
+ for (let pair of TeiPublisher.url.searchParams.entries()) {
473
+ if (regex.test(pair[0])) {
474
+ const param = params[pair[0]];
475
+ if (param) {
476
+ param.push(pair[1]);
477
+ } else {
478
+ params[pair[0]] = [pair[1]];
479
+ }
480
+ }
481
+ }
482
+ return params;
483
+ }
484
+
485
+ clearParametersMatching(regex) {
486
+ for (let pair of TeiPublisher.url.searchParams.entries()) {
487
+ if (regex.test(pair[0])) {
488
+ TeiPublisher.url.searchParams.delete(pair[0]);
489
+ }
490
+ }
491
+ }
492
+
493
+ setPath(path) {
494
+ const page = document.querySelector('pb-page');
495
+ if (page) {
496
+ const appRoot = page.appRoot;
497
+
498
+ this.getUrl().pathname = appRoot + '/' + path;
499
+ }
500
+ }
501
+
502
+ getUrl() {
503
+ return TeiPublisher.url;
504
+ }
505
+
506
+ pushHistory(msg, state) {
507
+ history.pushState(state, msg, TeiPublisher.url.toString());
508
+ }
509
+
510
+ getEndpoint() {
511
+ return this._endpoint;
512
+ }
513
+
514
+ toAbsoluteURL(relative, server) {
515
+ if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(relative)) {
516
+ return relative;
517
+ }
518
+ const endpoint = server || this.getEndpoint();
519
+ let base;
520
+ if (endpoint === '.') {
521
+ base = new URL(window.location.href);
522
+ // loaded in iframe
523
+ } else if (window.location.protocol === 'about:') {
524
+ base = document.baseURI
525
+ } else {
526
+ base = new URL(`${endpoint}/`, `${window.location.protocol}//${window.location.host}`);
527
+ }
528
+ return new URL(relative, base).href;
529
+ }
530
+
531
+ minApiVersion(requiredVersion) {
532
+ return cmpVersion(this._apiVersion, requiredVersion) >= 0;
533
+ }
534
+
535
+ lessThanApiVersion(requiredVersion) {
536
+ return cmpVersion(this._apiVersion, requiredVersion) < 0;
537
+ }
538
+
539
+ compareApiVersion(requiredVersion) {
540
+ return cmpVersion(this._apiVersion, requiredVersion);
541
+ }
542
+ }