(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>
<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, 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 setup lang="ts">
import cases from '~/data/nouns/cases.js';
import type { NounDeclension } from '~/src/classes.ts';
import { nounDeclensionTemplates } from '~/src/data.ts';
<script>
import cases from '../data/nouns/cases.js';
import { nounDeclensionTemplates } from '../src/data.ts';
const props = withDefaults(defineProps<{
word: string;
plural?: boolean;
singularOptions?: string[];
template?: NounDeclension;
open?: boolean;
condense?: boolean;
tooltip?: boolean;
}>(), {
plural: false,
});
export default {
props: {
word: { required: true },
plural: { type: Boolean },
singularOptions: { },
template: { },
open: { type: Boolean },
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() {
const visible = ref(props.open);
const declensionTemplate = computed(() => {
return props.template ?? findTemplate();
});
const findTemplate = (): NounDeclension | null => {
let longestMatch = 0;
let templates = [];
let templates: NounDeclension[] = [];
for (const t of nounDeclensionTemplates) {
const matchLength = t.matches(this.word, this.plural);
const matchLength = t.matches(props.word, props.plural);
if (matchLength === 0) {
continue;
}
@ -60,9 +41,9 @@ export default {
return null;
} else if (templates.length === 1) {
return templates[0];
} else if (this.plural && this.singularOptions) {
} else if (props.plural && props.singularOptions) {
for (const t of templates) {
for (const s of this.singularOptions) {
for (const s of props.singularOptions) {
if (t.matches(s)) {
return t;
}
@ -71,11 +52,32 @@ export default {
}
return templates[0];
},
},
};
</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>
ul.tooltip {
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>
<div>
<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">
<Declension
v-if="gender === 'neutr' && config.nouns.declension"
@ -13,7 +24,7 @@
</li>
</ul>
<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">
<Declension
v-if="gender === 'neutr' && config.nouns.declension"
@ -28,23 +39,3 @@
</ul>
</div>
</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>
<div class="text-nowrap">
<Icon :v="iconName" />
<span><T>nouns.{{ longIdentifier }}</T></span>
</div>
</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>
<section v-if="config.nouns.enabled && $user()" ref="section" class="scroll-mt-7">
<div v-if="afterSubmit" class="alert alert-success text-center">
@ -81,7 +169,7 @@
<Icon v="filter" />
</span>
<input
ref="templateFilter"
ref="templateFilterInput"
v-model="templateFilter"
class="form-control form-control-sm border-primary"
:placeholder="$t('crud.filterLong')"
@ -89,7 +177,7 @@
<button
v-if="templateFilter"
class="btn btn-sm btn-outline-danger"
@click="templateFilter = ''; $tRefs.templateFilter?.focus()"
@click="templateFilter = ''; templateFilterInput?.focus()"
>
<Icon v="times" />
</button>
@ -139,109 +227,3 @@
</div>
</section>
</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 rep = Object.keys(this[plurality])[0];
for (const ending of this[plurality][rep] || []) {
@ -1109,15 +1109,8 @@ export class NounDeclension {
const options = this[plurality];
return buildDict(function*() {
for (const k in options) {
if (!options.hasOwnProperty(k)) {
continue;
}
yield [
k,
// TODO: Check whether it is sensible to include a guard clause
options[k]!.map((o) => base + o),
];
for (const [caseName, caseSuffixes] of Object.entries(options)) {
yield [caseName, caseSuffixes?.map((caseSuffix) => base + caseSuffix) ?? []];
}
});
}