@beforesemicolon/site-builder 0.35.0 → 0.36.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/cjs/build-templates.js +5 -5
- package/dist/esm/build-templates.js +5 -5
- package/netlify/functions/auth-middleware.js +183 -0
- package/netlify/functions/auth0-config.js +77 -0
- package/netlify/functions/build.js +214 -0
- package/netlify/functions/copy-file-local.js +88 -0
- package/netlify/functions/github.js +771 -0
- package/netlify/functions/validate-user.js +109 -0
- package/netlify/functions/widgets.js +403 -0
- package/netlify.toml +67 -0
- package/package.json +2 -2
- package/scaffolds/.env.example +18 -0
- package/scaffolds/_redirects +12 -0
- package/scaffolds/admin/app.js +244 -0
- package/scaffolds/admin/auth-manager.js +275 -0
- package/scaffolds/admin/controls/code.control.js +22 -0
- package/scaffolds/admin/controls/controls.js +249 -0
- package/scaffolds/admin/controls/file.control.js +829 -0
- package/scaffolds/admin/controls/html.control.js +43 -0
- package/scaffolds/admin/controls/markdown.control.js +31 -0
- package/scaffolds/admin/data.js +543 -0
- package/scaffolds/admin/flashbar.js +104 -0
- package/scaffolds/admin/index.html +44 -0
- package/scaffolds/admin/modal.js +123 -0
- package/scaffolds/admin/preview-widget.js +102 -0
- package/scaffolds/admin/preview.js +197 -0
- package/scaffolds/admin/repository-manager.js +329 -0
- package/scaffolds/admin/styles.css +1526 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as authManager from './auth-manager.js'
|
|
2
|
+
import { Flashbars, showFlashbar } from './flashbar.js'
|
|
3
|
+
import { Modal, showModal } from './modal.js'
|
|
4
|
+
import { Preview } from './preview.js'
|
|
5
|
+
import { Controls } from './controls/controls.js'
|
|
6
|
+
import {
|
|
7
|
+
config,
|
|
8
|
+
currentPage,
|
|
9
|
+
currentWidget,
|
|
10
|
+
currentWidgetId,
|
|
11
|
+
hasPendingChanges,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loading,
|
|
14
|
+
loadWidget,
|
|
15
|
+
panel,
|
|
16
|
+
publishChanges,
|
|
17
|
+
publishing,
|
|
18
|
+
selectPage,
|
|
19
|
+
selectWidget,
|
|
20
|
+
setLoading,
|
|
21
|
+
setPanel,
|
|
22
|
+
} from './data.js'
|
|
23
|
+
import { WidgetPreview } from './preview-widget.js'
|
|
24
|
+
|
|
25
|
+
const { MARKUP } = window.BFS
|
|
26
|
+
const { html, when, is, isNot, repeat, pick, or } = MARKUP
|
|
27
|
+
|
|
28
|
+
const logout = async () => {
|
|
29
|
+
const doLogout = async () => {
|
|
30
|
+
await authManager.logout()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (hasPendingChanges()) {
|
|
34
|
+
return showModal({
|
|
35
|
+
title: 'Unsaved Changes',
|
|
36
|
+
content:
|
|
37
|
+
'You have unsaved changes. Are you sure you want to logout?',
|
|
38
|
+
actionLabel: 'Logout',
|
|
39
|
+
action: doLogout,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
doLogout()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const selectionOptions = () => {
|
|
47
|
+
if (!config()) return []
|
|
48
|
+
|
|
49
|
+
if (panel() === 'page') {
|
|
50
|
+
return config().pages.map((pg) => ({
|
|
51
|
+
label: pg.url.replace('.html', ''),
|
|
52
|
+
value: pg.id,
|
|
53
|
+
selected: is(currentPage, (p) => p.id === pg.id),
|
|
54
|
+
}))
|
|
55
|
+
} else {
|
|
56
|
+
return config().widgets.map((w) => ({
|
|
57
|
+
label: w.name,
|
|
58
|
+
value: w.id,
|
|
59
|
+
selected: is(currentWidgetId, (id) => id === w.id),
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const onOptionSelectionChange = (e) => {
|
|
65
|
+
if (panel() === 'page') {
|
|
66
|
+
selectPage(
|
|
67
|
+
config()?.pages.find((pg) => pg.id === e.target.value) ?? null
|
|
68
|
+
)
|
|
69
|
+
} else {
|
|
70
|
+
const w = config()?.widgets.find((w) => w.id === e.target.value) ?? null
|
|
71
|
+
|
|
72
|
+
if (w) {
|
|
73
|
+
loadWidget(w.id)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
html`
|
|
79
|
+
${when(
|
|
80
|
+
authManager.isAuthenticated,
|
|
81
|
+
() => html`
|
|
82
|
+
<div id="cms-app">
|
|
83
|
+
${Flashbars}
|
|
84
|
+
${when(
|
|
85
|
+
publishing,
|
|
86
|
+
html`
|
|
87
|
+
<div
|
|
88
|
+
class="publish-overlay"
|
|
89
|
+
role="status"
|
|
90
|
+
aria-live="polite"
|
|
91
|
+
></div>
|
|
92
|
+
`
|
|
93
|
+
)}
|
|
94
|
+
<!-- Top Navigation Bar -->
|
|
95
|
+
<header class="cms-header">
|
|
96
|
+
<div class="header-left">
|
|
97
|
+
<img
|
|
98
|
+
src="https://beforesemicolon.com/assets/new-logo-white-on-black@2x.png"
|
|
99
|
+
alt="Before Semicolon logo"
|
|
100
|
+
class="cms-logo"
|
|
101
|
+
height="40"
|
|
102
|
+
/>
|
|
103
|
+
<h1>Content Management System</h1>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="header-right">
|
|
106
|
+
<button
|
|
107
|
+
id="publish-btn"
|
|
108
|
+
class="btn btn-primary"
|
|
109
|
+
disabled="${or(
|
|
110
|
+
publishing,
|
|
111
|
+
isNot(hasPendingChanges, true)
|
|
112
|
+
)}"
|
|
113
|
+
onclick="${publishChanges}"
|
|
114
|
+
>
|
|
115
|
+
${when(
|
|
116
|
+
publishing,
|
|
117
|
+
'Publishing...',
|
|
118
|
+
'Publish Changes'
|
|
119
|
+
)}
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
id="logout-btn"
|
|
123
|
+
class="btn btn-secondary"
|
|
124
|
+
disabled="${publishing}"
|
|
125
|
+
onclick="${logout}"
|
|
126
|
+
>
|
|
127
|
+
Logout
|
|
128
|
+
</button>
|
|
129
|
+
<div
|
|
130
|
+
class="avatar"
|
|
131
|
+
title="${pick(authManager.user, 'name')}"
|
|
132
|
+
>
|
|
133
|
+
<img
|
|
134
|
+
src="${pick(authManager.user, 'picture')}"
|
|
135
|
+
alt="${pick(authManager.user, 'name')}"
|
|
136
|
+
width="40"
|
|
137
|
+
height="40"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</header>
|
|
142
|
+
<!-- Main Content Area -->
|
|
143
|
+
<main class="cms-main">
|
|
144
|
+
<aside class="cms-sidebar">
|
|
145
|
+
<div id="widget-editor" class="widget-editor">
|
|
146
|
+
${Controls({ disabled: publishing })}
|
|
147
|
+
</div>
|
|
148
|
+
</aside>
|
|
149
|
+
<!-- Right Panel: Live Preview -->
|
|
150
|
+
<section class="cms-preview">
|
|
151
|
+
<div class="row preview-selection">
|
|
152
|
+
<div class="tabs">
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
class="btn ${when(
|
|
156
|
+
is(panel, 'page'),
|
|
157
|
+
'active'
|
|
158
|
+
)}"
|
|
159
|
+
onclick="${() => {
|
|
160
|
+
setPanel('page')
|
|
161
|
+
selectWidget('', null)
|
|
162
|
+
}}"
|
|
163
|
+
>
|
|
164
|
+
Pages
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class="btn ${when(
|
|
169
|
+
is(panel, 'widgets'),
|
|
170
|
+
'active'
|
|
171
|
+
)}"
|
|
172
|
+
onclick="${() => {
|
|
173
|
+
if (!currentWidget()) {
|
|
174
|
+
const w = config().widgets[0]
|
|
175
|
+
loadWidget(w.id)
|
|
176
|
+
}
|
|
177
|
+
setPanel('widgets')
|
|
178
|
+
}}"
|
|
179
|
+
>
|
|
180
|
+
Widgets
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
<select
|
|
184
|
+
class="preview-dropdown"
|
|
185
|
+
onchange="${onOptionSelectionChange}"
|
|
186
|
+
>
|
|
187
|
+
${repeat(
|
|
188
|
+
selectionOptions,
|
|
189
|
+
(item) =>
|
|
190
|
+
html` <option
|
|
191
|
+
value="${item.value}"
|
|
192
|
+
selected="${item.selected}"
|
|
193
|
+
>
|
|
194
|
+
${item.label}
|
|
195
|
+
</option>`
|
|
196
|
+
)}
|
|
197
|
+
</select>
|
|
198
|
+
</div>
|
|
199
|
+
<div id="preview-container" class="preview-container">
|
|
200
|
+
${when(is(panel, 'page'), Preview, WidgetPreview)}
|
|
201
|
+
</div>
|
|
202
|
+
</section>
|
|
203
|
+
</main>
|
|
204
|
+
<!-- Loading Overlay -->
|
|
205
|
+
<div
|
|
206
|
+
id="loading-overlay"
|
|
207
|
+
class="loading-overlay ${when(loading, 'show')}"
|
|
208
|
+
>
|
|
209
|
+
<div class="loading-spinner">
|
|
210
|
+
<div class="spinner"></div>
|
|
211
|
+
<p>Loading...</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
${Modal}
|
|
216
|
+
`,
|
|
217
|
+
() => authManager.LoginView
|
|
218
|
+
)}
|
|
219
|
+
`
|
|
220
|
+
.onMount(() => {
|
|
221
|
+
// Initialize authentication
|
|
222
|
+
authManager
|
|
223
|
+
.initialize()
|
|
224
|
+
.then(() => loadConfig())
|
|
225
|
+
.catch((error) => {
|
|
226
|
+
console.error('Auth initialization failed:', error)
|
|
227
|
+
showFlashbar({
|
|
228
|
+
type: 'error',
|
|
229
|
+
title: 'Failed to auth user',
|
|
230
|
+
message: 'Authentication failed. Please try again.',
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
.finally(() => {
|
|
234
|
+
setLoading(false)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
window.addEventListener('beforeunload', (e) => {
|
|
238
|
+
if (hasPendingChanges()) {
|
|
239
|
+
e.preventDefault()
|
|
240
|
+
e.returnValue = ''
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
.replace(document.getElementById('app'))
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { showFlashbar } from './flashbar.js'
|
|
2
|
+
|
|
3
|
+
const { html, state } = window.BFS.MARKUP
|
|
4
|
+
|
|
5
|
+
const baseUrl = window.location.origin
|
|
6
|
+
const redirectUri = baseUrl + '/admin'
|
|
7
|
+
|
|
8
|
+
export const AUTH0_CONFIG = {
|
|
9
|
+
domain: 'cms-editor.us.auth0.com',
|
|
10
|
+
clientId: 'KL8nUmcNI8Y5mTLQVtftv9hXslWDwWED',
|
|
11
|
+
redirectUri: redirectUri,
|
|
12
|
+
scope: 'openid profile email',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Module state
|
|
16
|
+
let auth0Client = null
|
|
17
|
+
let authStateCallbacks = []
|
|
18
|
+
let initialized = false
|
|
19
|
+
let cachedUser = null
|
|
20
|
+
|
|
21
|
+
const [_isAuthenticated, setIsAuthenticated] = state(false)
|
|
22
|
+
const [_user, setUser] = state(null)
|
|
23
|
+
|
|
24
|
+
export const isAuthenticated = _isAuthenticated
|
|
25
|
+
export const user = _user
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Notify all registered callbacks of auth state change
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
function notifyAuthStateChange() {
|
|
32
|
+
const currentUser = user()
|
|
33
|
+
const isAuth = currentUser !== null
|
|
34
|
+
|
|
35
|
+
authStateCallbacks.forEach((callback) => {
|
|
36
|
+
callback(isAuth, currentUser)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize Auth0 client
|
|
42
|
+
*/
|
|
43
|
+
export async function initialize() {
|
|
44
|
+
if (initialized) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Get Auth0 config from configuration file
|
|
50
|
+
const domain = AUTH0_CONFIG.domain
|
|
51
|
+
const clientId = AUTH0_CONFIG.clientId
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
!domain ||
|
|
55
|
+
!clientId ||
|
|
56
|
+
domain === 'YOUR_AUTH0_DOMAIN' ||
|
|
57
|
+
clientId === 'YOUR_AUTH0_CLIENT_ID'
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'Auth0 configuration missing. Please update admin/auth-config.js with your Auth0 credentials.'
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create Auth0 client
|
|
65
|
+
auth0Client = await window.auth0.createAuth0Client({
|
|
66
|
+
domain,
|
|
67
|
+
clientId,
|
|
68
|
+
authorizationParams: {
|
|
69
|
+
redirect_uri: AUTH0_CONFIG.redirectUri,
|
|
70
|
+
audience: AUTH0_CONFIG.audience || `https://${domain}/api/v2/`,
|
|
71
|
+
scope: AUTH0_CONFIG.scope,
|
|
72
|
+
},
|
|
73
|
+
cacheLocation: 'localstorage',
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Handle redirect callback
|
|
77
|
+
if (
|
|
78
|
+
window.location.search.includes('code=') &&
|
|
79
|
+
window.location.search.includes('state=')
|
|
80
|
+
) {
|
|
81
|
+
await auth0Client.handleRedirectCallback()
|
|
82
|
+
// Clean up URL
|
|
83
|
+
window.history.replaceState(
|
|
84
|
+
{},
|
|
85
|
+
document.title,
|
|
86
|
+
window.location.pathname
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if user is authenticated
|
|
91
|
+
const isAuthenticated = await auth0Client.isAuthenticated()
|
|
92
|
+
|
|
93
|
+
if (isAuthenticated) {
|
|
94
|
+
// Cache user data
|
|
95
|
+
cachedUser = await auth0Client.getUser()
|
|
96
|
+
|
|
97
|
+
// NEW: Validate email against Netlify Identity
|
|
98
|
+
try {
|
|
99
|
+
const validation = await validateUserEmail(cachedUser.email)
|
|
100
|
+
|
|
101
|
+
if (!validation.authorized) {
|
|
102
|
+
await handleUnauthorizedAccess(validation.message)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setIsAuthenticated(true)
|
|
107
|
+
setUser(cachedUser)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Email validation error:', error)
|
|
110
|
+
await handleUnauthorizedAccess(
|
|
111
|
+
'Unable to validate user access. Please try again later.'
|
|
112
|
+
)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
initialized = true
|
|
118
|
+
|
|
119
|
+
// Notify after user data is cached
|
|
120
|
+
notifyAuthStateChange()
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Auth0 initialization failed:', error)
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Login with Auth0
|
|
129
|
+
*/
|
|
130
|
+
export async function login() {
|
|
131
|
+
if (!auth0Client) {
|
|
132
|
+
console.error('Auth0 client not initialized')
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await auth0Client.loginWithRedirect({
|
|
138
|
+
authorizationParams: {
|
|
139
|
+
redirect_uri: AUTH0_CONFIG.redirectUri,
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Login failed:', error)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate user email against Netlify Identity
|
|
149
|
+
* @param {string} email - User email from Auth0 token
|
|
150
|
+
* @returns {Promise<{authorized: boolean, message?: string}>}
|
|
151
|
+
*/
|
|
152
|
+
async function validateUserEmail(email) {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch('/api/validate-user', {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({ email }),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
if (response.status === 403) {
|
|
164
|
+
const result = await response.json()
|
|
165
|
+
return {
|
|
166
|
+
authorized: false,
|
|
167
|
+
message:
|
|
168
|
+
result.message ||
|
|
169
|
+
'Your email is not authorized to access this admin panel',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`Validation request failed: ${response.status}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return await response.json()
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error('Email validation failed:', error)
|
|
178
|
+
// On validation API failure, logout user as a safety measure
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Unable to validate user access. Please try again later.'
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Handle unauthorized access
|
|
187
|
+
* - Display error message
|
|
188
|
+
* - Logout from Auth0
|
|
189
|
+
* - Clear cached user data
|
|
190
|
+
* @param {string} message - Error message to display
|
|
191
|
+
*/
|
|
192
|
+
async function handleUnauthorizedAccess(message) {
|
|
193
|
+
try {
|
|
194
|
+
// Clear cached user data
|
|
195
|
+
cachedUser = null
|
|
196
|
+
setIsAuthenticated(false)
|
|
197
|
+
setUser(null)
|
|
198
|
+
|
|
199
|
+
// Display error message
|
|
200
|
+
showFlashbar({
|
|
201
|
+
type: 'error',
|
|
202
|
+
title: 'Access Denied',
|
|
203
|
+
message: 'You are not authorized to access this admin panel.',
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Logout from Auth0
|
|
207
|
+
if (auth0Client) {
|
|
208
|
+
await auth0Client.logout({
|
|
209
|
+
logoutParams: {
|
|
210
|
+
returnTo: window.location.origin,
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Error handling unauthorized access:', error)
|
|
216
|
+
// Force page reload as fallback
|
|
217
|
+
window.location.reload()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Logout current user
|
|
223
|
+
*/
|
|
224
|
+
export async function logout() {
|
|
225
|
+
if (!auth0Client) {
|
|
226
|
+
console.error('Auth0 client not initialized')
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
cachedUser = null
|
|
232
|
+
await auth0Client.logout({
|
|
233
|
+
logoutParams: {
|
|
234
|
+
returnTo: window.location.origin + '/admin',
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Logout failed:', error)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Login View Component
|
|
244
|
+
*/
|
|
245
|
+
export const LoginView = html`
|
|
246
|
+
<div id="login-screen" class="login-screen">
|
|
247
|
+
<div class="login-container">
|
|
248
|
+
<div class="login-header">
|
|
249
|
+
<img
|
|
250
|
+
src="https://beforesemicolon.com/assets/new-logo-white-on-black@2x.png"
|
|
251
|
+
alt="Before Semicolon logo"
|
|
252
|
+
class="login-logo"
|
|
253
|
+
height="60"
|
|
254
|
+
/>
|
|
255
|
+
<h1>CMS Login</h1>
|
|
256
|
+
<p>Please sign in to access the admin panel</p>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="login-form">
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
class="btn btn-primary btn-block"
|
|
262
|
+
onclick="${login}"
|
|
263
|
+
>
|
|
264
|
+
Sign In
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="login-footer">
|
|
269
|
+
<p>
|
|
270
|
+
© ${new Date().getFullYear()} Before Semicolon. All rights
|
|
271
|
+
reserved.
|
|
272
|
+
</p>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { html } = window.BFS.MARKUP
|
|
2
|
+
|
|
3
|
+
export const renderCodeEditor = (input, path, disabled, label, onChange) => {
|
|
4
|
+
return html`
|
|
5
|
+
<div class="control-field-block code-editor-control">
|
|
6
|
+
<span class="label">${label}</span>
|
|
7
|
+
<div class="code-editor-wrapper">
|
|
8
|
+
<textarea
|
|
9
|
+
class="code-editor"
|
|
10
|
+
disabled="${disabled}"
|
|
11
|
+
oninput="${(e) => {
|
|
12
|
+
onChange(path, e.target.value, 'code')
|
|
13
|
+
Prism.highlightElement(e.target)
|
|
14
|
+
}}"
|
|
15
|
+
rows="10"
|
|
16
|
+
>
|
|
17
|
+
${input.value || ''}</textarea
|
|
18
|
+
>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`
|
|
22
|
+
}
|