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