silexlabs / grapesjs-fonts

GrapesJS plugin to add custom fonts, with an optional UI for your users to manage their custom fonts
MIT License
27 stars 13 forks source link

Implementing apis #18

Open Ananthumpillai opened 10 hours ago

Ananthumpillai commented 10 hours ago

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

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)
}

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,

lexoyo commented 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 ?)

lexoyo commented 10 hours ago

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 ?

Ananthumpillai commented 9 hours ago

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
}