Browse Source

Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into update-component-styles

update-component-styles
mohamedsalem401 8 months ago
parent
commit
597e658e0c
  1. 64
      docs/api/datasources.md
  2. 2
      packages/core/package.json
  3. 7
      packages/core/src/abstract/Module.ts
  4. 2
      packages/core/src/block_manager/index.ts
  5. 1
      packages/core/src/canvas/index.ts
  6. 2
      packages/core/src/canvas/model/CanvasSpots.ts
  7. 1
      packages/core/src/canvas/view/FrameView.ts
  8. 2
      packages/core/src/code_manager/model/CssGenerator.ts
  9. 2
      packages/core/src/commands/view/SelectComponent.ts
  10. 11
      packages/core/src/css_composer/index.ts
  11. 15
      packages/core/src/css_composer/model/CssRule.ts
  12. 9
      packages/core/src/css_composer/types.ts
  13. 14
      packages/core/src/css_composer/view/CssRuleView.ts
  14. 24
      packages/core/src/data_sources/index.ts
  15. 10
      packages/core/src/data_sources/model/DataRecord.ts
  16. 2
      packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts
  17. 1
      packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts
  18. 56
      packages/core/src/data_sources/types.ts
  19. 32
      packages/core/src/dom_components/model/ComponentVideo.ts
  20. 14
      packages/core/src/dom_components/types.ts
  21. 9
      packages/core/src/dom_components/view/ComponentTextView.ts
  22. 3
      packages/core/src/dom_components/view/ComponentVideoView.ts
  23. 3
      packages/core/src/dom_components/view/ComponentsView.ts
  24. 5
      packages/core/src/domain_abstract/model/StyleableModel.ts
  25. 10
      packages/core/src/index.ts
  26. 5
      packages/core/src/parser/model/ParserHtml.ts
  27. 60
      packages/core/src/utils/AutoScroller.ts
  28. 4
      packages/core/src/utils/sorter/BaseComponentNode.ts
  29. 1
      packages/core/src/utils/sorter/CanvasNewComponentNode.ts
  30. 94
      packages/core/src/utils/sorter/DropLocationDeterminer.ts
  31. 2
      packages/core/test/specs/css_composer/index.ts
  32. 12
      packages/core/test/specs/parser/model/ParserHtml.ts

64
docs/api/datasources.md

