@llui/markdown-editor 0.3.0 → 0.4.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/dist/editor.d.ts +15 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +12 -2
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/block-drag.d.ts +73 -0
- package/dist/plugins/block-drag.d.ts.map +1 -0
- package/dist/plugins/block-drag.js +760 -0
- package/dist/plugins/block-drag.js.map +1 -0
- package/dist/plugins/code-language.d.ts +60 -0
- package/dist/plugins/code-language.d.ts.map +1 -0
- package/dist/plugins/code-language.js +308 -0
- package/dist/plugins/code-language.js.map +1 -0
- package/dist/plugins/frontmatter.d.ts +45 -0
- package/dist/plugins/frontmatter.d.ts.map +1 -0
- package/dist/plugins/frontmatter.js +281 -0
- package/dist/plugins/frontmatter.js.map +1 -0
- package/dist/plugins/overlay.d.ts +1 -0
- package/dist/plugins/overlay.d.ts.map +1 -1
- package/dist/plugins/overlay.js +7 -0
- package/dist/plugins/overlay.js.map +1 -1
- package/dist/plugins/wikilink.d.ts +91 -0
- package/dist/plugins/wikilink.d.ts.map +1 -0
- package/dist/plugins/wikilink.js +467 -0
- package/dist/plugins/wikilink.js.map +1 -0
- package/dist/styles/block-drag.css +63 -0
- package/dist/styles/editor.css +64 -0
- package/dist/transformers/code.d.ts +24 -0
- package/dist/transformers/code.d.ts.map +1 -0
- package/dist/transformers/code.js +167 -0
- package/dist/transformers/code.js.map +1 -0
- package/dist/transformers/gfm.d.ts.map +1 -1
- package/dist/transformers/gfm.js +7 -2
- package/dist/transformers/gfm.js.map +1 -1
- package/dist/transformers/registry.d.ts +3 -0
- package/dist/transformers/registry.d.ts.map +1 -1
- package/dist/transformers/registry.js +26 -0
- package/dist/transformers/registry.js.map +1 -1
- package/package.json +40 -26
- package/src/editor.ts +27 -4
- package/src/index.ts +58 -1
- package/src/plugins/block-drag.ts +912 -0
- package/src/plugins/code-language.ts +378 -0
- package/src/plugins/frontmatter.ts +324 -0
- package/src/plugins/overlay.ts +7 -0
- package/src/plugins/wikilink.ts +553 -0
- package/src/styles/block-drag.css +63 -0
- package/src/styles/editor.css +64 -0
- package/src/transformers/code.ts +174 -0
- package/src/transformers/gfm.ts +6 -2
- package/src/transformers/registry.ts +27 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// Code-block language plugin — makes a fenced block's **info string** visible
|
|
2
|
+
// and editable, and makes it round-trip verbatim.
|
|
3
|
+
//
|
|
4
|
+
// ## Why this package replaces `@lexical/markdown`'s CODE transformer
|
|
5
|
+
//
|
|
6
|
+
// Upstream's `CODE` captures the info string with `([\w-]+)?`, i.e. a SINGLE
|
|
7
|
+
// word-ish token, pushing the rest of the fence line into the code CONTENT:
|
|
8
|
+
//
|
|
9
|
+
// ```lance table → language 'lance', body 'table\nsum(x)' (corrupt)
|
|
10
|
+
// ```c++ → language 'c', body '++\nint main()' (corrupt)
|
|
11
|
+
//
|
|
12
|
+
// That is silent data loss for any consumer that keys off the info string
|
|
13
|
+
// (downstream, ```lance / ```lance table marks a formula block). The CommonMark
|
|
14
|
+
// -correct replacement lives in `transformers/code.ts` and is the DEFAULT in
|
|
15
|
+
// `GFM_TRANSFORMERS`, so the corruption is fixed for every consumer whether or
|
|
16
|
+
// not they enable this plugin, and plugin ORDER cannot reintroduce it. This
|
|
17
|
+
// plugin therefore adds only the editing chrome.
|
|
18
|
+
//
|
|
19
|
+
// ## Why no highlighter
|
|
20
|
+
//
|
|
21
|
+
// The package deliberately depends on `@lexical/code-core`, not `@lexical/code`,
|
|
22
|
+
// to keep Prism out of the bundle (see `transformers/gfm.ts`). The language is
|
|
23
|
+
// therefore an OPAQUE label: it is stored, shown, edited, and re-emitted, but
|
|
24
|
+
// nothing interprets it. That is precisely what lets an arbitrary token like
|
|
25
|
+
// `lance table` survive. The optional `languages` option only seeds a
|
|
26
|
+
// `<datalist>` of suggestions — it never constrains what may be typed.
|
|
27
|
+
//
|
|
28
|
+
// ## Chrome
|
|
29
|
+
//
|
|
30
|
+
// The editor uses the shared overlay seam (`overlayRoot` + `definePluginUI`),
|
|
31
|
+
// exactly like the table tools: a small portaled badge anchored to the code
|
|
32
|
+
// block's top-right corner whenever the caret is inside one.
|
|
33
|
+
|
|
34
|
+
import { $getNodeByKey, $getSelection, $isRangeSelection, SKIP_DOM_SELECTION_TAG } from 'lexical'
|
|
35
|
+
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
|
36
|
+
import { $isCodeNode, CodeHighlightNode, CodeNode } from '@lexical/code-core'
|
|
37
|
+
import { el, input, text, type Mountable } from '@llui/dom'
|
|
38
|
+
import { definePluginUI } from './ui.js'
|
|
39
|
+
import { OVERLAY_Z, hideOverlay, onViewportChange, overlayRoot } from './overlay.js'
|
|
40
|
+
import type { MarkdownPlugin } from './types.js'
|
|
41
|
+
|
|
42
|
+
/** This plugin's registry name (the `plugin` message envelope's `name`). */
|
|
43
|
+
export const CODE_LANGUAGE_PLUGIN = 'codeLanguage'
|
|
44
|
+
|
|
45
|
+
// The fenced-code transformer now lives in `transformers/code.ts` so that it is
|
|
46
|
+
// the DEFAULT in `GFM_TRANSFORMERS`, not an opt-in a consumer must remember to
|
|
47
|
+
// order ahead of `corePlugin()`. This plugin contributes the SAME object
|
|
48
|
+
// reference, so `buildTransformers`' reference de-duplication collapses the two
|
|
49
|
+
// contributions into one and plugin order cannot change the parse.
|
|
50
|
+
// Re-exported here because it is part of this plugin's documented surface.
|
|
51
|
+
import { CODE_INFO_TRANSFORMER, normalizeCodeInfo } from '../transformers/code.js'
|
|
52
|
+
|
|
53
|
+
export { CODE_INFO_TRANSFORMER, normalizeCodeInfo }
|
|
54
|
+
|
|
55
|
+
// ── Plugin UI state ─────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** The language badge's state. JSON-serializable, like every LLui state slice. */
|
|
58
|
+
export interface CodeLanguageState {
|
|
59
|
+
/** Whether the badge is shown. */
|
|
60
|
+
open: boolean
|
|
61
|
+
/** Viewport x of the anchor (the code block's right edge). */
|
|
62
|
+
x: number
|
|
63
|
+
/** Viewport y of the anchor (the code block's top edge). */
|
|
64
|
+
y: number
|
|
65
|
+
/** Node key of the anchored code block (`''` when none). */
|
|
66
|
+
key: string
|
|
67
|
+
/** The input's current value (the block's info string, or the in-flight edit). */
|
|
68
|
+
language: string
|
|
69
|
+
/** The info string as last read from the node — the baseline `cancel` restores
|
|
70
|
+
* and `commit` diffs against, so a no-op commit never touches the document. */
|
|
71
|
+
committed: string
|
|
72
|
+
/** Whether the input has focus; a refresh must not overwrite what's being typed. */
|
|
73
|
+
editing: boolean
|
|
74
|
+
/** A `hide` that arrived mid-edit, applied when the edit ends. */
|
|
75
|
+
pendingHide: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type CodeLanguageMsg =
|
|
79
|
+
| { type: 'show'; key: string; x: number; y: number; language: string | null }
|
|
80
|
+
| { type: 'hide' }
|
|
81
|
+
| { type: 'edit' }
|
|
82
|
+
| { type: 'input'; language: string }
|
|
83
|
+
| { type: 'commit' }
|
|
84
|
+
| { type: 'cancel' }
|
|
85
|
+
|
|
86
|
+
/** Write `language` (null clears it) onto the code block with node key `key`. */
|
|
87
|
+
export type CodeLanguageEffect = { type: 'apply'; key: string; language: string | null }
|
|
88
|
+
|
|
89
|
+
const INITIAL: CodeLanguageState = {
|
|
90
|
+
open: false,
|
|
91
|
+
x: 0,
|
|
92
|
+
y: 0,
|
|
93
|
+
key: '',
|
|
94
|
+
language: '',
|
|
95
|
+
committed: '',
|
|
96
|
+
editing: false,
|
|
97
|
+
pendingHide: false,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sameState(a: CodeLanguageState, b: CodeLanguageState): boolean {
|
|
101
|
+
return (
|
|
102
|
+
a.open === b.open &&
|
|
103
|
+
a.x === b.x &&
|
|
104
|
+
a.y === b.y &&
|
|
105
|
+
a.key === b.key &&
|
|
106
|
+
a.language === b.language &&
|
|
107
|
+
a.committed === b.committed &&
|
|
108
|
+
a.editing === b.editing &&
|
|
109
|
+
a.pendingHide === b.pendingHide
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** End an edit: drop the editing flag and honour a hide deferred during it. */
|
|
114
|
+
function endEdit(state: CodeLanguageState, language: string): CodeLanguageState {
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
language,
|
|
118
|
+
editing: false,
|
|
119
|
+
pendingHide: false,
|
|
120
|
+
open: state.pendingHide ? false : state.open,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function reduce(
|
|
125
|
+
state: CodeLanguageState,
|
|
126
|
+
msg: CodeLanguageMsg,
|
|
127
|
+
): CodeLanguageState | [CodeLanguageState, CodeLanguageEffect[]] {
|
|
128
|
+
switch (msg.type) {
|
|
129
|
+
case 'show': {
|
|
130
|
+
const language = normalizeCodeInfo(msg.language) ?? ''
|
|
131
|
+
// A refresh for the block being edited only re-anchors it: the typed value
|
|
132
|
+
// and the edit itself are preserved (the register listener re-emits `show`
|
|
133
|
+
// on every editor update, including the one this plugin's own commit made).
|
|
134
|
+
const keepEdit = state.editing && state.key === msg.key
|
|
135
|
+
const next: CodeLanguageState = {
|
|
136
|
+
open: true,
|
|
137
|
+
x: msg.x,
|
|
138
|
+
y: msg.y,
|
|
139
|
+
key: msg.key,
|
|
140
|
+
language: keepEdit ? state.language : language,
|
|
141
|
+
committed: language,
|
|
142
|
+
editing: keepEdit,
|
|
143
|
+
// Preserved across a same-block `show` for the same reason `language`
|
|
144
|
+
// and `editing` are. `onViewportChange(refresh)` re-emits `show` on
|
|
145
|
+
// every scroll/resize, so an unconditional reset let a scroll while the
|
|
146
|
+
// input was focused erase a deferred hide — `endEdit` then saw
|
|
147
|
+
// `pendingHide === false` and left the badge anchored over a block the
|
|
148
|
+
// caret had already left.
|
|
149
|
+
pendingHide: keepEdit ? state.pendingHide : false,
|
|
150
|
+
}
|
|
151
|
+
return sameState(state, next) ? state : next
|
|
152
|
+
}
|
|
153
|
+
case 'hide':
|
|
154
|
+
// Focusing the badge's input can collapse the editor selection, which makes
|
|
155
|
+
// the register listener emit `hide`. Closing then would yank the input out
|
|
156
|
+
// from under the caret, so remember it and apply it when the edit ends.
|
|
157
|
+
if (state.editing) return state.pendingHide ? state : { ...state, pendingHide: true }
|
|
158
|
+
return hideOverlay(state)
|
|
159
|
+
case 'edit':
|
|
160
|
+
return state.editing ? state : { ...state, editing: true }
|
|
161
|
+
case 'input':
|
|
162
|
+
return state.language === msg.language && state.editing
|
|
163
|
+
? state
|
|
164
|
+
: { ...state, language: msg.language, editing: true }
|
|
165
|
+
case 'commit': {
|
|
166
|
+
const next = normalizeCodeInfo(state.language)
|
|
167
|
+
const previous = normalizeCodeInfo(state.committed)
|
|
168
|
+
const ended = endEdit({ ...state, committed: next ?? '' }, next ?? '')
|
|
169
|
+
// An unchanged (or still-empty) value must NOT touch the document: doing so
|
|
170
|
+
// would dirty the editor and re-emit onChange for a no-op.
|
|
171
|
+
if (next === previous) return [endEdit(state, state.committed), []]
|
|
172
|
+
return [ended, [{ type: 'apply', key: state.key, language: next }]]
|
|
173
|
+
}
|
|
174
|
+
case 'cancel':
|
|
175
|
+
return endEdit(state, state.committed)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Plugin ──────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
export interface CodeLanguagePluginOptions {
|
|
182
|
+
/** Suggestions offered in the language input's `<datalist>`. Purely advisory —
|
|
183
|
+
* ANY info string may be typed, including multi-token ones. */
|
|
184
|
+
languages?: readonly string[]
|
|
185
|
+
/** Placeholder shown when a block has no language (default `'plain text'`). */
|
|
186
|
+
placeholder?: string
|
|
187
|
+
/** Accessible label for the language input (default `'Code block language'`). */
|
|
188
|
+
label?: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Unique per plugin instance so two mounted editors never share a datalist id. */
|
|
192
|
+
let datalistSeq = 0
|
|
193
|
+
|
|
194
|
+
export function codeLanguagePlugin(opts: CodeLanguagePluginOptions = {}): MarkdownPlugin {
|
|
195
|
+
const languages = opts.languages ?? []
|
|
196
|
+
const listId = languages.length > 0 ? `md-code-lang-${++datalistSeq}` : null
|
|
197
|
+
// Per-instance id so the command item can focus THIS editor's badge input even
|
|
198
|
+
// with several editors mounted at once (the overlay is portaled to <body>).
|
|
199
|
+
const inputId = `md-code-lang-input-${++datalistSeq}`
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
name: CODE_LANGUAGE_PLUGIN,
|
|
203
|
+
|
|
204
|
+
// The keyboard route to the badge. The overlay is portaled to a body-level
|
|
205
|
+
// sibling that exists only while the caret is in a code block, and Tab
|
|
206
|
+
// inside a Lexical CodeNode is consumed for indentation — so without a
|
|
207
|
+
// command the input was unreachable without a pointer, making the ONLY way
|
|
208
|
+
// to set or clear a code block's language mouse-only.
|
|
209
|
+
items: [
|
|
210
|
+
{
|
|
211
|
+
id: CODE_LANGUAGE_PLUGIN,
|
|
212
|
+
label: 'Set code block language',
|
|
213
|
+
icon: 'codeLanguage',
|
|
214
|
+
group: 'block',
|
|
215
|
+
keywords: ['code', 'language', 'fence', 'syntax', 'info string'],
|
|
216
|
+
isDisabled: () => false,
|
|
217
|
+
run: (editor) => {
|
|
218
|
+
// Only meaningful with the caret inside a code block; the overlay is
|
|
219
|
+
// open in exactly that case, so focusing its input is the whole action.
|
|
220
|
+
const focus = (): void => {
|
|
221
|
+
const el = editor.getRootElement()?.ownerDocument.getElementById(inputId)
|
|
222
|
+
if (el instanceof HTMLInputElement) {
|
|
223
|
+
el.focus()
|
|
224
|
+
el.select()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// The overlay may not be open yet if the caret only just moved; let the
|
|
228
|
+
// register listener's refresh land first.
|
|
229
|
+
if (typeof queueMicrotask === 'function') queueMicrotask(focus)
|
|
230
|
+
else focus()
|
|
231
|
+
},
|
|
232
|
+
surfaces: ['slash', 'context'],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
// Declared so the plugin stands alone (a code-only editor without
|
|
236
|
+
// `corePlugin`); the mount de-duplicates node classes across plugins.
|
|
237
|
+
nodes: [CodeNode, CodeHighlightNode],
|
|
238
|
+
transformers: [CODE_INFO_TRANSFORMER],
|
|
239
|
+
// Track the code block under the caret and its viewport rect. Mirrors
|
|
240
|
+
// `tablePlugin`: re-emit only when the anchor or the language actually
|
|
241
|
+
// changes, so typing inside a code block doesn't reconcile the overlay.
|
|
242
|
+
register: (editor, ctx) => {
|
|
243
|
+
let lastKey: string | null = null
|
|
244
|
+
let lastX = NaN
|
|
245
|
+
let lastY = NaN
|
|
246
|
+
let lastLanguage: string | null = null
|
|
247
|
+
const refresh = (): void => {
|
|
248
|
+
const found = editor.getEditorState().read(() => {
|
|
249
|
+
const selection = $getSelection()
|
|
250
|
+
if (!$isRangeSelection(selection)) return null
|
|
251
|
+
const code = $findMatchingParent(selection.anchor.getNode(), $isCodeNode)
|
|
252
|
+
if (!$isCodeNode(code)) return null
|
|
253
|
+
return { key: code.getKey(), language: normalizeCodeInfo(code.getLanguage()) }
|
|
254
|
+
})
|
|
255
|
+
const element = found ? editor.getElementByKey(found.key) : null
|
|
256
|
+
if (!found || !element) {
|
|
257
|
+
if (lastKey !== null) {
|
|
258
|
+
lastKey = null
|
|
259
|
+
lastX = NaN
|
|
260
|
+
lastY = NaN
|
|
261
|
+
lastLanguage = null
|
|
262
|
+
ctx.emit({
|
|
263
|
+
type: 'plugin',
|
|
264
|
+
name: CODE_LANGUAGE_PLUGIN,
|
|
265
|
+
msg: { type: 'hide' },
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
const rect = element.getBoundingClientRect()
|
|
271
|
+
if (
|
|
272
|
+
found.key === lastKey &&
|
|
273
|
+
rect.right === lastX &&
|
|
274
|
+
rect.top === lastY &&
|
|
275
|
+
found.language === lastLanguage
|
|
276
|
+
) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
lastKey = found.key
|
|
280
|
+
lastX = rect.right
|
|
281
|
+
lastY = rect.top
|
|
282
|
+
lastLanguage = found.language
|
|
283
|
+
ctx.emit({
|
|
284
|
+
type: 'plugin',
|
|
285
|
+
name: CODE_LANGUAGE_PLUGIN,
|
|
286
|
+
msg: {
|
|
287
|
+
type: 'show',
|
|
288
|
+
key: found.key,
|
|
289
|
+
x: rect.right,
|
|
290
|
+
y: rect.top,
|
|
291
|
+
language: found.language,
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
return mergeRegister(
|
|
296
|
+
editor.registerUpdateListener(() => refresh()),
|
|
297
|
+
onViewportChange(refresh),
|
|
298
|
+
)
|
|
299
|
+
},
|
|
300
|
+
ui: definePluginUI<CodeLanguageState, CodeLanguageMsg, CodeLanguageEffect>({
|
|
301
|
+
init: () => INITIAL,
|
|
302
|
+
update: reduce,
|
|
303
|
+
onEffect: (effect, ctx) => {
|
|
304
|
+
const editor = ctx.editor()
|
|
305
|
+
if (!editor) return
|
|
306
|
+
// SKIP_DOM_SELECTION_TAG is load-bearing, not decoration. `setLanguage`
|
|
307
|
+
// dirties the CodeNode, so an untagged update reaches
|
|
308
|
+
// `$updateDOMSelection`. Lexical does not clear its selection on blur
|
|
309
|
+
// ('blur' is a PASS_THROUGH_COMMAND), so while the badge input holds DOM
|
|
310
|
+
// focus the pending selection still points INSIDE the code block; the
|
|
311
|
+
// equality short-circuit therefore fails and the reconciler moves the
|
|
312
|
+
// native selection back into the contenteditable — stealing focus out of
|
|
313
|
+
// the input mid-edit. This package already defends against the same
|
|
314
|
+
// hazard in editor.ts (the explicit `$setSelection(null)`).
|
|
315
|
+
editor.update(
|
|
316
|
+
() => {
|
|
317
|
+
const node = $getNodeByKey(effect.key)
|
|
318
|
+
if ($isCodeNode(node)) node.setLanguage(effect.language)
|
|
319
|
+
},
|
|
320
|
+
{ tag: SKIP_DOM_SELECTION_TAG },
|
|
321
|
+
)
|
|
322
|
+
},
|
|
323
|
+
view: ({ state, send }) =>
|
|
324
|
+
overlayRoot({
|
|
325
|
+
open: state.at('open'),
|
|
326
|
+
x: state.at('x'),
|
|
327
|
+
y: state.at('y'),
|
|
328
|
+
zIndex: OVERLAY_Z.codeLanguage,
|
|
329
|
+
// Sit just above the block's top-right corner.
|
|
330
|
+
transform: 'transform:translate(-100%,-100%)',
|
|
331
|
+
attrs: { 'data-scope': 'md-code-language', 'data-part': 'bar' },
|
|
332
|
+
children: (): Mountable[] => {
|
|
333
|
+
const children: Mountable[] = [
|
|
334
|
+
input({
|
|
335
|
+
type: 'text',
|
|
336
|
+
'data-scope': 'md-code-language',
|
|
337
|
+
'data-part': 'input',
|
|
338
|
+
id: inputId,
|
|
339
|
+
'aria-label': opts.label ?? 'Code block language',
|
|
340
|
+
placeholder: opts.placeholder ?? 'plain text',
|
|
341
|
+
spellcheck: 'false',
|
|
342
|
+
autocapitalize: 'off',
|
|
343
|
+
autocomplete: 'off',
|
|
344
|
+
...(listId ? { list: listId } : {}),
|
|
345
|
+
value: state.at('language'),
|
|
346
|
+
onFocus: () => send({ type: 'edit' }),
|
|
347
|
+
onBlur: () => send({ type: 'commit' }),
|
|
348
|
+
onInput: (e: Event) =>
|
|
349
|
+
send({ type: 'input', language: (e.target as HTMLInputElement).value }),
|
|
350
|
+
// The badge lives in a portal outside the contenteditable, but
|
|
351
|
+
// document-level editor handlers would still see these keys.
|
|
352
|
+
onKeyDown: (e: KeyboardEvent) => {
|
|
353
|
+
e.stopPropagation()
|
|
354
|
+
if (e.key === 'Enter') {
|
|
355
|
+
e.preventDefault()
|
|
356
|
+
send({ type: 'commit' })
|
|
357
|
+
} else if (e.key === 'Escape') {
|
|
358
|
+
e.preventDefault()
|
|
359
|
+
send({ type: 'cancel' })
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
]
|
|
364
|
+
if (listId) {
|
|
365
|
+
children.push(
|
|
366
|
+
el(
|
|
367
|
+
'datalist',
|
|
368
|
+
{ id: listId },
|
|
369
|
+
languages.map((language) => el('option', { value: language }, [text(language)])),
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
return children
|
|
374
|
+
},
|
|
375
|
+
}),
|
|
376
|
+
}),
|
|
377
|
+
}
|
|
378
|
+
}
|