Skip to content

Commit

Permalink
feat: add tooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinchappell committed Nov 15, 2024
1 parent 207696e commit 7f3c0b8
Show file tree
Hide file tree
Showing 8 changed files with 1,353 additions and 784 deletions.
2,011 changes: 1,269 additions & 742 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"dependencies": {
"@draggable/formeo-languages": "^3.1.3",
"@draggable/i18n": "^1.0.7",
"@draggable/tooltip": "^1.2.1",
"lodash": "^4.17.21",
"sortablejs": "^1.15.3"
},
Expand Down
82 changes: 51 additions & 31 deletions src/lib/js/common/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ class DOM {
return
}

const { className, options, ...elem } = this.processTagName(elemArg)
const _this = this
const processed = ['children', 'content']
const { className, options, dataset, ...elem } = this.processTagName(elemArg)
processed.push('tag')
let childType
const { tag } = elem
const processed = ['children', 'content']
let i
const wrap = {
attrs: {},
Expand Down Expand Up @@ -141,11 +142,9 @@ class DOM {
boolean: () => null,
}

processed.push('tag')

// check for root className property
if (className) {
elem.attrs = { ...elem.attrs, className }
elem.attrs = merge(elem.attrs, { className })
}

if (options) {
Expand Down Expand Up @@ -182,7 +181,10 @@ class DOM {
const label = _this.label(elem)

if (!elem.config.hideLabel) {
const wrapContent = [...(_this.labelAfter(elem) ? [element, label] : [label, element])]
const wrapContent = [label, element]
if (_this.labelAfter(elem)) {
wrapContent.reverse()
}
wrap.children.push(wrapContent)
}
}
Expand All @@ -201,10 +203,10 @@ class DOM {
}

// Set the new element's dataset
if (elem.dataset) {
for (const data in elem.dataset) {
if (Object.hasOwn(elem.dataset, data)) {
element.dataset[data] = typeof elem.dataset[data] === 'function' ? elem.dataset[data]() : elem.dataset[data]
if (dataset) {
for (const data in dataset) {
if (Object.hasOwn(dataset, data)) {
element.dataset[data] = typeof dataset[data] === 'function' ? dataset[data]() : dataset[data]
}
}
processed.push('dataset')
Expand Down Expand Up @@ -268,7 +270,7 @@ class DOM {
const createSvgIconConfig = symbolId => ({
tag: 'svg',
attrs: {
className: `svg-icon ${symbolId}`,
className: ['svg-icon', symbolId],
},
children: [
{
Expand All @@ -283,9 +285,10 @@ class DOM {

this.iconSymbols = Array.from(iconSymbolNodes).reduce((acc, symbol) => {
const name = symbol.id.replace(iconPrefix, '')
acc[name] = dom.create(createSvgIconConfig(symbol.id))
acc[name] = createSvgIconConfig(symbol.id)
return acc
}, {})
this.cachedIcons = {}

return this.iconSymbols
}
Expand All @@ -296,20 +299,33 @@ class DOM {
* - we don't need the perks of having icons be DOM objects at this stage
* - it forces the icon to be appended using innerHTML which helps svg render
* @param {String} name - icon name
* @param {Function} config - dom element config object
* @return {String} icon markup
*/
icon(name = null, classNames = []) {
icon(name, config) {
if (!name) {
return
}

const icon = this.icons[name]
const cacheKey = `${name}?${new URLSearchParams(config).toString()}`

if (this.cachedIcons?.[cacheKey]) {
return this.cachedIcons[cacheKey]
}

const iconConfig = this.icons[name]

if (iconConfig) {
if (config) {
const mergedConfig = merge(iconConfig, config)

this.cachedIcons[cacheKey] = dom.create(mergedConfig).outerHTML

if (icon) {
const iconClone = icon.cloneNode(true)
iconClone.classList.add(...classNames)
return this.cachedIcons[cacheKey]
}

return iconClone.outerHTML
this.cachedIcons[cacheKey] = dom.create(iconConfig).outerHTML
return this.cachedIcons[cacheKey]
}

return iconFontTemplates[dom.options.iconFont]?.(name) || name
Expand All @@ -325,32 +341,36 @@ class DOM {
processAttrs(elem, element, isPreview) {
const { attrs = {} } = elem

if (!isPreview) {
if (!attrs.name && this.isInput(elem.tag)) {
element.setAttribute('name', uuid(elem))
}
if (!isPreview && !attrs.name && this.isInput(elem.tag)) {
element.setAttribute('name', uuid(elem))
}

// Set element attributes
for (const attr of Object.keys(attrs)) {
const name = h.safeAttrName(attr)
let value = attrs[attr] || ''

if (Array.isArray(value)) {
if (typeof value[0] === 'object') {
const selected = value.filter(t => t.selected === true)
value = selected.length ? selected[0].value : value[0].value
} else {
value = value.join(' ')
}
}
const value = this.processAttrValue(attrs[attr])

if (value) {
element.setAttribute(name, value === true ? '' : value)
}
}
}

processAttrValue(valueArg) {
let value = valueArg || ''
if (Array.isArray(value)) {
if (typeof value[0] === 'object') {
const selected = value.filter(t => t.selected === true)
value = selected.length ? selected[0].value : value[0].value
} else {
value = value.join(' ')
}
}

return value
}

/**
* Hide or show an Array or HTMLCollection of elements
* @param {Array} elems
Expand Down
16 changes: 12 additions & 4 deletions src/lib/js/common/utils/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,24 @@ export const uuid = elem => {

/**
* Merge one object with another.
* This is expensive, use as little as possible.
* This can be expensive, use as little as possible.
* @param {Object} obj1
* @param {Object} obj2
* @return {Object} merged object
*/
export const merge = (obj1, obj2, opts = Object.create(null)) => {
export const merge = (obj1, obj2) => {
const customizer = (objValue, srcValue) => {
if (Array.isArray(objValue)) {
if (Array.isArray(srcValue)) {
return unique(opts.mergeArray ? objValue.concat(srcValue) : srcValue)
if (srcValue !== undefined && srcValue !== null) {
return unique(objValue.concat(srcValue))
}

return srcValue
}

if (Array.isArray(srcValue)) {
if (objValue !== undefined && objValue !== null) {
return unique(srcValue.concat(objValue))
}

return srcValue
Expand Down
4 changes: 1 addition & 3 deletions src/lib/js/components/columns/column.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ export default class Column extends Component {
constructor(columnData) {
super('column', { ...DEFAULT_DATA(), ...columnData })

const _this = this

const children = this.createChildWrap()

this.dom = dom.create({
Expand All @@ -66,7 +64,7 @@ export default class Column extends Component {
events.columnResized = new window.CustomEvent('columnResized', {
detail: {
column: this.dom,
instance: _this,
instance: this,
},
})

Expand Down
6 changes: 3 additions & 3 deletions src/lib/js/components/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default class Component extends Data {
return {
className: [`${this.name}-actions`, 'group-actions'],
action: {
mouseenter: ({ target }) => {
mouseenter: () => {
Components.stages.active.dom.classList.add(`active-hover-${this.name}`)
this.dom.classList.add(...hoverClassnames)
},
Expand Down Expand Up @@ -144,10 +144,10 @@ export default class Component extends Data {
tag: 'span',
className: ['component-tag', `${this.name}-tag`],
children: [
(this.isColumn || this.isField) && dom.icon('component-corner', ['bottom-left']),
(this.isColumn || this.isField) && dom.icon('component-corner', { className: 'bottom-left' }),
dom.icon(`handle-${this.name}`),
toTitleCase(this.name),
(this.isColumn || this.isRow) && dom.icon('component-corner', ['bottom-right']),
(this.isColumn || this.isRow) && dom.icon('component-corner', { className: 'bottom-right' }),
].filter(Boolean),
})
}
Expand Down
15 changes: 14 additions & 1 deletion src/lib/js/components/rows/row.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,26 @@ export default class Row extends Component {
},
}

const rowTitleTooltip = {
tag: 'span',
content: ' ⓘ',
dataset: {
tooltip: 'Row title will be used as the legend for the fieldset',
},
}

const legendInput = {
tag: 'input',
attrs: {
type: 'text',
ariaLabel: 'Legend for fieldset',
value: this.get('config.legend'),
placeholder: 'Legend',
placeholder: 'Title',
},
config: {
label: {
children: ['Row Title', rowTitleTooltip],
},
},
action: {
input: ({ target: { value } }) => this.set('config.legend', value),
Expand Down
2 changes: 2 additions & 0 deletions src/lib/js/editor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '../sass/formeo.scss'
import i18n from '@draggable/i18n'
import { SmartTooltip } from '@draggable/tooltip'
import dom from './common/dom.js'
import Events from './common/events.js'
import Actions from './common/actions.js'
Expand Down Expand Up @@ -37,6 +38,7 @@ export class FormeoEditor {
this.dom = dom
Events.init({ debug, ...events })
Actions.init({ debug, sessionStorage: opts.sessionStorage, ...actions })
this.tooltip = new SmartTooltip()

// Load remote resources such as css and svg sprite
document.addEventListener('DOMContentLoaded', this.loadResources.bind(this))
Expand Down

0 comments on commit 7f3c0b8

Please sign in to comment.