@teipublisher/pb-components 1.44.2 → 2.0.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/demo/demos.json +2 -1
  3. package/dist/demo/pb-browse-docs.html +2 -1
  4. package/dist/demo/pb-combo-box.html +1 -1
  5. package/dist/demo/pb-document.html +2 -2
  6. package/dist/demo/pb-load.html +2 -2
  7. package/dist/demo/pb-select-feature.html +1 -1
  8. package/dist/demo/pb-select-feature2.html +10 -3
  9. package/dist/demo/pb-select-feature3.html +1 -1
  10. package/dist/demo/pb-select-odd.html +1 -1
  11. package/dist/demo/pb-table-grid.html +1 -1
  12. package/dist/demo/pb-tabs.html +8 -2
  13. package/dist/demo/pb-toggle-feature.html +2 -2
  14. package/dist/demo/pb-toggle-feature2.html +2 -2
  15. package/dist/demo/pb-toggle-feature3.html +2 -2
  16. package/dist/demo/pb-view.html +1 -1
  17. package/dist/demo/pb-view4.html +86 -0
  18. package/dist/pb-code-editor.js +1 -1
  19. package/dist/pb-component-docs.js +33 -33
  20. package/dist/pb-components-bundle.js +173 -173
  21. package/dist/pb-edit-app.js +6 -6
  22. package/dist/pb-elements.json +93 -27
  23. package/dist/{pb-i18n-3963b098.js → pb-i18n-8a90c591.js} +1 -1
  24. package/dist/pb-leaflet-map.js +1 -1
  25. package/dist/pb-mixin-8a593923.js +158 -0
  26. package/dist/pb-odd-editor.js +47 -47
  27. package/dist/{vaadin-element-mixin-08cf11b5.js → vaadin-element-mixin-672938e3.js} +18 -18
  28. package/i18n/common/en.json +9 -1
  29. package/package.json +4 -3
  30. package/pb-elements.json +93 -27
  31. package/src/dts-client.js +14 -14
  32. package/src/dts-select-endpoint.js +5 -5
  33. package/src/pb-ajax.js +4 -4
  34. package/src/pb-authority-lookup.js +2 -2
  35. package/src/pb-autocomplete.js +9 -11
  36. package/src/pb-blacklab-highlight.js +3 -3
  37. package/src/pb-browse-docs.js +44 -27
  38. package/src/pb-browse.js +9 -3
  39. package/src/pb-combo-box.js +2 -2
  40. package/src/pb-document.js +15 -1
  41. package/src/pb-download.js +2 -2
  42. package/src/pb-edit-app.js +2 -2
  43. package/src/pb-edit-xml.js +2 -2
  44. package/src/pb-events.js +26 -18
  45. package/src/pb-grid.js +55 -53
  46. package/src/pb-lang.js +2 -2
  47. package/src/pb-link.js +10 -16
  48. package/src/pb-load.js +35 -25
  49. package/src/pb-login.js +2 -2
  50. package/src/pb-manage-odds.js +2 -2
  51. package/src/pb-markdown.js +2 -2
  52. package/src/pb-mei.js +2 -2
  53. package/src/pb-mixin.js +103 -196
  54. package/src/pb-odd-editor.js +2 -2
  55. package/src/pb-page.js +30 -21
  56. package/src/pb-paginate.js +24 -19
  57. package/src/pb-print-preview.js +2 -2
  58. package/src/pb-repeat.js +2 -1
  59. package/src/pb-search.js +34 -8
  60. package/src/pb-select-feature.js +62 -39
  61. package/src/pb-select-odd.js +8 -7
  62. package/src/pb-select-template.js +5 -4
  63. package/src/pb-select.js +31 -28
  64. package/src/pb-split-list.js +18 -11
  65. package/src/pb-table-grid.js +9 -8
  66. package/src/pb-tabs.js +29 -12
  67. package/src/pb-toggle-feature.js +51 -55
  68. package/src/pb-upload.js +10 -3
  69. package/src/pb-view.js +118 -95
  70. package/src/theming.js +148 -149
  71. package/src/urls.js +233 -0
  72. package/dist/pb-mixin-88125cb2.js +0 -158
