@stellar-expert/ui-framework 1.9.11 → 1.10.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/api/explorer-tx-api.js +28 -28
- package/contract/sc-val.scss +2 -2
- package/effect/effect-description.js +288 -288
- package/effect/op-effects.scss +7 -7
- package/interaction/accordion.js +35 -35
- package/interaction/accordion.scss +31 -31
- package/meta/page-meta-tags.js +228 -228
- package/module/dynamic-module.js +47 -47
- package/package.json +4 -4
package/interaction/accordion.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
-
import cn from 'classnames'
|
|
3
|
-
import './accordion.scss'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Group of collapsible panels
|
|
7
|
-
* @param {{key: String, title: String|JSX.Element, content:*}[]} options - Accordion options
|
|
8
|
-
* @param {String} [collapsedSymbol] - Prefix to show for collapsed panels
|
|
9
|
-
* @param {String} [expandedSymbol] - Prefix to show for expanded panel
|
|
10
|
-
*/
|
|
11
|
-
export function Accordion({options, collapsedSymbol = '+', expandedSymbol = '-', ...otherProps}) {
|
|
12
|
-
const [selectedOption, setSelectedOption] = useState()
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const firstOption = options[0]
|
|
15
|
-
setSelectedOption(firstOption.key || firstOption.title)
|
|
16
|
-
}, [options])
|
|
17
|
-
const changeSelected = useCallback(e => setSelectedOption(e.currentTarget.dataset.key), [options])
|
|
18
|
-
return <div className="accordion" {...otherProps}>
|
|
19
|
-
{options.map(({key, title, content}) => {
|
|
20
|
-
const expanded = key ?
|
|
21
|
-
selectedOption === key :
|
|
22
|
-
selectedOption === title
|
|
23
|
-
return <div key={key || title} className={cn('option', {expanded})}>
|
|
24
|
-
<div className="accordion-header" data-collapsed={collapsedSymbol} data-expanded={expandedSymbol} data-key={key || title}
|
|
25
|
-
onClick={changeSelected}>
|
|
26
|
-
{title || key}
|
|
27
|
-
</div>
|
|
28
|
-
<div className="accordion-collapse">
|
|
29
|
-
<div className="accordion-body">
|
|
30
|
-
{content}
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
})}
|
|
35
|
-
</div>
|
|
1
|
+
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
import './accordion.scss'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Group of collapsible panels
|
|
7
|
+
* @param {{key: String, title: String|JSX.Element, content:*}[]} options - Accordion options
|
|
8
|
+
* @param {String} [collapsedSymbol] - Prefix to show for collapsed panels
|
|
9
|
+
* @param {String} [expandedSymbol] - Prefix to show for expanded panel
|
|
10
|
+
*/
|
|
11
|
+
export function Accordion({options, collapsedSymbol = '+', expandedSymbol = '-', ...otherProps}) {
|
|
12
|
+
const [selectedOption, setSelectedOption] = useState()
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const firstOption = options[0]
|
|
15
|
+
setSelectedOption(firstOption.key || firstOption.title)
|
|
16
|
+
}, [options])
|
|
17
|
+
const changeSelected = useCallback(e => setSelectedOption(e.currentTarget.dataset.key), [options])
|
|
18
|
+
return <div className="accordion" {...otherProps}>
|
|
19
|
+
{options.map(({key, title, content}) => {
|
|
20
|
+
const expanded = key ?
|
|
21
|
+
selectedOption === key :
|
|
22
|
+
selectedOption === title
|
|
23
|
+
return <div key={key || title} className={cn('option', {expanded})}>
|
|
24
|
+
<div className="accordion-header" data-collapsed={collapsedSymbol} data-expanded={expandedSymbol} data-key={key || title}
|
|
25
|
+
onClick={changeSelected}>
|
|
26
|
+
{title || key}
|
|
27
|
+
</div>
|
|
28
|
+
<div className="accordion-collapse">
|
|
29
|
+
<div className="accordion-body">
|
|
30
|
+
{content}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
})}
|
|
35
|
+
</div>
|
|
36
36
|
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
.accordion {
|
|
2
|
-
.accordion-header {
|
|
3
|
-
position: relative;
|
|
4
|
-
cursor: pointer;
|
|
5
|
-
user-select: none;
|
|
6
|
-
|
|
7
|
-
&:before {
|
|
8
|
-
content: attr(data-collapsed);
|
|
9
|
-
position: absolute;
|
|
10
|
-
top: 50%;
|
|
11
|
-
transform: translateY(-50%);
|
|
12
|
-
left: -20px;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.accordion-collapse {
|
|
17
|
-
height: 0;
|
|
18
|
-
overflow-y: hidden;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.option {
|
|
22
|
-
&.expanded {
|
|
23
|
-
.accordion-header:before {
|
|
24
|
-
content: attr(data-expanded);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.accordion-collapse {
|
|
28
|
-
height: auto;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
.accordion {
|
|
2
|
+
.accordion-header {
|
|
3
|
+
position: relative;
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
user-select: none;
|
|
6
|
+
|
|
7
|
+
&:before {
|
|
8
|
+
content: attr(data-collapsed);
|
|
9
|
+
position: absolute;
|
|
10
|
+
top: 50%;
|
|
11
|
+
transform: translateY(-50%);
|
|
12
|
+
left: -20px;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.accordion-collapse {
|
|
17
|
+
height: 0;
|
|
18
|
+
overflow-y: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.option {
|
|
22
|
+
&.expanded {
|
|
23
|
+
.accordion-header:before {
|
|
24
|
+
content: attr(data-expanded);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.accordion-collapse {
|
|
28
|
+
height: auto;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
32
|
}
|
package/meta/page-meta-tags.js
CHANGED
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
import {useEffect} from 'react'
|
|
2
|
-
import isEqual from 'react-fast-compare'
|
|
3
|
-
|
|
4
|
-
const {origin} = window.location
|
|
5
|
-
const domain = origin.replace(/https?:\/\//, '')
|
|
6
|
-
|
|
7
|
-
const metaProps = {
|
|
8
|
-
serviceTitle: '',
|
|
9
|
-
description: '',
|
|
10
|
-
facebookImage: '',
|
|
11
|
-
twitterImage: '',
|
|
12
|
-
twitterUsername: ''
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Initialize default meta properties for the application
|
|
17
|
-
* @param {{serviceTitle: String, description: String, facebookImage: String, twitterImage: String, twitterUsername: String}} appMetaProps
|
|
18
|
-
*/
|
|
19
|
-
export function initMeta(appMetaProps) {
|
|
20
|
-
Object.assign(metaProps, appMetaProps)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function formatPageTitle(title) {
|
|
24
|
-
if (!title)
|
|
25
|
-
return metaProps.serviceTitle
|
|
26
|
-
if (title.includes(metaProps.serviceTitle))
|
|
27
|
-
return title
|
|
28
|
-
return `${title} ${metaProps.serviceTitle}`
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function generateCanonicalLink(params, canonicalUrl) {
|
|
32
|
-
return {
|
|
33
|
-
tag: 'link',
|
|
34
|
-
locator: 'rel',
|
|
35
|
-
tags: [
|
|
36
|
-
{name: 'canonical', content: canonicalUrl, attribute: 'href'}
|
|
37
|
-
]
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function generateDescriptionMeta({description}) {
|
|
42
|
-
return {
|
|
43
|
-
locator: 'name',
|
|
44
|
-
tags: [
|
|
45
|
-
{name: 'description', content: description || metaProps.description}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function generateTwitterMeta({description, title, twitterImage}) {
|
|
51
|
-
return {
|
|
52
|
-
locator: 'name',
|
|
53
|
-
tags: [
|
|
54
|
-
{name: 'twitter:card', content: 'summary_large_image'},
|
|
55
|
-
{name: 'twitter:site', content: metaProps.twitterUsername},
|
|
56
|
-
{name: 'twitter:title', content: formatPageTitle(title)},
|
|
57
|
-
{name: 'twitter:description', content: description || metaProps.description},
|
|
58
|
-
{name: 'twitter:image:src', content: twitterImage || metaProps.twitterImage}
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function generateOpenGraphMeta({description, title, facebookImage}, canonicalUrl) {
|
|
64
|
-
let tags = [
|
|
65
|
-
{name: 'og:title', content: formatPageTitle(title)},
|
|
66
|
-
{name: 'og:url', content: canonicalUrl},
|
|
67
|
-
{name: 'og:site_name', content: formatPageTitle(metaProps.serviceTitle)},
|
|
68
|
-
{name: 'og:description', content: description || metaProps.description},
|
|
69
|
-
{name: 'og:type', content: 'website'}
|
|
70
|
-
]
|
|
71
|
-
if (facebookImage) {
|
|
72
|
-
tags.push({name: 'og:image', content: facebookImage})
|
|
73
|
-
} else {
|
|
74
|
-
tags.push({name: 'og:image', content: metaProps.facebookImage})
|
|
75
|
-
/*tags = tags.concat([
|
|
76
|
-
{name: 'og:image', content: metaProps.facebookImage},
|
|
77
|
-
{name: 'og:image:width', content: 1200},
|
|
78
|
-
{name: 'og:image:height', content: 630}])*/
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
locator: 'property',
|
|
82
|
-
tags
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function generateItemPropSchema({description, title, image}) {
|
|
87
|
-
return {
|
|
88
|
-
locator: 'itemprop',
|
|
89
|
-
tags: [
|
|
90
|
-
{name: 'name', content: title},
|
|
91
|
-
{name: 'description', content: description || metaProps.description},
|
|
92
|
-
{name: 'image', content: image || metaProps.facebookImage}
|
|
93
|
-
]
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function generateLdJsonSchema({title}) {
|
|
98
|
-
return {
|
|
99
|
-
tag: 'script',
|
|
100
|
-
locator: 'type',
|
|
101
|
-
tags: [
|
|
102
|
-
{
|
|
103
|
-
name: 'application/ld+json',
|
|
104
|
-
content: JSON.stringify({
|
|
105
|
-
'@context': 'http://schema.org',
|
|
106
|
-
'@type': 'WebSite',
|
|
107
|
-
'name': domain,
|
|
108
|
-
'alternateName': title,
|
|
109
|
-
'url': origin
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
]
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @param {MetaTagReplacement} replacement
|
|
118
|
-
*/
|
|
119
|
-
function replaceMetaTags(replacement) {
|
|
120
|
-
const {tag, tags, locator} = replacement
|
|
121
|
-
const selector = tag || 'meta'
|
|
122
|
-
tags.forEach(tagSettings => {
|
|
123
|
-
let tag = document.querySelector(`${selector}[${locator}="${tagSettings.name}"]`)
|
|
124
|
-
if (!tag) {
|
|
125
|
-
tag = createTag(selector)
|
|
126
|
-
tag.setAttribute(locator, tagSettings.name)
|
|
127
|
-
}
|
|
128
|
-
if (selector === 'meta') {
|
|
129
|
-
tag.content = tagSettings.content
|
|
130
|
-
} else if (tagSettings.attribute) {
|
|
131
|
-
tag[tagSettings.attribute] = tagSettings.content
|
|
132
|
-
} else {
|
|
133
|
-
tag.innerText = tagSettings.content
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function createTag(tagName, props) {
|
|
139
|
-
let tag = document.createElement(tagName)
|
|
140
|
-
for (let key in props) {
|
|
141
|
-
tag[key] = props[key]
|
|
142
|
-
}
|
|
143
|
-
document.head.appendChild(tag)
|
|
144
|
-
return tag
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function removeTag(selector) {
|
|
148
|
-
let tag = document.querySelector(selector)
|
|
149
|
-
if (tag) {
|
|
150
|
-
tag.parentElement.removeChild(tag)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let pageMeta = {}
|
|
155
|
-
|
|
156
|
-
const tagReplacerPipeline = [generateCanonicalLink, generateDescriptionMeta, generateOpenGraphMeta, generateTwitterMeta, generateLdJsonSchema] // generateItemPropSchema
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Update page metadata tags
|
|
160
|
-
* @param {PageMeta} meta - Page metadata
|
|
161
|
-
*/
|
|
162
|
-
export function setPageMetadata(meta) {
|
|
163
|
-
if (isEqual(pageMeta, meta))
|
|
164
|
-
return
|
|
165
|
-
const canonicalUrl = origin + location.pathname// + location.search
|
|
166
|
-
document.title = formatPageTitle(meta.title)
|
|
167
|
-
for (const replacer of tagReplacerPipeline) {
|
|
168
|
-
replaceMetaTags(replacer(meta, canonicalUrl))
|
|
169
|
-
}
|
|
170
|
-
if (meta.customMeta) {
|
|
171
|
-
replaceMetaTags(meta.customMeta)
|
|
172
|
-
}
|
|
173
|
-
pageMeta = meta
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Reset page metadata tags to their default values
|
|
178
|
-
* @param {PageMeta} meta - Page metadata
|
|
179
|
-
*/
|
|
180
|
-
export function resetPageMetadata(meta) {
|
|
181
|
-
setPageMetadata({
|
|
182
|
-
title: metaProps.serviceTitle,
|
|
183
|
-
description: metaProps.description,
|
|
184
|
-
twitterImage: metaProps.twitterImage,
|
|
185
|
-
facebookImage: metaProps.facebookImage
|
|
186
|
-
})
|
|
187
|
-
//TODO: add logic to cleanup custom page meta tags on page unload
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/*export function setPageNoIndex(noIndex) {
|
|
191
|
-
if (!noIndex) {
|
|
192
|
-
removeTag('meta[name=robots]')
|
|
193
|
-
} else {
|
|
194
|
-
replaceMetaTags({
|
|
195
|
-
locator: 'name',
|
|
196
|
-
tags: [
|
|
197
|
-
{name: 'robots', content: 'noindex,nofollow'}
|
|
198
|
-
]
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
}*/
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* React hook for setting page metadata
|
|
205
|
-
* @param {PageMeta} meta - Page metadata
|
|
206
|
-
* @param {*[]} dependencies
|
|
207
|
-
*/
|
|
208
|
-
export function usePageMetadata(meta, dependencies = []) {
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
setPageMetadata(meta)
|
|
211
|
-
return () => resetPageMetadata(meta)
|
|
212
|
-
}, [JSON.stringify(meta), ...dependencies])
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* @typedef {Object} PageMeta
|
|
217
|
-
* @property {String} title - Page title
|
|
218
|
-
* @property {String} description - Contents description
|
|
219
|
-
* @property {String} [twitterImage] - Twitter image url
|
|
220
|
-
* @property {String} [facebookImage] - Facebook image url
|
|
221
|
-
* @property {MetaTagReplacement} [customMeta] - Custom metadata tags
|
|
222
|
-
*/
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* @typedef {Object} MetaTagReplacement
|
|
226
|
-
* @property {String} [tag] - Tag name to search for ("meta" by default)
|
|
227
|
-
* @property {String} locator - Tag attribute to match
|
|
228
|
-
* @property {{name: String, content: String, [attribute]: String}[]} tags - Tag properties to set
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import isEqual from 'react-fast-compare'
|
|
3
|
+
|
|
4
|
+
const {origin} = window.location
|
|
5
|
+
const domain = origin.replace(/https?:\/\//, '')
|
|
6
|
+
|
|
7
|
+
const metaProps = {
|
|
8
|
+
serviceTitle: '',
|
|
9
|
+
description: '',
|
|
10
|
+
facebookImage: '',
|
|
11
|
+
twitterImage: '',
|
|
12
|
+
twitterUsername: ''
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize default meta properties for the application
|
|
17
|
+
* @param {{serviceTitle: String, description: String, facebookImage: String, twitterImage: String, twitterUsername: String}} appMetaProps
|
|
18
|
+
*/
|
|
19
|
+
export function initMeta(appMetaProps) {
|
|
20
|
+
Object.assign(metaProps, appMetaProps)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatPageTitle(title) {
|
|
24
|
+
if (!title)
|
|
25
|
+
return metaProps.serviceTitle
|
|
26
|
+
if (title.includes(metaProps.serviceTitle))
|
|
27
|
+
return title
|
|
28
|
+
return `${title} ${metaProps.serviceTitle}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generateCanonicalLink(params, canonicalUrl) {
|
|
32
|
+
return {
|
|
33
|
+
tag: 'link',
|
|
34
|
+
locator: 'rel',
|
|
35
|
+
tags: [
|
|
36
|
+
{name: 'canonical', content: canonicalUrl, attribute: 'href'}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function generateDescriptionMeta({description}) {
|
|
42
|
+
return {
|
|
43
|
+
locator: 'name',
|
|
44
|
+
tags: [
|
|
45
|
+
{name: 'description', content: description || metaProps.description}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function generateTwitterMeta({description, title, twitterImage}) {
|
|
51
|
+
return {
|
|
52
|
+
locator: 'name',
|
|
53
|
+
tags: [
|
|
54
|
+
{name: 'twitter:card', content: 'summary_large_image'},
|
|
55
|
+
{name: 'twitter:site', content: metaProps.twitterUsername},
|
|
56
|
+
{name: 'twitter:title', content: formatPageTitle(title)},
|
|
57
|
+
{name: 'twitter:description', content: description || metaProps.description},
|
|
58
|
+
{name: 'twitter:image:src', content: twitterImage || metaProps.twitterImage}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function generateOpenGraphMeta({description, title, facebookImage}, canonicalUrl) {
|
|
64
|
+
let tags = [
|
|
65
|
+
{name: 'og:title', content: formatPageTitle(title)},
|
|
66
|
+
{name: 'og:url', content: canonicalUrl},
|
|
67
|
+
{name: 'og:site_name', content: formatPageTitle(metaProps.serviceTitle)},
|
|
68
|
+
{name: 'og:description', content: description || metaProps.description},
|
|
69
|
+
{name: 'og:type', content: 'website'}
|
|
70
|
+
]
|
|
71
|
+
if (facebookImage) {
|
|
72
|
+
tags.push({name: 'og:image', content: facebookImage})
|
|
73
|
+
} else {
|
|
74
|
+
tags.push({name: 'og:image', content: metaProps.facebookImage})
|
|
75
|
+
/*tags = tags.concat([
|
|
76
|
+
{name: 'og:image', content: metaProps.facebookImage},
|
|
77
|
+
{name: 'og:image:width', content: 1200},
|
|
78
|
+
{name: 'og:image:height', content: 630}])*/
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
locator: 'property',
|
|
82
|
+
tags
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function generateItemPropSchema({description, title, image}) {
|
|
87
|
+
return {
|
|
88
|
+
locator: 'itemprop',
|
|
89
|
+
tags: [
|
|
90
|
+
{name: 'name', content: title},
|
|
91
|
+
{name: 'description', content: description || metaProps.description},
|
|
92
|
+
{name: 'image', content: image || metaProps.facebookImage}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function generateLdJsonSchema({title}) {
|
|
98
|
+
return {
|
|
99
|
+
tag: 'script',
|
|
100
|
+
locator: 'type',
|
|
101
|
+
tags: [
|
|
102
|
+
{
|
|
103
|
+
name: 'application/ld+json',
|
|
104
|
+
content: JSON.stringify({
|
|
105
|
+
'@context': 'http://schema.org',
|
|
106
|
+
'@type': 'WebSite',
|
|
107
|
+
'name': domain,
|
|
108
|
+
'alternateName': title,
|
|
109
|
+
'url': origin
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {MetaTagReplacement} replacement
|
|
118
|
+
*/
|
|
119
|
+
function replaceMetaTags(replacement) {
|
|
120
|
+
const {tag, tags, locator} = replacement
|
|
121
|
+
const selector = tag || 'meta'
|
|
122
|
+
tags.forEach(tagSettings => {
|
|
123
|
+
let tag = document.querySelector(`${selector}[${locator}="${tagSettings.name}"]`)
|
|
124
|
+
if (!tag) {
|
|
125
|
+
tag = createTag(selector)
|
|
126
|
+
tag.setAttribute(locator, tagSettings.name)
|
|
127
|
+
}
|
|
128
|
+
if (selector === 'meta') {
|
|
129
|
+
tag.content = tagSettings.content
|
|
130
|
+
} else if (tagSettings.attribute) {
|
|
131
|
+
tag[tagSettings.attribute] = tagSettings.content
|
|
132
|
+
} else {
|
|
133
|
+
tag.innerText = tagSettings.content
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createTag(tagName, props) {
|
|
139
|
+
let tag = document.createElement(tagName)
|
|
140
|
+
for (let key in props) {
|
|
141
|
+
tag[key] = props[key]
|
|
142
|
+
}
|
|
143
|
+
document.head.appendChild(tag)
|
|
144
|
+
return tag
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function removeTag(selector) {
|
|
148
|
+
let tag = document.querySelector(selector)
|
|
149
|
+
if (tag) {
|
|
150
|
+
tag.parentElement.removeChild(tag)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let pageMeta = {}
|
|
155
|
+
|
|
156
|
+
const tagReplacerPipeline = [generateCanonicalLink, generateDescriptionMeta, generateOpenGraphMeta, generateTwitterMeta, generateLdJsonSchema] // generateItemPropSchema
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Update page metadata tags
|
|
160
|
+
* @param {PageMeta} meta - Page metadata
|
|
161
|
+
*/
|
|
162
|
+
export function setPageMetadata(meta) {
|
|
163
|
+
if (isEqual(pageMeta, meta))
|
|
164
|
+
return
|
|
165
|
+
const canonicalUrl = origin + location.pathname// + location.search
|
|
166
|
+
document.title = formatPageTitle(meta.title)
|
|
167
|
+
for (const replacer of tagReplacerPipeline) {
|
|
168
|
+
replaceMetaTags(replacer(meta, canonicalUrl))
|
|
169
|
+
}
|
|
170
|
+
if (meta.customMeta) {
|
|
171
|
+
replaceMetaTags(meta.customMeta)
|
|
172
|
+
}
|
|
173
|
+
pageMeta = meta
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reset page metadata tags to their default values
|
|
178
|
+
* @param {PageMeta} meta - Page metadata
|
|
179
|
+
*/
|
|
180
|
+
export function resetPageMetadata(meta) {
|
|
181
|
+
setPageMetadata({
|
|
182
|
+
title: metaProps.serviceTitle,
|
|
183
|
+
description: metaProps.description,
|
|
184
|
+
twitterImage: metaProps.twitterImage,
|
|
185
|
+
facebookImage: metaProps.facebookImage
|
|
186
|
+
})
|
|
187
|
+
//TODO: add logic to cleanup custom page meta tags on page unload
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/*export function setPageNoIndex(noIndex) {
|
|
191
|
+
if (!noIndex) {
|
|
192
|
+
removeTag('meta[name=robots]')
|
|
193
|
+
} else {
|
|
194
|
+
replaceMetaTags({
|
|
195
|
+
locator: 'name',
|
|
196
|
+
tags: [
|
|
197
|
+
{name: 'robots', content: 'noindex,nofollow'}
|
|
198
|
+
]
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}*/
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* React hook for setting page metadata
|
|
205
|
+
* @param {PageMeta} meta - Page metadata
|
|
206
|
+
* @param {*[]} dependencies
|
|
207
|
+
*/
|
|
208
|
+
export function usePageMetadata(meta, dependencies = []) {
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
setPageMetadata(meta)
|
|
211
|
+
return () => resetPageMetadata(meta)
|
|
212
|
+
}, [JSON.stringify(meta), ...dependencies])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @typedef {Object} PageMeta
|
|
217
|
+
* @property {String} title - Page title
|
|
218
|
+
* @property {String} description - Contents description
|
|
219
|
+
* @property {String} [twitterImage] - Twitter image url
|
|
220
|
+
* @property {String} [facebookImage] - Facebook image url
|
|
221
|
+
* @property {MetaTagReplacement} [customMeta] - Custom metadata tags
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @typedef {Object} MetaTagReplacement
|
|
226
|
+
* @property {String} [tag] - Tag name to search for ("meta" by default)
|
|
227
|
+
* @property {String} locator - Tag attribute to match
|
|
228
|
+
* @property {{name: String, content: String, [attribute]: String}[]} tags - Tag properties to set
|
|
229
229
|
*/
|