@ -3,40 +3,62 @@
## DataSources
This module manages data sources within the editor.
You can initialize the module with the editor by passing an instance of `EditorModel`.
Once the editor is instantiated, you can use the following API to manage data sources:
```js
const editor = new EditorModel();
const dsm = new DataSourceManager(editor);
const editor = grapesjs.init({ ... });
const dsm = editor.DataSources;
```
Once the editor is instantiated, you can use the following API to manage data sources:
## Available Events
* `data:add` Added new data source.
```js
const dsm = editor.DataSources;
```javascript
editor.on('data:add', (dataSource) => { ... });
```
* [add][1] - Add a new data source.
* [get][2] - Retrieve a data source by its ID.
* [getAll][3] - Retrieve all data sources.
* [remove][4] - Remove a data source by its ID.
* [clear][5] - Remove all data sources.
* `data:remove` Data source removed.
Example of adding a data source:
```javascript
editor.on('data:remove', (dataSource) => { ... });
```
```js
const ds = dsm.add({
id: 'my_data_source_id',
records: [
{ id: 'id1', name: 'value1' },
{ id: 'id2', name: 'value2' }
]
* `data:update` Data source updated.
```javascript
editor.on('data:update', (dataSource, changes) => { ... });
```
* `data:path` Data record path update.
```javascript
editor.on('data:path:SOURCE_ID.RECORD_ID.PROP_NAME', ({ dataSource, dataRecord, path }) => { ... });
editor.on('data:path', ({ dataSource, dataRecord, path }) => {
console.log('Path update in any data source')
});
```
### Parameters
* `data:pathSource` Data record path update per source.
```javascript
editor.on('data:pathSource:SOURCE_ID', ({ dataSource, dataRecord, path }) => { ... });
```
* `data` Catch-all event for all the events mentioned above.
```javascript
editor.on('data', ({ event, model, ... }) => { ... });
```
## Methods
* [add][1] - Add a new data source.
* [get][2] - Retrieve a data source by its ID.
* [getAll][3] - Retrieve all data sources.
* [remove][4] - Remove a data source by its ID.
* [clear][5] - Remove all data sources.
* `em` **EditorModel** Editor model.
[DataSource]: datasource.html
## add

2
packages/core/package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.22.7",
"version": "0.22.8",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",

7
packages/core/src/abstract/Module.ts

@ -44,7 +44,7 @@ export default abstract class Module<T extends ModuleConfig = ModuleConfig> impl
collections: Collection[] = [];
cls: any[] = [];
state?: Model;
events: any;
events: object = {};
model?: any;
view?: any;
@ -129,18 +129,19 @@ export abstract class ItemManagerModule<
cls: any[] = [];
all: TCollection;
view?: View;
events!: Record<string, string>;
constructor(
em: EditorModel,
moduleName: string,
all: any,
events?: any,
events?: Record<string, string>,
defaults?: TConf,
opts: { skipListen?: boolean } = {},
) {
super(em, moduleName, defaults);
this.all = all;
this.events = events;
if (events) this.events = events;
!opts.skipListen && this.__initListen();
}

2
packages/core/src/block_manager/index.ts

@ -72,11 +72,11 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
onInit() {
const { config, blocks, blocksVisible } = this;
blocks.add(config.blocks || []);
// Setup the sync between the global and public collections
blocks.on('add', (model) => blocksVisible.add(model));
blocks.on('remove', (model) => blocksVisible.remove(model));
blocks.on('reset', (coll) => blocksVisible.reset(coll.models));
blocks.add(config.blocks || []);
}
/**

1
packages/core/src/canvas/index.ts

@ -617,6 +617,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
const el = this.getCanvasView().el;
this.autoScroller.start(el, el, {
zoom: this.em.getZoomDecimal(),
ignoredElement: this.getSpotsEl(),
});
}
}

2
packages/core/src/canvas/model/CanvasSpots.ts

@ -17,7 +17,7 @@ export default class CanvasSpots extends ModuleCollection<CanvasSpot> {
const { em } = this;
this.refreshDbn = debounce(() => this.refresh(), 0);
const evToRefreshDbn = `${ComponentsEvents.resize} styleable:change component:input ${ComponentsEvents.update} frame:updated undo redo`;
const evToRefreshDbn = `${ComponentsEvents.resize} styleable:change ${ComponentsEvents.input} ${ComponentsEvents.update} frame:updated undo redo`;
this.listenTo(em, evToRefreshDbn, () => this.refreshDbn());
}

1
packages/core/src/canvas/view/FrameView.ts

@ -229,6 +229,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
this.autoScroller.start(this.el, this.getWindow(), {
lastMaxHeight: this.getWrapper().offsetHeight - this.el.offsetHeight,
zoom: this.em.getZoomDecimal(),
ignoredElement: this.em.Canvas.getSpotsEl(),
});
}

2
packages/core/src/code_manager/model/CssGenerator.ts

@ -172,7 +172,7 @@ export default class CssGenerator extends Model {
});
if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule || !model) {
const block = rule.getDeclaration({ body: 1 });
const block = rule.getDeclaration();
block && (opts.json ? (result = rule) : (result += block));
} else {
dump.push(rule);

2
packages/core/src/commands/view/SelectComponent.ts

@ -96,7 +96,7 @@ export default {
methods[method](listenToEl, 'scroll', this.onContainerChange);
em[method](`component:toggled ${eventCmpUpdate} undo redo`, this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this);
em[method](`${ComponentsEvents.resize} styleable:change component:input`, this.updateGlobalPos, this);
em[method](`${ComponentsEvents.resize} styleable:change ${ComponentsEvents.input}`, this.updateGlobalPos, this);
em[method](`${eventCmpUpdate}:toolbar`, this._upToolbar, this);
em[method]('frame:updated', this.onFrameUpdated, this);
em[method]('canvas:updateTools', this.onFrameUpdated, this);

11
packages/core/src/css_composer/index.ts

@ -40,6 +40,8 @@ import EditorModel from '../editor/model/Editor';
import Component from '../dom_components/model/Component';
import { ObjectAny, PrevToNewIdMap } from '../common';
import { UpdateStyleOptions } from '../domain_abstract/model/StyleableModel';
import { CssEvents } from './types';
import CssRuleView from './view/CssRuleView';
/** @private */
interface RuleOptions {
@ -72,8 +74,15 @@ export interface GetSetRuleOptions extends UpdateStyleOptions {
type CssRuleStyle = Required<CssRuleProperties>['style'];
export default class CssComposer extends ItemManagerModule<CssComposerConfig & { pStylePrefix?: string }> {
classes = {
CssRule,
CssRules,
CssRuleView,
CssRulesView,
};
rules: CssRules;
rulesView?: CssRulesView;
events = CssEvents;
Selectors = Selectors;
@ -85,7 +94,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
* @private
*/
constructor(em: EditorModel) {
super(em, 'CssComposer', null, {}, defConfig());
super(em, 'CssComposer', null, CssEvents, defConfig());
const { config } = this;
const ppfx = config.pStylePrefix;

15
packages/core/src/css_composer/model/CssRule.ts

@ -1,6 +1,6 @@
import { isEmpty, forEach, isString, isArray } from 'underscore';
import { Model, ObjectAny, View } from '../../common';
import StyleableModel from '../../domain_abstract/model/StyleableModel';
import { Model, ObjectAny } from '../../common';
import StyleableModel, { StyleProps } from '../../domain_abstract/model/StyleableModel';
import Selectors from '../../selector_manager/model/Selectors';
import { getMediaLength } from '../../code_manager/model/CssGenerator';
import { isEmptyObj, hasWin } from '../../utils/mixins';
@ -8,6 +8,13 @@ import Selector, { SelectorProps } from '../../selector_manager/model/Selector';
import EditorModel from '../../editor/model/Editor';
import CssRuleView from '../view/CssRuleView';
export interface ToCssOptions {
important?: boolean | string[];
allowEmpty?: boolean;
style?: StyleProps;
inline?: boolean;
}
/** @private */
export interface CssRuleProperties {
/**
@ -214,7 +221,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
* });
* cssRule.getDeclaration() // ".class1{color:red;}"
*/
getDeclaration(opts: ObjectAny = {}) {
getDeclaration(opts: ToCssOptions = {}) {
let result = '';
const { important } = this.attributes;
const selectors = this.selectorsToString(opts);
@ -285,7 +292,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
* });
* cssRule.toCSS() // "@media (min-width: 500px){.class1{color:red;}}"
*/
toCSS(opts: ObjectAny = {}) {
toCSS(opts: ToCssOptions = {}) {
let result = '';
const atRule = this.getAtRule();
const block = this.getDeclaration(opts);

9
packages/core/src/css_composer/types.ts

@ -0,0 +1,9 @@
export enum CssEvents {
/**
* @event `css:mount` CSS rule is mounted in the canvas.
* @example
* editor.on('css:mount', ({ rule }) => { ... });
*/
mount = 'css:mount',
mountBefore = 'css:mount:before',
}

14
packages/core/src/css_composer/view/CssRuleView.ts

@ -1,6 +1,8 @@
import FrameView from '../../canvas/view/FrameView';
import { View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import CssRule from '../model/CssRule';
import { CssEvents } from '../types';
export default class CssRuleView extends View<CssRule> {
config: any;
@ -19,6 +21,10 @@ export default class CssRuleView extends View<CssRule> {
return this.config.frameView;
}
get em(): EditorModel {
return this.model.em!;
}
remove() {
super.remove();
this.model.removeView(this);
@ -35,9 +41,13 @@ export default class CssRuleView extends View<CssRule> {
}
render() {
const { model, el } = this;
const { model, el, em } = this;
const important = model.get('important');
el.innerHTML = model.toCSS({ important });
const css = model.toCSS({ important });
const mountProps = { rule: model, ruleView: this, css };
em?.trigger(CssEvents.mountBefore, mountProps);
el.innerHTML = mountProps.css;
em?.trigger(CssEvents.mount, mountProps);
return this;
}
}

24
packages/core/src/data_sources/index.ts

@ -1,38 +1,24 @@
/**
* This module manages data sources within the editor.
* You can initialize the module with the editor by passing an instance of `EditorModel`.
*
* ```js
* const editor = new EditorModel();
* const dsm = new DataSourceManager(editor);
* ```
*
* Once the editor is instantiated, you can use the following API to manage data sources:
*
* ```js
* const editor = grapesjs.init({ ... });
* const dsm = editor.DataSources;
* ```
*
* {REPLACE_EVENTS}
*
* ## Methods
* * [add](#add) - Add a new data source.
* * [get](#get) - Retrieve a data source by its ID.
* * [getAll](#getall) - Retrieve all data sources.
* * [remove](#remove) - Remove a data source by its ID.
* * [clear](#clear) - Remove all data sources.
*
* Example of adding a data source:
*
* ```js
* const ds = dsm.add({
* id: 'my_data_source_id',
* records: [
* { id: 'id1', name: 'value1' },
* { id: 'id2', name: 'value2' }
* ]
* });
* ```
* [DataSource]: datasource.html
*
* @module DataSources
* @param {EditorModel} em - Editor model.
*/
import { ItemManagerModule, ModuleConfig } from '../abstract/Module';

10
packages/core/src/data_sources/model/DataRecord.ts

@ -63,9 +63,9 @@ export default class DataRecord<T extends DataRecordProps = DataRecordProps> ext
* @private
* @name handleChange
*/
handleChange() {
handleChange(m: DataRecord, opts: SetOptions) {
const changed = this.changedAttributes();
keys(changed).forEach((prop) => this.triggerChange(prop));
keys(changed).forEach((prop) => this.triggerChange(prop, opts));
}
/**
@ -113,10 +113,12 @@ export default class DataRecord<T extends DataRecordProps = DataRecordProps> ext
* @param {String} [prop] - Optional property name to trigger a change event for a specific property.
* @name triggerChange
*/
triggerChange(prop?: string) {
triggerChange(prop?: string, options: SetOptions = {}) {
const { dataSource, em } = this;
const data = { dataSource, dataRecord: this };
const paths = this.getPaths(prop);
const data = { dataSource, dataRecord: this, path: paths[0], options };
em.trigger(DataSourcesEvents.path, data);
em.trigger(`${DataSourcesEvents.pathSource}:${dataSource.id}`, data);
paths.forEach((path) => em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path }));
}

2
packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts

@ -1,5 +1,3 @@
import { enumToArray } from '../../../utils';
import { DataConditionSimpleOperation } from '../types';
import { SimpleOperator } from './BaseOperator';
export enum BooleanOperation {

1
packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts

@ -1,4 +1,3 @@
import { enumToArray } from '../../../utils';
import { SimpleOperator } from './BaseOperator';
export enum NumberOperation {

56
packages/core/src/data_sources/types.ts

@ -58,6 +58,28 @@ export interface DataSourceTransformers {
onRecordSetValue?: (args: { id: string | number; key: string; value: any }) => any;
}
type DotSeparatedKeys<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}` | `${K}.${DotSeparatedKeys<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
export type DeepPartialDot<T> = {
[P in DotSeparatedKeys<T>]?: P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Rest extends DotSeparatedKeys<T[K]>
? DeepPartialDot<T[K]>[Rest]
: never
: never
: P extends keyof T
? T[P]
: never;
};
/**{START_EVENTS}*/
export enum DataSourcesEvents {
/**
@ -86,10 +108,20 @@ export enum DataSourcesEvents {
/**
* @event `data:path` Data record path update.
* @example
* editor.on('data:path:SOURCE_ID:RECORD_ID:PROP_NAME', ({ dataSource, dataRecord, path }) => { ... });
* editor.on('data:path:SOURCE_ID.RECORD_ID.PROP_NAME', ({ dataSource, dataRecord, path }) => { ... });
* editor.on('data:path', ({ dataSource, dataRecord, path }) => {
* console.log('Path update in any data source')
* });
*/
path = 'data:path',
/**
* @event `data:pathSource` Data record path update per source.
* @example
* editor.on('data:pathSource:SOURCE_ID', ({ dataSource, dataRecord, path }) => { ... });
*/
pathSource = 'data:pathSource:',
/**
* @event `data` Catch-all event for all the events mentioned above.
* @example
@ -98,24 +130,6 @@ export enum DataSourcesEvents {
all = 'data',
}
/**{END_EVENTS}*/
type DotSeparatedKeys<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}` | `${K}.${DotSeparatedKeys<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
export type DeepPartialDot<T> = {
[P in DotSeparatedKeys<T>]?: P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Rest extends DotSeparatedKeys<T[K]>
? DeepPartialDot<T[K]>[Rest]
: never
: never
: P extends keyof T
? T[P]
: never;
};
// need this to avoid the TS documentation generator to break
export default DataSourcesEvents;

32
packages/core/src/dom_components/model/ComponentVideo.ts

@ -26,7 +26,7 @@ export default class ComponentVideo extends ComponentImage {
viUrl: 'https://player.vimeo.com/video/',
loop: false,
poster: '',
muted: 0,
muted: false,
autoplay: false,
controls: true,
color: '',
@ -50,12 +50,13 @@ export default class ComponentVideo extends ComponentImage {
updatePropsFromAttr() {
if (this.get('provider') === defProvider) {
const { controls, autoplay, loop } = this.get('attributes')!;
const { controls, autoplay, loop, muted } = this.get('attributes')!;
const toUp: ObjectAny = {};
if (isDef(controls)) toUp.controls = !!controls;
if (isDef(autoplay)) toUp.autoplay = !!autoplay;
if (isDef(loop)) toUp.loop = !!loop;
if (isDef(muted)) toUp.muted = !!muted; // Update for muted
if (!isEmptyObj(toUp)) {
this.set(toUp);
@ -111,6 +112,7 @@ export default class ComponentVideo extends ComponentImage {
hasParam(qr.color) && this.set('color', qr.color);
qr.rel === '0' && this.set('rel', 0);
qr.modestbranding === '1' && this.set('modestbranding', 1);
qr.muted === '1' && this.set('muted', true);
break;
default:
}
@ -157,6 +159,7 @@ export default class ComponentVideo extends ComponentImage {
attr.loop = !!this.get('loop');
attr.autoplay = !!this.get('autoplay');
attr.controls = !!this.get('controls');
attr.muted = !!this.get('muted');
}
return attr;
@ -206,6 +209,7 @@ export default class ComponentVideo extends ComponentImage {
this.getAutoplayTrait(),
this.getLoopTrait(),
this.getControlsTrait(),
this.getMutedTrait(),
];
}
/**
@ -237,6 +241,7 @@ export default class ComponentVideo extends ComponentImage {
name: 'modestbranding',
changeProp: true,
},
this.getMutedTrait(),
];
}
@ -262,6 +267,7 @@ export default class ComponentVideo extends ComponentImage {
},
this.getAutoplayTrait(),
this.getLoopTrait(),
this.getMutedTrait(),
];
}
@ -307,6 +313,20 @@ export default class ComponentVideo extends ComponentImage {
};
}
/**
* Return object trait
* @return {Object}
* @private
*/
getMutedTrait() {
return {
type: 'checkbox',
label: 'Muted',
name: 'muted',
changeProp: true,
};
}
/**
* Returns url to youtube video
* @return {string}
@ -318,10 +338,9 @@ export default class ComponentVideo extends ComponentImage {
const list = this.get('list');
url += id + (id.indexOf('?') < 0 ? '?' : '');
url += list ? `&list=${list}` : '';
url += this.get('autoplay') ? '&autoplay=1&mute=1' : '';
url += this.get('autoplay') ? '&autoplay=1' : '';
url += this.get('muted') ? '&mute=1' : '';
url += !this.get('controls') ? '&controls=0&showinfo=0' : '';
// Loop works only with playlist enabled
// https://stackoverflow.com/questions/25779966/youtube-iframe-loop-doesnt-work
url += this.get('loop') ? `&loop=1&playlist=${id}` : '';
url += this.get('rel') ? '' : '&rel=0';
url += this.get('modestbranding') ? '&modestbranding=1' : '';
@ -347,7 +366,8 @@ export default class ComponentVideo extends ComponentImage {
getVimeoSrc() {
let url = this.get('viUrl') as string;
url += this.get('videoId') + '?';
url += this.get('autoplay') ? '&autoplay=1&muted=1' : '';
url += this.get('autoplay') ? '&autoplay=1' : '';
url += this.get('muted') ? '&muted=1' : '';
url += this.get('loop') ? '&loop=1' : '';
url += !this.get('controls') ? '&title=0&portrait=0&badge=0' : '';
url += this.get('color') ? '&color=' + this.get('color') : '';

14
packages/core/src/dom_components/types.ts

@ -69,6 +69,13 @@ export enum ComponentsEvents {
select = 'component:select',
selectBefore = 'component:select:before',
/**
* @event `component:mount` Component is mounted in the canvas.
* @example
* editor.on('component:mount', (component) => { ... });
*/
mount = 'component:mount',
/**
* @event `component:script:mount` Component with script is mounted.
* @example
@ -91,6 +98,13 @@ export enum ComponentsEvents {
*/
render = 'component:render',
/**
* @event `component:input` Event triggered on `input` DOM event. This is useful to catch direct input changes in the component (eg. Text component).
* @example
* editor.on('component:input', (component) => { ... });
*/
input = 'component:input',
/**
* @event `component:resize` Component resized. This event is triggered when the component is resized in the canvas.
* @example

9
packages/core/src/dom_components/view/ComponentTextView.ts

@ -9,6 +9,7 @@ import { getComponentIds } from '../model/Components';
import ComponentText from '../model/ComponentText';
import { ComponentDefinition } from '../model/types';
import ComponentView from './ComponentView';
import { ComponentsEvents } from '../types';
export default class ComponentTextView<TComp extends ComponentText = ComponentText> extends ComponentView<TComp> {
rte?: RichTextEditorModule;
@ -217,11 +218,9 @@ export default class ComponentTextView<TComp extends ComponentText = ComponentTe
* @param {Event} e
*/
onInput() {
const evPfx = 'component';
const ev = [`${evPfx}:update`, `${evPfx}:input`].join(' ');
// Update toolbars
this.em?.trigger(ev, this.model);
const { model } = this;
const events = [ComponentsEvents.update, ComponentsEvents.input];
events.forEach((ev) => model.emitWithEditor(ev, model));
}
/**

3
packages/core/src/dom_components/view/ComponentVideoView.ts

@ -18,7 +18,7 @@ export default class ComponentVideoView extends ComponentImageView<ComponentVide
// @ts-ignore
ComponentView.prototype.initialize.apply(this, arguments);
const { model } = this;
const props = ['loop', 'autoplay', 'controls', 'color', 'rel', 'modestbranding', 'poster'];
const props = ['loop', 'autoplay', 'controls', 'color', 'rel', 'modestbranding', 'poster', 'muted'];
const events = props.map((p) => `change:${p}`).join(' ');
this.listenTo(model, 'change:provider', this.updateProvider);
this.listenTo(model, 'change:src', this.updateSrc);
@ -80,6 +80,7 @@ export default class ComponentVideoView extends ComponentImageView<ComponentVide
el.autoplay = model.get('autoplay');
el.controls = model.get('controls');
el.poster = model.get('poster');
el.muted = model.get('muted');
}
}
}

3
packages/core/src/dom_components/view/ComponentsView.ts

@ -8,6 +8,7 @@ import ComponentView from './ComponentView';
import FrameView from '../../canvas/view/FrameView';
import Components from '../model/Components';
import { ResetComponentsOptions } from '../model/types';
import { ComponentsEvents } from '../types';
export default class ComponentsView extends View {
opts!: any;
@ -119,7 +120,7 @@ export default class ComponentsView extends View {
}
if (!model.opt.temporary) {
em?.trigger('component:mount', model);
em?.trigger(ComponentsEvents.mount, model);
}
return rendered;

5
packages/core/src/domain_abstract/model/StyleableModel.ts

@ -9,6 +9,7 @@ import CssRuleView from '../../css_composer/view/CssRuleView';
import ComponentView from '../../dom_components/view/ComponentView';
import Frame from '../../canvas/model/Frame';
import { DataConditionProps } from '../../data_sources/model/conditional_variables/DataCondition';
import { ToCssOptions } from '../../css_composer/model/CssRule';
import { ModelDataResolverWatchers } from '../../dom_components/model/ModelDataResolverWatchers';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import { DynamicWatchersOptions } from '../../dom_components/model/ModelResolverWatcher';
@ -189,9 +190,9 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T,
* @param {Object} [opts={}] Options
* @return {String}
*/
styleToString(opts: ObjectAny = {}) {
styleToString(opts: ToCssOptions = {}) {
const result: string[] = [];
const style = this.getStyle(opts);
const style = opts.style || this.getStyle(opts);
const imp = opts.important;
for (let prop in style) {

10
packages/core/src/index.ts

@ -147,5 +147,15 @@ export type { default as DataSources } from './data_sources/model/DataSources';
export type { default as DataSource } from './data_sources/model/DataSource';
export type { default as DataRecord } from './data_sources/model/DataRecord';
export type { default as DataRecords } from './data_sources/model/DataRecords';
export type { default as DataVariable } from './data_sources/model/DataVariable';
export type { default as ComponentDataVariable } from './data_sources/model/ComponentDataVariable';
export type { default as ComponentDataCollection } from './data_sources/model/data_collection/ComponentDataCollection';
export type { default as ComponentDataCondition } from './data_sources/model/conditional_variables/ComponentDataCondition';
export type {
DataCondition,
LogicGroupProps,
DataConditionProps,
ExpressionProps,
} from './data_sources/model/conditional_variables/DataCondition';
export default grapesjs;

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

@ -75,8 +75,9 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
while (str.indexOf('/*') >= 0) {
const start = str.indexOf('/*');
const end = str.indexOf('*/') + 2;
str = str.replace(str.slice(start, end), '');
const end = str.indexOf('*/');
const endIndex = end > -1 ? end + 2 : undefined;
str = str.replace(str.slice(start, endIndex), '');
}
const decls = str.split(';');

60
packages/core/src/utils/AutoScroller.ts

@ -15,6 +15,7 @@ export default class AutoScroller {
* are relative to the iframe's document, not the main window's.
*/
private rectIsInScrollIframe: boolean = false;
private ignoredElement?: HTMLElement; // If the mouse is over this element, don't autoscroll
constructor(
autoscrollLimit: number = 50,
@ -31,11 +32,20 @@ export default class AutoScroller {
bindAll(this, 'start', 'autoscroll', 'updateClientY', 'stop');
}
start(eventEl: HTMLElement, scrollEl: HTMLElement | Window, opts?: { lastMaxHeight?: number; zoom?: number }) {
start(
eventEl: HTMLElement,
scrollEl: HTMLElement | Window,
opts?: {
lastMaxHeight?: number;
zoom?: number;
ignoredElement?: HTMLElement;
},
) {
this.eventEl = eventEl;
this.scrollEl = scrollEl;
this.lastMaxHeight = opts?.lastMaxHeight || Number.POSITIVE_INFINITY;
this.zoom = opts?.zoom || 1;
this.ignoredElement = opts?.ignoredElement;
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
@ -47,24 +57,32 @@ export default class AutoScroller {
private autoscroll() {
const scrollEl = this.scrollEl;
if (this.dragging && scrollEl) {
const clientY = this.lastClientY ?? 0;
const limitTop = this.autoscrollLimit;
const eventElHeight = this.getEventElHeight();
const limitBottom = eventElHeight - limitTop;
let nextTop = 0;
if (clientY < limitTop) nextTop += clientY - limitTop;
if (clientY > limitBottom) nextTop += clientY - limitBottom;
const scrollTop = this.getElScrollTop(scrollEl);
if (this.lastClientY !== undefined && nextTop !== 0 && this.lastMaxHeight - nextTop > scrollTop) {
scrollEl.scrollBy({ top: nextTop, left: 0, behavior: 'auto' });
this.onScroll?.();
}
if (!this.dragging || !scrollEl) return;
if (this.lastClientY === undefined) {
setTimeout(() => {
requestAnimationFrame(this.autoscroll);
}, 50);
return;
}
requestAnimationFrame(this.autoscroll);
const clientY = this.lastClientY ?? 0;
const limitTop = this.autoscrollLimit;
const eventElHeight = this.getEventElHeight();
const limitBottom = eventElHeight - limitTop;
let scrollAmount = 0;
if (clientY < limitTop) scrollAmount += clientY - limitTop;
if (clientY > limitBottom) scrollAmount += clientY - limitBottom;
const scrollTop = this.getElScrollTop(scrollEl);
scrollAmount = Math.min(scrollAmount, this.lastMaxHeight - scrollTop);
scrollAmount = Math.max(scrollAmount, -scrollTop);
if (scrollAmount !== 0) {
scrollEl.scrollBy({ top: scrollAmount, behavior: 'auto' });
this.onScroll?.();
}
requestAnimationFrame(this.autoscroll);
}
private getEventElHeight() {
@ -76,6 +94,12 @@ export default class AutoScroller {
}
private updateClientY(ev: Event) {
const target = ev.target as HTMLElement;
if (this.ignoredElement && this.ignoredElement.contains(target)) {
return;
}
const scrollEl = this.scrollEl;
ev.preventDefault();
@ -99,5 +123,7 @@ export default class AutoScroller {
stop() {
this.toggleAutoscrollFx(false);
this.lastClientY = undefined;
this.ignoredElement = undefined;
}
}

4
packages/core/src/utils/sorter/BaseComponentNode.ts

@ -152,6 +152,10 @@ export abstract class BaseComponentNode extends SortableTreeNode<Component> {
return this.model.em.Components.canMove(this.model, source.model, this.getRealIndex(index)).result;
}
equals(node?: BaseComponentNode): node is BaseComponentNode {
return !!node?._model && this._model.getId() === node._model.getId();
}
/**
* Abstract method to get the view associated with this component.
* Subclasses must implement this method.

1
packages/core/src/utils/sorter/CanvasNewComponentNode.ts

@ -3,7 +3,6 @@ import CanvasComponentNode from './CanvasComponentNode';
import { getSymbolMain, getSymbolTop, isSymbol, isSymbolMain } from '../../dom_components/model/SymbolUtils';
import Component from '../../dom_components/model/Component';
import { ContentElement, ContentType } from './types';
import { isComponent } from '../mixins';
type CanMoveSource = Component | ContentType;

94
packages/core/src/utils/sorter/DropLocationDeterminer.ts

@ -36,6 +36,8 @@ type lastMoveData<NodeType> = {
hoveredNode?: NodeType;
/** The index where the placeholder or dragged element should be inserted. */
index?: number;
/** The index under the mouse pointer during this move. */
hoveredIndex?: number;
/** Placement relative to the target ('before' or 'after'). */
placement?: Placement;
/** The mouse event, used if we want to move placeholder with scrolling. */
@ -113,19 +115,28 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
private handleMove(mouseEvent: MouseEvent): void {
this.adjustForScroll();
const { targetNode: lastTargetNode } = this.lastMoveData;
this.eventHandlers.onMouseMove?.(mouseEvent);
const { mouseXRelative: mouseX, mouseYRelative: mouseY } = this.getMousePositionRelativeToContainer(
mouseEvent.clientX,
mouseEvent.clientY,
);
const targetNode = this.getTargetNode(mouseEvent);
const mouseTargetEl = this.getMouseTargetElement(mouseEvent);
const targetEl = this.getFirstElementWithAModel(mouseTargetEl);
const hoveredModel = targetEl ? $(targetEl)?.data('model') : undefined;
const hoveredNode = hoveredModel ? this.getOrCreateHoveredNode(hoveredModel) : undefined;
const hoveredIndex = hoveredNode
? this.getIndexInParent(hoveredNode!, hoveredNode!.nodeDimensions!, mouseX, mouseY)
: 0;
const targetNode = hoveredNode ? this.getValidParent(hoveredNode, 0, mouseX, mouseY) : undefined;
const targetChanged = !targetNode?.equals(lastTargetNode);
if (targetChanged) {
this.eventHandlers.onTargetChange?.(lastTargetNode, targetNode);
}
if (!targetNode) {
if (!targetNode || !hoveredNode) {
this.triggerLegacyOnMoveCallback(mouseEvent, 0);
this.triggerMoveEvent(mouseX, mouseY);
this.restLastMoveData();
@ -144,10 +155,11 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
}
this.lastMoveData = {
...this.lastMoveData,
targetNode,
hoveredNode,
mouseEvent,
index,
hoveredIndex,
placement,
placeholderDimensions,
};
@ -249,39 +261,6 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
};
}
/**
* Retrieves the target node based on the mouse event.
* Determines the element being hovered, its corresponding model, and
* calculates the valid parent node to use as the target node.
*
* @param mouseEvent - The mouse event containing the cursor position and target element.
* @returns The target node if a valid one is found, otherwise undefined.
*/
private getTargetNode(mouseEvent: MouseEvent): NodeType | undefined {
this.cacheContainerPosition(this.containerContext.container);
const { mouseXRelative, mouseYRelative } = this.getMousePositionRelativeToContainer(
mouseEvent.clientX,
mouseEvent.clientY,
);
// Get the element under the mouse
const mouseTargetEl = this.getMouseTargetElement(mouseEvent);
const targetEl = this.getFirstElementWithAModel(mouseTargetEl);
if (!targetEl) return;
const hoveredModel = $(targetEl)?.data('model');
if (!hoveredModel) return;
let hoveredNode = this.getOrCreateHoveredNode(hoveredModel);
// Get the drop position index based on the mouse position
const { index } = this.getDropPosition(hoveredNode, mouseXRelative, mouseYRelative);
// Determine the valid target node (or its valid parent)
let targetNode = this.getValidParent(hoveredNode, index, mouseXRelative, mouseYRelative);
return this.getOrReuseTargetNode(targetNode);
}
/**
* Creates a new hovered node or reuses the last hovered node if it is the same.
*
@ -291,8 +270,11 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
private getOrCreateHoveredNode(hoveredModel: T): NodeType {
const lastHoveredNode = this.lastMoveData.hoveredNode;
const hoveredNode = new this.treeClass(hoveredModel);
const newHoveredNode = hoveredNode.equals(lastHoveredNode) ? lastHoveredNode : hoveredNode;
this.lastMoveData.hoveredNode = newHoveredNode;
const sameHoveredNode = hoveredNode.equals(lastHoveredNode);
const newHoveredNode = sameHoveredNode ? lastHoveredNode : hoveredNode;
newHoveredNode.nodeDimensions = sameHoveredNode
? lastHoveredNode!.nodeDimensions!
: this.getDim(hoveredNode.element!);
return newHoveredNode;
}
@ -396,16 +378,23 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
private getValidParent(targetNode: NodeType, index: number, mouseX: number, mouseY: number): NodeType | undefined {
if (!targetNode) return;
const lastTargetNode = this.lastMoveData.targetNode;
const targetNotChanged = targetNode.equals(lastTargetNode);
targetNode.nodeDimensions = targetNotChanged ? lastTargetNode.nodeDimensions! : this.getDim(targetNode.element!);
const {
targetNode: lastTargetNode,
hoveredNode: lastHoveredNode,
hoveredIndex: lastHoveredIndex,
} = this.lastMoveData;
const sameHoveredNode = targetNode.equals(lastHoveredNode);
targetNode.nodeDimensions = sameHoveredNode ? lastHoveredNode!.nodeDimensions! : this.getDim(targetNode.element!);
const hoverIndex = this.getIndexInParent(targetNode, targetNode.nodeDimensions!, mouseX, mouseY);
const sameHoveredIndex = hoverIndex === lastHoveredIndex;
const sameHoverPosition = sameHoveredNode && sameHoveredIndex;
if (sameHoverPosition && lastTargetNode) return lastTargetNode;
if (!targetNode.isWithinDropBounds(mouseX, mouseY)) {
return this.handleParentTraversal(targetNode, mouseX, mouseY);
}
const positionNotChanged = targetNotChanged && index === this.lastMoveData.index;
if (positionNotChanged) return lastTargetNode;
const canMove = this.sourceNodes.some((node) => targetNode.canMove(node, index));
this.triggerDragValidation(canMove, targetNode);
if (canMove) return targetNode;
@ -417,17 +406,16 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
const parent = targetNode.getParent() as NodeType;
if (!parent) return;
const indexInParent = this.getIndexInParent(parent, targetNode, targetNode.nodeDimensions!, mouseX, mouseY);
const indexInParent = this.getIndexInParent(targetNode, targetNode.nodeDimensions!, mouseX, mouseY);
if (indexInParent === undefined) return;
return this.getValidParent(parent, indexInParent, mouseX, mouseY);
}
private getIndexInParent(
parent: NodeType,
targetNode: NodeType,
nodeDimensions: Dimension,
mouseX: number,
mouseY: number,
) {
private getIndexInParent(targetNode: NodeType, nodeDimensions: Dimension, mouseX: number, mouseY: number) {
const parent = targetNode.getParent() as NodeType;
if (!parent) return;
let indexInParent = parent?.indexOfChild(targetNode);
nodeDimensions.dir = this.getDirection(targetNode.element!, parent.element!);

2
packages/core/test/specs/css_composer/index.ts

@ -162,7 +162,7 @@ describe('Css Composer', () => {
const rule = obj.getIdRule(name)!;
expect(rule.selectorsToString()).toEqual(`#${name}`);
expect(rule.styleToString()).toEqual('color:red;');
expect(rule.styleToString({ important: 1 })).toEqual('color:red !important;');
expect(rule.styleToString({ important: true })).toEqual('color:red !important;');
expect(rule.styleToString({ important: ['color'] })).toEqual('color:red !important;');
});

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

@ -84,11 +84,17 @@ describe('ParserHtml', () => {
});
test('Parse style with comments', () => {
expect(obj.parseStyle('/* color #ffffff; */ width: 100px;')).toEqual({
expect(obj.parseStyle('/* color #ffffff; */ width: 100px; /* height: 10px; */')).toEqual({
width: '100px',
});
});
test('Parse style with broken comments', () => {
expect(obj.parseStyle('/* color #ffffff; */ height: 50px; /* width: 10px; ')).toEqual({
height: '50px',
});
});
test('Parse class string to array', () => {
const str = 'test1 test2 test3 test-4';
const result = ['test1', 'test2', 'test3', 'test-4'];
@ -922,8 +928,8 @@ describe('ParserHtml', () => {
test('converts data-gjs-data-resolver to dataResolver', () => {
const str = `
<div
data-gjs-type="data-variable"
<div
data-gjs-type="data-variable"
data-gjs-data-resolver='{"type":"data-variable","path":"some path","collectionId":"someCollectionId"}'
></div>
`;

Loading…
Cancel
Save