package/src/theming.js CHANGED
@@ -1,150 +1,149 @@
1
- import { waitOnce } from "./pb-mixin.js";
2
-
3
- /**
4
- * Maps theme selector to CSSStyleSheet or null.
5
- *
6
- * @type {Map<string,(CSSStyleSheet|null)>}
7
- */
8
- const themeMap = new Map();
9
-
10
- /**
11
- * Load one or more CSS stylesheet from the given URL and return
12
- * a CSSStyleSheet. The returned stylesheet can be assigned
13
- * to `adoptedStyleSheets`.
14
- *
15
- * @param {string[]} urls absolute URL
16
- * @returns {Promise<CSSStyleSheet|null>} constructed CSSStyleSheet or null
17
- */
18
- export async function loadStylesheets(urls) {
19
- const output = [];
20
- for(let url of urls) {
21
- const css = await loadResource(url);
22
- if (css) {
23
- output.push(css);
24
- }
25
- }
26
- if (output.length > 0) {
27
- const sheet = new CSSStyleSheet();
28
- return sheet.replace(output.join(''));
29
- }
30
- return null;
31
- }
32
-
33
- function loadResource(url) {
34
- return fetch(url)
35
- .then(response => {
36
- if (response.ok) {
37
- return response.text();
38
- }
39
- console.warn('<theming> Component stylesheet not found: %s', url);
40
- return null;
41
- })
42
- .then(text => {
43
- return text;
44
- })
45
- .catch(error => {
46
- console.error('<theming> Error loading stylesheet %s: %o', url, error);
47
- return null;
48
- });
49
- }
50
-
51
- /**
52
- * From the global component theme, import all rules which would apply to the
53
- * given element into a new CSSStyleSheet and return it.
54
- *
55
- * @param {HTMLElement} elem a web component or HTML element
56
- * @returns {CSSStyleSheet|null} a new CSSStylesheet or null
57
- */
58
- export function importStyles(elem) {
59
- const page = document.querySelector('pb-page');
60
- if (!page) {
61
- return null;
62
- }
63
- const theme = page.stylesheet;
64
- if (!theme) {
65
- // no component styles defined
66
- return null;
67
- }
68
-
69
- const selectors = getSelectors(elem).join('|');
70
- if (themeMap.has(selectors)) {
71
- return themeMap.get(selectors);
72
- }
73
- const prefixRegex = new RegExp(`^(${selectors})\\b`);
74
- let adoptedSheet = null;
75
- const rules = theme.cssRules;
76
- const newCSS = copyStyles(rules, prefixRegex, []);
77
- if (newCSS.length > 0) {
78
- adoptedSheet = new CSSStyleSheet();
79
- adoptedSheet.replaceSync(newCSS.join(''));
80
- }
81
- console.log('<theming> caching stylesheet for %s', selectors);
82
- themeMap.set(selectors, adoptedSheet);
83
- return adoptedSheet;
84
- }
85
-
86
- /**
87
- * Recursively copy matching styles from the theme CSS
88
- * to create a new CSS stylesheet having all styles required
89
- * by the component.
90
- *
91
- * @param {CSSRule[]} rules
92
- * @param {RegExp} prefixRegex
93
- * @param {string[]} output
94
- * @returns {string[]}
95
- */
96
- function copyStyles(rules, prefixRegex, output) {
97
- for (let i = 0; i < rules.length; i++) {
98
- const rule = rules[i];
99
- if (rule instanceof CSSStyleRule) {
100
- if (prefixRegex.test(rule.selectorText)) {
101
- const css = rule.cssText.replace(prefixRegex, `:host($1) `);
102
- output.push(css);
103
- }
104
- } else if (rule instanceof CSSMediaRule) {
105
- output.push(`\n@media ${rule.conditionText} {\n`);
106
- copyStyles(rule.cssRules, prefixRegex, output);
107
- output.push('\n}\n');
108
- } else if (rule instanceof CSSFontFaceRule) {
109
- // not allowed in constructed stylesheets
110
- } else {
111
- output.push(rule.cssText);
112
- }
113
- }
114
- return output;
115
- }
116
-
117
- /**
118
- * Get a list of selectors, which could match the given component.
119
- * This will return the local name of the component, a selector for the id
120
- * and all classes assigned.
121
- *
122
- * @param {HTMLElement} component the web component
123
- * @returns {string[]} list of selectors
124
- */
125
- function getSelectors(component) {
126
- const prefixes = [component.localName];
127
- if (component.id) {
128
- prefixes.push(`#${component.id}`);
129
- }
130
- component.classList.forEach((cls) => prefixes.push(`.${cls}`));
131
- return prefixes;
132
- }
133
-
134
- /**
135
- * Implements support for injecting user-defined styles into a web component's shadow DOM.
136
- * Styles will be copied from the global component theme CSS imported by `pb-page`
137
- * (see `theme` property on `pb-page`)
138
- */
139
- export const themableMixin = (superclass) => class ThemableMixin extends superclass {
140
-
141
- connectedCallback() {
142
- super.connectedCallback();
143
- waitOnce('pb-page-ready', (options) => {
144
- const theme = importStyles(this);
145
- if (theme) {
146
- this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, theme];
147
- }
148
- });
149
- }
1
+ import { waitOnce } from "./pb-mixin.js";
2
+ import 'construct-style-sheets-polyfill';
3
+
4
+ /**
5
+ * Maps theme selector to CSSStyleSheet or null.
6
+ *
7
+ * @type {Map<string,(CSSStyleSheet|null)>}
8
+ */
9
+ const themeMap = new Map();
10
+
11
+ /**
12
+ * Load one or more CSS stylesheet from the given URL and return
13
+ * a CSSStyleSheet. The returned stylesheet can be assigned
14
+ * to `adoptedStyleSheets`.
15
+ *
16
+ * @param {string[]} urls absolute URL
17
+ * @returns {Promise<CSSStyleSheet|null>} constructed CSSStyleSheet or null
18
+ */
19
+ export async function loadStylesheets(urls) {
20
+ const output = [];
21
+ for (const url of urls) {
22
+ const css = await loadResource(url);
23
+ if (css) {
24
+ output.push(css);
25
+ }
26
+ }
27
+ if (output.length > 0) {
28
+ const sheet = new CSSStyleSheet();
29
+ return sheet.replace(output.join(''));
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function loadResource(url) {
35
+ return fetch(url)
36
+ .then(response => {
37
+ if (response.ok) {
38
+ return response.text();
39
+ }
40
+ console.warn('<theming> Component stylesheet not found: %s', url);
41
+ return null;
42
+ })
43
+ .then(text => text)
44
+ .catch(error => {
45
+ console.error('<theming> Error loading stylesheet %s: %o', url, error);
46
+ return null;
47
+ });
48
+ }
49
+
50
+ /**
51
+ * From the global component theme, import all rules which would apply to the
52
+ * given element into a new CSSStyleSheet and return it.
53
+ *
54
+ * @param {HTMLElement} elem a web component or HTML element
55
+ * @returns {CSSStyleSheet|null} a new CSSStylesheet or null
56
+ */
57
+ export function importStyles(elem) {
58
+ const page = document.querySelector('pb-page');
59
+ if (!page) {
60
+ return null;
61
+ }
62
+ const theme = page.stylesheet;
63
+ if (!theme) {
64
+ // no component styles defined
65
+ return null;
66
+ }
67
+
68
+ const selectors = getSelectors(elem).join('|');
69
+ if (themeMap.has(selectors)) {
70
+ return themeMap.get(selectors);
71
+ }
72
+ const prefixRegex = new RegExp(`^(${selectors})\\b`);
73
+ let adoptedSheet = null;
74
+ const rules = theme.cssRules;
75
+ const newCSS = copyStyles(rules, prefixRegex, []);
76
+ if (newCSS.length > 0) {
77
+ adoptedSheet = new CSSStyleSheet();
78
+ adoptedSheet.replaceSync(newCSS.join(''));
79
+ }
80
+ console.log('<theming> caching stylesheet for %s', selectors);
81
+ themeMap.set(selectors, adoptedSheet);
82
+ return adoptedSheet;
83
+ }
84
+
85
+ /**
86
+ * Recursively copy matching styles from the theme CSS
87
+ * to create a new CSS stylesheet having all styles required
88
+ * by the component.
89
+ *
90
+ * @param {CSSRule[]} rules
91
+ * @param {RegExp} prefixRegex
92
+ * @param {string[]} output
93
+ * @returns {string[]}
94
+ */
95
+ function copyStyles(rules, prefixRegex, output) {
96
+ for (let i = 0; i < rules.length; i++) {
97
+ const rule = rules[i];
98
+ if (rule instanceof CSSStyleRule) {
99
+ if (prefixRegex.test(rule.selectorText)) {
100
+ const css = rule.cssText.replace(prefixRegex, `:host($1) `);
101
+ output.push(css);
102
+ }
103
+ } else if (rule instanceof CSSMediaRule) {
104
+ output.push(`\n@media ${rule.conditionText} {\n`);
105
+ copyStyles(rule.cssRules, prefixRegex, output);
106
+ output.push('\n}\n');
107
+ } else if (rule instanceof CSSFontFaceRule) {
108
+ // not allowed in constructed stylesheets
109
+ } else {
110
+ output.push(rule.cssText);
111
+ }
112
+ }
113
+ return output;
114
+ }
115
+
116
+ /**
117
+ * Get a list of selectors, which could match the given component.
118
+ * This will return the local name of the component, a selector for the id
119
+ * and all classes assigned.
120
+ *
121
+ * @param {HTMLElement} component the web component
122
+ * @returns {string[]} list of selectors
123
+ */
124
+ function getSelectors(component) {
125
+ const prefixes = [component.localName];
126
+ if (component.id) {
127
+ prefixes.push(`#${component.id}`);
128
+ }
129
+ component.classList.forEach((cls) => prefixes.push(`.${cls}`));
130
+ return prefixes;
131
+ }
132
+
133
+ /**
134
+ * Implements support for injecting user-defined styles into a web component's shadow DOM.
135
+ * Styles will be copied from the global component theme CSS imported by `pb-page`
136
+ * (see `theme` property on `pb-page`)
137
+ */
138
+ export const themableMixin = (superclass) => class ThemableMixin extends superclass {
139
+
140
+ connectedCallback() {
141
+ super.connectedCallback();
142
+ waitOnce('pb-page-ready', (options) => {
143
+ const theme = importStyles(this);
144
+ if (theme) {
145
+ this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, theme];
146
+ }
147
+ });
148
+ }
150
149
  };
package/src/urls.js ADDED
@@ -0,0 +1,233 @@
1
+ import { PbEvents } from "./pb-events.js";
2
+ import { getSubscribedChannels } from "./pb-mixin.js";
3
+
4
+ function log(...args) {
5
+ args[0] = `%c<registry>%c ${args[0]}`;
6
+ args.splice(1, 0, 'font-weight: bold; color: #99FF33;', 'color: inherit; font-weight: normal');
7
+ console.log.apply(null, args);
8
+ }
9
+
10
+ /**
11
+ * Central class for tracking state. We distinguish between
12
+ *
13
+ * 1. the initial state of the application as determined by the URL opened in the browser
14
+ * 2. state changes in components occurring while the user interacts with the page
15
+ *
16
+ * 1) is relevant if a user accesses a particular URL by following a link, entering an address into the location bar or
17
+ * opening a bookmark. In this case the components on the page will be in a fresh, empty state. However, they may react
18
+ * to URL parameters and initiate specific properties of their state accordingly. Users expect that navigating to a given URL will
19
+ * consistently result in the same display (i.e. restore a certain state). If users bookmark a specific page of a document shown in
20
+ * TEI Publisher's pb-view, they expect that the same page is displayed when they open the bookmark again. Thus the document path and
21
+ * the page need to be tracked in the URL.
22
+ *
23
+ * 2) applies while the user interacts with the application, i.e. triggers actions, which cause components to change state. Some
24
+ * actions may lead to a new page load, but many – like navigating pages in a `pb-view` – will only change the state of one or more components.
25
+ * From a user's point of view this should be irrelevant: moving back or forward in browser history should consistently restore the
26
+ * previous or following state.
27
+ *
28
+ * Components should thus comply with the following guidelines:
29
+ *
30
+ * - if the component initializes itself, it should retrieve needed parameters from `registry.state`
31
+ * - it may call `registry.replace` to make sure that all parameters required to later restore its initial state are present in the current URL
32
+ * - it should register a listener with `registry.subscribe` to be informed when the user moves back in history (without reloading the page)
33
+ * - it must call `registry.commit` after changing its current state
34
+ */
35
+ class Registry {
36
+
37
+ constructor() {
38
+ this.rootPath = '';
39
+ /**
40
+ * Records current state as determined from parsing the URL.
41
+ * This should be used to initialize components.
42
+ */
43
+ this.state = {};
44
+ /**
45
+ * Used to record state for a given channel. Will be updated
46
+ * if a component calls commit or replace.
47
+ */
48
+ this.channelStates = {};
49
+ this._listeners = [];
50
+ }
51
+
52
+ configure(usePath = true, rootPath = '') {
53
+ this.rootPath = rootPath;
54
+ this.usePath = usePath;
55
+
56
+ // determine initial state of the registry by parsing current URL
57
+ const initialState = this._stateFromURL();
58
+ if (!initialState) {
59
+ console.error('<registry> failed to parse URL: %s using template %s', window.location, this.urlTemplate);
60
+ } else {
61
+ this.state = initialState;
62
+ }
63
+ window.history.replaceState(null, '');
64
+
65
+ window.addEventListener('popstate', (ev) => {
66
+ if (ev.state) {
67
+ try {
68
+ this.channelStates = JSON.parse(ev.state);
69
+ } catch (e) {
70
+ console.error('<registry> error restoring state: %s', e.toString());
71
+ }
72
+ } else {
73
+ this.channelStates = {};
74
+ }
75
+ this.state = this._stateFromURL();
76
+ log('popstate: %o', this.channelStates);
77
+
78
+ this._listeners.forEach((entry) => {
79
+ entry.callback(this.getState(entry.component));
80
+ });
81
+
82
+ PbEvents.emit('pb-popstate', null, this.channelStates);
83
+ });
84
+ }
85
+
86
+ subscribe(component, callback) {
87
+ this._listeners.push({
88
+ component,
89
+ callback
90
+ });
91
+ }
92
+
93
+ _stateFromURL() {
94
+ const params = {};
95
+ if (window.location.hash.length > 0) {
96
+ params.id = window.location.hash.substring(1);
97
+ }
98
+ if (this.usePath) {
99
+ params.path = window.location.pathname.replace(new RegExp(`^${this.rootPath}/?`), '');
100
+ }
101
+ const urlParams = new URLSearchParams(window.location.search);
102
+ urlParams.forEach((value, key) => {
103
+ if (this.usePath && key === 'path') {
104
+ console.warn("Found path parameter in query, but usePath is set to true. The path parameter will be ignored.");
105
+ return;
106
+ }
107
+ params[key] = value;
108
+ });
109
+ console.log('root: %s; window: %s, rel: %s', this.rootPath, window.location.pathname, params.path);
110
+ return params;
111
+ }
112
+
113
+ getState(component) {
114
+ const channel = getSubscribedChannels(component)[0];
115
+ const state = this.channelStates[channel];
116
+ if (state) {
117
+ return state;
118
+ }
119
+ this.channelStates[channel] = {};
120
+ return this.channelStates[channel];
121
+ }
122
+
123
+ setState(component, newState) {
124
+ const channel = getSubscribedChannels(component)[0];
125
+ this.channelStates[channel] = Object.assign(this.channelStates[channel], newState);
126
+ }
127
+
128
+ clearParametersMatching (component, regex) {
129
+ const {state} = this
130
+
131
+ for (const key of Object.keys(state)) {
132
+ if (regex.test(key)) {
133
+ state[key] = null
134
+ }
135
+ }
136
+ }
137
+
138
+ get(path, defaultValue) {
139
+ if (!this.state) {
140
+ return undefined;
141
+ }
142
+ const value = path.split('.').reduce((state, component) => {
143
+ if (!state[component]) {
144
+ return undefined;
145
+ }
146
+ return state[component];
147
+ }, this.state);
148
+ return value || defaultValue;
149
+ }
150
+
151
+ set(path, value) {
152
+ if (!path.contains('.')) {
153
+ this.state[path] = value;
154
+ return;
155
+ }
156
+ const components = path.split('.');
157
+ const lastPart = components.pop()
158
+ // make sure all intermediate steps are available
159
+ const lastIntermediate = components.reduce((result, nextComponent) => {
160
+ if (!result[nextComponent]) {
161
+ // eslint-disable-next-line no-param-reassign
162
+ result[nextComponent] = {};
163
+ }
164
+ return result[nextComponent];
165
+ },
166
+ this.state
167
+ );
168
+ lastIntermediate[lastPart] = value;
169
+ }
170
+
171
+ commit(elem, newState, overwrite = false) {
172
+ this._commit(elem, newState, overwrite, false);
173
+ }
174
+
175
+ replace(elem, newState, overwrite = false) {
176
+ this._commit(elem, newState, overwrite, true);
177
+ }
178
+
179
+ _commit(elem, newState, overwrite, replace) {
180
+ this.state = overwrite ? newState : Object.assign(this.state, newState);
181
+ const resolved = this.urlFromState();
182
+
183
+ const chs = getSubscribedChannels(elem);
184
+ chs.forEach((channel) => {
185
+ if (overwrite || !this.channelStates[channel]) {
186
+ this.channelStates[channel] = newState;
187
+ } else {
188
+ Object.assign(this.channelStates[channel], newState);
189
+ }
190
+ });
191
+
192
+ const json = this.toJSON();
193
+ if (replace) {
194
+ window.history.replaceState(json, '', resolved);
195
+ log('replace %s: %o %d', resolved.toString(), this.channelStates, window.history.length);
196
+ } else {
197
+ window.history.pushState(json, '', resolved);
198
+ log('commit %s: %o %d', resolved.toString(), this.channelStates, window.history.length);
199
+ }
200
+ }
201
+
202
+ urlFromState() {
203
+ const newUrl = new URL(window.location.href);
204
+ for (const [param, value] of Object.entries(this.state)) {
205
+ if (( param !== 'path' || !this.usePath )
206
+ && param !== 'id') {
207
+ if (value === null) {
208
+ newUrl.searchParams.delete(param)
209
+ } else {
210
+ newUrl.searchParams.set(param, value)
211
+ }
212
+ }
213
+ }
214
+
215
+ if (this.usePath) {
216
+ newUrl.pathname = `${this.rootPath}/${this.state.path}`;
217
+ }
218
+
219
+ newUrl.hash = this.state.id ? `#${this.state.id}` : '';
220
+
221
+ console.log('urlFromState', newUrl.searchParams.toString())
222
+ return newUrl;
223
+ }
224
+
225
+ toJSON() {
226
+ return JSON.stringify(this.channelStates);
227
+ }
228
+ }
229
+
230
+ export const registry = new Registry();
231
+ if (!window.pbRegistry) {
232
+ window.pbRegistry = registry;
233
+ }