diff --git a/package-lock.json b/package-lock.json index 910889b..6adc4d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "2.1.4", "license": "MIT", "dependencies": { - "formeo-i18n": "^2.1.3", + "@draggable/formeo-languages": "^3.1.3", + "@draggable/i18n": "^1.0.7", "lodash": "^4.17.21", - "mi18n": "^1.0.2", "sortablejs": "^1.15.3" }, "devDependencies": { @@ -30,7 +30,8 @@ "vite": "^5.4.8", "vite-plugin-banner": "^0.8.0", "vite-plugin-compression": "^0.5.1", - "vite-plugin-html": "^3.2.2" + "vite-plugin-html": "^3.2.2", + "vite-plugin-static-copy": "^2.0.0" } }, "../../mI18N/mi18n": { @@ -546,6 +547,19 @@ "kuler": "^2.0.0" } }, + "node_modules/@draggable/formeo-languages": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@draggable/formeo-languages/-/formeo-languages-3.1.3.tgz", + "integrity": "sha512-DNYSxShxCrmd1Am0vqQsFGHSySwcUTJEi5hk87oY3e4QqEi5AzoZOT30e9RKO+x+tBsbLEKNgI3Flhz6H0wheA==", + "dependencies": { + "mi18n": "^1.0.3" + } + }, + "node_modules/@draggable/i18n": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@draggable/i18n/-/i18n-1.0.7.tgz", + "integrity": "sha512-Tw0YX27smQCrwFEtVwFVmeCLvjdwtxUSnSiYSbl/xakC0CZF9FZY68Ua/ULLArPh8I9hk8G9sN13V5eqnig63w==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -2334,6 +2348,19 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2449,6 +2476,18 @@ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", "dev": true }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2578,6 +2617,30 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -4077,14 +4140,6 @@ "node": ">= 6" } }, - "node_modules/formeo-i18n": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/formeo-i18n/-/formeo-i18n-2.1.3.tgz", - "integrity": "sha512-fFaE0FzhUrhKhL8u3b1TiirffflIq2RQBLXzKvO/Q2OlqWmmQxtvv78Htfh8xQV1qnvmfyVzV8mt72DdihEeVQ==", - "engines": { - "node": ">=10" - } - }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -4815,6 +4870,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -5761,10 +5828,9 @@ } }, "node_modules/mi18n": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mi18n/-/mi18n-1.0.2.tgz", - "integrity": "sha512-amRuUVYd1VpZWPcjCtzZ9qKrBlAC/tzb44CdWpDphWXUzK8SBZ0kmwfsM3INfOQNWWAtAzwmsjZMYbBxB4WZ4A==", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mi18n/-/mi18n-1.0.3.tgz", + "integrity": "sha512-1OhxKYEgN/BNiMX1Idlyoir+mEYoLLo7Rfeo+89hdOtbUs3PyCIuBoMcfgHSjMjHgs7e7xbRovd6lJq8hLh5mA==" }, "node_modules/micromatch": { "version": "4.0.8", @@ -5956,6 +6022,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -9924,6 +9999,18 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -12064,6 +12151,24 @@ "node": ">=12" } }, + "node_modules/vite-plugin-static-copy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.0.0.tgz", + "integrity": "sha512-b/quFjTUa/RY9t3geIyeeT2GtWEoRI0GawYFFjys5iMLGgVP638NTGu0RoMjwmi8MoZZ3BQw4OQvb1GpVcXZDA==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 62a3fcc..9955163 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,16 @@ "name": "formeo", "version": "2.1.4", "type": "module", - "main": "dist/formeo.min.js", + "main": "dist/formeo.cjs.js", + "module": "dist/formeo.es.js", + "unpkg": "dist/formeo.umd.js", + "exports": { + ".": { + "import": "./dist/formeo.es.js", + "require": "./dist/formeo.cjs.js", + "default": "./dist/formeo.umd.js" + } + }, "files": [ "dist/*", "demo/**/*" @@ -55,10 +64,9 @@ "scripts": { "dev": "vite", "preview": "vite preview", - "analyze": "webpack --mode production -p --progress --config tools/webpack.config --analyze", - "build": "npm-run-all -p build:icons build:formeo build:demo", - "build:formeo": "vite build --mode lib", - "build:formeo:watch": "npm run build:formeo -- --watch", + "build:lib": "vite build --config vite.config.lib.mjs", + "build": "npm-run-all -p build:icons build:demo", + "prebuild": "npm run build:lib", "build:demo": "vite build --mode demo", "build:demo:watch": "vite build --mode demo --watch", "build:icons": "node ./tools/generate-sprite", @@ -68,6 +76,7 @@ "test:ci": "yarn test --coverage", "start": "npm-run-all build:icons dev", "semantic-release": "semantic-release --ci --debug", + "copy:lang": "node ./tools/copy-directory.mjs ./node_modules/formeo-i18n/dist/lang ./src/demo/assets/lang", "travis-deploy-once": "travis-deploy-once --pro", "prepush": "yarn test", "defaults": "webpack-defaults" @@ -88,12 +97,13 @@ "vite": "^5.4.8", "vite-plugin-banner": "^0.8.0", "vite-plugin-compression": "^0.5.1", - "vite-plugin-html": "^3.2.2" + "vite-plugin-html": "^3.2.2", + "vite-plugin-static-copy": "^2.0.0" }, "dependencies": { - "formeo-i18n": "^2.1.3", + "@draggable/formeo-languages": "^3.1.3", + "@draggable/i18n": "^1.0.7", "lodash": "^4.17.21", - "mi18n": "^1.0.2", "sortablejs": "^1.15.3" }, "release": { diff --git a/src/demo/js/events.js b/src/demo/js/events.js index 21f555d..c2f50d4 100644 --- a/src/demo/js/events.js +++ b/src/demo/js/events.js @@ -8,11 +8,7 @@ export const editorEvents = editor => { editor.i18n.setLang(value) } controlFilter.addEventListener('input', onChangeFilterInput) - if (formeoLocale) { - localeSelect.value = JSON.parse(formeoLocale) - } else { - localeSelect.value = 'en-US' - } + localeSelect.value = formeoLocale || 'en-US' localeSelect.addEventListener('change', onChangeLocale, false) } diff --git a/src/demo/js/options/index.js b/src/demo/js/options/index.js index bcacc28..3b397c2 100644 --- a/src/demo/js/options/index.js +++ b/src/demo/js/options/index.js @@ -8,7 +8,7 @@ const renderContainer = '.render-form' export const editorOptions = { editorContainer, i18n: { - location: `./assets/lang`, + location: './assets/lang', }, actions: { // save: formData => null, // do something on save action @@ -32,4 +32,20 @@ export const editorOptions = { export const renderOptions = { renderContainer, external, + elements: { + tinymce: { + dependencies: { js: 'cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js' }, + action: { + onRender: elem => { + if (elem.id) { + const selector = `#${elem.id}` + window.tinymce.remove(selector) + window.tinymce.init({ + selector: selector, + }) + } + }, + }, + }, + }, } diff --git a/src/lib/js/common/actions.js b/src/lib/js/common/actions.js index 8c769e4..299d7da 100644 --- a/src/lib/js/common/actions.js +++ b/src/lib/js/common/actions.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import { SESSION_FORMDATA_KEY, CONDITION_TEMPLATE } from '../constants.js' import { identity, sessionStorage } from './utils/index.mjs' import events from './events.js' diff --git a/src/lib/js/common/dom.js b/src/lib/js/common/dom.js index f62d319..1dcc42e 100644 --- a/src/lib/js/common/dom.js +++ b/src/lib/js/common/dom.js @@ -1,5 +1,5 @@ import h, { forEach } from './helpers.mjs' -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import animate from './animation.js' import Components from '../components/index.js' import { uuid, componentType, merge } from './utils/index.mjs' @@ -90,7 +90,8 @@ class DOM { * @param {Boolean} isPreview generating element for preview or render? * @return {Object} DOM Object */ - create = (elem, isPreview = false) => { + create = (elemArg, isPreview = false) => { + let elem = elemArg if (!elem) { return } @@ -216,7 +217,7 @@ class DOM { // Set the new element's dataset if (elem.dataset) { for (const data in elem.dataset) { - if (elem.dataset.hasOwnProperty(data)) { + if (Object.hasOwn(elem.dataset, data)) { element.dataset[data] = typeof elem.dataset[data] === 'function' ? elem.dataset[data]() : elem.dataset[data] } } @@ -256,13 +257,18 @@ class DOM { actionHandler(node, actions) { const handlers = { onRender: dom.onRender, + render: dom.onRender, } const useCaptureEvts = ['focus', 'blur'] const defaultHandler = event => (node, cb) => node.addEventListener(event, cb, useCaptureEvts.includes(event)) - return Object.entries(actions).map(([key, cb]) => { - const action = handlers[key] || defaultHandler(key) - return action(node, cb) + return Object.entries(actions).map(([event, cb]) => { + const cbs = Array.isArray(cb) ? cb : [cb] + + return cbs.map(cb => { + const action = handlers[event] || defaultHandler(event) + return action(node, cb) + }) }) } @@ -340,7 +346,7 @@ class DOM { } // Set element attributes - Object.keys(attrs).forEach(attr => { + for (const attr of Object.keys(attrs)) { const name = h.safeAttrName(attr) let value = attrs[attr] || '' @@ -366,7 +372,7 @@ class DOM { element.setAttribute(name, value) } } - }) + } } /** @@ -729,7 +735,11 @@ class DOM { removeClasses(nodeList, className) { const removeClass = { string: elem => elem.classList.remove(className), - array: elem => className.forEach(name => elem.classList.remove(name)), + array: elem => { + for (const name of className) { + elem.classList.remove(name) + } + }, } removeClass.object = removeClass.string // handles regex map h.forEach(nodeList, removeClass[this.childType(className)]) @@ -744,7 +754,11 @@ class DOM { addClasses(nodeList, className) { const addClass = { string: elem => elem.classList.add(className), - array: elem => className.forEach(name => elem.classList.add(name)), + array: elem => { + for (const name of className) { + elem.classList.add(name) + } + }, } h.forEach(nodeList, addClass[this.childType(className)]) } diff --git a/src/lib/js/common/loaders.js b/src/lib/js/common/loaders.js index dad4da7..23b0efc 100644 --- a/src/lib/js/common/loaders.js +++ b/src/lib/js/common/loaders.js @@ -5,15 +5,27 @@ import { noop } from './utils/index.mjs' /* global fetch */ const loaded = { - js: [], - css: [], + js: new Set(), + css: new Set(), +} + +const onLoadStylesheet = (elem, cb) => { + elem.removeEventListener('load', onLoadStylesheet) + elem.rel = 'stylesheet' + cb(elem.src) +} + +const onLoadJavascript = (elem, cb) => { + elem.removeEventListener('load', onLoadJavascript) + cb(elem.src) } export const insertScript = src => { return new Promise((resolve, reject) => { - if (loaded.js.includes(src)) { + if (loaded.js.has(src)) { return resolve(src) } + loaded.js.add(src) // Create script element and set attributes const script = dom.create({ @@ -21,20 +33,16 @@ export const insertScript = src => { attrs: { type: 'text/javascript', async: true, - src: `//${this.src}`, + src: `//${src.replace(/^https?:\/\//, '')}`, }, action: { - load: () => { - loaded.js.push(src) - resolve(src) - }, - error: () => reject(new Error(`${this.src} failed to load.`)), + load: () => onLoadJavascript(script, resolve), + error: () => reject(new Error(`${src} failed to load.`)), }, }) - // Append the script to the DOM - const el = document.getElementsByTagName('script')[0] - el.parentNode.insertBefore(script, el) + // Append the script to the document head + document.head.appendChild(script) }) } @@ -43,15 +51,11 @@ export const insertStyle = srcs => { const promises = srcs.map( src => new Promise((resolve, reject) => { - if (loaded.css.includes(src)) { + if (loaded.css.has(src)) { return resolve(src) } - function onLoad() { - this.removeEventListener('load', onLoad) - this.rel = 'stylesheet' - loaded.css.push(src) - resolve(src) - } + loaded.css.add(src) + const styleLink = dom.create({ tag: 'link', attrs: { @@ -60,7 +64,7 @@ export const insertStyle = srcs => { as: 'style', }, action: { - load: onLoad, + load: () => onLoadStylesheet(styleLink, resolve), error: () => reject(new Error(`${this.src} failed to load.`)), }, }) @@ -72,6 +76,18 @@ export const insertStyle = srcs => { return Promise.all(promises) } +export const insertScripts = srcs => { + srcs = Array.isArray(srcs) ? srcs : [srcs] + const promises = srcs.map(src => insertScript(src)) + return Promise.all(promises) +} + +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 => { @@ -107,3 +123,15 @@ export const ajax = (file, callback, onError = noop) => { .catch(err => reject(new Error(onError(err)))) }) } + +export const LOADER_MAP = { + js: insertScripts, + css: insertStyles, +} + +export const fetchDependencies = dependencies => { + const promises = Object.entries(dependencies).map(([type, src]) => { + return LOADER_MAP[type](src) + }) + return Promise.all(promises) +} diff --git a/src/lib/js/common/utils/index.mjs b/src/lib/js/common/utils/index.mjs index 2621887..beeb919 100644 --- a/src/lib/js/common/utils/index.mjs +++ b/src/lib/js/common/utils/index.mjs @@ -215,18 +215,18 @@ export const memoize = (fn, resolver) => { export const sessionStorage = Object.create(null, { get: { value: key => { - const itemValue = window.sessionStorage && window.sessionStorage.getItem(key) + const itemValue = window.sessionStorage?.getItem(key) try { return JSON.parse(itemValue) - } catch (error) { - console.error(error) + } catch (_err) { + return itemValue } }, }, set: { value: (key, itemValue) => { try { - return window.sessionStorage && window.sessionStorage.setItem(key, JSON.stringify(itemValue)) + return window.sessionStorage?.setItem(key, JSON.stringify(itemValue)) } catch (error) { console.error(error) } diff --git a/src/lib/js/common/utils/object.mjs b/src/lib/js/common/utils/object.mjs index e13d8d5..ecd7a80 100644 --- a/src/lib/js/common/utils/object.mjs +++ b/src/lib/js/common/utils/object.mjs @@ -17,12 +17,12 @@ export const cleanObj = obj => { object: val => cleanObj(val), } - Object.keys(obj).forEach(key => { + for (const key of Object.keys(obj)) { const valType = typeof obj[key] if (typeMap[valType]) { fresh[key] = typeMap[valType](obj[key]) } - }) + } return fresh } @@ -55,9 +55,37 @@ export function deepClone(obj) { const cloned = {} for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (Object.hasOwn(obj, key)) { cloned[key] = deepClone(obj[key]) } } return cloned } + +/** + * Merges two action objects. If a key already exists in the target object, + * converts the value to an array and adds the value of the source object's key to the array. + * + * @param {Object} target - The target object to merge into. + * @param {Object} source - The source object to merge from. + * @returns {Object} - The merged object. + */ +export function mergeActions(target, source = {}) { + const result = { ...target } + + for (const key in source) { + if (Object.hasOwn(source, key)) { + if (Object.hasOwn(result, key)) { + if (Array.isArray(result[key])) { + result[key].push(source[key]) + } else { + result[key] = [result[key], source[key]] + } + } else { + result[key] = source[key] + } + } + } + + return result +} diff --git a/src/lib/js/components/autocomplete.js b/src/lib/js/components/autocomplete.js index a427bbc..d7ba931 100644 --- a/src/lib/js/components/autocomplete.js +++ b/src/lib/js/components/autocomplete.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import animate from '../common/animation.js' import dom from '../common/dom.js' import Components from './index.js' diff --git a/src/lib/js/components/columns/column.js b/src/lib/js/components/columns/column.js index 38c04e3..6542f4e 100644 --- a/src/lib/js/components/columns/column.js +++ b/src/lib/js/components/columns/column.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Sortable from 'sortablejs' import Component from '../component.js' import h from '../../common/helpers.mjs' @@ -70,7 +70,7 @@ export default class Column extends Component { }, }) - this.sortable = Sortable.create(children, { + Sortable.create(children, { animation: 150, fallbackClass: 'field-moving', forceFallback: true, diff --git a/src/lib/js/components/component.js b/src/lib/js/components/component.js index 0934fef..9f5a390 100644 --- a/src/lib/js/components/component.js +++ b/src/lib/js/components/component.js @@ -1,5 +1,5 @@ /* global MutationObserver */ -import { uuid, componentType, merge, clone, remove, identity, closest } from '../common/utils/index.mjs' +import { uuid, componentType, merge, clone, remove, identity } from '../common/utils/index.mjs' import { isInt, map, forEach, indexOfNode } from '../common/helpers.mjs' import dom from '../common/dom.js' import { @@ -174,7 +174,6 @@ export default class Component extends Data { } get buttons() { - const _this = this if (this.actionButtons) { return this.actionButtons } @@ -202,7 +201,7 @@ export default class Component extends Data { }, action: { click: evt => { - _this.toggleEdit() + this.toggleEdit() }, }, } @@ -216,13 +215,13 @@ export default class Component extends Data { }, action: { click: (evt, id) => { - animate.slideUp(_this.dom, ANIMATION_SPEED_BASE, () => { - if (_this.name === 'column') { - const row = _this.parent + animate.slideUp(this.dom, ANIMATION_SPEED_BASE, () => { + if (this.name === 'column') { + const row = this.parent row.autoColumnWidths() - _this.remove() + this.remove() } else { - _this.remove() + this.remove() } }) // @todo add onRemove to Events and Actions diff --git a/src/lib/js/components/controls/control.js b/src/lib/js/components/controls/control.js index e878565..05a3101 100644 --- a/src/lib/js/components/controls/control.js +++ b/src/lib/js/components/controls/control.js @@ -1,67 +1,69 @@ -import { insertScript, insertStyle } from '../../common/loaders.js' -import { identity } from '../../common/utils/index.mjs' - -const LOADER_MAP = { - js: insertScript, - css: insertStyle, -} +import i18n from '@draggable/i18n' +import dom from '../../common/dom.js' +import { fetchDependencies } from '../../common/loaders.js' +import { uuid } from '../../common/utils/index.mjs' +import { CONTROL_GROUP_CLASSNAME } from '../../constants.js' +import { indexOfNode } from '../../common/helpers.mjs' +import controls from './index.js' export default class Control { - constructor({ events = {}, dependencies = {}, ...restConfig } = {}) { - this.events = events - this.controlData = restConfig - this.depsLoaded = this.fetchDependencies(dependencies) - } - - onRenderPreview() {} - - fetchDependencies(dependencies, cache = true) { - const promises = Object.entries(dependencies).map(([type, src]) => { - return LOADER_MAP[type](src) - }) - return Promise.all(promises) - } + controlCache = new Set() /** - * code to execute for supported events - * to implement an onRender event in a child class, simply define an onRender method - * @param {String} eventType - optional type of event to retrieve an event function for. If not specified all events returned - * @return {Function/Object} - function to execute for specified event, or all events of no eventType is specified + * Constructs a new Control instance. + * + * @param {Object} [config={}] - The configuration object. + * @param {Object} [config.events={}] - The events associated with the control. ex { click: () => {} } + * @param {Object} [config.dependencies={}] - The dependencies required by the control. ex { js: 'https://example.com/script.js', css: 'https://example.com/style.css' } + * @param {...Object} [controlData] - Additional configuration properties. ex { meta: {}, config: { label: 'Control Name' } } */ - on(eventType) { - const events = { - // executed just prior to the row being returned by the layout class. Receives the DOMelement about to be passed back - prerender: identity, + constructor({ events = {}, dependencies = {}, controlAction, ...controlData }) { + this.events = events + this.controlData = controlData + this.controlAction = controlAction + this.dependencies = dependencies + this.id = controlData.id || uuid() + } - renderComponent: identity, + get controlId() { + return this.controlData.meta?.id + } - // onRender event to execute code each time an instance of this control is injected into the DOM - render: evt => { - // check for a class render event - default to an empty function - const onRender = () => { - if (this.onRender) { - this.onRender(evt) - } - } + get dom() { + const { meta, config } = this.controlData + const controlLabel = this.i18n(config.label) || config.label - this.depsLoaded.then(() => { - onRender(evt) - }) + const button = { + tag: 'button', + attrs: { + type: 'button', + }, + content: [{ tag: 'span', className: 'control-icon', children: dom.icon(meta.icon) }, controlLabel], + action: { + // this is used for keyboard navigation. when tabbing through controls it + // will auto navigated between the groups + focus: ({ target }) => { + const group = target.closest(`.${CONTROL_GROUP_CLASSNAME}`) + return group && controls.panels.nav.refresh(indexOfNode(group)) + }, + click: ({ target }) => { + controls.addElement(target.parentElement.id) + }, }, } - return eventType ? events[eventType] : events + return dom.create({ + tag: 'li', + id: this.id, + className: ['field-control', `${meta.group}-control`, `${meta.id}-control`], + content: button, + meta: meta, + action: this.controlAction, + }) } - /** - * Converts escaped HTML into usable HTML - * @param {String} html escaped HTML - * @return {String} parsed HTML - */ - parsedHtml = html => { - const escapeElement = document.createElement('textarea') - escapeElement.innerHTML = html - return escapeElement.textContent + promise() { + return fetchDependencies(this.dependencies) } /** @@ -72,23 +74,11 @@ export default class Control { * @param {Object|Number|String} args - string or key/val pairs for string lookups with variables * @return {String} the translated label */ - static i18n(lookup, args) { - const def = this.definition - let i18n = def.i18n || {} + i18n(lookup, args) { const locale = i18n.locale - i18n = i18n[locale] || i18n.default || i18n - - // if translation is defined in the control, return it - const value = typeof i18n === 'object' ? i18n[lookup] : i18n - if (value) { - return value - } + const controlTranslations = this.definition?.i18n + const localeTranslations = controlTranslations?.[locale] || {} - // otherwise check the i18n object - allow for mapping a lookup to a custom mi18n lookup - let mapped = def.i18n - if (typeof mapped === 'object') { - mapped = mapped[lookup] - } - return i18n.get(mapped, args) + return (localeTranslations[lookup]?.() ?? localeTranslations[lookup]) || i18n.get(lookup, args) } } diff --git a/src/lib/js/components/controls/form/button.js b/src/lib/js/components/controls/form/button.js index f70598b..9240dea 100644 --- a/src/lib/js/components/controls/form/button.js +++ b/src/lib/js/components/controls/form/button.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class ButtonControl extends Control { diff --git a/src/lib/js/components/controls/form/checkbox-group.js b/src/lib/js/components/controls/form/checkbox-group.js index 6f9b15b..99878b4 100644 --- a/src/lib/js/components/controls/form/checkbox-group.js +++ b/src/lib/js/components/controls/form/checkbox-group.js @@ -1,5 +1,5 @@ import { generateOptionConfig } from './shared.js' -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class CheckboxGroupControl extends Control { diff --git a/src/lib/js/components/controls/form/input.date.js b/src/lib/js/components/controls/form/input.date.js index 9d640e1..61bc1f2 100644 --- a/src/lib/js/components/controls/form/input.date.js +++ b/src/lib/js/components/controls/form/input.date.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class DateControl extends Control { diff --git a/src/lib/js/components/controls/form/input.file.js b/src/lib/js/components/controls/form/input.file.js index 96b2f0f..f18bd8e 100644 --- a/src/lib/js/components/controls/form/input.file.js +++ b/src/lib/js/components/controls/form/input.file.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class FileControl extends Control { diff --git a/src/lib/js/components/controls/form/input.hidden.js b/src/lib/js/components/controls/form/input.hidden.js index 18c3392..071d099 100644 --- a/src/lib/js/components/controls/form/input.hidden.js +++ b/src/lib/js/components/controls/form/input.hidden.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class HiddenControl extends Control { diff --git a/src/lib/js/components/controls/form/input.number.js b/src/lib/js/components/controls/form/input.number.js index 555931a..bfd3ba9 100644 --- a/src/lib/js/components/controls/form/input.number.js +++ b/src/lib/js/components/controls/form/input.number.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class NumberControl extends Control { diff --git a/src/lib/js/components/controls/form/input.text.js b/src/lib/js/components/controls/form/input.text.js index 3c7b30c..72d9eaa 100644 --- a/src/lib/js/components/controls/form/input.text.js +++ b/src/lib/js/components/controls/form/input.text.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' // import Components from '../..' diff --git a/src/lib/js/components/controls/form/radio-group.js b/src/lib/js/components/controls/form/radio-group.js index ec894f7..6c6b957 100644 --- a/src/lib/js/components/controls/form/radio-group.js +++ b/src/lib/js/components/controls/form/radio-group.js @@ -1,5 +1,5 @@ import { generateOptionConfig } from './shared.js' -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class RadioGroupControl extends Control { diff --git a/src/lib/js/components/controls/form/select.js b/src/lib/js/components/controls/form/select.js index c03c56a..f323ab0 100644 --- a/src/lib/js/components/controls/form/select.js +++ b/src/lib/js/components/controls/form/select.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import { generateOptionConfig } from './shared.js' import Control from '../control.js' diff --git a/src/lib/js/components/controls/form/shared.js b/src/lib/js/components/controls/form/shared.js index 7bba086..ca810ed 100644 --- a/src/lib/js/components/controls/form/shared.js +++ b/src/lib/js/components/controls/form/shared.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import { toTitleCase } from '../../../common/utils/string.mjs' export const generateOptionConfig = (type, count = 3) => diff --git a/src/lib/js/components/controls/form/textarea.js b/src/lib/js/components/controls/form/textarea.js index 72e19e4..ee5b5ce 100644 --- a/src/lib/js/components/controls/form/textarea.js +++ b/src/lib/js/components/controls/form/textarea.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class TextAreaControl extends Control { diff --git a/src/lib/js/components/controls/html/header.js b/src/lib/js/components/controls/html/header.js index c5e201d..57ad464 100644 --- a/src/lib/js/components/controls/html/header.js +++ b/src/lib/js/components/controls/html/header.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' const headerTags = Array.from(Array(5).keys()) @@ -8,6 +8,7 @@ const headerTags = Array.from(Array(5).keys()) const headerKey = 'controls.html.header' class HeaderControl extends Control { + constructor() { const header = { tag: headerTags[0], @@ -30,6 +31,15 @@ class HeaderControl extends Control { id: 'html.header', }, content: i18n.get(headerKey), + action: { + onRender: evt => { + console.log('evt', evt) + }, + click: evt => { + // debugger + console.log('evt', evt) + }, + }, } super(header) } @@ -41,7 +51,9 @@ class HeaderControl extends Control { return { // i18n custom mappings (defaults to camelCase type) i18n: { - header: 'Header222', + 'en-US': { + header: 'Custom English Header', + }, }, } } diff --git a/src/lib/js/components/controls/html/hr.js b/src/lib/js/components/controls/html/hr.js index 9b59853..04062cf 100644 --- a/src/lib/js/components/controls/html/hr.js +++ b/src/lib/js/components/controls/html/hr.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class HRControl extends Control { diff --git a/src/lib/js/components/controls/html/paragraph.js b/src/lib/js/components/controls/html/paragraph.js index 3831e73..4d21011 100644 --- a/src/lib/js/components/controls/html/paragraph.js +++ b/src/lib/js/components/controls/html/paragraph.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Control from '../control.js' class ParagraphControl extends Control { diff --git a/src/lib/js/components/controls/html/tinymce.js b/src/lib/js/components/controls/html/tinymce.js index 6721cd5..af7a7ae 100644 --- a/src/lib/js/components/controls/html/tinymce.js +++ b/src/lib/js/components/controls/html/tinymce.js @@ -1,7 +1,8 @@ +import { merge } from '../../../common/utils/index.mjs' import Control from '../control.js' class TinyMCEControl extends Control { - constructor() { + constructor(options) { const textAreaConfig = { tag: 'textarea', config: { @@ -16,19 +17,30 @@ class TinyMCEControl extends Control { attrs: { required: false, }, + dependencies: { js: 'cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js' }, + // this action is passed to the rendered control/element + // useful for actions and events on the control preview action: { - onRender: evt => { - if (evt.id) { - this.textareaID = evt.id - window.tinymce.remove('textarea#' + evt.id) - window.tinymce.init({ - selector: 'textarea#' + evt.id, - }) - } + onRender: elem => { + const selector = `#${elem.id}` + window.tinymce.remove(selector) + window.tinymce.init({ + selector: selector, + }) }, }, + controlAction: { + // callback when control is clicked + click: () => { + console.log('window.tinymce control clicked') + }, + // callback for when control is rendered + onRender: () => {}, + }, } - super(textAreaConfig) + + const mergedOptions = merge(textAreaConfig, options) + super(mergedOptions) } } diff --git a/src/lib/js/components/controls/index.js b/src/lib/js/components/controls/index.js index de2f0f7..14ff218 100644 --- a/src/lib/js/components/controls/index.js +++ b/src/lib/js/components/controls/index.js @@ -1,10 +1,10 @@ import Sortable from 'sortablejs' -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import actions from '../../common/actions.js' import { indexOfNode, orderObjectsBy } from '../../common/helpers.mjs' import events from '../../common/events.js' import dom from '../../common/dom.js' -import { match, unique, uuid, merge, clone } from '../../common/utils/index.mjs' +import { match, unique, merge, clone } from '../../common/utils/index.mjs' import Panels from '../panels.js' import Field from '../fields/field.js' import Control from './control.js' @@ -27,12 +27,16 @@ export class Controls { constructor() { this.data = new Map() - this.controlEvents = { + this.buttonActions = { + // this is used for keyboard navigation. when tabbing through controls it + // will auto navigated between the groups focus: ({ target }) => { const group = target.closest(`.${CONTROL_GROUP_CLASSNAME}`) return group && this.panels.nav.refresh(indexOfNode(group)) }, - click: ({ target }) => this.addElement(target.parentElement.id), + click: ({ target }) => { + this.addElement(target.parentElement.id) + }, } } @@ -40,10 +44,11 @@ export class Controls { * Methods to be called on initialization * @param {Object} controlOptions */ - init(controlOptions, sticky = false) { - this.applyOptions(controlOptions) - this.registerControls() + async init(controlOptions, sticky = false) { + // this.isReady = false + await this.applyOptions(controlOptions) this.buildDOM(sticky) + return this } @@ -51,36 +56,27 @@ export class Controls { * Generate control config for UI and bind actions * @return {Array} elementControls */ - registerControls() { - this.controls = this.elements.map(Element => { + registerControls(elements) { + this.controls = [] + return elements.map(async Element => { const isControl = typeof Element === 'function' - const control = isControl ? new Element() : new Control(Element) - const { - controlData: { meta, config }, - } = control - const controlLabel = isControl ? config.label : i18n.get(config.label) || config.label - const { id: controlId } = this.add(control) - const button = { - tag: 'button', - attrs: { - type: 'button', - }, - content: [{ tag: 'span', className: 'control-icon', children: dom.icon(meta.icon) }, controlLabel], - action: this.controlEvents, + + let control + if (isControl) { + control = new Element() + } else { + control = new Control(Element) } - control.dom = dom.create({ - tag: 'li', - id: controlId, - className: ['field-control', `${meta.group}-control`, `${meta.id}-control`], - content: button, - meta: meta, - }) + // not a fan of this pattern but its better + // than passing controls through Element constructor + // control.parent = this - return control.dom - }) + this.add(control) + this.controls.push(control.dom) - return this.controls + return control.promise() + }) } groupLabel = key => i18n.get(key) || key || '' @@ -153,11 +149,12 @@ export class Controls { } add(control = Object.create(null)) { - const { id, ...config } = control - const controlId = id || uuid() - const controlConfig = clone(config) - this.data.set(controlId, controlConfig) - return { id: controlId, ...controlConfig } + const controlConfig = clone(control) + this.data.set(controlConfig.id, controlConfig) + if (controlConfig.controlData.meta.id) { + this.data.set(controlConfig.controlData.meta.id, controlConfig.controlData) + } + return controlConfig } get(controlId) { @@ -371,12 +368,12 @@ export class Controls { return group !== 'layout' ? layoutTypes.field(controlData) : layoutTypes[metaId.replace('layout-', '')]() } - applyOptions = (controlOptions = {}) => { + applyOptions = async (controlOptions = {}) => { const { container, elements, groupOrder, ...options } = merge(defaultOptions, controlOptions) this.container = container this.groupOrder = unique(groupOrder.concat(['common', 'html', 'layout'])) - this.elements = elements.concat(defaultElements) this.options = options + return Promise.all(this.registerControls([...defaultElements, ...elements])) } } diff --git a/src/lib/js/components/fields/edit-panel-item.mjs b/src/lib/js/components/fields/edit-panel-item.mjs index 1e2c802..c301397 100644 --- a/src/lib/js/components/fields/edit-panel-item.mjs +++ b/src/lib/js/components/fields/edit-panel-item.mjs @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import { orderObjectsBy, indexOfNode } from '../../common/helpers.mjs' import dom from '../../common/dom.js' import animate from '../../common/animation.js' @@ -101,6 +101,7 @@ const ITEM_INPUT_TYPE_MAP = { }, object: val => { return Object.entries(val).map(([key, val]) => { + console.log(dom.childType(val)) return ITEM_INPUT_TYPE_MAP[dom.childType(val)](key, val) }) }, @@ -112,7 +113,10 @@ const INPUT_TYPE_ACTION = { boolean: (dataKey, field) => ({ click: ({ target: { checked } }) => { if (field.data?.attrs?.type === 'radio') { - field.set('options', field.data.options.map(option => ({ ...option, selected: false }))) + field.set( + 'options', + field.data.options.map(option => ({ ...option, selected: false })), + ) } field.set(dataKey, checked) field.updatePreview() @@ -207,7 +211,7 @@ export default class EditPanelItem { const orderedFields = orderObjectsBy( fields, CONDITION_INPUT_ORDER.map(fieldName => `condition-${fieldName}`), - 'className||dom.className' + 'className||dom.className', ) this.processConditionUIState(orderedFields) @@ -417,28 +421,31 @@ export default class EditPanelItem { itemInput(key, val) { const valType = dom.childType(val) || 'string' - const inputTypeConfig = Object.assign({}, { config: {}, attrs: {} }, ITEM_INPUT_TYPE_MAP[valType](key, val)) + const inputTypeConfig = { ...{ config: {}, attrs: {} }, ...ITEM_INPUT_TYPE_MAP[valType](key, val) } const dataKey = this.itemKey.replace(/.\d+$/, index => `${index}.${key}`) - const labelKey = dataKey - .split('.') - .filter(isNaN) - .join('.') + const labelKey = dataKey.split('.').filter(Number.isNaN).join('.') const [id, name] = [[...this.itemKey.split('.'), key], [key]].map(attrVars => - [this.field.id, ...attrVars].filter(Boolean).join('-') + [this.field.id, ...attrVars].filter(Boolean).join('-'), ) - inputTypeConfig.config = Object.assign({}, inputTypeConfig.config, { - label: this.panelName !== 'options' && labelHelper(labelKey), - labelAfter: false, - }) + inputTypeConfig.config = { + ...inputTypeConfig.config, + ...{ + label: this.panelName !== 'options' && labelHelper(labelKey), + labelAfter: false, + }, + } - inputTypeConfig.attrs = Object.assign({}, inputTypeConfig.attrs, { - name: inputTypeConfig.attrs.type === 'checkbox' ? `${name}[]` : name, - id, - disabled: this.isDisabled, - locked: this.isLocked, - }) + inputTypeConfig.attrs = { + ...inputTypeConfig.attrs, + ...{ + name: inputTypeConfig.attrs.type === 'checkbox' ? `${name}[]` : name, + id, + disabled: this.isDisabled, + locked: this.isLocked, + }, + } inputTypeConfig.action = { ...INPUT_TYPE_ACTION[valType](dataKey, this.field), diff --git a/src/lib/js/components/fields/edit-panel.js b/src/lib/js/components/fields/edit-panel.js index 8691143..e532732 100644 --- a/src/lib/js/components/fields/edit-panel.js +++ b/src/lib/js/components/fields/edit-panel.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import dom from '../../common/dom.js' import actions from '../../common/actions.js' import EditPanelItem from './edit-panel-item.mjs' diff --git a/src/lib/js/components/fields/field.js b/src/lib/js/components/fields/field.js index 4a57439..160a064 100644 --- a/src/lib/js/components/fields/field.js +++ b/src/lib/js/components/fields/field.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import dom from '../../common/dom.js' import Panels from '../panels.js' import { clone, throttle, unique } from '../../common/utils/index.mjs' @@ -7,6 +7,8 @@ import Component from '../component.js' import { FIELD_CLASSNAME, CONDITION_TEMPLATE, ANIMATION_SPEED_BASE } from '../../constants.js' import Components from '../index.js' import { toTitleCase } from '../../common/utils/string.mjs' +import { mergeActions } from '../../common/utils/object.mjs' +import controls from '../controls/index.js' const DEFAULT_DATA = () => ({ conditions: [CONDITION_TEMPLATE()], @@ -25,7 +27,7 @@ export default class Field extends Component { super('field', { ...DEFAULT_DATA(), ...fieldData }) this.label = dom.create(this.labelConfig) - this.preview = dom.create(this.fieldPreview()) + this.preview = dom.create({}) this.editPanels = [] const actionButtons = this.getActionButtons() @@ -186,7 +188,6 @@ export default class Field extends Component { * @return {Object} fieldEdit element config */ get fieldEdit() { - const _this = this this.editPanels = [] const editable = ['object', 'array'] const noPanels = ['config', 'meta', 'action', 'events', ...this.config.panels.disabled] @@ -197,18 +198,18 @@ export default class Field extends Component { className: ['field-edit', 'slide-toggle', 'formeo-panels-wrap'], } - allowedPanels.forEach(panelName => { + for (const panelName of allowedPanels) { const panelData = this.get(panelName) const propType = dom.childType(panelData) if (editable.includes(propType)) { const editPanel = new EditPanel(panelData, panelName, this) this.editPanels.push(editPanel) } - }) + } const panelsData = { panels: this.editPanels.map(({ panelConfig }) => panelConfig), - id: _this.id, + id: this.id, displayType: 'auto', } @@ -239,13 +240,52 @@ export default class Field extends Component { return fieldEdit } + get defaultPreviewActions() { + return { + change: evt => { + const { target } = evt + const { checked, type } = target + // @todo these kind of events should be added to control definitions + if (['checkbox', 'radio'].includes(type)) { + const optionIndex = +target.id.split('-').pop() + // uncheck options if radio + if (type === 'radio') { + this.set( + 'options', + this.get('options').map(option => ({ ...option, selected: false })), + ) + } + + const checkType = type === 'checkbox' ? 'checked' : 'selected' + this.set(`options.${optionIndex}.${checkType}`, checked) + } + }, + click: evt => { + if (evt.target.contentEditable === 'true') { + evt.preventDefault() + } + }, + input: evt => { + if (['input', 'meter', 'progress', 'button'].includes(this.data.tag)) { + super.set('attrs.value', evt.target.value) + } + + if (evt.target.contentEditable) { + super.set('content', evt.target.innerHTML) + } + }, + } + } + /** * Generate field preview config * @return {Object} fieldPreview */ fieldPreview() { const prevData = clone(this.data) + const { action = {} } = controls.get(prevData.meta.id) prevData.id = `prev-${this.id}` + prevData.action = action if (this.data?.config.editableContent) { prevData.attrs = { ...prevData.attrs, contenteditable: true } @@ -257,40 +297,7 @@ export default class Field extends Component { style: this.isEditing && 'display: none;', }, content: dom.create(prevData, true), - action: { - change: evt => { - const { target } = evt - const { checked, type } = target - // @todo these kind of events should be added to control definitions - if (['checkbox', 'radio'].includes(type)) { - const optionIndex = +target.id.split('-').pop() - // uncheck options if radio - if (type === 'radio') { - this.set( - 'options', - this.get('options').map(option => ({ ...option, selected: false })), - ) - } - - const checkType = type === 'checkbox' ? 'checked' : 'selected' - this.set(`options.${optionIndex}.${checkType}`, checked) - } - }, - click: evt => { - if (evt.target.contentEditable === 'true') { - evt.preventDefault() - } - }, - input: evt => { - if (['input', 'meter', 'progress', 'button'].includes(this.data.tag)) { - super.set('attrs.value', evt.target.value) - } - - if (evt.target.contentEditable) { - super.set('content', evt.target.innerHTML) - } - }, - }, + action: this.defaultPreviewActions, } return fieldPreview diff --git a/src/lib/js/components/index.js b/src/lib/js/components/index.js index 2f8051f..988472b 100644 --- a/src/lib/js/components/index.js +++ b/src/lib/js/components/index.js @@ -40,19 +40,14 @@ export class Components extends Data { } } - load = (formData, opts = this.opts || Object.create(null)) => { + load = (formDataArg, opts = this.opts || Object.create(null)) => { + let formData = formDataArg this.empty() - if (typeof formData === 'string') { - formData = JSON.parse(formData) + if (typeof formDataArg === 'string') { + formData = JSON.parse(formDataArg) } this.opts = opts - const { - stages = { [uuid()]: {} }, - rows, - columns, - fields, - id = uuid(), - } = { ...this.sessionFormData(), ...formData } + const { stages = { [uuid()]: {} }, rows, columns, fields, id = uuid() } = { ...this.sessionFormData(), ...formData } this.set('id', id) this.add('stages', Stages.load(stages)) this.add('rows', Rows.load(rows)) @@ -60,7 +55,9 @@ export class Components extends Data { this.add('fields', Fields.load(fields)) this.add('externals', Externals.load(opts.external)) - Object.values(this.get('stages')).forEach(stage => stage.loadChildren()) + for (const stage of Object.values(this.get('stages'))) { + stage.loadChildren() + } return this.data } @@ -72,9 +69,9 @@ export class Components extends Data { flatList(data = this.data, acculumator = Object.create(null)) { return Object.entries(data).reduce((acc, [type, components]) => { if (typeof components === 'object') { - Object.entries(components).forEach(([id, component]) => { + for (const [id, component] of Object.entries(components)) { acc[`${type}.${id}`] = component - }) + } } return acc }, acculumator) diff --git a/src/lib/js/components/panels.js b/src/lib/js/components/panels.js index 6350d4f..2763d28 100644 --- a/src/lib/js/components/panels.js +++ b/src/lib/js/components/panels.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Sortable from 'sortablejs' import h, { indexOfNode } from '../common/helpers.mjs' import dom from '../common/dom.js' @@ -10,7 +10,10 @@ const defaults = Object.freeze({ displayType: 'slider', }) -const getTransition = val => ({ transform: `translateX(${val ? `${val}px` : 0})` }) +const getTransition = val => { + const translateXVal = val ? `${val}px` : 0 + return { transform: `translateX(${translateXVal})` } +} /** * Edit and control sliding panels @@ -31,13 +34,19 @@ export default class Panels { const panelsWrap = this.createPanelsWrap() this.nav = this.navActions() - const resizeObserver = new window.ResizeObserver(([{ contentRect: { width } }]) => { - if (this.currentWidth !== width) { - this.toggleTabbedLayout() - this.currentWidth = width - this.nav.setTranslateX(this.activePanelIndex, false) - } - }) + const resizeObserver = new window.ResizeObserver( + ([ + { + contentRect: { width }, + }, + ]) => { + if (this.currentWidth !== width) { + this.toggleTabbedLayout() + this.currentWidth = width + this.nav.setTranslateX(this.activePanelIndex, false) + } + }, + ) const observeTimeout = window.setTimeout(() => { resizeObserver.observe(panelsWrap) @@ -47,7 +56,7 @@ export default class Panels { getPanelDisplay() { const column = this.panelsWrap - const width = parseInt(dom.getStyle(column, 'width')) + const width = Number.parseInt(dom.getStyle(column, 'width')) const autoDisplayType = width > 390 ? 'tabbed' : 'slider' const isAuto = this.opts.displayType === 'auto' this.panelDisplay = isAuto ? autoDisplayType : this.opts.displayType || defaults.displayType @@ -84,7 +93,7 @@ export default class Panels { createPanelsWrap() { const panelsWrap = dom.create({ className: 'panels', - content: this.opts.panels.map(({ config: { label }, ...panel }) => panel), + content: this.opts.panels.map(({ config: _config, ...panel }) => panel), }) if (this.opts.type === 'field') { @@ -104,24 +113,23 @@ export default class Panels { * @return {Array} panel groups */ sortableProperties(panels) { - const _this = this const groups = panels.getElementsByClassName('field-edit-group') - return h.forEach(groups, (group, index) => { - group.fieldId = _this.opts.id + return h.forEach(groups, group => { + group.fieldId = this.opts.id if (group.isSortable) { Sortable.create(group, { animation: 150, group: { - name: 'edit-' + group.editGroup, + name: `edit-${group.editGroup}`, pull: true, put: ['properties'], }, sort: true, handle: '.prop-order', onSort: evt => { - _this.propertySave(evt.to) - _this.resizePanels() + this.propertySave(evt.to) + this.resizePanels() }, }) } @@ -224,7 +232,7 @@ export default class Panels { let offset = { nav: 0, panel: 0 } let lastOffset = { ...offset } - const groupChange = newIndex => { + action.groupChange = newIndex => { const labels = labelWrap.children dom.removeClasses(siblingGroups, 'active-panel') dom.removeClasses(labels, 'active-tab') @@ -232,6 +240,7 @@ export default class Panels { this.currentPanel.classList.add('active-panel') labels[newIndex].classList.add('active-tab') + return this.currentPanel } @@ -272,18 +281,18 @@ export default class Panels { panelTransition.addEventListener('finish', handleFinish) } - action.setTranslateX = (panelIndex, animate = true) => { - offset = getOffset(panelIndex || this.activePanelIndex) + action.setTranslateX = (panelIndex = this.activePanelIndex, animate = true) => { + offset = getOffset(panelIndex) translateX({ offset, animate }) } - action.refresh = newIndex => { - if (newIndex !== undefined) { + action.refresh = (newIndex = this.activePanelIndex) => { + if (this.activePanelIndex !== newIndex) { this.activePanelIndex = newIndex - groupChange(newIndex) + action.groupChange(newIndex) } + action.setTranslateX(this.activePanelIndex) this.resizePanels() - action.setTranslateX(this.activePanelIndex, false) } /** @@ -293,7 +302,7 @@ export default class Panels { action.nextGroup = () => { const newIndex = this.activePanelIndex + 1 if (newIndex !== siblingGroups.length) { - const curPanel = groupChange(newIndex) + const curPanel = action.groupChange(newIndex) offset = { nav: -labelWrap.offsetWidth * newIndex, panel: -curPanel.offsetLeft, @@ -314,7 +323,7 @@ export default class Panels { action.prevGroup = () => { if (this.activePanelIndex !== 0) { const newIndex = this.activePanelIndex - 1 - const curPanel = groupChange(newIndex) + const curPanel = action.groupChange(newIndex) offset = { nav: -labelWrap.offsetWidth * newIndex, panel: -curPanel.offsetLeft, diff --git a/src/lib/js/components/rows/row.js b/src/lib/js/components/rows/row.js index 45ac2e6..ac93ad0 100644 --- a/src/lib/js/components/rows/row.js +++ b/src/lib/js/components/rows/row.js @@ -1,4 +1,4 @@ -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import Sortable from 'sortablejs' import Component from '../component.js' import dom from '../../common/dom.js' @@ -50,7 +50,7 @@ export default class Row extends Component { content: [this.getComponentTag(), this.getActionButtons(), this.editWindow, children], }) - this.sortable = Sortable.create(children, { + Sortable.create(children, { animation: 150, fallbackClass: 'column-moving', forceFallback: true, diff --git a/src/lib/js/components/stages/stage.js b/src/lib/js/components/stages/stage.js index 23e6481..76eb8dd 100644 --- a/src/lib/js/components/stages/stage.js +++ b/src/lib/js/components/stages/stage.js @@ -72,7 +72,7 @@ export default class Stage extends Component { children, }) - this.sortable = Sortable.create(children, { + Sortable.create(children, { animation: 150, fallbackClass: 'row-moving', forceFallback: true, diff --git a/src/lib/js/config.js b/src/lib/js/config.js index b15973d..011c66b 100644 --- a/src/lib/js/config.js +++ b/src/lib/js/config.js @@ -1,4 +1,4 @@ -import mi18n from 'mi18n' +import mi18n from '@draggable/i18n' import { isIE } from './common/helpers' const EN_US = import.meta.env.EN_US diff --git a/src/lib/js/constants.js b/src/lib/js/constants.js index 660fe79..da71b2e 100644 --- a/src/lib/js/constants.js +++ b/src/lib/js/constants.js @@ -101,7 +101,7 @@ export const COLUMN_TEMPLATES = new Map( columnTemplates.reduce((acc, cur, idx) => { acc.push([idx, cur]) return acc - }), + }, []), ) export const CHANGE_TYPES = [{ type: 'added', condition: (o, n) => Boolean(o === undefined && n) }] diff --git a/src/lib/js/editor.js b/src/lib/js/editor.js index b8cbd34..49a7c1b 100644 --- a/src/lib/js/editor.js +++ b/src/lib/js/editor.js @@ -1,6 +1,5 @@ -'use strict' import '../sass/formeo.scss' -import i18n from 'mi18n' +import i18n from '@draggable/i18n' import dom from './common/dom.js' import Events from './common/events.js' import Actions from './common/actions.js' @@ -8,7 +7,7 @@ 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 { sessionStorage, merge } from './common/utils/index.mjs' +import { merge } from './common/utils/index.mjs' import { defaults } from './config.js' import sprite from '../icons/formeo-sprite.svg?raw' @@ -22,7 +21,6 @@ export class FormeoEditor { * @return {Object} formeo references and actions */ constructor({ formData, ...options }, userFormData) { - const _this = this const mergedOptions = merge(defaults.editor, options) const { actions, events, debug, config, editorContainer, ...opts } = mergedOptions @@ -42,10 +40,10 @@ export class FormeoEditor { Actions.init({ debug, sessionStorage: opts.sessionStorage, ...actions }) // Load remote resources such as css and svg sprite - _this.loadResources().then(() => { + this.loadResources().then(() => { if (opts.allowEdit) { - _this.edit = _this.init.bind(_this) - _this.init() + // this.edit = this.init.bind(this) + this.init() } }) } @@ -82,7 +80,7 @@ export class FormeoEditor { promises.push(insertIcons(sprite)) } - promises.push(i18n.init({ ...this.opts.i18n, locale: sessionStorage.get(SESSION_LOCALE_KEY) })) + promises.push(i18n.init({ ...this.opts.i18n, locale: window.sessionStorage?.getItem(SESSION_LOCALE_KEY) })) return Promise.all(promises) } @@ -93,22 +91,22 @@ export class FormeoEditor { * dom elements, actions events and more. */ init() { - const _this = this - this.load(this.userFormData, _this.opts) - this.controls = Controls.init(_this.opts.controls, _this.opts.stickyControls) - _this.formId = Components.get('id') - this.i18n = { - setLang: formeoLocale => { - sessionStorage.set(SESSION_LOCALE_KEY, formeoLocale) - const loadLang = i18n.setCurrent(formeoLocale) - loadLang.then(() => { - this.controls = Controls.init(_this.opts.controls) - _this.render() - }, console.error) - }, - } + Controls.init(this.opts.controls, this.opts.stickyControls).then(controls => { + this.controls = controls + this.load(this.userFormData, this.opts) + this.formId = Components.get('id') + this.i18n = { + setLang: formeoLocale => { + window.sessionStorage?.setItem(SESSION_LOCALE_KEY, formeoLocale) + const loadLang = i18n.setCurrent(formeoLocale) + loadLang.then(() => { + this.init() + }, console.error) + }, + } - _this.render() + this.render() + }) } load(formData = this.userFormData, opts = this.opts) { @@ -120,16 +118,15 @@ export class FormeoEditor { * @return {void} */ render() { - const script = document.createElement('script') - script.src = 'https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js' - - document.head.appendChild(script) + if (!this.controls) { + return window.requestAnimationFrame(() => this.render()) + } this.stages = Object.values(Components.get('stages')) if (this.opts.controlOnLeft) { - this.stages.forEach(stage => { + for (const stage of this.stages) { stage.dom.style.order = 1 - }) + } } const elemConfig = { attrs: { diff --git a/src/lib/js/renderer.js b/src/lib/js/renderer.js index dc17b4f..cb9b099 100644 --- a/src/lib/js/renderer.js +++ b/src/lib/js/renderer.js @@ -1,7 +1,8 @@ import isEqual from 'lodash/isEqual' import dom from './common/dom' -import { uuid, isAddress, isExternalAddress } from './common/utils' +import { uuid, isAddress, isExternalAddress, merge } from './common/utils' import { STAGE_CLASSNAME, UUID_REGEXP } from './constants' +import { fetchDependencies } from './common/loaders' const RENDER_PREFIX = 'f-' @@ -37,12 +38,13 @@ const createRemoveButton = () => export default class FormeoRenderer { constructor(opts, formData = {}) { - const { renderContainer, external } = processOptions(opts) + const { renderContainer, external, elements } = processOptions(opts) this.container = renderContainer this.form = formData this.external = external this.dom = dom this.components = Object.create(null) + this.elements = elements } /** @@ -72,7 +74,11 @@ export default class FormeoRenderer { } } - orderChildren = (type, order) => order.reduce((acc, cur) => [...acc, this.form[type][cur]], []) + orderChildren = (type, order) => + order.reduce((acc, cur) => { + acc.push(this.form[type][cur]) + return acc + }, []) prefixId = id => RENDER_PREFIX + id @@ -91,10 +97,12 @@ export default class FormeoRenderer { }) processRows = stageId => - this.orderChildren('rows', this.form.stages[stageId].children).reduce( - (acc, row) => (row ? [...acc, this.processRow(row)] : acc), - [], - ) + this.orderChildren('rows', this.form.stages[stageId].children).reduce((acc, row) => { + if (row) { + acc.push(this.processRow(row)) + } + return acc + }, []) cacheComponent = data => { this.components[baseId(data.id)] = data @@ -118,10 +126,15 @@ export default class FormeoRenderer { { condition: config.inputGroup, result: () => this.addButton(id) }, ] - const children = configConditions.reduce((acc, { condition, result }) => (condition ? [...acc, result()] : acc), []) + const children = configConditions.reduce((acc, { condition, result }) => { + if (condition) { + acc.push(result()) + } + return acc + }, []) if (config.inputGroup) { - className.push(RENDER_PREFIX + 'input-group-wrap') + className.push(`${RENDER_PREFIX}input-group-wrap`) } return { @@ -164,12 +177,25 @@ export default class FormeoRenderer { ) } - processFields = fieldIds => { + processFieldsOrig = fieldIds => { return this.orderChildren('fields', fieldIds).map(({ id, ...field }) => this.cacheComponent(Object.assign({}, field, { id: this.prefixId(id) })), ) } + processFields = fieldIds => { + return this.orderChildren('fields', fieldIds).map(({ id, ...field }) => { + const { action = {}, dependencies = {} } = this.elements[field.meta.id] || {} + if (dependencies) { + fetchDependencies(dependencies) + } + + const mergedFieldData = merge({ action }, field) + + return this.cacheComponent({ ...mergedFieldData, id: this.prefixId(id) }) + }) + } + get processedData() { return Object.values(this.form.stages).map(stage => { stage.children = this.processRows(stage.id) @@ -233,7 +259,7 @@ export default class FormeoRenderer { // Compare as string, this allows values like "true" to be checked for properties like "checked". const sourceValue = String(evt.target[sourceProperty]) const targetValue = String(isAddress(target) ? this.getComponent(target)[targetProperty] : target) - return comparisonMap[comparison] && comparisonMap[comparison](sourceValue, targetValue) + return comparisonMap[comparison]?.(sourceValue, targetValue) } execResult = ({ assignment, target, targetProperty, value }) => { @@ -252,7 +278,7 @@ export default class FormeoRenderer { elem.required = elem._required }, } - propMap[targetProperty] && propMap[targetProperty]() + propMap[targetProperty]?.() }, } @@ -263,7 +289,7 @@ export default class FormeoRenderer { if (elem && elem._required === undefined) { elem._required = elem.required } - assignMap[assignment] && assignMap[assignment](elem) + assignMap[assignment]?.(elem) } } diff --git a/tools/copy-directory.mjs b/tools/copy-directory.mjs new file mode 100644 index 0000000..0a6661a --- /dev/null +++ b/tools/copy-directory.mjs @@ -0,0 +1,44 @@ +// copyDir.mjs +import { promises as fs } from 'node:fs' +import { join, basename } from 'node:path' +// import { fileURLToPath } from 'node:url' + +const [, , src, dest] = process.argv + +async function copyDirectory(srcDir, destDir) { + // Create destination directory if it doesn't exist + await fs.mkdir(destDir, { recursive: true }) + + // Read all items in the source directory + const items = await fs.readdir(srcDir, { withFileTypes: true }) + + for (const item of items) { + const srcPath = join(srcDir, item.name) + const destPath = join(destDir, item.name) + + if (item.isDirectory()) { + // Recursively copy subdirectories + await copyDirectory(srcPath, destPath) + } else { + // Copy files + await fs.copyFile(srcPath, destPath) + } + } +} + +// Entry point +async function main() { + if (!src || !dest) { + console.error('Usage: node copyDir.mjs ') + process.exit(1) + } + + try { + await copyDirectory(src, dest) + console.log(`Copied ${basename(src)} to ${dest}`) + } catch (error) { + console.error(`Error copying directory: ${error.message}`) + } +} + +main() diff --git a/vite.config.js b/vite.config.js index 1825161..0ed2e01 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,15 +1,13 @@ import { resolve } from 'node:path' import { defineConfig } from 'vite' -import formeoI18n from 'formeo-i18n' -import { I18N } from 'mi18n' +import { languageFileOptions, enUS } from '@draggable/formeo-languages' import banner from 'vite-plugin-banner' import compression from 'vite-plugin-compression' +import { viteStaticCopy } from 'vite-plugin-static-copy' import { createHtmlPlugin } from 'vite-plugin-html' import pkg from './package.json' -const { languageFiles, enUS } = formeoI18n - const bannerTemplate = ` /** ${pkg.name} - ${pkg.homepage} @@ -18,6 +16,15 @@ Author: ${pkg.author} */ ` +const isImage = extType => /png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType) +const getExtType = assetInfo => { + let [extType] = assetInfo.name.split('.').reverse() + if (isImage(extType)) { + extType = 'img' + } + return extType +} + const sharedConfig = { server: { open: true, @@ -37,58 +44,46 @@ const sharedConfig = { }, } -const libConfig = { - ...sharedConfig, - build: { - emptyOutDir: false, - lib: { - entry: 'src/lib/js/index.js', - name: 'Formeo', - fileName: () => 'formeo.min.js', - formats: ['umd'], - outDir: resolve(__dirname, 'dist'), - }, - minify: 'terser', - terserOptions: { - compress: { - drop_console: true, - drop_debugger: true, - }, - }, - }, - plugins: [ - banner(bannerTemplate), - compression({ - algorithm: 'gzip', - minRatio: 0.8, - ext: '.gz', - threshold: 10240, - }), - ], -} - -const demoConfig = { +export default defineConfig({ ...sharedConfig, root: 'src/demo', base: process.env.NODE_ENV === 'production' ? '/formeo/' : '/', resolve: { alias: { - 'formeo': resolve(__dirname, 'src/lib/js/index.js'), + formeo: resolve(__dirname, 'src/lib/js/index.js'), }, }, build: { - emptyOutDir: false, + emptyOutDir: true, rollupOptions: { input: { demo: resolve(__dirname, 'src/demo/index.html'), }, output: { - manualChunks:{ + manualChunks: { formeo: ['formeo'], }, + entryFileNames: 'assets/js/[name].min.js', + chunkFileNames: 'assets/js/[name].min.js', + assetFileNames: assetInfo => { + const extType = getExtType(assetInfo) + const ext = extType === 'img' ? '[ext]' : 'min.[ext]' + return `assets/${getExtType(assetInfo)}/[name].${ext}` + }, }, }, outDir: resolve(__dirname, 'dist/demo'), + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + output: { + comments: false, + beautify: false, + }, + }, }, plugins: [ createHtmlPlugin({ @@ -98,24 +93,32 @@ const demoConfig = { filename: 'index.html', inject: { data: { - langFiles: Object.entries(languageFiles).map(([locale, val]) => { - const lang = I18N.processFile(val) - return { - locale, - dir: lang.dir, - nativeName: lang[locale], - } - }), + langFiles: languageFileOptions, version: pkg.version, }, }, }), + banner(bannerTemplate), + compression({ + algorithm: 'gzip', + minRatio: 0.8, + ext: '.gz', + threshold: 10240, + }), + viteStaticCopy({ + targets: [ + { + src: resolve('src/lib/icons/formeo-sprite.svg'), + dest: './assets/img/', + }, + { + src: resolve('node_modules', '@draggable/formeo-languages/dist/lang/*'), + dest: './assets/lang', + }, + ], + }), ], -} - -export default defineConfig(({ mode }) => { - if (mode === 'lib') { - return libConfig - } - return demoConfig + // optimizeDeps: { + // exclude: ['@draggable/i18n'] + // } }) diff --git a/vite.config.lib.mjs b/vite.config.lib.mjs new file mode 100644 index 0000000..f0ca6d3 --- /dev/null +++ b/vite.config.lib.mjs @@ -0,0 +1,50 @@ +import { defineConfig } from 'vite' +import banner from 'vite-plugin-banner' +import { resolve } from 'node:path' + +import pkg from './package.json' + +const bannerTemplate = ` +/** +${pkg.name} - ${pkg.homepage} +Version: ${pkg.version} +Author: ${pkg.author} +*/ +` + +export default defineConfig({ + root: './', + build: { + lib: { + entry: resolve(__dirname, 'src/lib/js/index.js'), + name: 'formeo', + fileName: format => `[name].${format}.js`, + formats: ['es', 'cjs', 'umd'], + }, + outDir: 'dist', + emptyOutDir: true, + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + }, + rollupOptions: { + input: { + formeo: resolve(__dirname, 'src/lib/js/index.js'), + }, + output: { + assetFileNames: 'formeo.min.[ext]', + }, + }, + }, + plugins: [banner(bannerTemplate)], + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, + }, +})