@ucd-lib/theme-elements 0.0.8 → 0.0.9

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.
@@ -4,6 +4,7 @@ import {render, styles} from "./ucd-theme-header.tpl.js";
4
4
  import {
5
5
  IntersectionObserverController,
6
6
  MutationObserverController,
7
+ PopStateObserverController,
7
8
  WaitController } from '../../utils/controllers';
8
9
 
9
10
  /**
@@ -51,10 +52,12 @@ export default class UcdThemeHeader extends LitElement {
51
52
  preventFixed: {type: Boolean, attribute: "prevent-fixed"},
52
53
  isDemo: {type: Boolean, attribute: "is-demo"},
53
54
  _transitioning: {type: Boolean, state: true},
55
+ _hasPrimaryNav: {type: Boolean, state: true},
54
56
  _hasSlottedBranding: {type: Boolean, state: true},
55
57
  _hasQuickLinks: {type: Boolean, state: true},
56
58
  _hasSearch: {type: Boolean, state: true},
57
- _brandingBarInView: {type: Boolean, state: true}
59
+ _brandingBarInView: {type: Boolean, state: true},
60
+ _components: {type: Object, state: true}
58
61
  };
59
62
  }
60
63
 
@@ -68,6 +71,7 @@ export default class UcdThemeHeader extends LitElement {
68
71
 
69
72
  this.mutationObserver = new MutationObserverController(this);
70
73
  this.wait = new WaitController(this);
74
+ new PopStateObserverController(this, "_onLocationChange");
71
75
 
72
76
  this.siteName = "";
73
77
  this.siteUrl = "/";
@@ -77,14 +81,21 @@ export default class UcdThemeHeader extends LitElement {
77
81
  this.isDemo = false;
78
82
 
79
83
  this._transitioning = false;
84
+ this._hasPrimaryNav = false;
80
85
  this._hasSlottedBranding = false;
81
86
  this._hasQuickLinks = false;
82
87
  this._hasSearch = false;
83
88
  this._animationDuration = 500;
84
89
  this._brandingBarInView = false;
90
+ this._slottedComponents = {};
85
91
 
86
92
  }
87
93
 
94
+ /**
95
+ * @method connectedCallback
96
+ * @private
97
+ * @description Custom element lifecycle method
98
+ */
88
99
  connectedCallback(){
89
100
  super.connectedCallback();
90
101
  if ( !this.preventFixed ) {
@@ -92,6 +103,11 @@ export default class UcdThemeHeader extends LitElement {
92
103
  }
93
104
  }
94
105
 
106
+ /**
107
+ * @method firstUpdated
108
+ * @private
109
+ * @description Lit lifecycle hook
110
+ */
95
111
  firstUpdated(){
96
112
  if ( !this.preventFixed ) {
97
113
  let aboveNav = this.renderRoot.getElementById('branding-bar-container');
@@ -99,11 +115,33 @@ export default class UcdThemeHeader extends LitElement {
99
115
  }
100
116
  }
101
117
 
118
+ /**
119
+ * @method _onLocationChange
120
+ * @description Called when url changes by popstate controller
121
+ */
122
+ _onLocationChange(){
123
+ this.close();
124
+ if ( this._hasQuickLinks ){
125
+ this._slottedComponents.quickLinks.close();
126
+ }
127
+ if ( this._hasSearch ){
128
+ this._slottedComponents.search.close();
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @method _onBrandingBarIntersection
134
+ * @private
135
+ * @description Called by intersection observer when branding bar enters/exits screen
136
+ * @param {*} entries
137
+ */
102
138
  _onBrandingBarIntersection(entries){
103
139
  let offSetValue = 0;
104
140
  try {
105
141
  offSetValue = this.renderRoot.getElementById('nav-bar').getBoundingClientRect().height;
106
- } catch (error) {}
142
+ } catch (error) {
143
+ //
144
+ }
107
145
  if ( offSetValue > 150 ) offSetValue = 0;
108
146
  entries.forEach(entry => {
109
147
  this._brandingBarInView = entry.isIntersecting;
@@ -112,7 +150,7 @@ export default class UcdThemeHeader extends LitElement {
112
150
  } else {
113
151
  this.style.marginBottom = offSetValue + "px";
114
152
  }
115
- })
153
+ });
116
154
  }
117
155
 
118
156
  /**
@@ -232,14 +270,18 @@ export default class UcdThemeHeader extends LitElement {
232
270
  let primaryNav = this.querySelector('ucd-theme-primary-nav');
233
271
  if ( primaryNav ) {
234
272
  primaryNav.setAttribute('slot', 'primary-nav');
273
+ this._hasPrimaryNav = true;
274
+ this._slottedComponents.primaryNav = primaryNav;
235
275
  } else {
236
276
  console.warn("No 'ucd-theme-primary-nav' child element found!");
277
+ this._hasPrimaryNav = false;
237
278
  }
238
279
 
239
280
  let quickLinks = this.querySelector('ucd-theme-quick-links');
240
281
  if ( quickLinks ) {
241
282
  quickLinks.setAttribute('slot', 'quick-links');
242
283
  this._hasQuickLinks = true;
284
+ this._slottedComponents.quickLinks = quickLinks;
243
285
  } else {
244
286
  this._hasQuickLinks = false;
245
287
  }
@@ -248,6 +290,7 @@ export default class UcdThemeHeader extends LitElement {
248
290
  if ( search ) {
249
291
  search.setAttribute('slot', 'search');
250
292
  this._hasSearch = true;
293
+ this._slottedComponents.search = search;
251
294
  } else {
252
295
  this._hasSearch = false;
253
296
  }
@@ -256,6 +299,7 @@ export default class UcdThemeHeader extends LitElement {
256
299
  if ( UcdlibBrandingBar ) {
257
300
  UcdlibBrandingBar.setAttribute('slot', 'branding-bar');
258
301
  this._hasSlottedBranding = true;
302
+ this._slottedComponents.brandingBar = UcdlibBrandingBar;
259
303
  } else if ( this.querySelector("*[slot='branding-bar']") ){
260
304
  this._hasSlottedBranding = true;
261
305
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucd-lib/theme-elements",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Custom elements for the UCD brand theme",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,8 +8,9 @@ import { html, render } from "lit-html";
8
8
  * @param {TemplateResult} icons - SVG html string of icons
9
9
  * @param {String} name - name of iconset.
10
10
  * @param {Number} size - size of icons
11
+ * @param {String} label - Friendly name of iconset
11
12
  */
12
- function renderIconSet(icons, name, size=24){
13
+ function renderIconSet(icons, name, size=24, label=""){
13
14
  const containerId = `ucdlib-icons--${name}`;
14
15
  let container = document.getElementById(containerId);
15
16
  if ( !container ){
@@ -19,7 +20,7 @@ function renderIconSet(icons, name, size=24){
19
20
  document.head.appendChild(container);
20
21
  }
21
22
  const template = html`
22
- <ucdlib-iconset name=${name} size=${size}>
23
+ <ucdlib-iconset name=${name} size=${size} label=${label}>
23
24
  ${icons}
24
25
  </ucdlib-iconset>
25
26
  `;
@@ -9,6 +9,7 @@ import { MutationObserverController } from '../../utils/controllers';
9
9
  *
10
10
  * @property {String} name - Name of the icon set. Usage: <ucdlib-icon icon="{thisProperty}:{icon}"></ucdlib-icon>
11
11
  * @property {Number} size - The size of an individual icon. Note that icons must be square.
12
+ * @property {String} label - Optional friendly label for iconset.
12
13
  * @example
13
14
  * <ucdlib-iconset name="arrows">
14
15
  <svg>
@@ -26,6 +27,7 @@ export default class UcdlibIconset extends Mixin(LitElement)
26
27
  return {
27
28
  name: {type: String},
28
29
  size: {type: Number},
30
+ label: {type: String},
29
31
  _iconMap: {type: Object, state: true}
30
32
  };
31
33
  }
@@ -35,6 +37,7 @@ export default class UcdlibIconset extends Mixin(LitElement)
35
37
  this.mutationObserver = new MutationObserverController(this, {subtree: true, childList: true});
36
38
 
37
39
  this.name = "";
40
+ this.label = "";
38
41
  this.size = 24;
39
42
  this._iconMap = {};
40
43
  this.style.display = "none";
@@ -63,6 +66,17 @@ export default class UcdlibIconset extends Mixin(LitElement)
63
66
  return Object.keys(this._iconMap);
64
67
  }
65
68
 
69
+ /**
70
+ * @method getLabel
71
+ * @description Returns a friendly label of iconset
72
+ * @returns {String}
73
+ */
74
+ getLabel(){
75
+ if ( this.label ) return this.label;
76
+
77
+ return this.name.replace(/-/g, " ");
78
+ }
79
+
66
80
  /**
67
81
  * @method applyIcon
68
82
  * @description Adds icon to ucdlib-icon element from iconset
@@ -0,0 +1,138 @@
1
+ import { LitElement } from 'lit';
2
+ import {render, styles} from "./ucdlib-sils-search-redirect.tpl.js";
3
+
4
+ import { SilsPrimoController } from '../../utils/controllers';
5
+
6
+ /**
7
+ * @class UcdlibSilsSearchRedirect
8
+ * @classdesc Search widget that redirects a user's query to SILS Primo
9
+ * @property {String} query - The search query
10
+ * @property {Boolean} ucdOnly - Limits search to UC Davis libraries only
11
+ * @property {Boolean} darkBackground - Adjusts colors for display on a dark background
12
+ * @property {Boolean} preventRedirect - Will not send user to Primo on form submission
13
+ * @property {String} headingText - Text to display above main text input
14
+ * @property {String} inputPlaceholder - Placeholder for main text input
15
+ * @property {String} host - Primo host
16
+ */
17
+ export default class UcdlibSilsSearchRedirect extends LitElement {
18
+
19
+ static get properties() {
20
+ return {
21
+ query: {type: String},
22
+ ucdOnly: {type: Boolean, attribute: "ucd-only"},
23
+ darkBackground: {type: Boolean, attribute: "dark-background"},
24
+ preventRedirect: {type: Boolean, attribute: "prevent-redirect"},
25
+ headingText: {type: String, attribute: "heading-text"},
26
+ inputPlaceholder: {type: String, attribute: "input-placeholder"},
27
+ host: {type: String},
28
+ role: {type: String, reflect: true},
29
+ ariaLabel: {type: String, attribute: "aria-label", reflect: true}
30
+ };
31
+ }
32
+
33
+ static get styles() {
34
+ return styles();
35
+ }
36
+
37
+ constructor() {
38
+ super();
39
+ this.render = render.bind(this);
40
+ this.query = "";
41
+ this.ucdOnly = false;
42
+ this.darkBackground = false;
43
+ this.headingText = "Search UC Library Materials";
44
+ this.inputPlaceholder = "All UC books, journals, articles + more";
45
+ this.role = "form";
46
+ this.ariaLabel = "Search UC Library Materials";
47
+ this.host = "";
48
+ this._updatePrimoController();
49
+ }
50
+
51
+ /**
52
+ * @method willUpdate
53
+ * @description Lit lifecycle hook
54
+ * @private
55
+ * @param {Map} props - Properties that have changed
56
+ */
57
+ willUpdate(props){
58
+ this._updatePrimoController(props);
59
+ }
60
+
61
+ /**
62
+ * @method _onQueryChange
63
+ * @private
64
+ * @description Attached to primary text input
65
+ * @param {Event} e - Input event
66
+ */
67
+ _onQueryChange(e){
68
+ let text = e.target.value ? e.target.value : "";
69
+ this.query = text;
70
+ }
71
+
72
+ /**
73
+ * @method _onCorpusChange
74
+ * @private
75
+ * @description Attached to ucd-only checkbox
76
+ * @param {Event} e - Input event
77
+ */
78
+ _onCorpusChange(e){
79
+ this.ucdOnly = e.target.checked;
80
+ }
81
+
82
+ /**
83
+ * @method _onSubmit
84
+ * @description Called on form submit
85
+ * @private
86
+ * @param {Event} e - submit event
87
+ */
88
+ _onSubmit(e){
89
+ e.preventDefault();
90
+ let corpus = this.ucdOnly ? "ucd" : "everything";
91
+ let advanced = e.submitter.id == "advanced-search";
92
+
93
+ let url = this.primo.makeSearchUrl(this.query, corpus, advanced);
94
+ if ( this.preventRedirect ) {
95
+ this.dispatchEvent(new CustomEvent('search', {
96
+ detail : {url}
97
+ }));
98
+ } else {
99
+ window.location = url;
100
+ }
101
+ }
102
+
103
+
104
+ /**
105
+ * @method _updatePrimoController
106
+ * @description Updates the config values of the Primo controller based on ele attributes
107
+ * @private
108
+ * @param {Map} props - Properties that have changed in current lifecycle
109
+ */
110
+ _updatePrimoController(props){
111
+ let primoConfig = {};
112
+
113
+ // Get primo config values from ele attributes
114
+ const attrs = [
115
+ {ele: 'host', ctl: 'host'}
116
+ ];
117
+ if ( props ){
118
+ attrs.forEach(attr => {
119
+ if ( props.has(attr.ele) && this[attr.ele] ) {
120
+ primoConfig[attr.ctl] = this[attr.ele];
121
+ }
122
+ });
123
+ }
124
+
125
+ // instantiate or update controller
126
+ if ( !this.primo ) {
127
+ this.primo = new SilsPrimoController(
128
+ this,
129
+ primoConfig
130
+ );
131
+ } else {
132
+ this.primo.updateConfig(primoConfig);
133
+ }
134
+ }
135
+
136
+ }
137
+
138
+ customElements.define('ucdlib-sils-search-redirect', UcdlibSilsSearchRedirect);
@@ -0,0 +1,108 @@
1
+ import { html, css, unsafeCSS } from 'lit';
2
+
3
+ import normalizeCss from "@ucd-lib/theme-sass/normalize.css.js";
4
+ import headingCss from "@ucd-lib/theme-sass/1_base_html/_headings.css";
5
+ import headingClassesCss from "@ucd-lib/theme-sass/2_base_class/_headings.css";
6
+ import formsCss from "@ucd-lib/theme-sass/1_base_html/_forms.css";
7
+ import formsClassesCss from "@ucd-lib/theme-sass/2_base_class/_forms.css";
8
+ import buttonCss from "@ucd-lib/theme-sass/2_base_class/_buttons.css";
9
+ import spacingUtilityCss from "@ucd-lib/theme-sass/6_utility/_u-space.css";
10
+ import { categoryBrands } from "@ucd-lib/theme-sass/colors";
11
+
12
+ export function styles() {
13
+ const elementStyles = css`
14
+ :host {
15
+ display: block;
16
+ max-width: 500px;
17
+ margin: auto;
18
+ }
19
+ h2 {
20
+ text-align: center;
21
+ }
22
+ .search-bar {
23
+ display: flex;
24
+ flex-flow: row nowrap;
25
+ }
26
+ .search-bar button {
27
+ font-family: "Font Awesome 5 Free";
28
+ min-width: unset;
29
+ font-size: 1.2rem;
30
+ padding: 0 .75rem;
31
+ min-height: 0;
32
+ }
33
+ .search-bar button:hover {
34
+ padding-right: .75rem;
35
+ padding-left: .75rem;
36
+ }
37
+ .search-bar input {
38
+ flex-grow: 1;
39
+ }
40
+ .search-options {
41
+ display: flex;
42
+ flex-flow: row wrap;
43
+ align-items: center;
44
+ justify-content: space-between;
45
+ }
46
+ .search-options label {
47
+ color: ${unsafeCSS(categoryBrands.primary.hex)};
48
+ padding-bottom: 0;
49
+ }
50
+ input[type=checkbox] {
51
+ margin-right: 0;
52
+ }
53
+ .search-options button {
54
+ border: none;
55
+ background-color: inherit;
56
+ color: ${unsafeCSS(categoryBrands.primary.hex)};
57
+ text-decoration: underline;
58
+ padding: 0;
59
+ font-family: inherit;
60
+ }
61
+ .dark h2 {
62
+ color: ${unsafeCSS(categoryBrands.secondary.hex)}
63
+ }
64
+ .dark .search-options label {
65
+ color: #fff;
66
+ }
67
+ .dark .search-options button {
68
+ color: #fff;
69
+ }
70
+
71
+ `;
72
+
73
+ return [
74
+ normalizeCss,
75
+ headingCss,
76
+ headingClassesCss,
77
+ formsCss,
78
+ formsClassesCss,
79
+ buttonCss,
80
+ spacingUtilityCss,
81
+ elementStyles];
82
+ }
83
+
84
+ export function render() {
85
+ return html`
86
+ <form
87
+ @submit=${this._onSubmit}
88
+ aria-label=${this.ariaLabel}
89
+ class="${this.darkBackground ? 'dark' : 'light'}">
90
+
91
+ ${this.headingText ? html`
92
+ <h2 class="heading--highlight u-space-mb">${this.headingText}</h2>
93
+ ` : html``}
94
+
95
+ <div class="search-bar">
96
+ <input type="text" .value=${this.query} @input=${this._onQueryChange} placeholder=${this.inputPlaceholder}>
97
+ <button id="simple-search" type="submit" class="btn btn--primary-input"> &#xf002</button>
98
+ </div>
99
+
100
+ <div class="search-options">
101
+ <div class="checkbox u-space-mr--small u-space-mt--small">
102
+ <input id="corpus" type="checkbox" ?checked=${this.ucdOnly} @input=${this._onCorpusChange}><label for="corpus">UC Davis libraries only</label>
103
+ </div>
104
+ <button id="advanced-search" class="u-space-mt--small" type="submit">Advanced Search</button>
105
+ </div>
106
+ </form>
107
+
108
+ `;}
@@ -1,11 +1,16 @@
1
1
  import { BreakPointsController } from "./break-points";
2
2
  import { IntersectionObserverController } from "./intersection-observer";
3
3
  import { MutationObserverController } from "./mutation-observer";
4
+ import { PopStateObserverController } from "./popstate-observer";
5
+ import { SilsPrimoController } from "./sils-primo";
4
6
  import { WaitController } from "./wait";
5
7
 
8
+
6
9
  export {
7
10
  BreakPointsController,
8
11
  IntersectionObserverController,
9
12
  MutationObserverController,
13
+ PopStateObserverController,
14
+ SilsPrimoController,
10
15
  WaitController,
11
16
  };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @class PopStateObserverController
3
+ * @classdesc Controller for attaching a popstate event listener to a Lit element.
4
+ *
5
+ * @property {LitElement} host - 'this' from a Lit element
6
+ * @property {String} callback - Name of element method called on popstate. Default: '_onPopstate'
7
+ *
8
+ * @examples
9
+ * // Instantiate this controller in the constructor of your element
10
+ * new PopStateObserverController(this, "_onLocationChange");
11
+ */
12
+ export class PopStateObserverController{
13
+
14
+ constructor(host, callback="_onPopstate"){
15
+ (this.host = host).addController(this);
16
+ this.callback = callback;
17
+ this._onPopstate = this._onPopstate.bind(this);
18
+ }
19
+
20
+ hostConnected(){
21
+ window.addEventListener('popstate', this._onPopstate);
22
+ }
23
+
24
+ hostDisconnected(){
25
+ window.removeEventListener('popstate', this._onPopstate);
26
+ }
27
+
28
+ _onPopstate(e){
29
+ if ( !this.host[this.callback]){
30
+ console.warn(
31
+ `Element has no '${this.callback}' method.
32
+ Either add this method, or change the 'callback' argument on instantiation.`
33
+ );
34
+ return;
35
+ }
36
+ let locationObject = this._getLocationObject();
37
+ this.host[this.callback](locationObject, e);
38
+
39
+ }
40
+
41
+ _getLocationObject(){
42
+ let location = {
43
+ fullpath : window.location.href.replace(window.location.origin, '').replace(/^\/+/, '/'),
44
+ pathname : window.location.pathname.replace(/^\/+/, '/'),
45
+ path : window.location.pathname.replace(/(^\/+|\/+$)/g, '').split('/'),
46
+ query : new URLSearchParams(window.location.search),
47
+ hash : window.location.hash.replace(/^#/, '')
48
+ };
49
+ return location;
50
+ }
51
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @class SilsPrimoController
3
+ * @classdesc Utility for interacting with UC Libraries' discovery tool
4
+ *
5
+ * @property {LitElement} host - 'this' from a Lit element
6
+ * @property {Object} config - Basic Primo configuration values (host, uris, etc)
7
+ */
8
+ export class SilsPrimoController{
9
+
10
+ /**
11
+ * @method constructor
12
+ * @description Called on instantiation
13
+ * @param {LitElement} host - Element
14
+ * @param {Object} config - Config values
15
+ */
16
+ constructor(host, config={}){
17
+ (this.host = host).addController(this);
18
+ this.updateConfig(config);
19
+ }
20
+
21
+ /**
22
+ * @method updateConfig
23
+ * @description Updates the config property.
24
+ * @param {Object} config - Values to overide the default.
25
+ */
26
+ updateConfig(config){
27
+ const UCD_TAB = "LibraryCatalog";
28
+ let _config = {
29
+ host: "https://search.library.ucdavis.edu",
30
+ paths: {
31
+ search: "discovery/search",
32
+ browse: "discovery/browse"
33
+ },
34
+ defaultParams: {
35
+ vid: "01UCD_INST:UCD"
36
+ },
37
+ corpora: {
38
+ everything: {
39
+ tab: "UCSILSDefaultSearch",
40
+ scope: "DN_and_CI"
41
+ },
42
+ uc: {
43
+ tab: "UCSDiscoveryNetwork",
44
+ scope: "UCSDiscoveryNetwork"
45
+ },
46
+ ucd: {
47
+ tab: UCD_TAB,
48
+ scope: "MyInstitution",
49
+ },
50
+ specialCollections: {
51
+ tab: UCD_TAB,
52
+ scope: "SSPEC"
53
+ },
54
+ medical: {
55
+ tab: UCD_TAB,
56
+ scope: "BLAISDELL"
57
+ },
58
+ healthSciences: {
59
+ tab: UCD_TAB,
60
+ scope: "CARLSON"
61
+ },
62
+ law: {
63
+ tab: UCD_TAB,
64
+ scope: "Mabie"
65
+ }
66
+ }
67
+ };
68
+
69
+ this.config = Object.assign(_config, config);
70
+ }
71
+
72
+ /**
73
+ * @method makeSearchUrl
74
+ * @description Makes a Primo Search URL
75
+ * @param {String} query - A search term or phrase
76
+ * @param {String} corpus - The bib corpus to search against.
77
+ * Sets 'tab' and 'search_scope' params. Must be a recognized keyword in the corpora config object:
78
+ * everything, uc, ucd, specialCollections, medical, healthSciences, law
79
+ * @param {Boolean} advanced - Expands the advanced search interface
80
+ * @param {Object} additionalParams - Any additional url params. Has the final say.
81
+ * @returns
82
+ */
83
+ makeSearchUrl( query, corpus="everything", advanced=false, additionalParams={} ){
84
+ let url = `${this._trailingSlashIt(this.config.host)}${this.config.paths.search}`;
85
+
86
+ let params = Object.assign({}, this.config.defaultParams);
87
+
88
+ if ( advanced ) {
89
+ params['mode'] = 'advanced';
90
+ }
91
+
92
+ if ( query ) {
93
+ params['query'] = 'any,contains,' + query.replace(/,/g, ' ');
94
+ }
95
+
96
+ if ( this.config.corpora[corpus] ) {
97
+ params['tab'] = this.config.corpora[corpus].tab;
98
+ params['search_scope'] = this.config.corpora[corpus].scope;
99
+ } else {
100
+ console.warn(`${corpus} is not a recognized corpus`);
101
+ }
102
+
103
+ if ( additionalParams ){
104
+ Object.assign(params, additionalParams);
105
+ }
106
+
107
+ params = new URLSearchParams(params);
108
+ return `${url}?${params.toString()}`;
109
+ }
110
+
111
+ /**
112
+ * @method _trailingSlashIt
113
+ * @description Adds trailing slash to string if not already present
114
+ * @private
115
+ * @param {String} str
116
+ * @returns
117
+ */
118
+ _trailingSlashIt(str){
119
+ if ( str.endsWith('/') ){
120
+ return str;
121
+ }
122
+ return str + "/";
123
+ }
124
+ }