implement font-family module
This commit is contained in:
parent
6f5efb6fbc
commit
cb9fd93e51
137 changed files with 1177 additions and 21 deletions
|
|
@ -23,7 +23,8 @@
|
|||
<label class="label-with-tooltip" data-css="font-family">Police</label>
|
||||
<div class="field-font__options">
|
||||
<select v-model="fontFamily">
|
||||
<option v-for="f in fonts" :key="f" :value="f">{{ f }}</option>
|
||||
<option value="sans-serif">Police système (sans-serif)</option>
|
||||
<option v-for="f in projectFonts" :key="f.name" :value="f.name" :style="{ fontFamily: `'${f.name}', ${f.category}` }">{{ f.name }}</option>
|
||||
</select>
|
||||
<div class="field-checkbox">
|
||||
<input type="checkbox" v-model="italic" />
|
||||
|
|
@ -264,11 +265,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import { ref, reactive, computed, watch, nextTick, onMounted } from 'vue';
|
||||
import { useStylesheetStore } from '../stores/stylesheet';
|
||||
import { useDebounce } from '../composables/useDebounce';
|
||||
import { useCssSync } from '../composables/useCssSync';
|
||||
import { useTextDefaults } from '../composables/useTextDefaults';
|
||||
import { useProjectFonts } from '../composables/useProjectFonts';
|
||||
import NumberInput from './ui/NumberInput.vue';
|
||||
import InputWithUnit from './ui/InputWithUnit.vue';
|
||||
import BasePopup from './ui/BasePopup.vue';
|
||||
|
|
@ -277,6 +279,7 @@ import { convertUnit } from '../utils/unit-conversion';
|
|||
const stylesheetStore = useStylesheetStore();
|
||||
const { extractValue: cssExtractValue, extractNumericValue } = useCssSync();
|
||||
const textDefaults = useTextDefaults();
|
||||
const { fonts: projectFonts, loadFont, loadAllFontPreviews } = useProjectFonts();
|
||||
|
||||
const props = defineProps({
|
||||
iframeRef: Object,
|
||||
|
|
@ -299,7 +302,7 @@ let isUpdatingFromStore = false;
|
|||
const { debouncedUpdate } = useDebounce(500);
|
||||
|
||||
// Style properties — flat refs for simple values, reactive for value+unit
|
||||
const fontFamily = ref('Alegreya Sans');
|
||||
const fontFamily = ref('sans-serif');
|
||||
const italic = ref(false);
|
||||
const bold = ref(false);
|
||||
const textAlign = ref('left');
|
||||
|
|
@ -357,12 +360,9 @@ const settingEnabled = reactive({
|
|||
padding: false,
|
||||
});
|
||||
|
||||
// Constants
|
||||
const fonts = ['Alegreya Sans', 'Alegreya', 'Arial', 'Georgia', 'Times New Roman'];
|
||||
|
||||
// Style property descriptors (with group field)
|
||||
const styleProps = [
|
||||
{ css: 'font-family', group: 'font', get: () => fontFamily.value, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false },
|
||||
{ css: 'font-family', group: 'font', get: () => fontFamily.value === 'sans-serif' ? 'sans-serif' : `"${fontFamily.value}"`, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false },
|
||||
{ css: 'font-style', group: 'font', get: () => italic.value ? 'italic' : 'normal', set: v => italic.value = v === 'italic', debounce: false, skipWhenDefault: v => v !== 'italic' },
|
||||
{ css: 'font-weight', group: 'font', get: () => bold.value ? 'bold' : 'normal', set: v => bold.value = v === 'bold' || parseInt(v) >= 700, debounce: false, skipWhenDefault: v => v === 'normal' },
|
||||
{ css: 'text-align', group: 'textAlign', get: () => textAlign.value, set: v => textAlign.value = v, debounce: false },
|
||||
|
|
@ -555,7 +555,7 @@ const displayedCss = computed(() => {
|
|||
{ group: 'fontSize', css: 'font-size', getValue: () => `${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).value}${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).unit}` },
|
||||
{ group: 'lineHeight', css: 'line-height', getValue: () => `${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).value}${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).unit}` },
|
||||
{ group: 'color', css: 'color', getValue: () => settingEnabled.color ? color.value : textDefaults.color },
|
||||
{ group: 'font', css: 'font-family', getValue: () => `"${settingEnabled.font ? fontFamily.value : textDefaults.fontFamily}"` },
|
||||
{ group: 'font', css: 'font-family', getValue: () => { const val = settingEnabled.font ? fontFamily.value : textDefaults.fontFamily; return val === 'sans-serif' ? 'sans-serif' : `"${val}"`; } },
|
||||
];
|
||||
|
||||
// For disabled special groups: annotate existing lines with comment
|
||||
|
|
@ -662,7 +662,8 @@ const applyDefaultsForGroup = (group) => {
|
|||
} else if (group === 'color') {
|
||||
updateProp('color', textDefaults.color);
|
||||
} else if (group === 'font') {
|
||||
updateProp('font-family', `"${textDefaults.fontFamily}"`);
|
||||
const fontVal = textDefaults.fontFamily === 'sans-serif' ? 'sans-serif' : `"${textDefaults.fontFamily}"`;
|
||||
updateProp('font-family', fontVal);
|
||||
removeProps(['font-style', 'font-weight']);
|
||||
}
|
||||
};
|
||||
|
|
@ -686,6 +687,11 @@ const onToggleSetting = (group, enabled) => {
|
|||
nextTick(() => { isUpdatingFromStore = false; });
|
||||
};
|
||||
|
||||
// Load font when fontFamily changes
|
||||
watch(fontFamily, async (val) => {
|
||||
if (val && val !== 'sans-serif') await loadFont(val);
|
||||
});
|
||||
|
||||
// Watchers — simple props (with group guard)
|
||||
for (const prop of styleProps) {
|
||||
watch(prop.get, () => {
|
||||
|
|
@ -967,6 +973,10 @@ const handleIframeClick = (event, targetElement = null, elementCount = null) =>
|
|||
open(element, event, elementCount);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadAllFontPreviews();
|
||||
});
|
||||
|
||||
defineExpose({ handleIframeClick, close, visible });
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@
|
|||
<!-- Police -->
|
||||
<div class="settings-subsection">
|
||||
<div class="field field-font">
|
||||
<label for="text-font" class="label-with-tooltip field--view-only" data-css="font-family" title="Fonctionnalité à venir">Police</label>
|
||||
<label for="text-font" class="label-with-tooltip" data-css="font-family">Police</label>
|
||||
<div class="field-font__options">
|
||||
<select id="text-font" v-model="font" disabled class="field--view-only" title="Fonctionnalité à venir">
|
||||
<option v-for="f in fonts" :key="f" :value="f">{{ f }}</option>
|
||||
<select id="text-font" v-model="font">
|
||||
<option value="sans-serif">Police système (sans-serif)</option>
|
||||
<option v-for="f in projectFonts" :key="f.name" :value="f.name" :style="{ fontFamily: `'${f.name}', ${f.category}` }">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -78,17 +79,16 @@ import { useCssUpdater } from '../../composables/useCssUpdater';
|
|||
import { useCssSync } from '../../composables/useCssSync';
|
||||
import { useDebounce } from '../../composables/useDebounce';
|
||||
import { useTextDefaults } from '../../composables/useTextDefaults';
|
||||
import { useProjectFonts } from '../../composables/useProjectFonts';
|
||||
|
||||
const { updateStyle } = useCssUpdater();
|
||||
const { extractValue, extractNumericValue } = useCssSync();
|
||||
const { debouncedUpdate } = useDebounce(500);
|
||||
const textDefaults = useTextDefaults();
|
||||
|
||||
// Constants
|
||||
const fonts = ['Alegreya Sans', 'Arial', 'Georgia', 'Helvetica', 'Times New Roman'];
|
||||
const { fonts: projectFonts, loadFont, loadAllFontPreviews } = useProjectFonts();
|
||||
|
||||
// State
|
||||
const font = ref('Alegreya Sans');
|
||||
const font = ref('sans-serif');
|
||||
const fontSize = ref({ value: 16, unit: 'px' });
|
||||
const lineHeight = ref({ value: 20, unit: 'px' });
|
||||
const color = ref('rgb(0, 0, 0)');
|
||||
|
|
@ -97,10 +97,12 @@ const colorInput = ref(null);
|
|||
let isUpdatingFromStore = false;
|
||||
|
||||
// Watchers for body styles
|
||||
watch(font, (val) => {
|
||||
watch(font, async (val) => {
|
||||
textDefaults.fontFamily = val;
|
||||
if (val !== 'sans-serif') await loadFont(val);
|
||||
if (isUpdatingFromStore) return;
|
||||
updateStyle('body', 'font-family', `"${val}"`);
|
||||
const cssValue = val === 'sans-serif' ? 'sans-serif' : `"${val}"`;
|
||||
updateStyle('body', 'font-family', cssValue);
|
||||
}, { immediate: true });
|
||||
|
||||
watch(color, (val) => {
|
||||
|
|
@ -130,6 +132,12 @@ watch(lineHeight, (val) => {
|
|||
const syncFromStore = () => {
|
||||
isUpdatingFromStore = true;
|
||||
|
||||
const fontVal = extractValue('body', 'font-family');
|
||||
if (fontVal) {
|
||||
const cleaned = fontVal.replace(/['"]/g, '').trim();
|
||||
font.value = cleaned || 'sans-serif';
|
||||
}
|
||||
|
||||
const colorVal = extractValue('body', 'color');
|
||||
if (colorVal) color.value = colorVal;
|
||||
|
||||
|
|
@ -155,6 +163,7 @@ onMounted(() => {
|
|||
});
|
||||
syncFromStore();
|
||||
setTimeout(updateColorisButtons, 100);
|
||||
loadAllFontPreviews();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -57,10 +57,12 @@ export function usePreviewRenderer({
|
|||
}
|
||||
|
||||
// Render to the hidden frame
|
||||
// Font-face CSS is injected as a separate style block so PagedJS doesn't try to parse its URLs
|
||||
hiddenFrame.srcdoc = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style id="font-faces">${stylesheetStore.fontFaceCss}</style>
|
||||
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
|
||||
<style id="dynamic-styles">${stylesheetStore.content}</style>
|
||||
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
|
||||
|
|
@ -136,6 +138,15 @@ export function usePreviewRenderer({
|
|||
}
|
||||
);
|
||||
|
||||
// Watch for font-face CSS changes (new font loaded) and re-render
|
||||
watch(
|
||||
() => stylesheetStore.fontFaceCss,
|
||||
() => {
|
||||
if (!initialized.value) return;
|
||||
renderPreview();
|
||||
}
|
||||
);
|
||||
|
||||
// Re-render when narrative data changes
|
||||
watch(
|
||||
() => narrativeStore.data,
|
||||
|
|
|
|||
153
src/composables/useProjectFonts.js
Normal file
153
src/composables/useProjectFonts.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { reactive, computed } from 'vue';
|
||||
import { useStylesheetStore } from '../stores/stylesheet';
|
||||
|
||||
const PROJECT_FONTS = [
|
||||
{
|
||||
name: 'Alegreya',
|
||||
category: 'serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Alegreya/Alegreya-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Alegreya/Alegreya-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EB Garamond',
|
||||
category: 'serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/EB_Garamond/EBGaramond-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/EB_Garamond/EBGaramond-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Fira Sans',
|
||||
category: 'sans-serif',
|
||||
files: {
|
||||
regular: '/assets/fonts/project/Fira_Sans/FiraSans-Regular.ttf',
|
||||
bold: '/assets/fonts/project/Fira_Sans/FiraSans-Bold.ttf',
|
||||
italic: '/assets/fonts/project/Fira_Sans/FiraSans-Italic.ttf',
|
||||
boldItalic: '/assets/fonts/project/Fira_Sans/FiraSans-BoldItalic.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Montserrat',
|
||||
category: 'sans-serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Montserrat/Montserrat-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Montserrat/Montserrat-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Open Sans',
|
||||
category: 'sans-serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Source Code Pro',
|
||||
category: 'monospace',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Source_Code_Pro/SourceCodePro-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Source_Code_Pro/SourceCodePro-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Singleton state
|
||||
const fonts = reactive(
|
||||
PROJECT_FONTS.map((f) => ({ ...f, loaded: false }))
|
||||
);
|
||||
|
||||
const loadedSet = new Set();
|
||||
|
||||
function absoluteUrl(path) {
|
||||
return window.location.origin + path.replace(/,/g, '%2C');
|
||||
}
|
||||
|
||||
function generateFontFaceCss(font) {
|
||||
const rules = [];
|
||||
const { name, files } = font;
|
||||
|
||||
if (files.variable) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.variable)}") format("truetype");\n font-weight: 100 900;\n font-style: normal;\n}`
|
||||
);
|
||||
}
|
||||
if (files.variableItalic) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.variableItalic)}") format("truetype");\n font-weight: 100 900;\n font-style: italic;\n}`
|
||||
);
|
||||
}
|
||||
if (files.regular) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.regular)}") format("truetype");\n font-weight: 400;\n font-style: normal;\n}`
|
||||
);
|
||||
}
|
||||
if (files.bold) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.bold)}") format("truetype");\n font-weight: 700;\n font-style: normal;\n}`
|
||||
);
|
||||
}
|
||||
if (files.italic) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.italic)}") format("truetype");\n font-weight: 400;\n font-style: italic;\n}`
|
||||
);
|
||||
}
|
||||
if (files.boldItalic) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.boldItalic)}") format("truetype");\n font-weight: 700;\n font-style: italic;\n}`
|
||||
);
|
||||
}
|
||||
|
||||
return rules.join('\n\n');
|
||||
}
|
||||
|
||||
async function loadFontInDocument(font) {
|
||||
const file = font.files.variable || font.files.regular;
|
||||
if (!file) return;
|
||||
|
||||
const face = new FontFace(font.name, `url("${file}")`);
|
||||
await face.load();
|
||||
document.fonts.add(face);
|
||||
}
|
||||
|
||||
const loadedFontsCss = computed(() => {
|
||||
return fonts
|
||||
.filter((f) => f.loaded)
|
||||
.map((f) => generateFontFaceCss(f))
|
||||
.join('\n\n');
|
||||
});
|
||||
|
||||
async function loadFont(fontName) {
|
||||
if (!fontName || fontName === 'sans-serif' || loadedSet.has(fontName)) return;
|
||||
|
||||
const font = fonts.find((f) => f.name === fontName);
|
||||
if (!font) return;
|
||||
|
||||
await loadFontInDocument(font);
|
||||
font.loaded = true;
|
||||
loadedSet.add(fontName);
|
||||
|
||||
// Update stylesheet store font-face CSS (injected separately in iframe)
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
stylesheetStore.setFontFaceCss(loadedFontsCss.value);
|
||||
}
|
||||
|
||||
async function loadAllFontPreviews() {
|
||||
// Only loads fonts into document.fonts for select preview — does NOT update the stylesheet store
|
||||
const promises = fonts
|
||||
.filter((f) => !loadedSet.has(f.name))
|
||||
.map((f) => loadFontInDocument(f).catch(() => {}));
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
export function useProjectFonts() {
|
||||
return {
|
||||
fonts,
|
||||
loadFont,
|
||||
loadAllFontPreviews,
|
||||
loadedFontsCss,
|
||||
};
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { reactive } from 'vue';
|
|||
const defaults = reactive({
|
||||
fontSize: { value: 16, unit: 'px' },
|
||||
lineHeight: { value: 20, unit: 'px' },
|
||||
fontFamily: 'Alegreya Sans',
|
||||
fontFamily: 'sans-serif',
|
||||
color: 'rgb(0, 0, 0)',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const useStylesheetStore = defineStore('stylesheet', () => {
|
|||
// Base state
|
||||
const baseCss = ref('');
|
||||
const customCss = ref('');
|
||||
const fontFaceCss = ref('');
|
||||
const isEditing = ref(false);
|
||||
let formatTimer = null;
|
||||
let isFormatting = false;
|
||||
|
|
@ -23,7 +24,11 @@ export const useStylesheetStore = defineStore('stylesheet', () => {
|
|||
const saveError = ref(null);
|
||||
const narrativeId = ref(null);
|
||||
|
||||
// Computed: combined CSS for preview
|
||||
const setFontFaceCss = (css) => {
|
||||
fontFaceCss.value = css;
|
||||
};
|
||||
|
||||
// Computed: combined CSS for preview (font-face excluded — injected separately in iframe)
|
||||
const content = computed(() => {
|
||||
if (!baseCss.value) return customCss.value;
|
||||
if (!customCss.value) return baseCss.value;
|
||||
|
|
@ -253,12 +258,14 @@ export const useStylesheetStore = defineStore('stylesheet', () => {
|
|||
|
||||
return {
|
||||
// Core state
|
||||
content, // computed: baseCss + customCss
|
||||
content, // computed: fontFaceCss + baseCss + customCss
|
||||
baseCss,
|
||||
customCss,
|
||||
fontFaceCss,
|
||||
isEditing,
|
||||
|
||||
// Methods
|
||||
setFontFaceCss,
|
||||
loadStylesheet,
|
||||
updateProperty,
|
||||
extractValue,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue