@bagelink/vue 1.2.79 → 1.2.81

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.
@@ -7,10 +7,9 @@ declare global {
7
7
  }
8
8
  }
9
9
  import { appendStyle, appendScript } from '@bagelink/vue'
10
- import { nextTick, onMounted, watch } from 'vue'
10
+ import { onMounted, ref, computed, watch } from 'vue'
11
11
 
12
- // Props
13
- const { language, readonly = false, modelValue = '', autodetect, ignoreIllegals = true, label, height = '300px' } = defineProps<{
12
+ interface CodeEditorProps {
14
13
  language?: Language
15
14
  readonly?: boolean
16
15
  modelValue?: string
@@ -18,39 +17,50 @@ const { language, readonly = false, modelValue = '', autodetect, ignoreIllegals
18
17
  ignoreIllegals?: boolean
19
18
  label?: string
20
19
  height?: string
21
- }>()
20
+ }
21
+ // Props with default values
22
+ const props = withDefaults(defineProps<CodeEditorProps>(), {
23
+ language: 'html',
24
+ readonly: false,
25
+ modelValue: '',
26
+ autodetect: true,
27
+ ignoreIllegals: true,
28
+ label: '',
29
+ height: '240px'
30
+ })
22
31
 
23
32
  const emit = defineEmits(['update:modelValue'])
33
+ // State
34
+ const code = ref(props.modelValue || '')
35
+ const editorRef = ref<HTMLDivElement>()
36
+ const loaded = ref(false)
37
+ const hljs = ref<HilightJS | null>(null)
38
+ // Computed
39
+ const maxHeight = computed(() => {
40
+ const h = props.height ?? '240px'
41
+ return h.match(/^\d+$/) ? `${h}px` : h
42
+ })
24
43
 
25
- let hljs = $ref<HilightJS | null>(null)
44
+ const formattedCode = computed(() => {
45
+ if (!hljs.value) return escapeHtml(code.value)
26
46
 
27
- let elHeight = $ref(height)
28
- // State and refs
29
- let code = $ref('')
30
- const textarea = $ref<HTMLTextAreaElement>()
31
- let loaded = $ref(false)
47
+ try {
48
+ const lang = props.language || ''
32
49
 
33
- // Computed properties
34
- const cannotDetectLanguage = $computed(() => {
35
- const lang = language || ''
36
- return !(autodetect ?? !lang) && !hljs?.getLanguage(lang)
37
- })
50
+ if (lang && !props.autodetect && !hljs.value.getLanguage(lang)) {
51
+ console.warn(`The language "${lang}" is not available.`)
52
+ return escapeHtml(code.value)
53
+ }
38
54
 
39
- const className = $computed(() => {
40
- if (cannotDetectLanguage) return ''
41
- return `hljs ${language || ''}`
42
- })
55
+ const result = props.autodetect
56
+ ? hljs.value.highlightAuto(code.value)
57
+ : hljs.value.highlight(code.value, { language: lang, ignoreIllegals: props.ignoreIllegals })
43
58
 
44
- const highlightedCode = $computed(() => {
45
- if (cannotDetectLanguage) {
46
- console.warn(`The language "${language}" you specified could not be found.`)
47
- return escapeHtml(code)
59
+ return result.value || escapeHtml(code.value)
60
+ } catch (error) {
61
+ console.error('Highlighting error:', error)
62
+ return escapeHtml(code.value)
48
63
  }
49
- const lang = language || ''
50
- const result = autodetect
51
- ? hljs?.highlightAuto(code)
52
- : hljs?.highlight(code, { language: lang, ignoreIllegals })
53
- return result?.value || ''
54
64
  })
55
65
 
56
66
  // Methods
@@ -67,113 +77,165 @@ function escapeHtml(unsafe: string) {
67
77
  })
68
78
  }
69
79
 
80
+ function handleInput(e: Event) {
81
+ const target = e.target as HTMLTextAreaElement
82
+ code.value = target.value
83
+ emit('update:modelValue', code.value)
84
+ }
85
+
70
86
  function handleTab(event: KeyboardEvent) {
87
+ if (event.key !== 'Tab') return
88
+
89
+ event.preventDefault()
71
90
  const target = event.target as HTMLTextAreaElement
72
91
  const start = target.selectionStart
73
- const tab = ' '
74
- code = code.slice(0, start) + tab + code.slice(start)
75
- nextTick(() => {
76
- target.selectionStart = target.selectionEnd = start + tab.length
77
- })
78
- }
92
+ const end = target.selectionEnd
79
93
 
80
- function adjustHeight() {
81
- if (textarea?.scrollHeight && textarea.scrollHeight > Number.parseInt(elHeight)) {
82
- elHeight = `${textarea.scrollHeight}px`
83
- }
94
+ // Add tab or indent selected text
95
+ const newValue = `${code.value.substring(0, start)} ${code.value.substring(end)}`
96
+ code.value = newValue
97
+ emit('update:modelValue', code.value)
98
+
99
+ // Move cursor position after the inserted tab
100
+ setTimeout(() => {
101
+ target.selectionStart = target.selectionEnd = start + 2
102
+ }, 0)
84
103
  }
85
104
 
86
105
  // Lifecycle
87
106
  onMounted(async () => {
88
- // Append scripts and styles for Highlight.js
89
- await appendScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js', { id: 'hljs-cdn' })
90
- await appendStyle('https://cdn.jsdelivr.net/npm/highlight.js/styles/atom-one-dark.min.css')
91
-
92
- // Initialize hljs
93
- if (window.hljs) {
94
- hljs = window.hljs as HilightJS
95
- loaded = true
96
- } else {
97
- console.error('Highlight.js failed to load.')
107
+ try {
108
+ // Load highlight.js
109
+ await appendScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js', { id: 'hljs-cdn' })
110
+ await appendStyle('https://cdn.jsdelivr.net/npm/highlight.js/styles/atom-one-dark.min.css')
111
+
112
+ if (window.hljs) {
113
+ hljs.value = window.hljs
114
+ loaded.value = true
115
+ } else {
116
+ console.error('Failed to load highlight.js')
117
+ }
118
+ } catch (error) {
119
+ console.error('Error loading highlight.js:', error)
98
120
  }
99
121
  })
100
122
 
101
- watch(() => modelValue, (newVal) => {
102
- adjustHeight()
103
- if (newVal !== code) {
104
- code = newVal
123
+ // Watch for external modelValue changes
124
+ watch(() => props.modelValue, (newVal) => {
125
+ if (newVal !== undefined && newVal !== code.value) {
126
+ code.value = newVal
105
127
  }
106
128
  }, { immediate: true })
107
129
  </script>
108
130
 
109
131
  <template>
110
- <div class="mb-05">
111
- <label v-if="label" class="label txt-start">{{ label }}</label>
112
- <div v-if="loaded" class="code-editor-wrap grid rounded p-1 overflow hm-300px ltr txt-start relative h-100p">
113
- <div class="relative block h-100" :style="{ height: `calc(${elHeight} - 2rem)` }">
114
- <pre class=" overflow-hidden absolute inset-0 p-0 m-0 h-100 codeText">
115
- <code class="absolute inset-0" :class="className" v-html="highlightedCode" />
116
- </pre>
132
+ <div class="code-editor-container ltr" :style="{ maxHeight }">
133
+ <label v-if="label" class="label">{{ label }}</label>
134
+ <div
135
+ v-if="loaded"
136
+ ref="editorRef"
137
+ class="code-editor-grandpa"
138
+ >
139
+ <div class="editor-content-papa relative">
140
+ <pre class="code-display" wrap><code v-html="formattedCode" /></pre>
117
141
  <textarea
118
142
  v-if="!readonly"
119
- ref="textarea"
120
- v-model="code"
121
- class="code-editor absolute inset-0 bg-transparent overflow-hidden h-100 p-0 m-0 codeText border-none txt-start"
143
+ :value="code"
144
+ class="code-input"
122
145
  spellcheck="false"
123
- placeholder="Write your code here"
124
- aria-label="Code Editor"
125
- data-gramm="false"
126
- @keydown.tab.prevent="handleTab"
127
- @input="emit('update:modelValue', code)"
146
+ autocomplete="off"
147
+ autocorrect="off"
148
+ autocapitalize="off"
149
+ @input="handleInput"
150
+ @keydown="handleTab"
128
151
  />
129
152
  </div>
130
153
  </div>
131
154
  </div>
132
155
  </template>
133
156
 
134
- <style>
135
- pre code.hljs{
136
- padding: 0 !important;
137
- inset: 0 !important;
138
- position: absolute;
157
+ <style scoped>
158
+ .code-editor-container {
159
+ margin-bottom: 0.5rem;
160
+ height: 100%;
139
161
  }
140
- </style>
141
162
 
142
- <style scoped>
143
- .codeText{
144
- font-family: monospace;
145
- white-space: pre-wrap;
146
- word-wrap: break-word;
147
- caret-color: var(--bgl-white);
148
- color: var(--bgl-white);
163
+ .label {
164
+ display: block;
165
+ text-align: left;
166
+ margin-bottom: 0.25rem;
149
167
  }
150
- .code-editor-wrap {
151
- background: #282c34;
152
- height: max-content;
168
+
169
+ .code-editor-grandpa {
170
+ background: #22252A;
171
+ border-radius: 0.25rem;
172
+ width: 100%;
173
+ height: 100%;
174
+ overflow: auto;
175
+ padding: 1ch;
176
+ padding-inline-start: 2ch;
153
177
  }
154
- .code-editor-wrap:focus-within, .code-editor-wrap:focus-visible, .code-editor-wrap:focus {
155
- box-shadow: inset 0 0 10px #00000021;
156
- outline: solid 1px var(--border-color);
157
- /* outline: -webkit-focus-ring-color auto 1px; */
158
178
 
179
+ .code-editor-grandpa:focus-within {
180
+ outline: solid 1px var(--border-color, #4f575f);
181
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.13);
159
182
  }
160
183
 
161
- .code-editor-wrap:focus-within:has(textarea:focus) + .label,
162
- .label:has(+ .code-editor-wrap:focus-within) {
163
- color: var(--bgl-primary) !important;
184
+ .editor-content-papa {
185
+ position: relative;
186
+ width: 100%;
187
+ padding-bottom: calc(100% - 5lh);
188
+ }
189
+ .code-display,
190
+ .code-input {
191
+ inset: 0;
192
+ margin: 0;
193
+ padding: 0;
194
+ width: 100%;
195
+ height: 100%;
196
+ overflow: auto;
197
+ font-family: monospace;
198
+ font-size: 1em;
199
+ line-height: 1.5;
200
+ tab-size: 2;
201
+ word-break: keep-all;
202
+ text-align: left;
164
203
  }
165
204
 
166
- .code-editor {
167
- color: transparent;
168
- resize: none;
205
+ .code-display {
206
+ position: relative;
207
+ color: #fff;
208
+ pointer-events: none;
209
+ z-index: 1;
169
210
  }
170
211
 
171
- .code-editor::selection {
172
- background: #2466bc30;
173
- color: inherit;
212
+ .code-display code {
213
+ display: block;
214
+ background: transparent !important;
215
+ padding: 0 !important;
174
216
  }
175
217
 
176
- .code-editor:focus {
177
- outline: none;
218
+ .code-input {
219
+ position: absolute;
220
+ background: transparent;
221
+ color: transparent;
222
+ caret-color: #fff;
223
+ border: none;
224
+ resize: none;
225
+ outline: none;
226
+ z-index: 2;
227
+ }
228
+
229
+ .code-input::selection {
230
+ background-color: rgba(36, 102, 188, 0.3);
231
+ color: transparent;
232
+ }
233
+ </style>
234
+
235
+ <style>
236
+ /* Global styles */
237
+ pre code.hljs {
238
+ padding: 0 !important;
239
+ background: transparent !important;
178
240
  }
179
241
  </style>
@@ -15,6 +15,9 @@ const editor = useEditor()
15
15
  const isInitializing = ref(false)
16
16
  const hasInitialized = ref(false)
17
17
 
18
+ // Initialize content from modelValue
19
+ editor.state.content = props.modelValue
20
+
18
21
  // Initialize debugger if debug mode is enabled
19
22
  if (props.debug) {
20
23
  editor.initDebugger()
@@ -83,6 +86,9 @@ async function initEditor() {
83
86
  // Set default direction based on content
84
87
  doc.body.dir = hasRTL ? 'rtl' : 'ltr'
85
88
 
89
+ // Ensure editor.state.content is set to the current HTML content
90
+ editor.state.content = doc.body.innerHTML
91
+
86
92
  editor.init(doc)
87
93
  useEditorKeyboard(doc, commands)
88
94
 
@@ -92,6 +98,8 @@ async function initEditor() {
92
98
  p.dir = doc.body.dir
93
99
  p.innerHTML = '<br>'
94
100
  doc.body.appendChild(p)
101
+ // Update state.content after changes
102
+ editor.state.content = doc.body.innerHTML
95
103
  } else {
96
104
  // Convert any direct text nodes to paragraphs
97
105
  const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
@@ -112,6 +120,8 @@ async function initEditor() {
112
120
  doc.body.removeChild(textNode)
113
121
  }
114
122
  })
123
+ // Update state.content after cleanup
124
+ editor.state.content = doc.body.innerHTML
115
125
  }
116
126
 
117
127
  doc.body.focus()
@@ -168,7 +178,7 @@ watch(() => editor.state.content, (newValue) => {
168
178
  v-if="editor.state.isSplitView"
169
179
  v-model="editor.state.content"
170
180
  language="html"
171
- class="code-editor"
181
+ :height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : '250px'"
172
182
  @update:modelValue="editor.updateState.content('html')"
173
183
  />
174
184
  </div>
@@ -196,12 +206,6 @@ watch(() => editor.state.content, (newValue) => {
196
206
  .content-area li{
197
207
  line-height: 1.65;
198
208
  }
199
-
200
- .code-editor {
201
- flex: 1;
202
- min-height: 240px !important;
203
- height: 100%;
204
- }
205
209
  </style>
206
210
 
207
211
  <style scoped>
@@ -213,7 +217,7 @@ watch(() => editor.state.content, (newValue) => {
213
217
 
214
218
  .editor-container {
215
219
  display: flex;
216
- gap: 1rem;
220
+ gap: 0.5rem;
217
221
  }
218
222
 
219
223
  .content-area,
@@ -274,9 +278,6 @@ watch(() => editor.state.content, (newValue) => {
274
278
  height: 100%;
275
279
  overflow-y: auto;
276
280
  }
277
- .fullscreen-mode .code-editor{
278
- height: 100% !important;
279
- }
280
281
 
281
282
  .debug-controls {
282
283
  display: flex;
@@ -124,26 +124,51 @@ export function sleep(ms: number = 100) {
124
124
  return new Promise(resolve => setTimeout(resolve, ms))
125
125
  }
126
126
 
127
- export function appendScript(src: string, options?: { id?: string }): Promise<void> {
128
- return new Promise((resolve, reject) => {
129
- if (options?.id) {
130
- if (document.getElementById(options.id)) {
131
- resolve()
132
- return
133
- }
134
- } else if (document.querySelector(`script[src="${src}"]`)) {
135
- resolve()
136
- return
137
- }
127
+ // Keep track of loading scripts
128
+ const scriptsLoading = new Map<string, Promise<void>>()
129
+
130
+ export async function appendScript(src: string, options?: { id?: string }): Promise<void> {
131
+ const scriptId = options?.id || src
132
+ await sleep(1)
133
+ // If this script is already loading, return the existing promise
134
+ if (scriptsLoading.has(scriptId)) {
135
+ return scriptsLoading.get(scriptId)!
136
+ }
137
+
138
+ // Check if script is already in the document
139
+ if (options?.id && document.getElementById(options.id)) {
140
+ return Promise.resolve()
141
+ } else if (document.querySelector(`script[src="${src}"]`)) {
142
+ return Promise.resolve()
143
+ }
144
+
145
+ // Create a new loading promise for this script
146
+ const loadingPromise = new Promise<void>((resolve, reject) => {
138
147
  const script = document.createElement('script')
139
148
  script.src = src
140
149
  if (options?.id) {
141
150
  script.id = options.id
142
151
  }
143
- script.onload = () => { resolve() }
144
- script.onerror = reject
152
+
153
+ script.onload = () => {
154
+ resolve()
155
+ // Remove from loading scripts map when done
156
+ scriptsLoading.delete(scriptId)
157
+ }
158
+
159
+ script.onerror = (err) => {
160
+ reject(err)
161
+ // Remove from loading scripts map on error
162
+ scriptsLoading.delete(scriptId)
163
+ }
164
+
145
165
  document.head.append(script)
146
166
  })
167
+
168
+ // Store the loading promise for this script
169
+ scriptsLoading.set(scriptId, loadingPromise)
170
+
171
+ return loadingPromise
147
172
  }
148
173
 
149
174
  export function appendStyle(src: string): Promise<void> {