@kubex/zinc 1.1.115 → 1.1.116
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/dist/custom-elements.json +411 -175
- package/dist/vscode.html-custom-data.json +71 -26
- package/dist/web-types.json +171 -51
- package/dist/zn.d.ts +182 -48
- package/dist/zn.min.js +581 -588
- package/docs/pages/components/dialog.md +45 -16
- package/docs/pages/components/translation-group.md +102 -93
- package/docs/pages/components/translations.md +64 -114
- package/package.json +1 -1
- package/src/components/dialog/dialog.component.ts +2 -1
- package/src/components/dialog/dialog.scss +8 -0
- package/src/components/header/header.component.ts +2 -1
- package/src/components/select/select.component.ts +5 -0
- package/src/components/select/select.scss +11 -0
- package/src/components/translation-group/translation-group.component.ts +171 -198
- package/src/components/translation-group/translation-group.scss +124 -1
- package/src/components/translation-group/translation-group.test.ts +371 -2
- package/src/components/translations/translations.component.ts +249 -209
- package/src/components/translations/translations.scss +3 -25
- package/src/components/translations/translations.test.ts +214 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {classMap} from 'lit/directives/class-map.js';
|
|
2
|
+
import {defaultValue} from '../../internal/default-value';
|
|
2
3
|
import {FormControlController, validValidityState} from '../../internal/form';
|
|
3
4
|
import {HasSlotController} from '../../internal/slot';
|
|
4
5
|
import {html, nothing, unsafeCSS} from 'lit';
|
|
@@ -7,37 +8,76 @@ import {keyed} from 'lit/directives/keyed.js';
|
|
|
7
8
|
import {live} from 'lit/directives/live.js';
|
|
8
9
|
import {parseSlashItems, type SlashMenuItem} from '../slash-menu';
|
|
9
10
|
import {property, state} from 'lit/decorators.js';
|
|
10
|
-
import {ResizeController} from '@lit-labs/observers/resize-controller.js';
|
|
11
11
|
import ZincElement from '../../internal/zinc-element';
|
|
12
|
-
import
|
|
13
|
-
import ZnButtonGroup from '../button-group';
|
|
14
|
-
import ZnDropdown from '../dropdown';
|
|
12
|
+
import ZnChip from '../chip';
|
|
15
13
|
import ZnInlineEdit from '../inline-edit';
|
|
16
14
|
import ZnInput from '../input';
|
|
17
|
-
import
|
|
15
|
+
import ZnOption from '../option';
|
|
16
|
+
import ZnSelect from '../select';
|
|
17
|
+
import ZnTextarea from '../textarea';
|
|
18
18
|
import type {PropertyValues} from 'lit';
|
|
19
19
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
20
|
-
import type {ZnMenuSelectEvent} from '../../events/zn-menu-select';
|
|
21
20
|
|
|
22
21
|
import formControlStyles from '../../form-control.scss';
|
|
23
22
|
import styles from './translations.scss';
|
|
24
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @summary Collects one piece of text in several languages, one language at a time.
|
|
26
|
+
* @documentation https://zinc.style/components/translations
|
|
27
|
+
* @status experimental
|
|
28
|
+
* @since 1.0
|
|
29
|
+
*
|
|
30
|
+
* A select above the field chooses the language being edited. Closed, it carries how many languages are translated —
|
|
31
|
+
* `1/5`; open, every language it offers carries a chip saying whether it has a translation of its own or falls back
|
|
32
|
+
* to English. Blank languages fall back to English at render
|
|
33
|
+
* time, so a blank field shows the English text as its placeholder rather than looking empty.
|
|
34
|
+
*
|
|
35
|
+
* The value submitted is a JSON object keyed by language code. A language stays out of it until it is typed into, so
|
|
36
|
+
* browsing the languages does not pad the payload with empty translations.
|
|
37
|
+
*
|
|
38
|
+
* Put several of these in a `zn-translation-group` to have one select drive all of them.
|
|
39
|
+
*
|
|
40
|
+
* @dependency zn-chip
|
|
41
|
+
* @dependency zn-inline-edit
|
|
42
|
+
* @dependency zn-input
|
|
43
|
+
* @dependency zn-option
|
|
44
|
+
* @dependency zn-select
|
|
45
|
+
* @dependency zn-textarea
|
|
46
|
+
*
|
|
47
|
+
* @slot label - The field's label. Alternatively, use the `label` attribute.
|
|
48
|
+
* @slot help-text - Text describing how to fill the field in, shown below it and shared by every language.
|
|
49
|
+
* Alternatively, use the `help-text` attribute.
|
|
50
|
+
*
|
|
51
|
+
* @event zn-change - Emitted when a translation's value changes.
|
|
52
|
+
* @event zn-input - Emitted when a translation receives input.
|
|
53
|
+
*
|
|
54
|
+
* @csspart form-control - The form control that wraps the label, the language select, the field and the help text.
|
|
55
|
+
* @csspart form-control-label - The label's wrapper.
|
|
56
|
+
* @csspart form-control-input - The wrapper around the field being edited.
|
|
57
|
+
* @csspart form-control-help-text - The help text's wrapper.
|
|
58
|
+
* @csspart language-select - The select that chooses the language being edited.
|
|
59
|
+
*/
|
|
25
60
|
export default class ZnTranslations extends ZincElement implements ZincFormControl {
|
|
26
61
|
static styles = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
27
62
|
static dependencies = {
|
|
28
|
-
'zn-
|
|
29
|
-
'zn-button-group': ZnButtonGroup,
|
|
30
|
-
'zn-dropdown': ZnDropdown,
|
|
63
|
+
'zn-chip': ZnChip,
|
|
31
64
|
'zn-inline-edit': ZnInlineEdit,
|
|
32
65
|
'zn-input': ZnInput,
|
|
33
|
-
'zn-
|
|
66
|
+
'zn-option': ZnOption,
|
|
67
|
+
'zn-select': ZnSelect,
|
|
68
|
+
'zn-textarea': ZnTextarea
|
|
34
69
|
};
|
|
35
70
|
|
|
36
71
|
private readonly formControlController: FormControlController = new FormControlController(this);
|
|
37
|
-
private readonly hasSlotController = new HasSlotController(this, 'label', '
|
|
72
|
+
private readonly hasSlotController = new HasSlotController(this, 'label', 'help-text');
|
|
38
73
|
|
|
74
|
+
/** The name submitted with the form. */
|
|
39
75
|
@property() name = '';
|
|
76
|
+
|
|
77
|
+
/** The translations as a JSON object keyed by language code. The mirror of `values` in attribute form. */
|
|
40
78
|
@property() value = '{"en":""}';
|
|
79
|
+
|
|
80
|
+
/** The label shown above the field. If you need HTML, use the `label` slot instead. */
|
|
41
81
|
@property() label: string = '';
|
|
42
82
|
|
|
43
83
|
/**
|
|
@@ -45,12 +85,28 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
45
85
|
* `help-text` slot instead.
|
|
46
86
|
*/
|
|
47
87
|
@property({attribute: 'help-text'}) helpText: string = '';
|
|
88
|
+
|
|
89
|
+
/** Disables editing in every language. */
|
|
48
90
|
@property({type: Boolean, reflect: true}) disabled = false;
|
|
91
|
+
|
|
92
|
+
/** Marks the label required. Validity is not enforced per language. */
|
|
49
93
|
@property({type: Boolean, reflect: true}) required = false;
|
|
94
|
+
|
|
95
|
+
/** Removes the component's own padding. */
|
|
50
96
|
@property({type: Boolean, reflect: true}) flush = false;
|
|
51
|
-
|
|
97
|
+
|
|
98
|
+
/** The control each translation is edited through. */
|
|
99
|
+
@property({attribute: "input-type"}) inputType: 'text' | 'number' | 'textarea' = 'text';
|
|
100
|
+
|
|
101
|
+
/** Rows of the textarea, when `input-type` is `textarea`. */
|
|
52
102
|
@property({attribute: "textarea-rows", type: Number}) textareaRows: number | undefined;
|
|
53
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Edits the translation through a `zn-inline-edit` — the value reads as text until it is clicked — rather than a
|
|
106
|
+
* plain input or textarea.
|
|
107
|
+
*/
|
|
108
|
+
@property({type: Boolean, reflect: true, attribute: 'inline-edit'}) inlineEdit = false;
|
|
109
|
+
|
|
54
110
|
/**
|
|
55
111
|
* Quick insertions offered by the slash menu on `text` and `textarea` inputs. Accepts a JSON array of items, or
|
|
56
112
|
* the shorthand `Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}`. Every language shares the list.
|
|
@@ -82,36 +138,27 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
82
138
|
/** Resolves additional slash menu items each time the menu opens. JavaScript only. */
|
|
83
139
|
@property({attribute: false}) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
|
|
84
140
|
|
|
85
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Hides this component's own language select and defers the choice to a parent zn-translation-group. The group sets
|
|
143
|
+
* this on its children itself.
|
|
144
|
+
*/
|
|
86
145
|
@property({type: Boolean, reflect: true}) grouped = false;
|
|
87
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The languages on offer, as language code to display name — `{"en": "English", "fr": "French"}`. Writing the code
|
|
149
|
+
* as the name (`{"en": "EN"}`) is also accepted. `en` is the language every other one falls back to.
|
|
150
|
+
*/
|
|
88
151
|
@property({type: Object}) languages: Record<string, string> = {
|
|
89
152
|
'en': 'EN'
|
|
90
153
|
};
|
|
154
|
+
|
|
155
|
+
/** The translations as an object keyed by language code. The mirror of `value` in property form. */
|
|
91
156
|
@property({type: Object}) values: Record<string, string> = {};
|
|
92
157
|
|
|
158
|
+
/** The serialized translations a form reset restores. Taken from the `value` attribute where there is one. */
|
|
159
|
+
@defaultValue() defaultValue = '{"en":""}';
|
|
160
|
+
|
|
93
161
|
@state() private _activeLanguage = 'en';
|
|
94
|
-
@state() private _overflowIndex = -1;
|
|
95
|
-
|
|
96
|
-
private _lastObservedWidth = 0;
|
|
97
|
-
private _measureRafId = 0;
|
|
98
|
-
|
|
99
|
-
constructor() {
|
|
100
|
-
super();
|
|
101
|
-
// eslint-disable-next-line no-new
|
|
102
|
-
new ResizeController(this, {
|
|
103
|
-
callback: entries => {
|
|
104
|
-
const width = entries[0]?.contentRect.width ?? 0;
|
|
105
|
-
if (Math.abs(width - this._lastObservedWidth) < 1) return;
|
|
106
|
-
this._lastObservedWidth = width;
|
|
107
|
-
if (this._overflowIndex !== -1) {
|
|
108
|
-
this._overflowIndex = -1;
|
|
109
|
-
} else {
|
|
110
|
-
this._scheduleLangOverflow();
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
162
|
|
|
116
163
|
get validity(): ValidityState {
|
|
117
164
|
return validValidityState;
|
|
@@ -137,9 +184,11 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
137
184
|
// no-op
|
|
138
185
|
}
|
|
139
186
|
|
|
140
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Sets the active language externally. Used by zn-translation-group. Browsing to a language does not create a key
|
|
189
|
+
* for it — an untouched language stays absent from `values` so it is not submitted as an empty translation.
|
|
190
|
+
*/
|
|
141
191
|
public setActiveLanguage(language: string) {
|
|
142
|
-
this.addLanguageKey(language);
|
|
143
192
|
this._activeLanguage = language;
|
|
144
193
|
this.requestUpdate();
|
|
145
194
|
}
|
|
@@ -163,6 +212,27 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
163
212
|
return Object.keys(this.pendingValues());
|
|
164
213
|
}
|
|
165
214
|
|
|
215
|
+
/** Whether the language carries a translation of its own, rather than falling back to English. */
|
|
216
|
+
public hasTranslation(language: string): boolean {
|
|
217
|
+
return (this.pendingValues()[language] ?? '').trim() !== '';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The chip shown against a language, in the select's value and against each of its options. */
|
|
221
|
+
private languageState(language: string): { type: 'success' | 'error'; label: string } {
|
|
222
|
+
if (this.hasTranslation(language)) return {type: 'success', label: 'Translated'};
|
|
223
|
+
return {type: 'error', label: language === 'en' ? 'Empty' : 'English'};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* `English (EN)` — the configured name plus its code, unless the name already is the code, in which case the code
|
|
228
|
+
* alone. `languages` is written both ways: `{"en": "English"}` and `{"en": "EN"}`.
|
|
229
|
+
*/
|
|
230
|
+
private languageLabel(language: string): string {
|
|
231
|
+
const name = this.languages[language] ?? language.toUpperCase();
|
|
232
|
+
const code = language.toUpperCase();
|
|
233
|
+
return name.toUpperCase() === code ? name : `${name} (${code})`;
|
|
234
|
+
}
|
|
235
|
+
|
|
166
236
|
/**
|
|
167
237
|
* `values`, falling back to the `value` attribute it is built from while that is still pending. A parent
|
|
168
238
|
* zn-translation-group syncs its children from its own first update, which runs before theirs, so reading
|
|
@@ -177,66 +247,13 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
177
247
|
}
|
|
178
248
|
}
|
|
179
249
|
|
|
180
|
-
disconnectedCallback() {
|
|
181
|
-
super.disconnectedCallback();
|
|
182
|
-
if (this._measureRafId) {
|
|
183
|
-
cancelAnimationFrame(this._measureRafId);
|
|
184
|
-
this._measureRafId = 0;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
250
|
protected firstUpdated() {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
super.updated(changedProperties);
|
|
194
|
-
this._scheduleLangOverflow();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private _scheduleLangOverflow() {
|
|
198
|
-
if (this._measureRafId) return;
|
|
199
|
-
this._measureRafId = requestAnimationFrame(() => {
|
|
200
|
-
this._measureRafId = 0;
|
|
201
|
-
this._computeLangOverflow();
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private _computeLangOverflow() {
|
|
206
|
-
const group = this.shadowRoot?.querySelector<HTMLElement>('.translations__language-group');
|
|
207
|
-
if (!group) return;
|
|
208
|
-
|
|
209
|
-
const buttons = Array.from(group.querySelectorAll<HTMLElement>('zn-button[data-lang-btn]'));
|
|
210
|
-
if (buttons.length < 2) {
|
|
211
|
-
if (this._overflowIndex !== -1) this._overflowIndex = -1;
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const firstTop = buttons[0].getBoundingClientRect().top;
|
|
216
|
-
let wrapAt = -1;
|
|
217
|
-
for (let i = 1; i < buttons.length; i++) {
|
|
218
|
-
if (buttons[i].getBoundingClientRect().top > firstTop + 1) {
|
|
219
|
-
wrapAt = i;
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (wrapAt === -1) {
|
|
225
|
-
const trailing = group.querySelector<HTMLElement>('[data-lang-overflow], [data-lang-add]');
|
|
226
|
-
if (trailing && trailing.getBoundingClientRect().top > firstTop + 1) {
|
|
227
|
-
wrapAt = buttons.length;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (wrapAt === -1) return;
|
|
232
|
-
|
|
233
|
-
const newIndex = this._overflowIndex === -1
|
|
234
|
-
? wrapAt
|
|
235
|
-
: Math.max(1, Math.min(wrapAt, this._overflowIndex - 1));
|
|
236
|
-
|
|
237
|
-
if (newIndex !== this._overflowIndex) {
|
|
238
|
-
this._overflowIndex = newIndex;
|
|
251
|
+
// The decorator only follows the `value` attribute; a component given its translations through `values`, or
|
|
252
|
+
// through either property, would otherwise reset to an empty English translation.
|
|
253
|
+
if (!this.hasAttribute('value')) {
|
|
254
|
+
this.defaultValue = this.value;
|
|
239
255
|
}
|
|
256
|
+
this.formControlController.updateValidity();
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
willUpdate(changedProperties: PropertyValues) {
|
|
@@ -276,7 +293,9 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
276
293
|
// In grouped mode the parent zn-translation-group manages the active language,
|
|
277
294
|
// so we must not override what it set via setActiveLanguage().
|
|
278
295
|
if (!this.grouped) {
|
|
279
|
-
|
|
296
|
+
const isKnown = Object.prototype.hasOwnProperty.call(this.values, this._activeLanguage)
|
|
297
|
+
|| Object.prototype.hasOwnProperty.call(this.languages, this._activeLanguage);
|
|
298
|
+
if (!this._activeLanguage || (!isKnown && this._activeLanguage !== 'en')) {
|
|
280
299
|
const keys = Object.keys(this.values);
|
|
281
300
|
if (keys.length > 0) {
|
|
282
301
|
this._activeLanguage = keys[0];
|
|
@@ -288,34 +307,30 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
288
307
|
}
|
|
289
308
|
}
|
|
290
309
|
|
|
291
|
-
|
|
310
|
+
/**
|
|
311
|
+
* The select's own change and input events are stopped here: they describe the language being browsed, not the
|
|
312
|
+
* translation being edited, and a consumer listening on zn-translations reads either as a value change.
|
|
313
|
+
*/
|
|
314
|
+
private handleLanguageSelect = (e: Event) => {
|
|
292
315
|
e.stopPropagation();
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// Add new language with empty string
|
|
297
|
-
this.values = {...this.values, [languageCode]: ''};
|
|
298
|
-
this._activeLanguage = languageCode;
|
|
299
|
-
this.updateValue();
|
|
316
|
+
const language = (e.target as ZnSelect).value;
|
|
317
|
+
if (typeof language === 'string' && language && language !== this._activeLanguage) {
|
|
318
|
+
this.switchLanguage(language);
|
|
300
319
|
}
|
|
301
320
|
};
|
|
302
321
|
|
|
303
|
-
private
|
|
322
|
+
private handleLanguageInput = (e: Event) => {
|
|
304
323
|
e.stopPropagation();
|
|
305
|
-
const element = e.detail.element as HTMLElement;
|
|
306
|
-
const languageCode = element.getAttribute('data-path');
|
|
307
|
-
if (languageCode) {
|
|
308
|
-
this.switchLanguage(languageCode);
|
|
309
|
-
}
|
|
310
324
|
};
|
|
311
325
|
|
|
326
|
+
/** The language shown in the field, without touching `values`. */
|
|
312
327
|
private switchLanguage = (lang: string) => {
|
|
313
328
|
this._activeLanguage = lang;
|
|
314
329
|
this.requestUpdate();
|
|
315
330
|
};
|
|
316
331
|
|
|
317
332
|
private handleValueUpdate = (e: CustomEvent) => {
|
|
318
|
-
const target = e.target as (ZnInput | ZnInlineEdit);
|
|
333
|
+
const target = e.target as (ZnInput | ZnInlineEdit | ZnTextarea);
|
|
319
334
|
if (this._activeLanguage) {
|
|
320
335
|
const newValue: string = target.value as string;
|
|
321
336
|
if (newValue !== this.values[this._activeLanguage]) {
|
|
@@ -333,7 +348,8 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
333
348
|
|
|
334
349
|
private handleKeyDown = (event: KeyboardEvent) => {
|
|
335
350
|
if (event.key === 'Enter') {
|
|
336
|
-
|
|
351
|
+
// An inline edit commits on Enter itself, and a textarea needs it for the newline.
|
|
352
|
+
if (event.target instanceof ZnInlineEdit || event.target instanceof ZnTextarea) {
|
|
337
353
|
return;
|
|
338
354
|
}
|
|
339
355
|
|
|
@@ -349,45 +365,113 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
349
365
|
this.formControlController.submit();
|
|
350
366
|
};
|
|
351
367
|
|
|
368
|
+
/** The control the active language's translation is edited through. */
|
|
369
|
+
private renderField(value: string, placeholder: string, isRTL: boolean) {
|
|
370
|
+
const dir = isRTL ? 'rtl' : 'ltr';
|
|
371
|
+
|
|
372
|
+
if (this.inlineEdit) {
|
|
373
|
+
return html`
|
|
374
|
+
<zn-inline-edit
|
|
375
|
+
input-type=${this.inputType}
|
|
376
|
+
textarea-rows=${ifDefined(this.textareaRows)}
|
|
377
|
+
.value=${live(value)}
|
|
378
|
+
name="${this.name}"
|
|
379
|
+
placeholder="${placeholder}"
|
|
380
|
+
dir="${dir}"
|
|
381
|
+
?disabled="${this.disabled}"
|
|
382
|
+
slash-trigger="${this.slashTrigger}"
|
|
383
|
+
slash-heading="${this.slashHeading}"
|
|
384
|
+
slash-preset="${this.slashPreset}"
|
|
385
|
+
slash-recent-key="${this.slashRecentKey}"
|
|
386
|
+
?slash-hide-keys="${this.slashHideKeys}"
|
|
387
|
+
.slashItems="${this.slashItems}"
|
|
388
|
+
.slashItemsProvider="${this.slashItemsProvider}"
|
|
389
|
+
@zn-change="${this.handleValueUpdate}"
|
|
390
|
+
@zn-input="${this.handleValueUpdate}"
|
|
391
|
+
@zn-submit="${this.handleSubmit}"
|
|
392
|
+
></zn-inline-edit>`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (this.inputType === 'textarea') {
|
|
396
|
+
return html`
|
|
397
|
+
<zn-textarea
|
|
398
|
+
rows=${ifDefined(this.textareaRows)}
|
|
399
|
+
resize="auto"
|
|
400
|
+
.value=${live(value)}
|
|
401
|
+
name="${this.name}"
|
|
402
|
+
placeholder="${placeholder}"
|
|
403
|
+
dir="${dir}"
|
|
404
|
+
?disabled="${this.disabled}"
|
|
405
|
+
slash-trigger="${this.slashTrigger}"
|
|
406
|
+
slash-heading="${this.slashHeading}"
|
|
407
|
+
slash-preset="${this.slashPreset}"
|
|
408
|
+
slash-recent-key="${this.slashRecentKey}"
|
|
409
|
+
?slash-hide-keys="${this.slashHideKeys}"
|
|
410
|
+
.slashItems="${this.slashItems}"
|
|
411
|
+
.slashItemsProvider="${this.slashItemsProvider}"
|
|
412
|
+
@zn-change="${this.handleValueUpdate}"
|
|
413
|
+
@zn-input="${this.handleValueUpdate}"
|
|
414
|
+
></zn-textarea>`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return html`
|
|
418
|
+
<zn-input
|
|
419
|
+
type="${this.inputType === 'number' ? 'number' : 'text'}"
|
|
420
|
+
clearable
|
|
421
|
+
.value=${live(value)}
|
|
422
|
+
name="${this.name}"
|
|
423
|
+
placeholder="${placeholder}"
|
|
424
|
+
dir="${dir}"
|
|
425
|
+
?disabled="${this.disabled}"
|
|
426
|
+
slash-trigger="${this.slashTrigger}"
|
|
427
|
+
slash-heading="${this.slashHeading}"
|
|
428
|
+
slash-preset="${this.slashPreset}"
|
|
429
|
+
slash-recent-key="${this.slashRecentKey}"
|
|
430
|
+
?slash-hide-keys="${this.slashHideKeys}"
|
|
431
|
+
.slashItems="${this.slashItems}"
|
|
432
|
+
.slashItemsProvider="${this.slashItemsProvider}"
|
|
433
|
+
@zn-change="${this.handleValueUpdate}"
|
|
434
|
+
@zn-input="${this.handleValueUpdate}"
|
|
435
|
+
></zn-input>`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Arabic and Hebrew read right to left, so the field's `dir` follows the language being edited. */
|
|
352
439
|
private isRTLLanguage(languageCode: string): boolean {
|
|
353
|
-
// Check if language is Arabic or Hebrew (including variants)
|
|
354
440
|
return languageCode.startsWith('ar') || languageCode === 'he' || languageCode === 'iw';
|
|
355
441
|
}
|
|
356
442
|
|
|
357
443
|
render() {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const overflowLangTabs = visibleTabs.slice(overflowCutoff);
|
|
374
|
-
const overflowActions = overflowLangTabs.map(code => ({
|
|
375
|
-
title: code.toUpperCase(),
|
|
376
|
-
type: 'dropdown',
|
|
377
|
-
path: code,
|
|
378
|
-
icon: code === this._activeLanguage ? 'check' : ''
|
|
379
|
-
}));
|
|
444
|
+
// A value can carry a language `languages` does not list — server-rendered content outliving a config change.
|
|
445
|
+
// Offer those too, or the translation is stranded in the value with no way to reach it.
|
|
446
|
+
const values = this.pendingValues();
|
|
447
|
+
const languageCodes = [
|
|
448
|
+
...Object.keys(this.languages),
|
|
449
|
+
...Object.keys(values).filter(code => !Object.prototype.hasOwnProperty.call(this.languages, code))
|
|
450
|
+
];
|
|
451
|
+
// Closed, the select answers "how much is left to do" rather than the state of the one language on show — that
|
|
452
|
+
// is what the options are for.
|
|
453
|
+
const targets = languageCodes.filter(code => code !== 'en');
|
|
454
|
+
const translated = targets.filter(code => this.hasTranslation(code)).length;
|
|
455
|
+
const summary = {
|
|
456
|
+
label: `${translated}/${targets.length}`,
|
|
457
|
+
type: translated === targets.length ? 'success' : translated > 0 ? 'warning' : 'error'
|
|
458
|
+
};
|
|
380
459
|
|
|
381
460
|
const currentTranslation = this.values[this._activeLanguage] ?? '';
|
|
382
461
|
const isRTL = this.isRTLLanguage(this._activeLanguage);
|
|
383
462
|
|
|
463
|
+
// A blank translation falls back to English at render time, so show that English text as the placeholder rather
|
|
464
|
+
// than leaving the field looking like it has nothing behind it.
|
|
465
|
+
const englishValue = this.pendingValues().en ?? '';
|
|
466
|
+
const placeholder = !currentTranslation && this._activeLanguage !== 'en' && englishValue
|
|
467
|
+
? englishValue
|
|
468
|
+
: 'Enter translation...';
|
|
469
|
+
|
|
384
470
|
const hasLabelSlot = this.hasSlotController.test('label');
|
|
385
471
|
const hasLabel = this.label ? true : hasLabelSlot;
|
|
386
|
-
const hasExpandSlot = this.hasSlotController.test('expand');
|
|
387
472
|
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
|
388
473
|
const hasHelpText = this.helpText ? true : hasHelpTextSlot;
|
|
389
|
-
const
|
|
390
|
-
const showActions = !this.grouped && (hasMultipleLanguages || hasExpandSlot);
|
|
474
|
+
const showLanguageSelect = !this.grouped && languageCodes.length > 1;
|
|
391
475
|
|
|
392
476
|
return html`
|
|
393
477
|
<div part="form-control"
|
|
@@ -401,80 +485,36 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
401
485
|
'form-control--has-help-text': hasHelpText,
|
|
402
486
|
})}"
|
|
403
487
|
@keydown="${this.handleKeyDown}">
|
|
404
|
-
<
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
${
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
<zn-menu
|
|
432
|
-
.actions=${overflowActions}
|
|
433
|
-
@zn-menu-select="${this.handleOverflowSelect}"
|
|
434
|
-
></zn-menu>
|
|
435
|
-
</zn-dropdown>
|
|
436
|
-
` : nothing}
|
|
437
|
-
${availableLanguages.length > 0 ? html`
|
|
438
|
-
<zn-dropdown placement="bottom-end" data-lang-add>
|
|
439
|
-
<zn-button
|
|
440
|
-
slot="trigger"
|
|
441
|
-
color="default"
|
|
442
|
-
outline
|
|
443
|
-
>+
|
|
444
|
-
</zn-button>
|
|
445
|
-
<zn-menu
|
|
446
|
-
.actions=${availableLanguages}
|
|
447
|
-
@zn-menu-select="${this.handleLanguageAdd}"
|
|
448
|
-
></zn-menu>
|
|
449
|
-
</zn-dropdown>
|
|
450
|
-
` : nothing}
|
|
451
|
-
</zn-button-group>
|
|
452
|
-
` : nothing}
|
|
453
|
-
</div>
|
|
454
|
-
` : nothing}
|
|
455
|
-
</div>
|
|
488
|
+
<label part="form-control-label" class="form-control__label" for="input"
|
|
489
|
+
aria-hidden=${hasLabel ? 'false' : 'true'}>
|
|
490
|
+
<slot name="label">${this.label}</slot>
|
|
491
|
+
</label>
|
|
492
|
+
|
|
493
|
+
${showLanguageSelect ? html`
|
|
494
|
+
<div class="translations__language">
|
|
495
|
+
<zn-select
|
|
496
|
+
class="translations__language-select"
|
|
497
|
+
part="language-select"
|
|
498
|
+
hoist
|
|
499
|
+
?disabled="${this.disabled}"
|
|
500
|
+
.value="${this._activeLanguage}"
|
|
501
|
+
@zn-change="${this.handleLanguageSelect}"
|
|
502
|
+
@zn-input="${this.handleLanguageInput}">
|
|
503
|
+
<zn-chip slot="suffix" type="${summary.type}">${summary.label}</zn-chip>
|
|
504
|
+
${languageCodes.map(code => {
|
|
505
|
+
const optionState = this.languageState(code);
|
|
506
|
+
return html`
|
|
507
|
+
<zn-option value="${code}">
|
|
508
|
+
${this.languageLabel(code)}
|
|
509
|
+
<zn-chip slot="suffix" type="${optionState.type}">${optionState.label}</zn-chip>
|
|
510
|
+
</zn-option>`;
|
|
511
|
+
})}
|
|
512
|
+
</zn-select>
|
|
513
|
+
</div>
|
|
514
|
+
` : nothing}
|
|
456
515
|
|
|
457
516
|
<div part="form-control-input" class="translations__body">
|
|
458
|
-
${keyed(this._activeLanguage,
|
|
459
|
-
<zn-inline-edit
|
|
460
|
-
input-type=${this.inputType}
|
|
461
|
-
textarea-rows=${ifDefined(this.textareaRows)}
|
|
462
|
-
.value=${live(currentTranslation)}
|
|
463
|
-
name="${this.name}"
|
|
464
|
-
placeholder="Enter translation..."
|
|
465
|
-
dir="${isRTL ? 'rtl' : 'ltr'}"
|
|
466
|
-
slash-trigger="${this.slashTrigger}"
|
|
467
|
-
slash-heading="${this.slashHeading}"
|
|
468
|
-
slash-preset="${this.slashPreset}"
|
|
469
|
-
slash-recent-key="${this.slashRecentKey}"
|
|
470
|
-
?slash-hide-keys="${this.slashHideKeys}"
|
|
471
|
-
.slashItems="${this.slashItems}"
|
|
472
|
-
.slashItemsProvider="${this.slashItemsProvider}"
|
|
473
|
-
@zn-change="${this.handleValueUpdate}"
|
|
474
|
-
@zn-input="${this.handleValueUpdate}"
|
|
475
|
-
@zn-submit="${this.handleSubmit}"
|
|
476
|
-
></zn-inline-edit>
|
|
477
|
-
`)}
|
|
517
|
+
${keyed(this._activeLanguage, this.renderField(currentTranslation, placeholder, isRTL))}
|
|
478
518
|
</div>
|
|
479
519
|
|
|
480
520
|
${hasHelpText ? html`
|
|
@@ -7,38 +7,16 @@
|
|
|
7
7
|
padding: var(--zn-spacing-small) var(--zn-spacing-medium);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
justify-content: space-between;
|
|
14
|
-
gap: var(--zn-spacing-small);
|
|
15
|
-
margin-bottom: var(--zn-spacing-2x-small);
|
|
16
|
-
min-height: var(--zn-input-height-small);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.translations__actions {
|
|
20
|
-
display: flex;
|
|
21
|
-
align-items: center;
|
|
22
|
-
gap: var(--zn-spacing-x-small);
|
|
23
|
-
margin-left: auto;
|
|
10
|
+
// The select and the field below it are two stacked controls, so they take the same gap zn-form-group uses.
|
|
11
|
+
.translations__language {
|
|
12
|
+
margin-bottom: var(--zn-spacing-medium);
|
|
24
13
|
}
|
|
25
14
|
|
|
26
15
|
.translations__body {
|
|
27
16
|
display: block;
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
.form-control__label {
|
|
31
|
-
margin-bottom: 0 !important;
|
|
32
|
-
white-space: nowrap;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
19
|
.translations--flush,
|
|
36
20
|
.translations--grouped {
|
|
37
21
|
padding: 0;
|
|
38
22
|
}
|
|
39
|
-
|
|
40
|
-
.translations--grouped {
|
|
41
|
-
.translations__header {
|
|
42
|
-
margin-bottom: 0;
|
|
43
|
-
}
|
|
44
|
-
}
|