From 46771a8e96d6c41d440a8bb9ba5a42dfae35a9fa Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Wed, 6 Nov 2024 16:19:13 -0800 Subject: [PATCH] feat: auto load missing resources --- .github/workflows/publish.yaml | 2 + src/demo/js/actionButtons.js | 4 +- src/lib/js/common/animation.js | 8 +- src/lib/js/common/dom.js | 4 +- src/lib/js/common/helpers.mjs | 2 - src/lib/js/common/loaders.js | 104 +++++++++++++----- src/lib/js/components/controls/html/header.js | 10 +- .../js/components/controls/html/tinymce.js | 4 +- src/lib/js/config.js | 7 +- src/lib/js/constants.js | 11 +- src/lib/js/editor.js | 34 +++--- src/lib/sass/formeo.scss | 3 +- vite.config.js | 16 ++- 13 files changed, 134 insertions(+), 75 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ff1e2a38..9363597b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -29,6 +29,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release + - run: npm run build + if: success() - name: Deploy - https://draggable.github.io/formeo/ if: success() uses: peaceiris/actions-gh-pages@v4 diff --git a/src/demo/js/actionButtons.js b/src/demo/js/actionButtons.js index 073dc46b..e94390d1 100644 --- a/src/demo/js/actionButtons.js +++ b/src/demo/js/actionButtons.js @@ -9,7 +9,9 @@ const editorActions = (editor, renderer) => ({ }, logJSON: () => console.log(editor.json), viewData: () => { - Object.entries(editor.formData).forEach(([key, val]) => console.log(key, val)) + for (const [key, val] of Object.entries(editor.formData)) { + console.log(key, val) + } }, resetEditor: () => { window.sessionStorage.removeItem('formeo-formData') diff --git a/src/lib/js/common/animation.js b/src/lib/js/common/animation.js index 97863d28..141af68c 100644 --- a/src/lib/js/common/animation.js +++ b/src/lib/js/common/animation.js @@ -1,9 +1,9 @@ const animate = { /** - * Gets the computed style for an element - * @param {[type]} elem [description] - * @param {Boolean} property [description] - * @return {[type]} [description] + * Get the computed style for DOM element + * @param {Object} elem dom element + * @param {Boolean} property style eg. width, height, opacity + * @return {String} computed style */ getStyle: (elem, property = false) => { let style diff --git a/src/lib/js/common/dom.js b/src/lib/js/common/dom.js index 1dcc42e1..2ecb7c58 100644 --- a/src/lib/js/common/dom.js +++ b/src/lib/js/common/dom.js @@ -11,6 +11,7 @@ import { CONTROL_GROUP_CLASSNAME, CHILD_CLASSNAME_MAP, iconPrefix, + formeoSpriteId, } from '../constants.js' const iconFontTemplates = { @@ -277,7 +278,8 @@ class DOM { return this.iconSymbols } - const iconSymbolNodes = document.querySelectorAll('#formeo-sprite svg symbol') + const formeoSprite = document.getElementById(formeoSpriteId) + const iconSymbolNodes = formeoSprite.querySelectorAll('svg symbol') const createSvgIconConfig = symbolId => ({ tag: 'svg', diff --git a/src/lib/js/common/helpers.mjs b/src/lib/js/common/helpers.mjs index d2a494d4..c11bf48d 100644 --- a/src/lib/js/common/helpers.mjs +++ b/src/lib/js/common/helpers.mjs @@ -1,7 +1,5 @@ -'use strict' import { unique } from './utils/index.mjs' import { get } from './utils/object.mjs' -import { slugify } from './utils/string.mjs' /** * Tests if is whole number. returns false if n is Float diff --git a/src/lib/js/common/loaders.js b/src/lib/js/common/loaders.js index 23b0efc6..971dc28c 100644 --- a/src/lib/js/common/loaders.js +++ b/src/lib/js/common/loaders.js @@ -1,5 +1,12 @@ import dom from './dom.js' -import { POLYFILLS } from '../constants.js' +import { + CSS_URL, + FALLBACK_CSS_URL, + FALLBACK_SVG_SPRITE_URL, + POLYFILLS, + SVG_SPRITE_URL, + formeoSpriteId, +} from '../constants.js' import { noop } from './utils/index.mjs' /* global fetch */ @@ -9,6 +16,19 @@ const loaded = { css: new Set(), } +export const ajax = (fileUrl, callback, onError = noop) => { + return new Promise(resolve => { + return fetch(fileUrl) + .then(data => { + if (!data.ok) { + return resolve(onError(data)) + } + resolve(callback ? callback(data) : data) + }) + .catch(err => onError(err)) + }) +} + const onLoadStylesheet = (elem, cb) => { elem.removeEventListener('load', onLoadStylesheet) elem.rel = 'stylesheet' @@ -82,31 +102,51 @@ export const insertScripts = srcs => { return Promise.all(promises) } +/** + * Inserts multiple stylesheets into the document. + * + * @param {string|string[]} srcs - A single stylesheet URL or an array of stylesheet URLs to be inserted. + * @returns {Promise} A promise that resolves when all stylesheets have been inserted. + */ export const insertStyles = srcs => { srcs = Array.isArray(srcs) ? srcs : [srcs] const promises = srcs.map(src => insertStyle(src)) return Promise.all(promises) } -export const insertIcons = resp => { - const spritePromise = typeof resp === 'string' ? Promise.resolve(resp) : resp.text() - return spritePromise.then(iconSvgStr => { - const id = 'formeo-sprite' - let iconSpriteWrap = document.getElementById(id) - if (!iconSpriteWrap) { - iconSpriteWrap = dom.create({ - id, - children: iconSvgStr, - attrs: { - hidden: true, - style: 'display: none;', - }, - }) +/** + * Inserts SVG icons into the document if they are not already present. + * + * @param {string} iconSvgStr - A string containing SVG icons. + * @returns {HTMLElement} The element wrapping the inserted SVG icons. + */ +export const insertIcons = iconSvgStr => { + let iconSpriteWrap = document.getElementById(formeoSpriteId) + if (!iconSpriteWrap) { + iconSpriteWrap = dom.create({ + id: formeoSpriteId, + children: iconSvgStr, + attrs: { + hidden: true, + style: 'display: none;', + }, + }) - document.body.insertBefore(iconSpriteWrap, document.body.childNodes[0]) - } - return iconSpriteWrap - }) + document.body.insertBefore(iconSpriteWrap, document.body.childNodes[0]) + } + return iconSpriteWrap +} + +/** + * Fetches icons from the provided URL and inserts them into the document. + * If the primary URL fails, it attempts to fetch from a fallback URL. + * + * @param {string} iconSpriteUrl - The URL to fetch the icon sprite from. + * @returns {Promise} A promise that resolves when the icons are fetched and inserted. + */ +export const fetchIcons = async (iconSpriteUrl = SVG_SPRITE_URL) => { + const parseResp = async resp => insertIcons(await resp.text()) + return ajax(iconSpriteUrl, parseResp, () => ajax(FALLBACK_SVG_SPRITE_URL, parseResp)) } export const loadPolyfills = polyfillConfig => { @@ -116,14 +156,6 @@ export const loadPolyfills = polyfillConfig => { return Promise.all(polyfills.map(({ src }) => insertScript(src))) } -export const ajax = (file, callback, onError = noop) => { - return new Promise((resolve, reject) => { - return fetch(file) - .then(data => resolve(callback ? callback(data) : data)) - .catch(err => reject(new Error(onError(err)))) - }) -} - export const LOADER_MAP = { js: insertScripts, css: insertStyles, @@ -135,3 +167,21 @@ export const fetchDependencies = dependencies => { }) return Promise.all(promises) } + +export const isCssLoaded = () => { + const formeoSprite = document.getElementById(formeoSpriteId) + const computedStyle = window.getComputedStyle(formeoSprite) + + return computedStyle.visibility === 'hidden' +} + +export const fetchFormeoStyle = async () => { + // check if necessary styles were loaded + if (!isCssLoaded()) { + await insertStyle(CSS_URL) + // check again and use fallback if necessary styles were not loaded + if (!isCssLoaded()) { + return await insertStyle(FALLBACK_CSS_URL) + } + } +} diff --git a/src/lib/js/components/controls/html/header.js b/src/lib/js/components/controls/html/header.js index 57ad4641..665d15c5 100644 --- a/src/lib/js/components/controls/html/header.js +++ b/src/lib/js/components/controls/html/header.js @@ -8,7 +8,6 @@ const headerTags = Array.from(Array(5).keys()) const headerKey = 'controls.html.header' class HeaderControl extends Control { - constructor() { const header = { tag: headerTags[0], @@ -32,13 +31,8 @@ class HeaderControl extends Control { }, content: i18n.get(headerKey), action: { - onRender: evt => { - console.log('evt', evt) - }, - click: evt => { - // debugger - console.log('evt', evt) - }, + // onRender: evt => {}, + // click: evt => {}, }, } super(header) diff --git a/src/lib/js/components/controls/html/tinymce.js b/src/lib/js/components/controls/html/tinymce.js index af7a7ae7..104c583f 100644 --- a/src/lib/js/components/controls/html/tinymce.js +++ b/src/lib/js/components/controls/html/tinymce.js @@ -31,9 +31,7 @@ class TinyMCEControl extends Control { }, controlAction: { // callback when control is clicked - click: () => { - console.log('window.tinymce control clicked') - }, + click: () => {}, // callback for when control is rendered onRender: () => {}, }, diff --git a/src/lib/js/config.js b/src/lib/js/config.js index 011c66be..b7b0511a 100644 --- a/src/lib/js/config.js +++ b/src/lib/js/config.js @@ -1,8 +1,9 @@ import mi18n from '@draggable/i18n' import { isIE } from './common/helpers' -const EN_US = import.meta.env.EN_US +import { SVG_SPRITE_URL } from './constants' +const enUS = import.meta.env.enUS -mi18n.addLanguage('en-US', EN_US) +mi18n.addLanguage('en-US', enUS) export const defaults = { get editor() { @@ -14,7 +15,7 @@ export const defaults = { sessionStorage: false, editorContainer: null, // element or selector to attach editor to external: {}, // assign external data to be used in conditions autolinker - svgSprite: null, // change to null + svgSprite: SVG_SPRITE_URL, // change to null iconFont: null, // 'glyphicons' || 'font-awesome' || 'fontello' config: {}, // stages, rows, columns, fields events: {}, diff --git a/src/lib/js/constants.js b/src/lib/js/constants.js index da71b2e4..c9704a3b 100644 --- a/src/lib/js/constants.js +++ b/src/lib/js/constants.js @@ -1,15 +1,21 @@ import pkg from '../../../package.json' with { type: 'json' } +const isProd = import.meta.env.PROD + const name = pkg.name +const version = pkg.version export const PACKAGE_NAME = name +export const formeoSpriteId = 'formeo-sprite' export const POLYFILLS = [ { name: 'cssPreload', src: '//cdnjs.cloudflare.com/ajax/libs/loadCSS/2.0.1/cssrelpreload.min.js' }, { name: 'mutationObserver', src: '//cdn.jsdelivr.net/npm/mutationobserver-shim/dist/mutationobserver.min.js' }, { name: 'fetch', src: 'https://unpkg.com/unfetch/polyfill' }, ] - -export const FALLBACK_SVG_SPRITE = 'https://draggable.github.io/formeo/assets/img/formeo-sprite.svg' +export const SVG_SPRITE_URL = isProd ? `https://cdn.jsdelivr.net/npm/formeo@${version}/dist/${formeoSpriteId}.svg` : `assets/img/${formeoSpriteId}.svg` +export const FALLBACK_SVG_SPRITE_URL = `https://draggable.github.io/formeo/assets/img/${formeoSpriteId}.svg` +export const CSS_URL = `https://cdn.jsdelivr.net/npm/formeo@${version}/dist/formeo.min.css` +export const FALLBACK_CSS_URL = 'https://draggable.github.io/formeo/assets/css/formeo.min.css' export const CONTROL_GROUP_CLASSNAME = 'control-group' export const STAGE_CLASSNAME = `${PACKAGE_NAME}-stage` @@ -21,7 +27,6 @@ export const CUSTOM_COLUMN_OPTION_CLASSNAME = 'custom-column-widths' export const COLUMN_PRESET_CLASSNAME = 'column-preset' export const COLUMN_RESIZE_CLASSNAME = 'resizing-columns' - export const CHILD_CLASSNAME_MAP = new Map([ [STAGE_CLASSNAME, ROW_CLASSNAME], [ROW_CLASSNAME, COLUMN_CLASSNAME], diff --git a/src/lib/js/editor.js b/src/lib/js/editor.js index 49a7c1be..bc93f037 100644 --- a/src/lib/js/editor.js +++ b/src/lib/js/editor.js @@ -1,15 +1,14 @@ -import '../sass/formeo.scss' import i18n from '@draggable/i18n' import dom from './common/dom.js' import Events from './common/events.js' import Actions from './common/actions.js' import Controls from './components/controls/index.js' import Components from './components/index.js' -import { loadPolyfills, insertStyle, insertIcons, ajax } from './common/loaders.js' -import { SESSION_LOCALE_KEY, FALLBACK_SVG_SPRITE } from './constants.js' +import { loadPolyfills, insertStyle, fetchIcons, isCssLoaded, fetchFormeoStyle } from './common/loaders.js' +import { SESSION_LOCALE_KEY, CSS_URL } from './constants.js' import { merge } from './common/utils/index.mjs' import { defaults } from './config.js' -import sprite from '../icons/formeo-sprite.svg?raw' +import '../sass/formeo.scss' /** * Main class @@ -40,12 +39,7 @@ export class FormeoEditor { Actions.init({ debug, sessionStorage: opts.sessionStorage, ...actions }) // Load remote resources such as css and svg sprite - this.loadResources().then(() => { - if (opts.allowEdit) { - // this.edit = this.init.bind(this) - this.init() - } - }) + document.addEventListener('DOMContentLoaded', this.loadResources.bind(this)) } get formData() { @@ -62,7 +56,9 @@ export class FormeoEditor { * Load remote resources * @return {Promise} asynchronously loaded remote resources */ - loadResources() { + async loadResources() { + document.removeEventListener('DOMContentLoaded', this.loadResources) + const promises = [] if (this.opts.polyfills) { @@ -74,15 +70,19 @@ export class FormeoEditor { } // Ajax load svgSprite and inject into markup. - if (this.opts.svgSprite) { - promises.push(ajax(this.opts.svgSprite, insertIcons, () => ajax(FALLBACK_SVG_SPRITE, insertIcons))) - } else { - promises.push(insertIcons(sprite)) - } + promises.push(fetchIcons(this.opts.svgSprite)) promises.push(i18n.init({ ...this.opts.i18n, locale: window.sessionStorage?.getItem(SESSION_LOCALE_KEY) })) - return Promise.all(promises) + const resolvedPromises = await Promise.all(promises) + + fetchFormeoStyle() + + if (this.opts.allowEdit) { + this.init() + } + + return resolvedPromises } /** diff --git a/src/lib/sass/formeo.scss b/src/lib/sass/formeo.scss index 19e587b0..166f399d 100644 --- a/src/lib/sass/formeo.scss +++ b/src/lib/sass/formeo.scss @@ -5,8 +5,9 @@ @use "components/autocomplete"; @use "components/panels"; -.formeo-sprite { +#formeo-sprite { display: none !important; + visibility: hidden !important; } .formeo { diff --git a/vite.config.js b/vite.config.js index 16461842..53f443f6 100644 --- a/vite.config.js +++ b/vite.config.js @@ -33,7 +33,7 @@ const sharedConfig = { }, }, define: { - 'import.meta.env.EN_US': JSON.stringify(enUS), + 'import.meta.env.enUS': JSON.stringify(enUS), }, css: { preprocessorOptions: { @@ -65,7 +65,7 @@ export default defineConfig({ assetFileNames: assetInfo => { const extType = getExtType(assetInfo) const ext = extType === 'img' ? '[ext]' : 'min.[ext]' - return `assets/${getExtType(assetInfo)}/[name].${ext}` + return `assets/${extType}/[name].${ext}` }, }, external: ['formeo'], @@ -109,6 +109,10 @@ export default defineConfig({ src: resolve('src/lib/icons/formeo-sprite.svg'), dest: './assets/img/', }, + { + src: resolve('src/lib/icons/formeo-sprite.svg'), + dest: resolve('dist/'), + }, { src: resolve('node_modules', '@draggable/formeo-languages/dist/lang/*'), dest: './assets/lang', @@ -118,6 +122,11 @@ export default defineConfig({ dest: './assets/js/', rename: 'formeo.min.js', }, + { + src: resolve('dist/formeo.umd.js'), + dest: resolve('dist/'), + rename: 'formeo.min.js', + }, { src: resolve('dist/formeo.min.css'), dest: './assets/css/', @@ -125,7 +134,4 @@ export default defineConfig({ ], }), ], - // optimizeDeps: { - // exclude: ['@draggable/i18n'] - // } })