Open Ananthumpillai opened 10 hours ago
Hello I'm not sure I understand, please give me some conyext are you using the silexlabs/grapesjs-fonts plugin ? It is supposed to add it to the editor by itself (or am I wrong ?)
Oh ok I understand what you want to do, you want to add other fonts in addition to Google fonts
I'm not sure that it will work but maybe the best way to do this would be using the options of the plugin. We should add an option with an array of additional fonts or maybe a callback function that the plugin would call to get the list of fonts?
I hope the functions to get the html imports will still work properly
Would you like to contribute ?
Hi @lexoyo I think your didn't get my context. See I am using this plugin for adding custom fonts to my project. In my editor setup storageManager is set to false and I have a save button which will save the html and css of the canvas. So the issue is whenever I reload, the selected fonts won't be showing up in the font family drop-down.
So as a solution for this what I have done is whenever I add a custom font I will add that to my db via api and on load of the plugin i will add the same on the dropdown.
I didn't done much changes on the code, all I have done is like I just added two apis one for posting the selected fonts and one for getting the fonts.
I don't know whether I missed anything,
let _fontsList
let fonts
let defaults = []
/**
* Options
*/
let fontServer = 'https://fonts.googleapis.com'
let fontApi = 'https://www.googleapis.com'
let customapi = 'http://localhost:8217/api'
/**
* Load available fonts only once per session
* Use local storage
*/
async function getFonts() {
try {
const response = await fetch(`${customapi}/fonts`)
if (!response.ok) {
throw new Error(
`Failed to fetch fonts: ${response.status} ${response.statusText}`
)
}
const fonts = await response.json()
return fonts // Return the list of fonts
} catch (error) {
console.error('Error fetching fonts:', error)
throw error // Re-throw the error for further handling if needed
}
}
try {
_fontsList = JSON.parse(localStorage.getItem(LS_FONTS))
// _fontsList = getFonts()
} catch (e) {
console.error('Could not get fonts from local storage:', e)
}
/**
* Promised wait function
*/
async function wait(ms = 0) {
return new Promise((resolve) => setTimeout(() => resolve(), ms))
}
/**
* When the dialog is opened
*/
async function loadFonts(editor) {
const savedFonts = await getFonts()
const editorFonts = structuredClone(editor.getModel().get('fonts')) || []
const mergedFonts = [...editorFonts, ...savedFonts].filter(
(font, index, self) => self.findIndex((f) => f.name === font.name) === index
)
fonts = mergedFonts
}
/**
* When the dialog is closed
*/
function saveFonts(editor, opts) {
const model = editor.getModel()
// Store the modified fonts
model.set('fonts', fonts)
// Update the HTML head with style sheets to load
updateHead(editor, fonts)
// Update the "font family" dropdown
updateUi(editor, fonts, opts)
// Save website if auto save is on
model.set('changesCount', editor.getDirtyCount() + 1)
}
/**
* Load the available fonts from google
*/
async function addFonts(editor, params) {
try {
const response = await fetch(`${customapi}/fonts/addFonts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
})
if (!response.ok) {
throw new Error(
`Failed to add fonts: ${response.status} ${response.statusText}`
)
}
const data = await response.json() // Assuming the API returns some JSON response
return data // Return the response data if needed
} catch (error) {
console.error('Error adding fonts:', error)
throw error // Re-throw the error for further handling
}
}
async function loadFontList(url) {
_fontsList = _fontsList ?? (await (await fetch(url)).json())?.items
localStorage.setItem(LS_FONTS, JSON.stringify(_fontsList))
await wait() // let the dialog open
return _fontsList
}
export const fontsDialogPlugin = (editor, opts) => {
defaults = editor.StyleManager.getBuiltIn('font-family').options
if (opts.server_url) fontServer = opts.server_url
if (opts.api_url) fontApi = opts.api_url
if (!opts.api_key)
throw new Error(
editor.I18n.t('grapesjs-fonts.You must provide Google font api key')
)
editor.Commands.add(cmdOpenFonts, {
/* eslint-disable-next-line */
run: async (_, sender) => {
modal = editor.Modal.open({
title: editor.I18n.t('grapesjs-fonts.Fonts'),
content: '',
attributes: { class: 'fonts-dialog' },
}).onceClose(() => {
editor.stopCommand(cmdOpenFonts) // apparently this is needed to be able to run the command several times
})
modal.setContent(el)
await loadFonts(editor)
displayFonts(editor, opts, [])
loadFontList(`${fontApi}/webfonts/v1/webfonts?key=${opts.api_key}`).then(
(fontsList) => {
// the run command will terminate before this is done, better for performance
displayFonts(editor, opts, fontsList)
const form = el.querySelector('form')
form.onsubmit = (event) => {
event.preventDefault()
saveFonts(editor, opts)
editor.stopCommand(cmdOpenFonts)
}
form.querySelector('input')?.focus()
}
)
return modal
},
stop: () => {
modal.close()
},
})
// add fonts to the website on save
editor.on('storage:start:store', (data) => {
data.fonts = editor.getModel().get('fonts')
})
// add fonts to the website on load
editor.on('storage:end:load', (data) => {
const fonts = data.fonts || []
editor.getModel().set('fonts', fonts)
// FIXME: remove this timeout which is a workaround for issues in Silex storage providers
setTimeout(() => refresh(editor, opts), 1000)
})
// update the head and the ui when the frame is loaded
editor.on('canvas:frame:load', () => refresh(editor, opts))
// When the page changes, update the dom
editor.on('page', () => refresh(editor, opts))
}
function match(hay, s) {
const regExp = new RegExp(s, 'i')
return hay.search(regExp) !== -1
}
const searchInputRef = createRef()
const fontRef = createRef()
function displayFonts(editor, config, fontsList) {
const searchInput = searchInputRef.value
const activeFonts = fontsList.filter((f) =>
match(f.family, searchInput?.value || '')
)
searchInput?.focus()
function findFont(font) {
return fontsList.find((f) => font.name === f.family)
}
render(
html`
<form class="silex-form grapesjs-fonts">
<div class="silex-form__group">
<div class="silex-bar">
<input
style=${styleMap({
width: '100%',
})}
placeholder="${editor.I18n.t('grapesjs-fonts.Search')}"
type="text"
${ref(searchInputRef)}
@keydown=${() => {
//(fontRef.value as HTMLSelectElement).selectedIndex = 0
setTimeout(() => displayFonts(editor, config, fontsList))
}}
/>
<select
style=${styleMap({
width: '150px',
})}
${ref(fontRef)}
>
${map(
activeFonts,
(f) => html`
<option value=${f['family']}>${f['family']}</option>
`
)}
</select>
<button
class="silex-button"
?disabled=${!fontRef.value || activeFonts.length === 0}
type="button"
@click=${() => {
addFont(
editor,
config,
fonts,
activeFonts[fontRef.value.selectedIndex]
)
displayFonts(editor, config, fontsList)
}}
>
${editor.I18n.t('grapesjs-fonts.Add font')}
</button>
</div>
</div>
<hr />
<div class="silex-form__element">
<h2>${editor.I18n.t('grapesjs-fonts.Installed fonts')}</h2>
<ol class="silex-list">
${map(
fonts,
(f) => html`
<li>
<div class="silex-list__item__header">
<h4>${f.name}</h4>
</div>
<div class="silex-list__item__body">
<fieldset class="silex-group--simple full-width">
<legend>CSS rules</legend>
<input
class="full-width"
type="text"
name="name"
.value=${live(f.value)}
@change=${(e) => {
updateRules(editor, fonts, f, e.target.value)
displayFonts(editor, config, fontsList)
}}
/>
</fieldset>
<fieldset class="silex-group--simple full-width">
<legend>Variants</legend>
${map(
// keep only variants which are letters, no numbers
// FIXME: we need the weights
findFont(f)?.variants.filter(
(v) => v.replace(/[a-z]/g, '') === ''
),
(v) => html`
<div>
<input
id=${f.name + v}
type="checkbox"
value=${v}
?checked=${f.variants?.includes(v)}
@change=${(e) => {
updateVariant(
editor,
fonts,
f,
v,
e.target.checked
)
displayFonts(editor, config, fontsList)
}}
/><label for=${f.name + v}>${v}</label>
</div>
`
)}
</fieldset>
</div>
<div class="silex-list__item__footer">
<button
class="silex-button"
type="button"
@click=${() => {
removeFont(editor, fonts, f)
displayFonts(editor, config, fontsList)
}}
>
${editor.I18n.t('grapesjs-fonts.Remove')}
</button>
</div>
</li>
`
)}
</ol>
</div>
<footer>
<input
class="silex-button"
type="button"
@click=${() => editor.stopCommand(cmdOpenFonts)}
value="${editor.I18n.t('grapesjs-fonts.Cancel')}"
/>
<input
class="silex-button"
type="submit"
@click=${() => addFonts(editor, fonts)}
value="${editor.I18n.t('grapesjs-fonts.Save')}"
/>
</footer>
</form>
`,
el
)
}
function addFont(editor, config, fonts, font) {
const name = font.family
const value = `"${font.family}", ${font.category}`
fonts.push({ name, value, variants: [] })
}
function removeFont(editor, fonts, font) {
const idx = fonts.findIndex((f) => f === font)
fonts.splice(idx, 1)
}
function removeAll(doc, attr) {
const all = doc.head.querySelectorAll(`[${attr}]`)
Array.from(all).forEach((el) => el.remove())
}
const GOOGLE_FONTS_ATTR = 'data-silex-gstatic'
function updateHead(editor, fonts) {
const doc = editor.Canvas.getDocument()
if (!doc) {
// This happens while grapesjs is not ready
return
}
removeAll(doc, GOOGLE_FONTS_ATTR)
const html = getHtml(fonts, GOOGLE_FONTS_ATTR)
doc.head.insertAdjacentHTML('beforeend', html)
}
function updateUi(editor, fonts, opts) {
const styleManager = editor.StyleManager
const fontProperty = styleManager.getProperty('typography', 'font-family')
if (!fontProperty) {
// This happens while grapesjs is not ready
return
}
if (opts.preserveDefaultFonts) {
fonts = defaults.concat(fonts)
} else if (fonts.length === 0) {
fonts = defaults
}
fontProperty.setOptions(fonts)
}
export async function refresh(editor, opts) {
const savedFonts = await getFonts()
const fonts = editor.getModel().get('fonts') || []
const finalFonts = [...fonts, ...savedFonts]
updateHead(editor, finalFonts)
updateUi(editor, finalFonts, opts)
}
function updateRules(editor, fonts, font, value) {
font.value = value
}
function updateVariant(editor, fonts, font, variant, checked) {
const has = font.variants?.includes(variant)
if (has && !checked)
font.variants = font.variants.filter((v) => v !== variant)
else if (!has && checked) font.variants.push(variant)
}
export function getHtml(fonts, attr = '') {
// FIXME: how to use google fonts v2?
// google fonts V2: https://developers.google.com/fonts/docs/css2
//fonts.forEach(f => {
// const prefix = f.variants.length ? ':' : ''
// const variants = prefix + f.variants.map(v => {
// const weight = parseInt(v)
// const axis = v.replace(/\d+/g, '')
// return `${axis},wght@${weight}`
// }).join(',')
// insert(doc, GOOGLE_FONTS_ATTR, 'link', { 'href': `${ fontServer }/css2?family=${f.name.replace(/ /g, '+')}${variants}&display=swap`, 'rel': 'stylesheet' })
//})
// Google fonts v1
// https://developers.google.com/fonts/docs/getting_started#a_quick_example
const preconnect = `<link href="${fontServer}" rel="preconnect" ${attr}><link href="https://fonts.gstatic.com" rel="preconnect" crossorigin ${attr}>`
const links = fonts
.map((f) => {
const prefix = f.variants.length ? ':' : ''
const variants =
prefix +
f.variants
.map((v) => v.replace(/\d+/g, ''))
.filter((v) => !!v)
.join(',')
return `<link href="${fontServer}/css?family=${f.name.replace(
/ /g,
'+'
)}${variants}&display=swap" rel="stylesheet" ${attr}>`
})
.join('')
return preconnect + links
}
Hi I was trying to implement api for saving the selected fonts and retrieve them on page load. Now I am able to save the selected fonts on my db, but on load of the page fonts coming from apis are not getting added properly. I have added the get api in refresh function like this
This is actually working but api is called several times. Is there any way to get the fonts from the api and set it to the editor from any other functions? I tried multiple approached but nothing is working properly.
Thanks in advance,