brut-js 0.0.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/.projections.json +10 -0
- package/CODE_OF_CONDUCT.txt +99 -0
- package/LICENSE.txt +370 -0
- package/README.md +118 -0
- package/package.json +30 -0
- package/specs/AjaxSubmit.spec.js +241 -0
- package/specs/Autosubmit.spec.js +127 -0
- package/specs/ConfirmSubmit.spec.js +193 -0
- package/specs/ConstraintViolationMessage.spec.js +33 -0
- package/specs/ConstraintViolationMessages.spec.js +27 -0
- package/specs/Form.spec.js +136 -0
- package/specs/I18nTranslation.spec.js +19 -0
- package/specs/LocaleDetection.spec.js +22 -0
- package/specs/Message.spec.js +15 -0
- package/specs/Tabs.spec.js +41 -0
- package/specs/config/asset_metadata.json +7 -0
- package/specs/public/js/bundle.js +1284 -0
- package/specs/public/js/bundle.js.map +7 -0
- package/src/AjaxSubmit.js +364 -0
- package/src/Autosubmit.js +61 -0
- package/src/BaseCustomElement.js +261 -0
- package/src/ConfirmSubmit.js +114 -0
- package/src/ConfirmationDialog.js +141 -0
- package/src/ConstraintViolationMessage.js +101 -0
- package/src/ConstraintViolationMessages.js +90 -0
- package/src/Form.js +117 -0
- package/src/I18nTranslation.js +59 -0
- package/src/LocaleDetection.js +93 -0
- package/src/Logger.js +90 -0
- package/src/Message.js +55 -0
- package/src/RichString.js +113 -0
- package/src/Tabs.js +167 -0
- package/src/appForTestingOnly.js +15 -0
- package/src/index.js +119 -0
- package/src/testing/index.js +348 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import BaseCustomElement from "./BaseCustomElement"
|
|
2
|
+
import RichString from "./RichString"
|
|
3
|
+
import ConstraintViolationMessage from "./ConstraintViolationMessage"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom element to translate keys from {@link external:ValidityState} into
|
|
7
|
+
* actual messges for a human. This works by inserting `<brut-constraint-violation-message>` elements
|
|
8
|
+
* as children, where the key represents the particular errors present in the `ValidityState` passed
|
|
9
|
+
* to `createMessages`.
|
|
10
|
+
*
|
|
11
|
+
* @property {boolean} server-side if true, this indicates the element contains constraint violation messages
|
|
12
|
+
* from the server. Currently doesn't affect this element's behavior, however
|
|
13
|
+
* AjaxSubmit will use it to locate where it should insert server-side errors.
|
|
14
|
+
* @property {string} input-name if set, this indicates this element contains constraint violation messages
|
|
15
|
+
* for the input with this name inside the form this element is in. Currently doesn't affect
|
|
16
|
+
* this element's behavior, however AjaxSubmit will use it to locate where it
|
|
17
|
+
* should insert server-side errors.
|
|
18
|
+
*
|
|
19
|
+
* @see Form
|
|
20
|
+
* @see ConstraintViolationMessage
|
|
21
|
+
* @see AjaxSubmit
|
|
22
|
+
*/
|
|
23
|
+
class ConstraintViolationMessages extends BaseCustomElement {
|
|
24
|
+
static tagName = "brut-constraint-violation-messages"
|
|
25
|
+
|
|
26
|
+
static observedAttributes = [
|
|
27
|
+
"show-warnings",
|
|
28
|
+
"server-side",
|
|
29
|
+
"input-name",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
serverSideChangedCallback({newValueAsBoolean}) {
|
|
34
|
+
// attribute listed for documentation purposes only
|
|
35
|
+
}
|
|
36
|
+
inputNameChangedCallback({newValue}) {
|
|
37
|
+
// attribute listed for documentation purposes only
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates error messages based on the passed `ValidityState` and input name.
|
|
42
|
+
*
|
|
43
|
+
* This should be called as part of a Form validation event to provide a customized UX for
|
|
44
|
+
* the error messages, beyond what the browser would do by default. The keys used are the same
|
|
45
|
+
* as the attributes of a `ValidityState`, so for example, a range underflow would mean that `validity.rangeUnderflow` would return
|
|
46
|
+
* true. Thus, a `<brut-constraint-violation-message>` would be created with `key="general.cv.fe.rangeUnderflow"`.
|
|
47
|
+
*
|
|
48
|
+
* The `cv.fe` is hard-coded to be consistent with Brut's server-side translation management.
|
|
49
|
+
*
|
|
50
|
+
* @param {ValidityState} validityState - the return from an element's `validity` when it's found to have constraint violations.
|
|
51
|
+
* @param {String} inputName - the element's `name`.
|
|
52
|
+
*/
|
|
53
|
+
createMessages({validityState,inputName}) {
|
|
54
|
+
const errors = this.#VALIDITY_STATE_ATTRIBUTES.filter( (attribute) => validityState[attribute] )
|
|
55
|
+
this.clearMessages()
|
|
56
|
+
errors.forEach( (key) => {
|
|
57
|
+
const options = {
|
|
58
|
+
key: key,
|
|
59
|
+
"input-name": inputName,
|
|
60
|
+
}
|
|
61
|
+
const showWarnings = this.getAttribute("show-warnings")
|
|
62
|
+
if (showWarnings != null) {
|
|
63
|
+
options["show-warnings"] = showWarnings
|
|
64
|
+
}
|
|
65
|
+
const element = ConstraintViolationMessage.createElement(document,options)
|
|
66
|
+
this.appendChild(element)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clear all messages. Useful for when an input has become valid during a session.
|
|
72
|
+
*/
|
|
73
|
+
clearMessages() {
|
|
74
|
+
this.textContent = ""
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#VALIDITY_STATE_ATTRIBUTES = [
|
|
78
|
+
"badInput",
|
|
79
|
+
"customError",
|
|
80
|
+
"patternMismatch",
|
|
81
|
+
"rangeOverflow",
|
|
82
|
+
"rangeUnderflow",
|
|
83
|
+
"stepMismatch",
|
|
84
|
+
"tooLong",
|
|
85
|
+
"tooShort",
|
|
86
|
+
"typeMismatch",
|
|
87
|
+
"valueMissing",
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
export default ConstraintViolationMessages
|
package/src/Form.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import BaseCustomElement from "./BaseCustomElement"
|
|
2
|
+
import RichString from "./RichString"
|
|
3
|
+
import AjaxSubmit from "./AjaxSubmit"
|
|
4
|
+
import ConstraintViolationMessages from "./ConstraintViolationMessages"
|
|
5
|
+
|
|
6
|
+
/** A web component that enhances a form it contains to make constraint validations
|
|
7
|
+
* easier to manage and control.
|
|
8
|
+
*
|
|
9
|
+
* This provides two main features:
|
|
10
|
+
*
|
|
11
|
+
* * Using the `:invalid` pseudo selector isn't great, because freshly rendered forms
|
|
12
|
+
* that have `required` elements will match the `:invalid` selector. You really want
|
|
13
|
+
* to only show errors if the user has tried to submit the form. Thus, the `FORM` inside
|
|
14
|
+
* this custom element will be given the attribute `data-submitted` if a submission
|
|
15
|
+
* has been attempted. This allows you to target your CSS at invalid inputs
|
|
16
|
+
* only when submission has occured.
|
|
17
|
+
* * You may wish to control the messaging of client-side constraint violations
|
|
18
|
+
* beyond what the browser gives you. Assuming your `INPUT` tags are inside a container
|
|
19
|
+
* like `LABEL`, a `brut-constraint-violation-messages` tag found in that container
|
|
20
|
+
* (i.e. a sibling of your `INPUT`) will be modified to contain error messages specific
|
|
21
|
+
* to the {@link external:ValidityState} of the control.
|
|
22
|
+
*
|
|
23
|
+
* @fires brut:invalid Fired when any element is found to be invalid
|
|
24
|
+
* @fires brut:valid Fired when no element is found to be invalid. This should be reliable to know
|
|
25
|
+
* when constraint violations have cleared.
|
|
26
|
+
*
|
|
27
|
+
* @example <caption>Basic Structure Required</caption>
|
|
28
|
+
* <brut-form>
|
|
29
|
+
* <form ...>
|
|
30
|
+
* <label>
|
|
31
|
+
* <input type="text" required name="username">
|
|
32
|
+
* <brut-constraint-violation-messages>
|
|
33
|
+
* </brut-constraint-violation-messages>
|
|
34
|
+
* </label>
|
|
35
|
+
* <div> <!-- container need not be a label -->
|
|
36
|
+
* <input type="text" required minlength="4" name="alias">
|
|
37
|
+
* <brut-constraint-violation-messages>
|
|
38
|
+
* </brut-constraint-violation-messages>
|
|
39
|
+
* </div>
|
|
40
|
+
* </form>
|
|
41
|
+
* </brut-form>
|
|
42
|
+
*
|
|
43
|
+
* @see ConstraintViolationMessages
|
|
44
|
+
*/
|
|
45
|
+
class Form extends BaseCustomElement {
|
|
46
|
+
static tagName = "brut-form"
|
|
47
|
+
static observedAttributes = [
|
|
48
|
+
"show-warnings",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
#markFormSubmitted = (event) => {
|
|
52
|
+
const form = event.target.form
|
|
53
|
+
if (!form) {
|
|
54
|
+
this.logger.warn("%o had no form",event.target)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
form.dataset["submitted"] = true
|
|
58
|
+
}
|
|
59
|
+
#updateValidity = (event) => {
|
|
60
|
+
this.#updateErrorMessages(event)
|
|
61
|
+
}
|
|
62
|
+
#sendValid = () => {
|
|
63
|
+
this.dispatchEvent(new CustomEvent("brut:valid"))
|
|
64
|
+
}
|
|
65
|
+
#sendInvalid = () => {
|
|
66
|
+
this.dispatchEvent(new CustomEvent("brut:invalid"))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
update() {
|
|
70
|
+
const forms = this.querySelectorAll("form")
|
|
71
|
+
if (forms.length == 0) {
|
|
72
|
+
this.logger.warn("Didn't find any forms. Ignoring")
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
forms.forEach( (form) => {
|
|
76
|
+
Array.from(form.elements).forEach( (formElement) => {
|
|
77
|
+
formElement.addEventListener("invalid", this.#updateValidity)
|
|
78
|
+
formElement.addEventListener("invalid", this.#markFormSubmitted)
|
|
79
|
+
formElement.addEventListener("input", this.#updateValidity)
|
|
80
|
+
})
|
|
81
|
+
form.querySelectorAll(AjaxSubmit.tagName).forEach( (ajaxSubmits) => {
|
|
82
|
+
ajaxSubmits.addEventListener("brut:submitok", this.#sendValid)
|
|
83
|
+
ajaxSubmits.addEventListener("brut:submitinvalid", this.#sendInvalid)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#updateErrorMessages(event) {
|
|
89
|
+
const element = event.target
|
|
90
|
+
const selector = `${ConstraintViolationMessages.tagName}:not([server-side])`
|
|
91
|
+
const errorLabels = element.parentNode.querySelectorAll(selector)
|
|
92
|
+
if (errorLabels.length == 0) {
|
|
93
|
+
this.logger.warn(`Did not find any elements matching ${selector}, so no error messages will be shown`)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
let anyErrors = false
|
|
97
|
+
errorLabels.forEach( (errorLabel) => {
|
|
98
|
+
if (element.validity.valid) {
|
|
99
|
+
errorLabel.clearMessages()
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
anyErrors = true
|
|
103
|
+
errorLabel.createMessages({
|
|
104
|
+
validityState: element.validity,
|
|
105
|
+
inputName: element.name
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
if (anyErrors) {
|
|
110
|
+
this.#sendInvalid()
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.#sendValid()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export default Form
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import BaseCustomElement from "./BaseCustomElement"
|
|
2
|
+
|
|
3
|
+
/** Manages a translation based on a key and value, with the value potentially having interpolation.
|
|
4
|
+
*
|
|
5
|
+
* This is intended to be server-rendered with the subset of keys the server manages that are relevant
|
|
6
|
+
* to the front-end. Any other code on the page can then locate an element with the desired key and
|
|
7
|
+
* call `translation` to get the human-readable key. It is assumed that the server would render
|
|
8
|
+
* in the language required by the visitor, so there is no need to first select by locale.
|
|
9
|
+
*
|
|
10
|
+
* @property {string} key - an i18n key, presumably dot-delimited, however it can be any valid attribute value.
|
|
11
|
+
* @property {string} value - the value of the key, in the browser's locale. It may contain placeholders for interpolation using `%{«placeholder»}` syntax.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <brut-i18n-translation key="greeting" value="Hello %{username}"></brut-i18n-translation>
|
|
15
|
+
*/
|
|
16
|
+
class I18nTranslation extends BaseCustomElement {
|
|
17
|
+
static tagName = "brut-i18n-translation"
|
|
18
|
+
|
|
19
|
+
static observedAttributes = [
|
|
20
|
+
"show-warnings",
|
|
21
|
+
"key",
|
|
22
|
+
"value",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
#key = null
|
|
26
|
+
#value = ""
|
|
27
|
+
|
|
28
|
+
keyChangedCallback({newValue}) {
|
|
29
|
+
this.#key = newValue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
valueChangedCallback({newValue}) {
|
|
33
|
+
this.#value = newValue ? String(newValue) : ""
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Called by other JavaScript to get the translated string.
|
|
38
|
+
* @param {Object} interpolatedValues - Object where the keys are placeholders in the string for interpolation and the values are
|
|
39
|
+
* the values to replace. Placeholders not in the translated value are ignored. Missing placeholders won't cause an error, but the
|
|
40
|
+
* placeholder will be present verbatim in the translated string.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const element = document.querySeletor("brut-i18n-translation[key='greeting']")
|
|
44
|
+
* if (element) {
|
|
45
|
+
* const translation = element.translation({ username: "Pat" })
|
|
46
|
+
* alert(translation) // Shows 'Hello Pat'
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
translation(interpolatedValues) {
|
|
50
|
+
return this.#value.replaceAll(/%\{([^}%]+)\}/g, (match,key) => {
|
|
51
|
+
if (interpolatedValues[key]) {
|
|
52
|
+
return interpolatedValues[key]
|
|
53
|
+
}
|
|
54
|
+
return match
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
export default I18nTranslation
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import BaseCustomElement from "./BaseCustomElement"
|
|
2
|
+
|
|
3
|
+
class LocaleDetection extends BaseCustomElement {
|
|
4
|
+
static tagName = "brut-locale-detection"
|
|
5
|
+
|
|
6
|
+
static observedAttributes = [
|
|
7
|
+
"locale-from-server",
|
|
8
|
+
"timezone-from-server",
|
|
9
|
+
"url",
|
|
10
|
+
"timeout-before-ping-ms",
|
|
11
|
+
"show-warnings",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
#localeFromServer = null
|
|
15
|
+
#timezoneFromServer = null
|
|
16
|
+
#reportingURL = null
|
|
17
|
+
#timeoutBeforePing = 1000
|
|
18
|
+
#serverContacted = false
|
|
19
|
+
|
|
20
|
+
localeFromServerChangedCallback({newValue}) {
|
|
21
|
+
this.#localeFromServer = newValue
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
timezoneFromServerChangedCallback({newValue}) {
|
|
25
|
+
this.#timezoneFromServer = newValue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
urlChangedCallback({newValue}) {
|
|
29
|
+
if (this.#serverContacted) {
|
|
30
|
+
this.#serverContacted = false
|
|
31
|
+
}
|
|
32
|
+
this.#reportingURL = newValue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
timeoutBeforePingMsChangedCallback({newValue}) {
|
|
36
|
+
this.#timeoutBeforePing = newValue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
update() {
|
|
40
|
+
if (this.#timeoutBeforePing == 0) {
|
|
41
|
+
this.#pingServerWithLocaleInfo()
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
setTimeout(this.#pingServerWithLocaleInfo.bind(this), this.#timeoutBeforePing)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#pingServerWithLocaleInfo() {
|
|
49
|
+
if (!this.#reportingURL) {
|
|
50
|
+
this.logger.info("no url= set, so nowhere to report to")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (this.#localeFromServer && this.#timezoneFromServer) {
|
|
54
|
+
this.logger.info("locale and timezone both set, not contacting server")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (this.#serverContacted) {
|
|
59
|
+
this.logger.info("server has already been contacted at the given url, not doing it again")
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
this.#serverContacted = true
|
|
63
|
+
|
|
64
|
+
const formatOptions = Intl.DateTimeFormat().resolvedOptions()
|
|
65
|
+
const request = new Request(
|
|
66
|
+
this.#reportingURL,
|
|
67
|
+
{
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
},
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
locale: formatOptions.locale,
|
|
74
|
+
timeZone: formatOptions.timeZone,
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
window.fetch(request).then( (response) => {
|
|
80
|
+
if (response.ok) {
|
|
81
|
+
this.logger.info("Server gave us the OK")
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.warn(response)
|
|
85
|
+
}
|
|
86
|
+
}).catch( (e) => {
|
|
87
|
+
console.warn(e)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
export default LocaleDetection
|
package/src/Logger.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract interface for logging information from a component.
|
|
3
|
+
* This is intended to allow prefixed messages to be optionally shown
|
|
4
|
+
* in the console to help debug.
|
|
5
|
+
*
|
|
6
|
+
* @see BufferedLogger
|
|
7
|
+
* @see PrefixedLogger
|
|
8
|
+
* @see BaseCustomElement#logger
|
|
9
|
+
*/
|
|
10
|
+
class Logger {
|
|
11
|
+
/** Create a logger for the given prefix.
|
|
12
|
+
*
|
|
13
|
+
* @param {string|false} stringOrFalse - if false,returns a {@link BufferedLogger}. Otherwise, returns a {@link PrefixedLogger} using the param's value as the prefix.
|
|
14
|
+
*
|
|
15
|
+
* @returns {Logger}
|
|
16
|
+
*/
|
|
17
|
+
static forPrefix(stringOrFalse) {
|
|
18
|
+
if (!stringOrFalse) {
|
|
19
|
+
return new BufferedLogger()
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
return new PrefixedLogger(stringOrFalse)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Subclasses must implement this.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} level - 'info' or 'warn' to indicate the logging level
|
|
29
|
+
* @param {...*} args - args to pass directly to console.log
|
|
30
|
+
*/
|
|
31
|
+
log() {
|
|
32
|
+
throw `Subclass must implement`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Log an informational bit of information */
|
|
36
|
+
info(...args) { this.log("info",...args) }
|
|
37
|
+
/** Log a warning */
|
|
38
|
+
warn(...args) { this.log("warn",...args) }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Logger that buffers, but does not print, its logged messages.
|
|
42
|
+
* The reason it buffers them is to allow custom elements to retroatively log
|
|
43
|
+
* information captured before warnings were turned on.
|
|
44
|
+
*/
|
|
45
|
+
class BufferedLogger extends Logger {
|
|
46
|
+
constructor() {
|
|
47
|
+
super()
|
|
48
|
+
this.messages = []
|
|
49
|
+
}
|
|
50
|
+
log(...args) {
|
|
51
|
+
this.messages.push(args)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Log information to the JavaScript console.
|
|
56
|
+
*/
|
|
57
|
+
class PrefixedLogger extends Logger {
|
|
58
|
+
/** Create a PrefixedLogger.
|
|
59
|
+
*
|
|
60
|
+
* @param {string|true} prefixOrTrue - if true, uses the prefix `"debug"`, otherwise uses the param as the prefix to all
|
|
61
|
+
* messages output.
|
|
62
|
+
*/
|
|
63
|
+
constructor(prefixOrTrue) {
|
|
64
|
+
super()
|
|
65
|
+
this.prefix = prefixOrTrue === true ? "debug" : prefixOrTrue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Dumps hte contents of a {@link BufferedLogger} to this logger's output.
|
|
69
|
+
*
|
|
70
|
+
* @param {BufferedLogger} bufferedLogger - a logger with pent-up messages, waiting to be logged.
|
|
71
|
+
*/
|
|
72
|
+
dump(bufferedLogger) {
|
|
73
|
+
if (bufferedLogger instanceof BufferedLogger) {
|
|
74
|
+
bufferedLogger.messages.forEach( (args) => {
|
|
75
|
+
this.log(...args)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log(level,...args) {
|
|
81
|
+
if (typeof(args[0]) === "string") {
|
|
82
|
+
const message = `[prefix:${this.prefix}]:${args[0]}`
|
|
83
|
+
console[level](message,...(args.slice(1)))
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console[level](this.prefix,...args)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export default Logger
|
package/src/Message.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import BaseCustomElement from "./BaseCustomElement"
|
|
2
|
+
import RichString from "./RichString"
|
|
3
|
+
import I18nTranslation from "./I18nTranslation"
|
|
4
|
+
|
|
5
|
+
/** Renders a translated message for a given key, handling all the needed interpolation based
|
|
6
|
+
* on the existence of `<brut-i18n-translation>` elements on the page.
|
|
7
|
+
*
|
|
8
|
+
* When the `key` attribute has a value, this element will locate the `<brut-i18-translation>` element and call `translate`. Note that
|
|
9
|
+
* interpolation is not supported.
|
|
10
|
+
*
|
|
11
|
+
* @property {string} key - the i18n translation key to use. It must map to the `key` of a `<brut-i18n-translation>` on the page or
|
|
12
|
+
* the element will not render any text.
|
|
13
|
+
*
|
|
14
|
+
* @see I18nTranslation
|
|
15
|
+
* @see ConstraintViolationMessage
|
|
16
|
+
*/
|
|
17
|
+
class Message extends BaseCustomElement {
|
|
18
|
+
static tagName = "brut-message"
|
|
19
|
+
|
|
20
|
+
static observedAttributes = [
|
|
21
|
+
"show-warnings",
|
|
22
|
+
"key",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
static createElement(document,attributes) {
|
|
26
|
+
const element = document.createElement(Message.tagName)
|
|
27
|
+
element.setAttribute("key",attributes.key)
|
|
28
|
+
element.setAttribute("show-warnings",attributes["show-warnings"])
|
|
29
|
+
return element
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#key = null
|
|
33
|
+
|
|
34
|
+
keyChangedCallback({newValue}) {
|
|
35
|
+
this.#key = newValue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
update() {
|
|
39
|
+
if (!this.#key) {
|
|
40
|
+
this.logger.info("No key attribute, so can't do anything")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const selector = `${I18nTranslation.tagName}[key='${this.#key}']`
|
|
45
|
+
const translation = document.querySelector(selector)
|
|
46
|
+
if (!translation) {
|
|
47
|
+
this.logger.info("Could not find translation based on selector '%s'",selector)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.textContent = RichString.fromString(translation.translation()).capitalize().toString()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
export default Message
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** A wrapper around a string that provides useful utility functions
|
|
2
|
+
* not present in the standard library.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
*
|
|
6
|
+
* const string = RichString.fromString(element.textContent)
|
|
7
|
+
* element.textContent = string.humanize().toString()
|
|
8
|
+
*/
|
|
9
|
+
class RichString {
|
|
10
|
+
/** Prefer this over the constructor, as this will
|
|
11
|
+
* wrap `possiblyDefinedStringOrRichString` only if necessary
|
|
12
|
+
* as well as handle `null`.
|
|
13
|
+
*
|
|
14
|
+
* @param {null|undefined|String|RichString} possiblyDefinedStringOrRichString - if `null`, `undefined`, or otherwise falsey, this method returns `null`. If a String, returns a new `RichString` wrapping it. If a `RichString`, returns the `RichString` unchanged.
|
|
15
|
+
*/
|
|
16
|
+
static fromString(possiblyDefinedStringOrRichString) {
|
|
17
|
+
if (possiblyDefinedStringOrRichString instanceof RichString) {
|
|
18
|
+
return possiblyDefinedStringOrRichString
|
|
19
|
+
}
|
|
20
|
+
if (!possiblyDefinedStringOrRichString) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
return new RichString(String(possiblyDefinedStringOrRichString))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Prefer `fromString` */
|
|
27
|
+
constructor(string) {
|
|
28
|
+
if (typeof string !== "string") {
|
|
29
|
+
throw `You may only construct a RichString with a String, not a ${typeof string}`
|
|
30
|
+
}
|
|
31
|
+
this.string = string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns a `RichString` with the string capitalized. */
|
|
35
|
+
capitalize() {
|
|
36
|
+
return new RichString(this.string.charAt(0).toUpperCase() + this.string.slice(1))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Returns a `RichString` with the string un-capitalized. */
|
|
40
|
+
decapitalize() {
|
|
41
|
+
return new RichString(this.string.charAt(0).toLowerCase() + this.string.slice(1))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returns a `RichString` with the string converted from snake or kebab case into camel case. */
|
|
45
|
+
camelize() {
|
|
46
|
+
// Taken from camelize npm module
|
|
47
|
+
return RichString.fromString(this.string.replace(/[_.-](\w|$)/g, function (_, x) {
|
|
48
|
+
return x.toUpperCase()
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Returns a 'humanized' `RichString`, which is basically a de-camelized version with the first letter
|
|
53
|
+
* capitalized.
|
|
54
|
+
*/
|
|
55
|
+
humanize() {
|
|
56
|
+
return this.decamlize({spacer: " "}).capitalize()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns a `RichString` with the string converted from camel case to snake or kebab case.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} parameters
|
|
62
|
+
* @param {string} parameters.spacer ["_"] - a string to use when joining words together.
|
|
63
|
+
*
|
|
64
|
+
*/
|
|
65
|
+
decamlize({spacer="_"} = {}) {
|
|
66
|
+
// Taken from decamelize NPM module
|
|
67
|
+
|
|
68
|
+
// Checking the second character is done later on. Therefore process shorter strings here.
|
|
69
|
+
if (this.string.length < 2) {
|
|
70
|
+
return new RichString(this.string.toLowerCase())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const replacement = `$1${spacer}$2`
|
|
74
|
+
|
|
75
|
+
// Split lowercase sequences followed by uppercase character.
|
|
76
|
+
// `dataForUSACounties` → `data_For_USACounties`
|
|
77
|
+
// `myURLstring → `my_URLstring`
|
|
78
|
+
const decamelized = this.string.replace(
|
|
79
|
+
/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu,
|
|
80
|
+
replacement,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Split multiple uppercase characters followed by one or more lowercase characters.
|
|
84
|
+
// `my_URLstring` → `my_ur_lstring`
|
|
85
|
+
return new RichString(decamelized.
|
|
86
|
+
replace(
|
|
87
|
+
/(\p{Uppercase_Letter})(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
|
|
88
|
+
replacement,
|
|
89
|
+
).
|
|
90
|
+
toLowerCase()
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Return the underlying String value */
|
|
95
|
+
toString() { return this.string }
|
|
96
|
+
|
|
97
|
+
/** Return the underlying String value or null if the string is blank */
|
|
98
|
+
toStringOrNull() {
|
|
99
|
+
if (this.isBlank()) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return this.string
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Returns true if this string has only whitespace in it */
|
|
108
|
+
isBlank() {
|
|
109
|
+
return this.string.trim() == ""
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
export default RichString
|