Browse Source

Add an option to convert attributes with hyphens to camel case (#6501)

* Add convertDataGjsAttributesHyphens option

* up

* A few fixes

---------

Co-authored-by: Artur Arseniev <artur.catch@hotmail.it>
add-custom-renderer-hook
mohamed yahia 10 months ago
committed by GitHub
parent
commit
12f376b752
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      packages/core/src/data_sources/model/conditional_variables/DataCondition.ts
  2. 8
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  3. 12
      packages/core/src/parser/config/config.ts
  4. 22
      packages/core/src/parser/model/ParserHtml.ts
  5. 6
      packages/core/src/utils/dom.ts
  6. 161
      packages/core/test/specs/parser/model/ParserHtml.ts

13
packages/core/src/data_sources/model/conditional_variables/DataCondition.ts

@ -1,13 +1,12 @@
import { Model } from '../../../common';
import EditorModel from '../../../editor/model/Editor';
import DataVariable, { DataVariableProps } from '../DataVariable';
import { isDataVariable, valueOrResolve } from '../../utils';
import { DataCollectionStateMap } from '../data_collection/types';
import DataResolverListener from '../DataResolverListener';
import { valueOrResolve, isDataVariable } from '../../utils';
import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator';
import DataVariable, { DataVariableProps } from '../DataVariable';
import { ConditionProps, DataConditionEvaluator } from './DataConditionEvaluator';
import { BooleanOperation } from './operators/BooleanOperator';
import { StringOperation } from './operators/StringOperator';
import { isUndefined } from 'underscore';
import { DataCollectionStateMap } from '../data_collection/types';
import { DataConditionSimpleOperation } from './types';
export const DataConditionType = 'data-condition' as const;
@ -59,10 +58,6 @@ export class DataCondition extends Model<DataConditionProps> {
}
constructor(props: DataConditionProps, opts: DataConditionOptions) {
if (isUndefined(props.condition)) {
opts.em.logError('No condition was provided to a conditional component.');
}
super(props, opts);
this.em = opts.em;
this.collectionsStateMap = opts.collectionsStateMap ?? {};

8
packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts

@ -29,6 +29,7 @@ export default class ComponentDataCollection extends Component {
// @ts-ignore
...super.defaults,
droppable: false,
dataResolver: {},
type: DataCollectionType,
components: [
{
@ -39,8 +40,6 @@ export default class ComponentDataCollection extends Component {
}
constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) {
const dataResolver = props[keyCollectionDefinition];
if (opt.forCloning) {
return super(props as any, opt) as unknown as ComponentDataCollection;
}
@ -48,11 +47,6 @@ export default class ComponentDataCollection extends Component {
const em = opt.em;
const newProps = { ...props, droppable: false } as any;
const cmp: ComponentDataCollection = super(newProps, opt) as unknown as ComponentDataCollection;
if (!dataResolver) {
em.logError('missing collection definition');
return cmp;
}
this.rebuildChildrenFromCollection = this.rebuildChildrenFromCollection.bind(this);
this.listenToPropsChange();
this.rebuildChildrenFromCollection();

12
packages/core/src/parser/config/config.ts

@ -72,6 +72,17 @@ export interface HTMLParserOptions extends OptionAsDocument {
* preParser: htmlString => DOMPurify.sanitize(htmlString)
*/
preParser?: (input: string, opts: { editor: Editor }) => string;
/**
* Configures whether `data-gjs-*` attributes should be automatically converted from hyphenated to camelCase.
*
* When `true`:
* - Hyphenated `data-gjs-*` attributes (e.g., `data-gjs-my-component`) are transformed into camelCase (`data-gjs-myComponent`).
* - If `defaults` contains the camelCase version and not the original attribute, the camelCase will be used; otherwise, the original name is kept.
*
* @default false
*/
convertDataGjsAttributesHyphens?: boolean;
}
export interface ParserConfig {
@ -121,6 +132,7 @@ const config: () => ParserConfig = () => ({
allowUnsafeAttr: false,
allowUnsafeAttrValue: false,
keepEmptyTextNodes: false,
convertDataGjsAttributesHyphens: false,
},
});

22
packages/core/src/parser/model/ParserHtml.ts

@ -1,10 +1,10 @@
import { each, isArray, isFunction, isUndefined } from 'underscore';
import { each, isArray, isFunction, isUndefined, result } from 'underscore';
import { ObjectAny, ObjectStrings } from '../../common';
import { ComponentDefinitionDefined, ComponentStackItem } from '../../dom_components/model/types';
import EditorModel from '../../editor/model/Editor';
import { HTMLParseResult, HTMLParserOptions, ParseNodeOptions, ParserConfig } from '../config/config';
import BrowserParserHtml from './BrowserParserHtml';
import { doctypeToString } from '../../utils/dom';
import { doctypeToString, processDataGjsAttributeHyphen } from '../../utils/dom';
import { isDef } from '../../utils/mixins';
import { ParserEvents } from '../types';
@ -125,13 +125,17 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
return result;
},
parseNodeAttr(node: HTMLElement, result?: ComponentDefinitionDefined) {
const model = result || {};
parseNodeAttr(node: HTMLElement, modelResult?: ComponentDefinitionDefined) {
const model = modelResult || {};
const attrs = node.attributes || [];
const attrsLen = attrs.length;
const convertHyphens = !!config?.optionsHtml?.convertDataGjsAttributesHyphens;
const defaults =
(convertHyphens && !!model.type && result(em?.Components.getType(model.type)?.model.prototype, 'defaults')) ||
{};
for (let i = 0; i < attrsLen; i++) {
const nodeName = attrs[i].nodeName;
let nodeName = attrs[i].nodeName;
let nodeValue: string | boolean = attrs[i].nodeValue!;
if (nodeName == 'style') {
@ -142,7 +146,13 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
continue;
} else if (nodeName.indexOf(this.modelAttrStart) === 0) {
const propsResult = this.getPropAttribute(nodeName, nodeValue);
model[propsResult.name] = propsResult.value;
let resolvedName = propsResult.name;
if (convertHyphens && !(resolvedName in defaults)) {
const transformed = processDataGjsAttributeHyphen(resolvedName);
resolvedName = transformed in defaults ? transformed : resolvedName;
}
model[resolvedName] = propsResult.value;
} else {
// @ts-ignore Check for attributes from props (eg. required, disabled)
if (nodeValue === '' && node[nodeName] === true) {

6
packages/core/src/utils/dom.ts

@ -247,3 +247,9 @@ export const off = <E extends Event = Event>(
els.forEach((el) => el?.removeEventListener(ev, fn as EventListener, opts));
});
};
export const processDataGjsAttributeHyphen = (str: string): string => {
const camelCased = str.replace(/-([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
return camelCased;
};

161
packages/core/test/specs/parser/model/ParserHtml.ts

@ -816,4 +816,165 @@ describe('ParserHtml', () => {
});
});
});
describe('with convertDataGjsAttributesHyphens OFF (default)', () => {
beforeEach(() => {
em = new Editor({});
em.Components.addType('test-cmp', {
isComponent: (el) => el.tagName === 'a',
model: {
defaults: {
type: 'default',
testAttr: 'value',
otherAttr: 'value',
},
},
});
obj = ParserHtml(em, {
textTags: ['br', 'b', 'i', 'u'],
textTypes: ['text', 'textnode', 'comment'],
returnArray: true,
optionsHtml: { convertDataGjsAttributesHyphens: false },
});
obj.compTypes = em.Components.componentTypes;
});
test('keeps original attribute names', () => {
const str = '<a data-gjs-type="test-cmp" data-gjs-test-attr="value1" data-gjs-other-attr="value2"></a>';
const result = [
{
tagName: 'a',
type: 'test-cmp',
'test-attr': 'value1',
'other-attr': 'value2',
},
];
expect(obj.parse(str).html).toEqual(result);
});
test('does not convert data-gjs-data-resolver', () => {
const str = '<div data-gjs-type="data-variable" data-gjs-data-resolver="test"></div>';
const result = [
{
type: 'data-variable',
tagName: 'div',
'data-resolver': 'test',
},
];
expect(obj.parse(str).html).toEqual(result);
});
});
describe('with convertDataGjsAttributesHyphens ON', () => {
beforeEach(() => {
em = new Editor({});
em.Components.addType('test-cmp', {
isComponent: (el) => el.tagName === 'a',
model: {
defaults: {
testAttr: 'value',
otherAttr: 'value',
nullAttr: null,
undefinedAttr: undefined,
'hyphen-attr': 'value',
duplicatedAttr: 'value',
'duplicated-attr': 'value',
},
},
});
obj = ParserHtml(em, {
returnArray: true,
optionsHtml: { convertDataGjsAttributesHyphens: true },
});
obj.compTypes = em.Components.componentTypes;
});
test('converts hyphenated to camelCase', () => {
const str = '<a data-gjs-type="test-cmp" data-gjs-test-attr="value1" data-gjs-other-attr="value2"></a>';
const result = [
{
tagName: 'a',
type: 'test-cmp',
testAttr: 'value1',
otherAttr: 'value2',
},
];
expect(obj.parse(str).html).toEqual(result);
});
test('handles null/undefined values', () => {
const str = '<a data-gjs-type="test-cmp" data-gjs-null-attr="value" data-gjs-undefined-attr="some value"></a>';
const result = [
{
tagName: 'a',
type: 'test-cmp',
nullAttr: 'value',
undefinedAttr: 'some value',
},
];
expect(obj.parse(str).html).toEqual(result);
});
test('converts data-gjs-data-resolver to dataResolver', () => {
const str = `
<div
data-gjs-type="data-variable"
data-gjs-data-resolver='{"type":"data-variable","path":"some path","collectionId":"someCollectionId"}'
></div>
`;
const result = [
{
tagName: 'div',
type: 'data-variable',
dataResolver: {
type: 'data-variable',
path: 'some path',
collectionId: 'someCollectionId',
},
},
];
expect(obj.parse(str).html).toEqual(result);
});
test('handles defaults with original hyphenated', () => {
const str = '<a data-gjs-type="test-cmp" data-gjs-hyphen-attr="value1"></a>';
const result = [
{
tagName: 'a',
type: 'test-cmp',
'hyphen-attr': 'value1',
},
];
expect(obj.parse(str).html).toEqual(result);
});
test('handles defaults not containing camelCase or hyphenated', () => {
const str = '<a data-gjs-type="test-cmp" data-gjs-new-attr="value1"></a>';
const result = [
{
tagName: 'a',
type: 'test-cmp',
'new-attr': 'value1',
},
];
expect(obj.parse(str).html).toEqual(result);
});
test('handles defaults with hyphenated and camelCase', () => {
const str = '<a data-gjs-type="test-cmp" data-gjs-duplicated-attr="value1"></a>';
const result = [
{
tagName: 'a',
type: 'test-cmp',
'duplicated-attr': 'value1',
},
];
expect(obj.parse(str).html).toEqual(result);
});
});
});

Loading…
Cancel
Save