diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 60f16f285f..6341c454ae 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -4,30 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]"> - <div :class="$style.codeEditorScroller"> - <textarea - ref="inputEl" - v-model="vModel" - :class="[$style.textarea]" - :disabled="disabled" - :required="required" - :readonly="readonly" - autocomplete="off" - wrap="off" - spellcheck="false" - @focus="focused = true" - @blur="focused = false" - @keydown="onKeydown($event)" - @input="onInput" - ></textarea> - <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/> +<div> + <div :class="$style.label" @click="focus"><slot name="label"></slot></div> + <div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]"> + <div :class="$style.codeEditorScroller"> + <textarea + ref="inputEl" + v-model="vModel" + :class="[$style.textarea]" + :disabled="disabled" + :required="required" + :readonly="readonly" + autocomplete="off" + wrap="off" + spellcheck="false" + @focus="focused = true" + @blur="focused = false" + @keydown="onKeydown($event)" + @input="onInput" + ></textarea> + <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/> + </div> </div> + <div :class="$style.caption"><slot name="caption"></slot></div> + <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> <script lang="ts" setup> import { ref, watch, toRefs, shallowRef, nextTick } from 'vue'; +import { debounce } from 'throttle-debounce'; +import MkButton from '@/components/MkButton.vue'; +import { i18n } from '@/i18n.js'; import XCode from '@/components/MkCode.core.vue'; const props = withDefaults(defineProps<{ @@ -36,6 +44,8 @@ const props = withDefaults(defineProps<{ required?: boolean; readonly?: boolean; disabled?: boolean; + debounce?: boolean; + manualSave?: boolean; }>(), { lang: 'js', }); @@ -54,6 +64,8 @@ const focused = ref(false); const changed = ref(false); const inputEl = shallowRef<HTMLTextAreaElement>(); +const focus = () => inputEl.value?.focus(); + const onInput = (ev) => { v.value = ev.target?.value ?? v.value; changed.value = true; @@ -100,16 +112,48 @@ const updated = () => { emit('update:modelValue', v.value); }; +const debouncedUpdated = debounce(1000, updated); + watch(modelValue, newValue => { v.value = newValue ?? ''; }); -watch(v, () => { - updated(); +watch(v, newValue => { + if (!props.manualSave) { + if (props.debounce) { + debouncedUpdated(); + } else { + updated(); + } + } }); </script> <style lang="scss" module> +.label { + font-size: 0.85em; + padding: 0 0 8px 0; + user-select: none; + + &:empty { + display: none; + } +} + +.caption { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); + + &:empty { + display: none; + } +} + +.save { + margin: 8px 0 0 0; +} + .codeEditorRoot { min-width: 100%; max-width: 100%; @@ -117,6 +161,7 @@ watch(v, () => { overflow-y: hidden; box-sizing: border-box; margin: 0; + border-radius: 6px; padding: 0; color: var(--fg); border: solid 1px var(--panel); @@ -157,9 +202,10 @@ watch(v, () => { caret-color: rgb(225, 228, 232); background-color: transparent; border: 0; + border-radius: 6px; outline: 0; min-width: calc(100% - 24px); - height: calc(100% - 24px); + height: 100%; padding: 12px; line-height: 1.5em; font-size: 1em; diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 365b054f7a..67a655b677 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts._play.summary }}</template> </MkTextarea> <MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ti ti-chevron-down"></i></MkButton> - <MkTextarea v-model="script" code tall spellcheck="false"> + <MkCodeEditor v-model="script" lang="is"> <template #label>{{ i18n.ts._play.script }}</template> - </MkTextarea> + </MkCodeEditor> <div class="_buttons"> <MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton> @@ -40,6 +40,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkTextarea from '@/components/MkTextarea.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import { useRouter } from '@/router.js'; diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index 1b2cf9f237..8efc0e0504 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -26,9 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> </FormSplit> - <MkTextarea v-model="valueForEditor" tall code> + <MkCodeEditor v-model="valueForEditor" lang="json5"> <template #label>{{ i18n.ts.value }} (JSON)</template> - </MkTextarea> + </MkCodeEditor> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> @@ -52,7 +52,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import FormSplit from '@/components/form/split.vue'; import FormInfo from '@/components/MkInfo.vue'; diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue index 8e52686c12..2564562089 100644 --- a/packages/frontend/src/pages/settings/custom-css.vue +++ b/packages/frontend/src/pages/settings/custom-css.vue @@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo> - <MkTextarea v-model="localCustomCss" manualSave tall code style="tab-size: 2;"> + <MkCodeEditor v-model="localCustomCss" manualSave lang="css"> <template #label>CSS</template> - </MkTextarea> + </MkCodeEditor> </div> </template> <script lang="ts" setup> import { ref, watch, computed } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import FormInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { unisonReload } from '@/scripts/unison-reload.js'; diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue index 44472d47d9..c174ba176f 100644 --- a/packages/frontend/src/pages/settings/plugin.install.vue +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <FormInfo warn>{{ i18n.ts._plugin.installWarn }}</FormInfo> - <MkTextarea v-model="code" tall code> + <MkCodeEditor v-model="code" lang="is"> <template #label>{{ i18n.ts.code }}</template> - </MkTextarea> + </MkCodeEditor> <div> <MkButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, ref, computed } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkButton from '@/components/MkButton.vue'; import FormInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue index 0f3fbad0b7..f9be5720e0 100644 --- a/packages/frontend/src/pages/settings/theme.install.vue +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> - <MkTextarea v-model="installThemeCode" code> + <MkCodeEditor v-model="installThemeCode" lang="json5"> <template #label>{{ i18n.ts._theme.code }}</template> - </MkTextarea> + </MkCodeEditor> <div class="_buttons"> <MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkButton from '@/components/MkButton.vue'; import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index c4138e23de..eee3e49db1 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -51,9 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.editCode }}</template> <div class="_gaps_m"> - <MkTextarea v-model="themeCode" tall code> + <MkCodeEditor v-model="themeCode" lang="json5"> <template #label>{{ i18n.ts._theme.code }}</template> - </MkTextarea> + </MkCodeEditor> <MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton> </div> </MkFolder> @@ -80,6 +80,7 @@ import { v4 as uuid } from 'uuid'; import JSON5 from 'json5'; import MkButton from '@/components/MkButton.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkFolder from '@/components/MkFolder.vue';