@uniweb/kit 0.11.2 → 0.12.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.
- package/package.json +3 -3
- package/src/components/Icon/Icon.jsx +26 -4
- package/src/hooks/useFormSubmit.js +15 -9
- package/src/hooks/useFormValues.js +1 -1
- package/src/index.js +1 -3
- package/src/styled/Render/index.jsx +49 -12
- package/src/utils/index.js +5 -2
- package/src/utils/services.js +45 -25
- package/src/utils/submitTarget.js +4 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"shiki": "^3.0.0",
|
|
45
45
|
"tailwind-merge": "^3.6.0",
|
|
46
46
|
"temml": "^0.13.2",
|
|
47
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/semantic-parser": "^1.2.2",
|
|
48
48
|
"@uniweb/scene": "^0.1.3",
|
|
49
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/core": "^0.8.5"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "^19.0.0",
|
|
@@ -38,6 +38,28 @@ const BUILT_IN_ICONS = {
|
|
|
38
38
|
* @param {string} svgContent - Raw SVG string
|
|
39
39
|
* @returns {Object} { viewBox, content, width, height }
|
|
40
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Coerce a size prop into a valid CSS length.
|
|
43
|
+
*
|
|
44
|
+
* React appends `px` to a NUMERIC style value but passes a string through
|
|
45
|
+
* verbatim — and this component's default is the string `'24'`, so it emitted
|
|
46
|
+
* `style="width:24;height:24"`. Both declarations are invalid CSS and the
|
|
47
|
+
* browser drops them, leaving the <svg> at its default replaced-element size.
|
|
48
|
+
*
|
|
49
|
+
* That stayed invisible for as long as every caller sized the icon with a class
|
|
50
|
+
* (`h-8 w-8`) or placed it where a stray size did not matter. It became visible
|
|
51
|
+
* when icons started rendering INLINE inside prose: with no valid width, the
|
|
52
|
+
* glyph could not share a line and broke the sentence across three lines.
|
|
53
|
+
*
|
|
54
|
+
* Only a bare number is touched, so `'2rem'`, `'1.5em'`, `'100%'` and a real
|
|
55
|
+
* number are all unchanged.
|
|
56
|
+
*/
|
|
57
|
+
function cssLength(value) {
|
|
58
|
+
return typeof value === 'string' && /^-?\d*\.?\d+$/.test(value.trim())
|
|
59
|
+
? `${value.trim()}px`
|
|
60
|
+
: value
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
/**
|
|
42
64
|
* Extract an attribute value from an SVG opening tag string
|
|
43
65
|
*/
|
|
@@ -253,7 +275,7 @@ export function Icon({
|
|
|
253
275
|
loadingComponent || (
|
|
254
276
|
<span
|
|
255
277
|
className={cn('inline-flex items-center justify-center', className)}
|
|
256
|
-
style={{ width: size, height: size }}
|
|
278
|
+
style={{ width: cssLength(size), height: cssLength(size) }}
|
|
257
279
|
role="img"
|
|
258
280
|
aria-hidden="true"
|
|
259
281
|
/>
|
|
@@ -287,7 +309,7 @@ export function Icon({
|
|
|
287
309
|
return (
|
|
288
310
|
<span
|
|
289
311
|
className={cn('inline-flex items-center justify-center', className)}
|
|
290
|
-
style={{ width: size, height: size }}
|
|
312
|
+
style={{ width: cssLength(size), height: cssLength(size) }}
|
|
291
313
|
role="img"
|
|
292
314
|
aria-hidden="true"
|
|
293
315
|
/>
|
|
@@ -298,8 +320,8 @@ export function Icon({
|
|
|
298
320
|
|
|
299
321
|
// Build style
|
|
300
322
|
const style = {
|
|
301
|
-
width: size,
|
|
302
|
-
height: size,
|
|
323
|
+
width: cssLength(size),
|
|
324
|
+
height: cssLength(size),
|
|
303
325
|
...(color && !preserveColors ? { color } : {})
|
|
304
326
|
}
|
|
305
327
|
|
|
@@ -9,19 +9,27 @@ import { useWebsite } from './useWebsite.js'
|
|
|
9
9
|
*
|
|
10
10
|
* The hook resolves *where* to submit from the site's configuration (`submit:`
|
|
11
11
|
* in site.yml) or from its host, so a component never names an endpoint. When
|
|
12
|
-
* neither supplies one, `canSubmit` is false
|
|
13
|
-
* render the control disabled rather than letting someone fill in a form whose
|
|
12
|
+
* neither supplies one, `canSubmit` is false — don't render a form whose
|
|
14
13
|
* contents have nowhere to go. See `resolveSubmitTarget` for the precedence.
|
|
15
14
|
*
|
|
16
15
|
* ```jsx
|
|
17
|
-
* const { submit, status, error, canSubmit
|
|
16
|
+
* const { submit, status, error, canSubmit } =
|
|
18
17
|
* useFormSubmit({ block, context: { formId: 'contact' } })
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* if (!canSubmit) return null // or fall back to contact details the
|
|
20
|
+
* // site already carries in its content
|
|
21
|
+
*
|
|
22
|
+
* <button type="submit" disabled={status === 'submitting'}>Send</button>
|
|
23
23
|
* ```
|
|
24
24
|
*
|
|
25
|
+
* ⛔ **There is no `unavailableReason`, and there was one — it was a mistake.**
|
|
26
|
+
* It carried a canned English sentence and this very example rendered it as
|
|
27
|
+
* button copy. A visitor has no stake in which services the operator bought,
|
|
28
|
+
* the string reads like a breakage when nothing is broken, and it cannot be
|
|
29
|
+
* translated on a framework whose sites are usually multilingual and often not
|
|
30
|
+
* English. Text a visitor reads is *site content*. `canSubmit` is the whole
|
|
31
|
+
* signal.
|
|
32
|
+
*
|
|
25
33
|
* Pass `block` and the submission carries where it came from — section type,
|
|
26
34
|
* section id, page id and label — without every component assembling that by
|
|
27
35
|
* hand. Anything in `context` wins over what the block supplies.
|
|
@@ -39,7 +47,6 @@ import { useWebsite } from './useWebsite.js'
|
|
|
39
47
|
* error: Error | null,
|
|
40
48
|
* response: object | null,
|
|
41
49
|
* canSubmit: boolean,
|
|
42
|
-
* unavailableReason: string | null,
|
|
43
50
|
* submit: (formData: object, overrides?: object) => Promise<object>,
|
|
44
51
|
* reset: () => void,
|
|
45
52
|
* }}
|
|
@@ -50,7 +57,7 @@ export function useFormSubmit(defaults = {}) {
|
|
|
50
57
|
const [response, setResponse] = useState(null)
|
|
51
58
|
|
|
52
59
|
const { website } = useWebsite()
|
|
53
|
-
const { url: target
|
|
60
|
+
const { url: target } = resolveSubmitTarget(website)
|
|
54
61
|
|
|
55
62
|
// `defaults` is a fresh object every render, so listing it as a dependency
|
|
56
63
|
// would rebuild the callback on each one. A ref is what actually makes "the
|
|
@@ -103,7 +110,6 @@ export function useFormSubmit(defaults = {}) {
|
|
|
103
110
|
error,
|
|
104
111
|
response,
|
|
105
112
|
canSubmit: !!target,
|
|
106
|
-
unavailableReason,
|
|
107
113
|
// Whether attachments can be delivered. True once there is a target: the
|
|
108
114
|
// client sends the manifest, then the bytes, then finalizes.
|
|
109
115
|
//
|
|
@@ -51,7 +51,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
51
51
|
* It lists the paths of `required` controls that are still empty. It does not
|
|
52
52
|
* block anything: whether an incomplete form disables the button, shows a
|
|
53
53
|
* message, or submits anyway is a design decision. `useFormSubmit` draws the
|
|
54
|
-
* same line with `canSubmit`
|
|
54
|
+
* same line with `canSubmit` — the kit works out the
|
|
55
55
|
* fact, the foundation decides what it looks like.
|
|
56
56
|
*
|
|
57
57
|
* Empty means `undefined`, `null`, `''`, or `[]`. A `false` boolean is a VALUE,
|
package/src/index.js
CHANGED
|
@@ -130,11 +130,9 @@ export {
|
|
|
130
130
|
submitForm,
|
|
131
131
|
deriveSummary,
|
|
132
132
|
resolveSubmitTarget,
|
|
133
|
-
NO_SUBMIT_TARGET_REASON,
|
|
134
133
|
// Site services (search / submit / assistant / anything a host offers)
|
|
135
134
|
resolveService,
|
|
136
|
-
resolveServiceUrl
|
|
137
|
-
NO_SERVICE_REASON
|
|
135
|
+
resolveServiceUrl
|
|
138
136
|
} from './utils/index.js'
|
|
139
137
|
|
|
140
138
|
// ============================================================================
|
|
@@ -59,6 +59,22 @@ import { Divider } from '../Section/renderers/Divider.jsx'
|
|
|
59
59
|
|
|
60
60
|
const INLINE_INSET_RE = /<uniweb-inset data-ref-id="([^"]+)"><\/uniweb-inset>/g
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Inline atoms the parser leaves as markers in a paragraph's HTML, because each
|
|
64
|
+
* needs a React component rather than markup: an inset resolves to a Block, an
|
|
65
|
+
* icon to <Icon> (which fetches library+name from the icon CDN).
|
|
66
|
+
*
|
|
67
|
+
* One regex for both so a paragraph carrying a mix is split in a single pass and
|
|
68
|
+
* every fragment keeps its authored position.
|
|
69
|
+
*/
|
|
70
|
+
const INLINE_MARKER_RE =
|
|
71
|
+
/<uniweb-inset data-ref-id="([^"]+)"><\/uniweb-inset>|<uniweb-icon data-index="(\d+)"><\/uniweb-icon>/g
|
|
72
|
+
|
|
73
|
+
/** Does this HTML string carry any inline marker needing component substitution? */
|
|
74
|
+
function hasInlineMarkers(html) {
|
|
75
|
+
return typeof html === 'string' && /<uniweb-(?:inset|icon)\b/.test(html)
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
/**
|
|
63
79
|
* Element types this engine knows about and deliberately renders NOTHING for.
|
|
64
80
|
*
|
|
@@ -130,22 +146,37 @@ function toSequence(content, block) {
|
|
|
130
146
|
* positions via the framework's child-block renderer — the same path
|
|
131
147
|
* block-level insets take, scoped to one inset.
|
|
132
148
|
*/
|
|
133
|
-
function renderParagraphWithInsets(html, block) {
|
|
134
|
-
|
|
149
|
+
function renderParagraphWithInsets(html, block, element) {
|
|
150
|
+
// An icon needs no Block, so a paragraph carrying only icons still resolves
|
|
151
|
+
// without one — only insets require `block` for the getInset() lookup.
|
|
152
|
+
const icons = (element?.children || []).filter((c) => c?.type === 'icon')
|
|
153
|
+
if (!block && !icons.length) return <SafeHtml value={html} as="span" />
|
|
154
|
+
|
|
135
155
|
const InsetRenderer = getChildBlockRenderer()
|
|
136
156
|
const parts = []
|
|
137
157
|
let lastIdx = 0
|
|
138
|
-
|
|
158
|
+
INLINE_MARKER_RE.lastIndex = 0
|
|
139
159
|
let match
|
|
140
|
-
while ((match =
|
|
160
|
+
while ((match = INLINE_MARKER_RE.exec(html)) !== null) {
|
|
141
161
|
if (match.index > lastIdx) {
|
|
142
162
|
parts.push(<SafeHtml key={`t${lastIdx}`} value={html.slice(lastIdx, match.index)} as="span" />)
|
|
143
163
|
}
|
|
164
|
+
|
|
144
165
|
const refId = match[1]
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
166
|
+
const iconIndex = match[2]
|
|
167
|
+
|
|
168
|
+
if (refId !== undefined) {
|
|
169
|
+
const insetBlock = block?.getInset?.(refId)
|
|
170
|
+
if (insetBlock && InsetRenderer) {
|
|
171
|
+
parts.push(<InsetRenderer key={`i${refId}`} blocks={[insetBlock]} />)
|
|
172
|
+
}
|
|
173
|
+
} else if (iconIndex !== undefined) {
|
|
174
|
+
// Ordinal into the icons of this element's `children`, in document order
|
|
175
|
+
// — the contract semantic-parser's getTextContent() emits the marker on.
|
|
176
|
+
const icon = icons[Number(iconIndex)]
|
|
177
|
+
if (icon) parts.push(<Icon key={`ic${iconIndex}`} {...icon.attrs} />)
|
|
148
178
|
}
|
|
179
|
+
|
|
149
180
|
lastIdx = match.index + match[0].length
|
|
150
181
|
}
|
|
151
182
|
if (lastIdx < html.length) {
|
|
@@ -187,8 +218,8 @@ function renderCell(cell, block, components) {
|
|
|
187
218
|
|
|
188
219
|
if (only?.type === 'paragraph') {
|
|
189
220
|
if (!only.text) return null
|
|
190
|
-
return
|
|
191
|
-
renderParagraphWithInsets(only.text, block)
|
|
221
|
+
return hasInlineMarkers(only.text) ? (
|
|
222
|
+
renderParagraphWithInsets(only.text, block, only)
|
|
192
223
|
) : (
|
|
193
224
|
<SafeHtml value={only.text} as="span" />
|
|
194
225
|
)
|
|
@@ -229,17 +260,23 @@ export function SequenceElement({ element, block, components }) {
|
|
|
229
260
|
const Tag = `h${level}`
|
|
230
261
|
// The id is the anchor `useHeadings()` and every in-page nav link
|
|
231
262
|
// resolve against, so it is behaviour rather than decoration.
|
|
263
|
+
// headingId() runs stripTags() first, so a marker in the text does not
|
|
264
|
+
// change the anchor — an icon in a heading keeps the id the TOC links to.
|
|
232
265
|
return (
|
|
233
266
|
<Tag id={authoredId(element) || headingId(element.text || '')}>
|
|
234
|
-
|
|
267
|
+
{hasInlineMarkers(element.text) ? (
|
|
268
|
+
renderParagraphWithInsets(element.text, block, element)
|
|
269
|
+
) : (
|
|
270
|
+
<SafeHtml value={element.text} as="span" />
|
|
271
|
+
)}
|
|
235
272
|
</Tag>
|
|
236
273
|
)
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
case 'paragraph': {
|
|
240
277
|
if (!element.text) return null
|
|
241
|
-
if (
|
|
242
|
-
return <p>{renderParagraphWithInsets(element.text, block)}</p>
|
|
278
|
+
if (hasInlineMarkers(element.text)) {
|
|
279
|
+
return <p>{renderParagraphWithInsets(element.text, block, element)}</p>
|
|
243
280
|
}
|
|
244
281
|
return (
|
|
245
282
|
<p>
|
package/src/utils/index.js
CHANGED
|
@@ -248,8 +248,11 @@ export function detectMediaType(url) {
|
|
|
248
248
|
// ─────────────────────────────────────────────────────────────────
|
|
249
249
|
|
|
250
250
|
export { submitForm, deriveSummary } from './submitForm.js'
|
|
251
|
-
export { resolveSubmitTarget
|
|
252
|
-
export {
|
|
251
|
+
export { resolveSubmitTarget } from './submitTarget.js'
|
|
252
|
+
export {
|
|
253
|
+
resolveService,
|
|
254
|
+
resolveServiceUrl
|
|
255
|
+
} from './services.js'
|
|
253
256
|
|
|
254
257
|
/**
|
|
255
258
|
* The text of a ProseMirror node, flattened.
|
package/src/utils/services.js
CHANGED
|
@@ -17,8 +17,8 @@ import { applyBasePath } from './href.js'
|
|
|
17
17
|
* 2. **The host**, served — `config.services.<name>` in the payload. What the
|
|
18
18
|
* deployment offers, which the site never had to know about.
|
|
19
19
|
*
|
|
20
|
-
* Absent from both means the site has no such service, and the component
|
|
21
|
-
* rather than guessing an address. That is the same rule for every service, and
|
|
20
|
+
* Absent from both means the site has no such service, and the component renders
|
|
21
|
+
* for that rather than guessing an address. That is the same rule for every service, and
|
|
22
22
|
* it is why this module exists: it was previously implemented three times — the
|
|
23
23
|
* search provider, the submit resolver, and a hand-rolled copy inside a
|
|
24
24
|
* foundation — with three slightly different base-joining rules between them.
|
|
@@ -37,11 +37,33 @@ import { applyBasePath } from './href.js'
|
|
|
37
37
|
*
|
|
38
38
|
* ## What this deliberately does not model
|
|
39
39
|
*
|
|
40
|
-
* **Entitlement.** A host that will not serve a service
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* **Entitlement.** A host that will not serve a service omits it, or declares
|
|
41
|
+
* the name with no address. The framework never learns why — no plan names, no
|
|
42
|
+
* tiers, no "paid" anywhere. That is not squeamishness: `@uniweb/kit` is
|
|
43
|
+
* public, and a framework that encodes which capabilities cost money ships the
|
|
44
|
+
* business model into open source.
|
|
45
|
+
*
|
|
46
|
+
* ⛔ **There is deliberately no explanatory string, and there was one — it was
|
|
47
|
+
* a mistake.** Until 2026-08-13 a declining host could supply a `reason` that
|
|
48
|
+
* this module relayed "to the UI verbatim", with an English default
|
|
49
|
+
* (`NO_SERVICE_REASON`) when nothing did. Removed, on two counts:
|
|
50
|
+
*
|
|
51
|
+
* 1. **Wrong audience.** A visitor has no stake in which services an operator
|
|
52
|
+
* provisioned. "Submissions are not enabled for this site" reports someone's
|
|
53
|
+
* billing state to the public and reads like a breakage. It is neither — it
|
|
54
|
+
* is a service that was not bought, and **a generic component is supposed to
|
|
55
|
+
* be smart about that.**
|
|
56
|
+
* 2. **Wrong language, unfixably.** Sites here are multilingual, or unilingual
|
|
57
|
+
* and not English. A host-supplied sentence bypasses the site's entire
|
|
58
|
+
* localization pipeline, and a canned constant in a public package cannot
|
|
59
|
+
* be translated at all. Any text a visitor should read is *site content*,
|
|
60
|
+
* which is authored and localized — never a string a service layer invents.
|
|
61
|
+
*
|
|
62
|
+
* ⇒ **`url` is the whole answer, and absence is a rendering decision rather
|
|
63
|
+
* than a message.** No submit endpoint → render no form, or degrade to
|
|
64
|
+
* something that still serves the visitor: a `mailto:` or a number the site
|
|
65
|
+
* already carries in its content. No assistant → render no Ask-AI affordance.
|
|
66
|
+
* Nobody is told why, because nobody visiting needs to know.
|
|
45
67
|
*
|
|
46
68
|
* **The site's own base.** `config.base` is where the site *lives*, not a
|
|
47
69
|
* service it consumes — it is load-bearing for routing and asset URLs too. It
|
|
@@ -51,9 +73,6 @@ import { applyBasePath } from './href.js'
|
|
|
51
73
|
/** Anything with a scheme, or protocol-relative — never joined to a base. */
|
|
52
74
|
const ABSOLUTE_URL_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i
|
|
53
75
|
|
|
54
|
-
/** Shown when nothing supplies a service and the caller gives no wording. */
|
|
55
|
-
export const NO_SERVICE_REASON = 'This site has no such service configured.'
|
|
56
|
-
|
|
57
76
|
/**
|
|
58
77
|
* Read an endpoint out of either declaration form.
|
|
59
78
|
*
|
|
@@ -108,19 +127,19 @@ export function resolveServiceUrl(endpoint, basePath = '') {
|
|
|
108
127
|
* Resolve where a named service lives for this site.
|
|
109
128
|
*
|
|
110
129
|
* ```js
|
|
111
|
-
* const { url
|
|
112
|
-
* if (!url) return
|
|
130
|
+
* const { url } = resolveService(website, 'submit')
|
|
131
|
+
* if (!url) return null // no endpoint — render no form, or degrade
|
|
113
132
|
* ```
|
|
114
133
|
*
|
|
115
134
|
* @param {object} website - the active Website
|
|
116
135
|
* @param {string} name - service name, e.g. 'submit' · 'search' · 'assistant'
|
|
117
|
-
* @
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
136
|
+
* @returns {{ url: string|null, source: 'site'|'host'|null }}
|
|
137
|
+
* `url` is the whole answer for rendering. `source` says which declaration
|
|
138
|
+
* answered — a diagnostic, and the thing to check when a host's value appears
|
|
139
|
+
* not to be taking effect. `'host'` with a null `url` means the host answered
|
|
140
|
+
* and offered no address; `null` means nothing declared the service at all.
|
|
122
141
|
*/
|
|
123
|
-
export function resolveService(website, name
|
|
142
|
+
export function resolveService(website, name) {
|
|
124
143
|
const config = website?.config
|
|
125
144
|
const basePath = website?.basePath
|
|
126
145
|
|
|
@@ -128,21 +147,22 @@ export function resolveService(website, name, options = {}) {
|
|
|
128
147
|
// means it, including on a host that offers one.
|
|
129
148
|
const authored = readEndpoint(config?.[name])
|
|
130
149
|
if (authored) {
|
|
131
|
-
return { url: resolveServiceUrl(authored, basePath),
|
|
150
|
+
return { url: resolveServiceUrl(authored, basePath), source: 'site' }
|
|
132
151
|
}
|
|
133
152
|
|
|
134
153
|
// 2 — what the host says it offers.
|
|
135
154
|
const hostDeclaration = config?.services?.[name]
|
|
136
155
|
const hostEndpoint = readEndpoint(hostDeclaration)
|
|
137
156
|
if (hostEndpoint) {
|
|
138
|
-
return { url: resolveServiceUrl(hostEndpoint, basePath),
|
|
157
|
+
return { url: resolveServiceUrl(hostEndpoint, basePath), source: 'host' }
|
|
139
158
|
}
|
|
140
159
|
|
|
141
|
-
// A host
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
// A host may declare the name while offering no address — a decline. It is
|
|
161
|
+
// still the host answering, which is all a caller can use: any *wording* for
|
|
162
|
+
// that state would be ours to invent, in one language, for a visitor who has
|
|
163
|
+
// no stake in it. See the entitlement note above.
|
|
164
|
+
if (hostDeclaration !== undefined) return { url: null, source: 'host' }
|
|
145
165
|
|
|
146
166
|
// 3 — nobody supplied one.
|
|
147
|
-
return { url: null,
|
|
167
|
+
return { url: null, source: null }
|
|
148
168
|
}
|
|
@@ -31,24 +31,16 @@ import { resolveService, resolveServiceUrl } from './services.js'
|
|
|
31
31
|
* only have come from its host.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* Shown when a site declares no endpoint and no host offers one. English, and a
|
|
36
|
-
* foundation is free to ignore it and render its own copy — check `url` for the
|
|
37
|
-
* yes/no and treat this as a default rather than a string to translate.
|
|
38
|
-
*/
|
|
39
|
-
export const NO_SUBMIT_TARGET_REASON =
|
|
40
|
-
'This site has no form submission endpoint configured.'
|
|
41
|
-
|
|
42
34
|
/**
|
|
43
35
|
* Resolve the submission target for a site.
|
|
44
36
|
*
|
|
45
37
|
* @param {object} website - The active Website instance
|
|
46
|
-
* @returns {{ url: string|null,
|
|
47
|
-
* `url` is the resolved endpoint, or null when nothing supplies one
|
|
48
|
-
*
|
|
38
|
+
* @returns {{ url: string|null, source: 'site'|'host'|null }}
|
|
39
|
+
* `url` is the resolved endpoint, or null when nothing supplies one — in which
|
|
40
|
+
* case render no form, or degrade to contact details the site already carries.
|
|
49
41
|
*/
|
|
50
42
|
export function resolveSubmitTarget(website) {
|
|
51
|
-
return resolveService(website, 'submit'
|
|
43
|
+
return resolveService(website, 'submit')
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
/**
|