mirror of
https://github.com/heyman/heynote.git
synced 2024-12-27 17:18:53 +01:00
Add ability to change language using a language selector dialog
This commit is contained in:
parent
41573820ae
commit
8214bf1bbb
@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import StatusBar from './StatusBar.vue'
|
||||
import Editor from './Editor.vue'
|
||||
|
||||
import LanguageSelector from './LanguageSelector.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Editor,
|
||||
StatusBar,
|
||||
LanguageSelector,
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -19,6 +20,7 @@
|
||||
initialTheme: window.darkMode.initial,
|
||||
systemTheme: 'system',
|
||||
development: window.location.href.indexOf("dev=1") !== -1,
|
||||
showLanguageSelector: false,
|
||||
}
|
||||
},
|
||||
|
||||
@ -56,35 +58,65 @@
|
||||
this.language = e.language
|
||||
this.languageAuto = e.languageAuto
|
||||
},
|
||||
|
||||
openLanguageSelector() {
|
||||
this.showLanguageSelector = true
|
||||
},
|
||||
|
||||
closeLanguageSelector() {
|
||||
this.showLanguageSelector = false
|
||||
this.$refs.editor.focus()
|
||||
},
|
||||
|
||||
onLanguageSelect(language) {
|
||||
this.showLanguageSelector = false
|
||||
this.$refs.editor.setLanguage(language)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Editor
|
||||
@cursorChange="onCursorChange"
|
||||
:theme="theme"
|
||||
:development="development"
|
||||
class="editor"
|
||||
/>
|
||||
<StatusBar
|
||||
:line="line"
|
||||
:column="column"
|
||||
:language="language"
|
||||
:languageAuto="languageAuto"
|
||||
:theme="theme"
|
||||
:systemTheme="systemTheme"
|
||||
@toggleTheme="toggleTheme"
|
||||
class="status"
|
||||
/>
|
||||
<div class="container">
|
||||
<Editor
|
||||
@cursorChange="onCursorChange"
|
||||
:theme="theme"
|
||||
:development="development"
|
||||
class="editor"
|
||||
ref="editor"
|
||||
@openLanguageSelector="openLanguageSelector"
|
||||
/>
|
||||
<StatusBar
|
||||
:line="line"
|
||||
:column="column"
|
||||
:language="language"
|
||||
:languageAuto="languageAuto"
|
||||
:theme="theme"
|
||||
:systemTheme="systemTheme"
|
||||
@toggleTheme="toggleTheme"
|
||||
@openLanguageSelector="openLanguageSelector"
|
||||
class="status"
|
||||
/>
|
||||
<div class="overlay">
|
||||
<LanguageSelector
|
||||
v-if="showLanguageSelector"
|
||||
@select="onLanguageSelect"
|
||||
@close="closeLanguageSelector"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.editor
|
||||
height: calc(100% - 21px)
|
||||
.status
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: 0
|
||||
.container
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: relative
|
||||
.editor
|
||||
height: calc(100% - 21px)
|
||||
.status
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: 0
|
||||
</style>
|
||||
|
@ -10,6 +10,7 @@
|
||||
Welcome to Heynote!
|
||||
|
||||
[${modChar} + Enter] Insert new note block
|
||||
[${modChar} + L] Change block language
|
||||
[${modChar} + Down] Goto next block
|
||||
[${modChar} + Up] Goto previous block
|
||||
[${modChar} + A] Select all text in a note block. Press again to select the whole scratchpad
|
||||
@ -32,6 +33,10 @@ Welcome to Heynote!
|
||||
})
|
||||
})
|
||||
|
||||
this.$refs.editor.addEventListener("openLanguageSelector", (e) => {
|
||||
this.$emit("openLanguageSelector")
|
||||
})
|
||||
|
||||
this.editor = new HeynoteEditor({
|
||||
element: this.$refs.editor,
|
||||
content: this.development ? testContent : initialContent,
|
||||
@ -44,6 +49,21 @@ Welcome to Heynote!
|
||||
this.editor.setTheme(newTheme)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLanguage(language) {
|
||||
if (language === "auto") {
|
||||
this.editor.setCurrentLanguage("text", true)
|
||||
} else {
|
||||
this.editor.setCurrentLanguage(language, false)
|
||||
}
|
||||
this.editor.focus()
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.editor.focus()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
142
src/components/LanguageSelector.vue
Normal file
142
src/components/LanguageSelector.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<script>
|
||||
import { LANGUAGES } from '../editor/languages.js'
|
||||
|
||||
const items = LANGUAGES.map(l => {
|
||||
return {
|
||||
"token": l.token,
|
||||
"name": l.name
|
||||
}
|
||||
}).sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
items.unshift({token: "auto", name:"Auto-detect"})
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selected: 0,
|
||||
filter: "",
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$refs.container.focus()
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredItems() {
|
||||
return items.filter((lang) => {
|
||||
return lang.name.toLowerCase().indexOf(this.filter.toLowerCase()) !== -1
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeydown(event) {
|
||||
if (event.key === "ArrowDown") {
|
||||
this.selected = Math.min(this.selected + 1, this.filteredItems.length - 1)
|
||||
event.preventDefault()
|
||||
} else if (event.key === "ArrowUp") {
|
||||
this.selected = Math.max(this.selected - 1, 0)
|
||||
event.preventDefault()
|
||||
} else if (event.key === "Enter") {
|
||||
this.$emit("select", this.filteredItems[this.selected].token)
|
||||
event.preventDefault()
|
||||
} else if (event.key === "Escape") {
|
||||
this.$emit("close")
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
|
||||
onInput(event) {
|
||||
// reset selection
|
||||
this.selected = 0
|
||||
},
|
||||
|
||||
onFocusOut(event) {
|
||||
let container = this.$refs.container
|
||||
if (container !== event.relatedTarget && !container.contains(event.relatedTarget)) {
|
||||
this.$emit("close")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="language-selector" tabindex="-1" @focusout="onFocusOut" ref="container">
|
||||
<input
|
||||
type="text"
|
||||
ref="input"
|
||||
@keydown="onKeydown"
|
||||
@input="onInput"
|
||||
v-model="filter"
|
||||
/>
|
||||
<ul class="items">
|
||||
<li
|
||||
v-for="item, idx in filteredItems"
|
||||
:key="item.token"
|
||||
:class="idx === selected ? 'selected' : ''"
|
||||
@click="$emit('select', item.token)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
=dark-mode()
|
||||
@media (prefers-color-scheme: dark)
|
||||
@content
|
||||
|
||||
.language-selector
|
||||
font-size: 13px
|
||||
padding: 10px
|
||||
//background: #48b57e
|
||||
background: #efefef
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
border-radius: 0 0 5px 5px
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||
+dark-mode
|
||||
background: #151516
|
||||
input
|
||||
background: #fff
|
||||
padding: 4px 5px
|
||||
border: 1px solid #ccc
|
||||
box-sizing: border-box
|
||||
border-radius: 2px
|
||||
width: 400px
|
||||
margin-bottom: 10px
|
||||
&:focus
|
||||
outline: none
|
||||
border: 1px solid #fff
|
||||
outline: 2px solid #48b57e
|
||||
+dark-mode
|
||||
background: #3b3b3b
|
||||
border: 1px solid #5a5a5a
|
||||
&:focus
|
||||
border: 1px solid #3b3b3b
|
||||
|
||||
.items
|
||||
> li
|
||||
border-radius: 3px
|
||||
padding: 5px 12px
|
||||
cursor: pointer
|
||||
&:hover
|
||||
background: #e2e2e2
|
||||
&.selected
|
||||
background: #48b57e
|
||||
color: #fff
|
||||
+dark-mode
|
||||
color: rgba(255,255,255, 0.53)
|
||||
&:hover
|
||||
background: #29292a
|
||||
&.selected
|
||||
background: #1b6540
|
||||
color: rgba(255,255,255, 0.87)
|
||||
</style>
|
@ -35,7 +35,10 @@
|
||||
Col <span class="num">{{ column }}</span>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="status-block lang clickable">
|
||||
<div
|
||||
@click="$emit('openLanguageSelector')"
|
||||
class="status-block lang clickable"
|
||||
>
|
||||
{{ languageName }}
|
||||
<span v-if="languageAuto" class="auto">(auto)</span>
|
||||
</div>
|
||||
@ -61,6 +64,7 @@
|
||||
padding-right: 0px
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
user-select: none
|
||||
|
||||
+dark-mode
|
||||
|
@ -1,2 +1,3 @@
|
||||
@import "reset"
|
||||
@import "font"
|
||||
@import "base"
|
||||
|
@ -3,7 +3,7 @@ html, body
|
||||
padding: 0
|
||||
background: #fff
|
||||
color: #444
|
||||
font-family: 'Gill Sans','Gill Sans MT',Calibri,'Trebuchet MS',sans-serif
|
||||
font-family: "Open Sans"
|
||||
height: 100%
|
||||
font-size: 12px
|
||||
overscroll-behavior-y: none
|
||||
|
31
src/css/reset.sass
Normal file
31
src/css/reset.sass
Normal file
@ -0,0 +1,31 @@
|
||||
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video
|
||||
margin: 0
|
||||
padding: 0
|
||||
border: 0
|
||||
font-size: 100%
|
||||
font: inherit
|
||||
vertical-align: baseline
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section
|
||||
display: block
|
||||
|
||||
body
|
||||
line-height: 1
|
||||
|
||||
ol, ul
|
||||
list-style: none
|
||||
|
||||
blockquote, q
|
||||
quotes: none
|
||||
|
||||
blockquote:before, blockquote:after, q:before, q:after
|
||||
content: ''
|
||||
content: none
|
||||
|
||||
table
|
||||
border-collapse: collapse
|
||||
border-spacing: 0
|
||||
|
||||
input
|
||||
font-size: 1em
|
@ -276,7 +276,7 @@ const blockLineNumbers = lineNumbers({
|
||||
}
|
||||
})
|
||||
|
||||
const emitCursorChange = (element) => ViewPlugin.fromClass(
|
||||
const emitCursorChange = (editor) => ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update) {
|
||||
// if the selection changed or the language changed (can happen without selection change),
|
||||
@ -285,7 +285,7 @@ const emitCursorChange = (element) => ViewPlugin.fromClass(
|
||||
if (update.selectionSet || langChange) {
|
||||
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
|
||||
const block = getActiveNoteBlock(update.state)
|
||||
element.dispatchEvent(new SelectionChangeEvent({
|
||||
editor.element.dispatchEvent(new SelectionChangeEvent({
|
||||
cursorLine,
|
||||
language: block?.language.name,
|
||||
languageAuto: block?.language.auto,
|
||||
@ -295,7 +295,7 @@ const emitCursorChange = (element) => ViewPlugin.fromClass(
|
||||
}
|
||||
)
|
||||
|
||||
export const noteBlockExtension = (element) => {
|
||||
export const noteBlockExtension = (editor) => {
|
||||
return [
|
||||
blockState,
|
||||
noteBlockWidget(),
|
||||
@ -304,6 +304,6 @@ export const noteBlockExtension = (element) => {
|
||||
preventFirstBlockFromBeingDeleted,
|
||||
preventSelectionBeforeFirstBlock,
|
||||
blockLineNumbers,
|
||||
emitCursorChange(element),
|
||||
emitCursorChange(editor),
|
||||
]
|
||||
}
|
||||
|
@ -76,6 +76,11 @@ export function changeLanguageTo(state, dispatch, block, language, auto) {
|
||||
}
|
||||
}
|
||||
|
||||
export function changeCurrentBlockLanguage(state, dispatch, language, auto) {
|
||||
const block = getActiveNoteBlock(state)
|
||||
changeLanguageTo(state, dispatch, block, language, auto)
|
||||
}
|
||||
|
||||
export function gotoPreviousBlock({state, dispatch}) {
|
||||
const blocks = state.facet(blockState)
|
||||
const newSelection = EditorSelection.create(state.selection.ranges.map(sel => {
|
||||
|
@ -8,18 +8,20 @@ import { heynoteBase } from "./theme/base.js"
|
||||
import { customSetup } from "./setup.js"
|
||||
import { heynoteLang } from "./lang-heynote/heynote.js"
|
||||
import { noteBlockExtension } from "./block/block.js"
|
||||
import { changeCurrentBlockLanguage } from "./block/commands.js"
|
||||
import { heynoteKeymap } from "./keymap.js"
|
||||
import { languageDetection } from "./language-detection/autodetect.js"
|
||||
|
||||
|
||||
export class HeynoteEditor {
|
||||
constructor({element, content, focus=true, theme="light"}) {
|
||||
this.element = element
|
||||
this.theme = new Compartment
|
||||
|
||||
this.state = EditorState.create({
|
||||
const state = EditorState.create({
|
||||
doc: content || "",
|
||||
extensions: [
|
||||
heynoteKeymap,
|
||||
heynoteKeymap(this),
|
||||
|
||||
//minimalSetup,
|
||||
customSetup,
|
||||
@ -31,7 +33,7 @@ export class HeynoteEditor {
|
||||
return {top: 80, bottom: 80}
|
||||
}),
|
||||
heynoteLang(),
|
||||
noteBlockExtension(element),
|
||||
noteBlockExtension(this),
|
||||
languageDetection(() => this.view),
|
||||
|
||||
// set cursor blink rate to 1 second
|
||||
@ -45,7 +47,7 @@ export class HeynoteEditor {
|
||||
})
|
||||
|
||||
this.view = new EditorView({
|
||||
state: this.state,
|
||||
state: state,
|
||||
parent: element,
|
||||
})
|
||||
|
||||
@ -58,11 +60,23 @@ export class HeynoteEditor {
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.view.focus()
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
this.view.dispatch({
|
||||
effects: this.theme.reconfigure(theme === "dark" ? heynoteDark : heynoteLight),
|
||||
})
|
||||
}
|
||||
|
||||
openLanguageSelector() {
|
||||
this.element.dispatchEvent(new Event("open-language-selector"))
|
||||
}
|
||||
|
||||
setCurrentLanguage(lang, auto=false) {
|
||||
changeCurrentBlockLanguage(this.view.state, this.view.dispatch, lang, auto)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,41 +2,48 @@ import { keymap } from "@codemirror/view"
|
||||
import { indentWithTab, insertTab, indentLess, indentMore } from "@codemirror/commands"
|
||||
import { insertNewNote, moveLineUp, selectAll, gotoPreviousBlock, gotoNextBlock } from "./block/commands.js";
|
||||
|
||||
export const heynoteKeymap = keymap.of([
|
||||
{
|
||||
key: "Tab",
|
||||
preventDefault: true,
|
||||
//run: insertTab,
|
||||
run: indentMore,
|
||||
},
|
||||
{
|
||||
key: 'Shift-Tab',
|
||||
preventDefault: true,
|
||||
run: indentLess,
|
||||
},
|
||||
{
|
||||
key: "Mod-Enter",
|
||||
preventDefault: true,
|
||||
run: insertNewNote,
|
||||
},
|
||||
{
|
||||
key: "Mod-a",
|
||||
preventDefault: true,
|
||||
run: selectAll,
|
||||
},
|
||||
{
|
||||
key: "Alt-ArrowUp",
|
||||
preventDefault: true,
|
||||
run: moveLineUp,
|
||||
},
|
||||
{
|
||||
key: "Mod-ArrowUp",
|
||||
preventDefault: true,
|
||||
run: gotoPreviousBlock,
|
||||
},
|
||||
{
|
||||
key: "Mod-ArrowDown",
|
||||
preventDefault: true,
|
||||
run: gotoNextBlock,
|
||||
},
|
||||
])
|
||||
export function heynoteKeymap(editor) {
|
||||
return keymap.of([
|
||||
{
|
||||
key: "Tab",
|
||||
preventDefault: true,
|
||||
//run: insertTab,
|
||||
run: indentMore,
|
||||
},
|
||||
{
|
||||
key: 'Shift-Tab',
|
||||
preventDefault: true,
|
||||
run: indentLess,
|
||||
},
|
||||
{
|
||||
key: "Mod-Enter",
|
||||
preventDefault: true,
|
||||
run: insertNewNote,
|
||||
},
|
||||
{
|
||||
key: "Mod-a",
|
||||
preventDefault: true,
|
||||
run: selectAll,
|
||||
},
|
||||
{
|
||||
key: "Alt-ArrowUp",
|
||||
preventDefault: true,
|
||||
run: moveLineUp,
|
||||
},
|
||||
{
|
||||
key: "Mod-ArrowUp",
|
||||
preventDefault: true,
|
||||
run: gotoPreviousBlock,
|
||||
},
|
||||
{
|
||||
key: "Mod-ArrowDown",
|
||||
preventDefault: true,
|
||||
run: gotoNextBlock,
|
||||
},
|
||||
{
|
||||
key: "Mod-l",
|
||||
preventDefault: true,
|
||||
run: () => editor.openLanguageSelector(),
|
||||
}
|
||||
])
|
||||
}
|
@ -11,7 +11,7 @@ const HIGHLIGHTJS_TO_TOKEN = Object.fromEntries(LANGUAGES.map(l => [l.highlightj
|
||||
|
||||
|
||||
export function languageDetection(getView) {
|
||||
const previousBlockContent = []
|
||||
const previousBlockContent = {}
|
||||
let idleCallbackId = null
|
||||
|
||||
const detectionWorker = new Worker('langdetect-worker.js?worker');
|
||||
@ -50,11 +50,6 @@ export function languageDetection(getView) {
|
||||
cancelIdleCallback(idleCallbackId)
|
||||
idleCallbackId = null
|
||||
}
|
||||
if (update.transactions.every(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE))) {
|
||||
// don't run language detection if the change was triggered by a language change
|
||||
//console.log("ignoring check after language change")
|
||||
return
|
||||
}
|
||||
|
||||
idleCallbackId = requestIdleCallback(() => {
|
||||
idleCallbackId = null
|
||||
@ -69,7 +64,12 @@ export function languageDetection(getView) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (block === null || block.language.auto === false) {
|
||||
if (block === null) {
|
||||
return
|
||||
} else if (block.language.auto === false) {
|
||||
// if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection
|
||||
// immediately if the user changes the language to auto
|
||||
delete previousBlockContent[idx]
|
||||
return
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user