(ts) migrate nouns components to composition API

This commit is contained in:
Valentyne Stigloher 2024-11-11 14:15:39 +01:00
parent 3bfbb19bae
commit 455c59498d
5 changed files with 198 additions and 236 deletions

View File

@ -1,50 +1,31 @@
<template> <script setup lang="ts">
<span class="position-relative"> import cases from '~/data/nouns/cases.js';
<template v-if="declensionTemplate"> import type { NounDeclension } from '~/src/classes.ts';
<a v-if="!open" href="#" :class="tooltip && visible ? 'fw-bold' : ''" @click.prevent="visible = !visible"><Spelling :text="word" /></a> import { nounDeclensionTemplates } from '~/src/data.ts';
<ul v-if="visible" :class="['list-unstyled', 'small', open ? '' : 'm-2 p-3 pe-5 border bg-light', tooltip ? 'tooltip' : '']">
<li v-for="(declined, c) in declensionTemplate.decline(word, plural)" class="text-nowrap">
<strong>{{ c }} <small v-if="!condense">({{ cases[c] }})</small></strong> {{ declined.join(' / ') }}
</li>
<li v-if="tooltip" class="close"><a href="#" @click.prevent="visible = false"><Icon v="times" /></a></li>
</ul>
</template>
<Spelling v-else :text="word" />
</span>
</template>
<script> const props = withDefaults(defineProps<{
import cases from '../data/nouns/cases.js'; word: string;
import { nounDeclensionTemplates } from '../src/data.ts'; plural?: boolean;
singularOptions?: string[];
template?: NounDeclension;
open?: boolean;
condense?: boolean;
tooltip?: boolean;
}>(), {
plural: false,
});
export default { const visible = ref(props.open);
props: {
word: { required: true }, const declensionTemplate = computed(() => {
plural: { type: Boolean }, return props.template ?? findTemplate();
singularOptions: { }, });
template: { },
open: { type: Boolean }, const findTemplate = (): NounDeclension | null => {
condense: { type: Boolean },
tooltip: { type: Boolean },
},
data() {
return {
declensionTemplate: this.template || this.findTemplate(),
cases,
visible: this.open,
};
},
watch: {
template() {
this.declensionTemplate = this.template || this.findTemplate();
},
},
methods: {
findTemplate() {
let longestMatch = 0; let longestMatch = 0;
let templates = []; let templates: NounDeclension[] = [];
for (const t of nounDeclensionTemplates) { for (const t of nounDeclensionTemplates) {
const matchLength = t.matches(this.word, this.plural); const matchLength = t.matches(props.word, props.plural);
if (matchLength === 0) { if (matchLength === 0) {
continue; continue;
} }
@ -60,9 +41,9 @@ export default {
return null; return null;
} else if (templates.length === 1) { } else if (templates.length === 1) {
return templates[0]; return templates[0];
} else if (this.plural && this.singularOptions) { } else if (props.plural && props.singularOptions) {
for (const t of templates) { for (const t of templates) {
for (const s of this.singularOptions) { for (const s of props.singularOptions) {
if (t.matches(s)) { if (t.matches(s)) {
return t; return t;
} }
@ -71,11 +52,32 @@ export default {
} }
return templates[0]; return templates[0];
},
},
}; };
</script> </script>
<template>
<span class="position-relative">
<template v-if="declensionTemplate">
<a v-if="!open" href="#" :class="tooltip && visible ? 'fw-bold' : ''" @click.prevent="visible = !visible"><Spelling :text="word" /></a>
<ul v-if="visible" :class="['list-unstyled', 'small', open ? '' : 'm-2 p-3 pe-5 border bg-light', tooltip ? 'tooltip' : '']">
<li
v-for="(declined, caseName) in declensionTemplate.decline(word, plural)"
:key="caseName"
class="text-nowrap"
>
<strong>
{{ caseName }}
<small v-if="!condense">({{ (cases as Record<string, string>)[caseName] }})</small>
</strong>
{{ declined.join(' / ') }}
</li>
<li v-if="tooltip" class="close"><a href="#" @click.prevent="visible = false"><Icon v="times" /></a></li>
</ul>
</template>
<Spelling v-else :text="word" />
</span>
</template>
<style lang="scss" scoped> <style lang="scss" scoped>
ul.tooltip { ul.tooltip {
position: absolute; position: absolute;

View File

@ -1,7 +1,18 @@
<script setup lang="ts">
import type { genders, MinimalNoun } from '~/src/classes.ts';
defineProps<{
noun: MinimalNoun;
gender: typeof genders[number];
}>();
const config = useConfig();
</script>
<template> <template>
<div> <div>
<ul class="list-singular"> <ul class="list-singular">
<li v-for="w in noun[gender]"> <li v-for="(w, i) in noun[gender]" :key="i">
<Abbreviation v-slot="{ word }" :v="w"> <Abbreviation v-slot="{ word }" :v="w">
<Declension <Declension
v-if="gender === 'neutr' && config.nouns.declension" v-if="gender === 'neutr' && config.nouns.declension"
@ -13,7 +24,7 @@
</li> </li>
</ul> </ul>
<ul v-if="config.nouns.plurals" class="list-plural"> <ul v-if="config.nouns.plurals" class="list-plural">
<li v-for="w in noun[`${gender}Pl`]"> <li v-for="(w, i) in noun[`${gender}Pl`]" :key="i">
<Abbreviation v-slot="{ word }" :v="w"> <Abbreviation v-slot="{ word }" :v="w">
<Declension <Declension
v-if="gender === 'neutr' && config.nouns.declension" v-if="gender === 'neutr' && config.nouns.declension"
@ -28,23 +39,3 @@
</ul> </ul>
</div> </div>
</template> </template>
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import useConfig from '../composables/useConfig.ts';
import type { genders, MinimalNoun } from '../src/classes.ts';
export default defineComponent({
props: {
noun: { required: true, type: Object as PropType<MinimalNoun> },
gender: { required: true, type: String as PropType<typeof genders[number]> },
},
setup() {
return {
config: useConfig(),
};
},
});
</script>

View File

@ -1,37 +1,31 @@
<script setup lang="ts">
import type { genders } from '~/src/classes.ts';
const props = defineProps<{
gender: typeof genders[number];
}>();
const iconName = computed((): string => {
const iconNames = {
masc: 'mars',
fem: 'venus',
neutr: 'neuter',
};
return iconNames[props.gender];
});
const longIdentifier = computed((): string => {
const longIdentifiers = {
masc: 'masculine',
fem: 'feminine',
neutr: 'neuter',
};
return longIdentifiers[props.gender];
});
</script>
<template> <template>
<div class="text-nowrap"> <div class="text-nowrap">
<Icon :v="iconName" /> <Icon :v="iconName" />
<span><T>nouns.{{ longIdentifier }}</T></span> <span><T>nouns.{{ longIdentifier }}</T></span>
</div> </div>
</template> </template>
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import type { genders } from '../src/classes.ts';
export default defineComponent({
props: {
gender: { required: true, type: String as PropType<typeof genders[number]> },
},
computed: {
iconName(): string {
const iconNames = {
masc: 'mars',
fem: 'venus',
neutr: 'neuter',
};
return iconNames[this.gender];
},
longIdentifier(): string {
const longIdentifiers = {
masc: 'masculine',
fem: 'feminine',
neutr: 'neuter',
};
return longIdentifiers[this.gender];
},
},
});
</script>

View File

@ -1,3 +1,91 @@
<script setup lang="ts">
import type { Config } from '~/locale/config.ts';
import { genders } from '~/src/classes.ts';
import type { Noun, MinimalNoun } from '~/src/classes.ts';
import { abbreviations } from '~/src/data.ts';
const emptyForm = (config: Config): MinimalNoun => {
return {
masc: [''],
fem: [''],
neutr: [''],
mascPl: config.nouns.pluralsRequired ? [''] : [],
femPl: config.nouns.pluralsRequired ? [''] : [],
neutrPl: config.nouns.pluralsRequired ? [''] : [],
categories: [],
sources: [],
base: null,
};
};
const emit = defineEmits<{
submit: [];
}>();
const { $translator: translator } = useNuxtApp();
const config = useConfig();
const section = useTemplateRef<HTMLElement>('section');
const templateFilterInput = useTemplateRef<HTMLInputElement>('templateFilterInput');
const form = ref(emptyForm(config));
const submitting = ref(false);
const afterSubmit = ref(false);
const templateBase = ref('');
const templateFilter = ref('');
const templateVisible = ref(false);
const dialogue = useDialogue();
const applyTemplate = async (template: MinimalNoun): Promise<void> => {
if (JSON.stringify(form.value) !== JSON.stringify(emptyForm(config))) {
await dialogue.confirm(translator.translate('nouns.template.overwrite'));
}
form.value = template;
templateVisible.value = false;
await nextTick();
section.value?.scrollIntoView();
};
const submit = async () => {
submitting.value = true;
try {
await dialogue.postWithAlertOnError('/api/nouns/submit', form.value);
afterSubmit.value = true;
form.value = emptyForm(config);
templateVisible.value = false;
templateBase.value = '';
focus(false);
emit('submit');
} finally {
submitting.value = false;
}
};
const edit = (word: Noun): void => {
form.value = {
masc: word.masc,
fem: word.fem,
neutr: word.neutr,
mascPl: word.mascPl,
femPl: word.femPl,
neutrPl: word.neutrPl,
categories: word.categories,
sources: word.sources,
base: word.id,
};
focus();
};
const focus = (editable = true): void => {
if (editable) {
afterSubmit.value = false;
}
section.value?.scrollIntoView();
};
defineExpose({ edit, focus });
</script>
<template> <template>
<section v-if="config.nouns.enabled && $user()" ref="section" class="scroll-mt-7"> <section v-if="config.nouns.enabled && $user()" ref="section" class="scroll-mt-7">
<div v-if="afterSubmit" class="alert alert-success text-center"> <div v-if="afterSubmit" class="alert alert-success text-center">
@ -81,7 +169,7 @@
<Icon v="filter" /> <Icon v="filter" />
</span> </span>
<input <input
ref="templateFilter" ref="templateFilterInput"
v-model="templateFilter" v-model="templateFilter"
class="form-control form-control-sm border-primary" class="form-control form-control-sm border-primary"
:placeholder="$t('crud.filterLong')" :placeholder="$t('crud.filterLong')"
@ -89,7 +177,7 @@
<button <button
v-if="templateFilter" v-if="templateFilter"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
@click="templateFilter = ''; $tRefs.templateFilter?.focus()" @click="templateFilter = ''; templateFilterInput?.focus()"
> >
<Icon v="times" /> <Icon v="times" />
</button> </button>
@ -139,109 +227,3 @@
</div> </div>
</section> </section>
</template> </template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import useConfig from '../composables/useConfig.ts';
import useDialogue from '../composables/useDialogue.ts';
import type { Config } from '../locale/config.ts';
import { genders } from '../src/classes.ts';
import type { Noun, MinimalNoun } from '../src/classes.ts';
import { abbreviations } from '../src/data.ts';
interface Refs {
templateFilter: HTMLInputElement | undefined;
}
const emptyForm = (config: Config): MinimalNoun => {
return {
masc: [''],
fem: [''],
neutr: [''],
mascPl: config.nouns.pluralsRequired ? [''] : [],
femPl: config.nouns.pluralsRequired ? [''] : [],
neutrPl: config.nouns.pluralsRequired ? [''] : [],
categories: [],
sources: [],
base: null,
};
};
export default defineComponent({
emits: ['submit'],
setup() {
const config = useConfig();
const section = useTemplateRef<HTMLElement>('section');
return {
config,
dialogue: useDialogue(),
section,
form: ref(emptyForm(config)),
};
},
data() {
return {
submitting: false,
afterSubmit: false,
templateBase: '',
templateFilter: '',
templateVisible: false,
abbreviations,
genders,
};
},
computed: {
$tRefs(): Refs {
return this.$refs as unknown as Refs;
},
},
methods: {
async applyTemplate(template: MinimalNoun): Promise<void> {
if (JSON.stringify(this.form) !== JSON.stringify(emptyForm(this.config))) {
await this.dialogue.confirm(this.$t('nouns.template.overwrite'));
}
this.form = template;
this.templateVisible = false;
this.$nextTick(() => {
this.$el.scrollIntoView();
});
},
async submit() {
this.submitting = true;
try {
await this.dialogue.postWithAlertOnError('/api/nouns/submit', this.form);
this.afterSubmit = true;
this.form = emptyForm(this.config);
this.templateVisible = false;
this.templateBase = '';
this.focus(false);
this.$emit('submit');
} finally {
this.submitting = false;
}
},
edit(word: Noun): void {
this.form = {
masc: word.masc,
fem: word.fem,
neutr: word.neutr,
mascPl: word.mascPl,
femPl: word.femPl,
neutrPl: word.neutrPl,
categories: word.categories,
sources: word.sources,
base: word.id,
};
this.focus();
},
focus(editable = true): void {
if (editable) {
this.afterSubmit = false;
}
this.section?.scrollIntoView();
},
},
});
</script>

View File

@ -1084,7 +1084,7 @@ export class NounDeclension {
} }
} }
matches(word: string, plural: boolean): number { matches(word: string, plural?: boolean): number {
const plurality = plural ? 'plural' : 'singular'; const plurality = plural ? 'plural' : 'singular';
const rep = Object.keys(this[plurality])[0]; const rep = Object.keys(this[plurality])[0];
for (const ending of this[plurality][rep] || []) { for (const ending of this[plurality][rep] || []) {
@ -1109,15 +1109,8 @@ export class NounDeclension {
const options = this[plurality]; const options = this[plurality];
return buildDict(function*() { return buildDict(function*() {
for (const k in options) { for (const [caseName, caseSuffixes] of Object.entries(options)) {
if (!options.hasOwnProperty(k)) { yield [caseName, caseSuffixes?.map((caseSuffix) => base + caseSuffix) ?? []];
continue;
}
yield [
k,
// TODO: Check whether it is sensible to include a guard clause
options[k]!.map((o) => base + o),
];
} }
}); });
} }