Browse Source

Merge branch 'dev' into rule-view-data-var

rule-view-data-var
Daniel Starns 2 years ago
committed by GitHub
parent
commit
4ec1429f98
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      packages/core/src/asset_manager/config/config.ts
  2. 4
      packages/core/src/asset_manager/index.ts
  3. 4
      packages/core/src/block_manager/config/config.ts
  4. 4
      packages/core/src/block_manager/index.ts
  5. 4
      packages/core/src/canvas/config/config.ts
  6. 6
      packages/core/src/canvas/index.ts
  7. 4
      packages/core/src/code_manager/config/config.ts
  8. 4
      packages/core/src/code_manager/index.ts
  9. 4
      packages/core/src/commands/config/config.ts
  10. 4
      packages/core/src/commands/index.ts
  11. 9
      packages/core/src/commands/view/SelectPosition.ts
  12. 4
      packages/core/src/css_composer/config/config.ts
  13. 4
      packages/core/src/css_composer/index.ts
  14. 4
      packages/core/src/device_manager/config/config.ts
  15. 4
      packages/core/src/device_manager/index.ts
  16. 6
      packages/core/src/dom_components/config/config.ts
  17. 11
      packages/core/src/dom_components/index.ts
  18. 75
      packages/core/src/dom_components/model/Component.ts
  19. 13
      packages/core/src/dom_components/model/ComponentWrapper.ts
  20. 5
      packages/core/src/dom_components/model/Components.ts
  21. 6
      packages/core/src/dom_components/model/types.ts
  22. 10
      packages/core/src/dom_components/view/ComponentsView.ts
  23. 4
      packages/core/src/editor/config/config.ts
  24. 3
      packages/core/src/editor/index.ts
  25. 1
      packages/core/src/editor/model/Editor.ts
  26. 4
      packages/core/src/i18n/config.ts
  27. 4
      packages/core/src/i18n/index.ts
  28. 4
      packages/core/src/keymaps/config.ts
  29. 4
      packages/core/src/keymaps/index.ts
  30. 4
      packages/core/src/modal_dialog/config/config.ts
  31. 4
      packages/core/src/modal_dialog/index.ts
  32. 4
      packages/core/src/navigator/config/config.ts
  33. 4
      packages/core/src/navigator/index.ts
  34. 4
      packages/core/src/panels/config/config.ts
  35. 9
      packages/core/src/panels/index.ts
  36. 9
      packages/core/src/parser/config/config.ts
  37. 4
      packages/core/src/parser/index.ts
  38. 19
      packages/core/src/parser/model/ParserHtml.ts
  39. 4
      packages/core/src/rich_text_editor/config/config.ts
  40. 4
      packages/core/src/rich_text_editor/index.ts
  41. 4
      packages/core/src/selector_manager/config/config.ts
  42. 5
      packages/core/src/selector_manager/index.ts
  43. 4
      packages/core/src/storage_manager/config/config.ts
  44. 4
      packages/core/src/storage_manager/index.ts
  45. 6
      packages/core/src/style_manager/config/config.ts
  46. 4
      packages/core/src/style_manager/index.ts
  47. 4
      packages/core/src/trait_manager/config/config.ts
  48. 4
      packages/core/src/trait_manager/index.ts
  49. 4
      packages/core/src/undo_manager/config.ts
  50. 4
      packages/core/src/undo_manager/index.ts
  51. 14
      packages/core/src/utils/Droppable.ts
  52. 41
      packages/core/src/utils/sorter/BaseComponentNode.ts
  53. 5
      packages/core/src/utils/sorter/CanvasComponentNode.ts
  54. 13
      packages/core/src/utils/sorter/CanvasNewComponentNode.ts
  55. 31
      packages/core/src/utils/sorter/ComponentSorter.ts
  56. 158
      packages/core/src/utils/sorter/Dimension.ts
  57. 379
      packages/core/src/utils/sorter/DropLocationDeterminer.ts
  58. 33
      packages/core/src/utils/sorter/PlaceholderClass.ts
  59. 32
      packages/core/src/utils/sorter/RateLimiter.ts
  60. 37
      packages/core/src/utils/sorter/SortableTreeNode.ts
  61. 8
      packages/core/src/utils/sorter/Sorter.ts
  62. 17
      packages/core/src/utils/sorter/SorterUtils.ts
  63. 19
      packages/core/src/utils/sorter/types.ts
  64. 11
      packages/core/test/common.ts
  65. 1
      packages/core/test/specs/css_composer/index.ts
  66. 49
      packages/core/test/specs/dom_components/index.ts
  67. 150
      packages/core/test/specs/grapesjs/index.ts
  68. 14
      packages/core/test/specs/navigator/view/ItemView.ts

4
packages/core/src/asset_manager/config/config.ts

@ -141,7 +141,7 @@ export interface AssetManagerConfig {
dropzoneContent?: string;
}
const config: AssetManagerConfig = {
const config: () => AssetManagerConfig = () => ({
assets: [],
noAssets: '',
stylePrefix: 'am-',
@ -163,6 +163,6 @@ const config: AssetManagerConfig = {
dropzone: false,
openAssetsOnDrop: true,
dropzoneContent: '',
};
});
export default config;

4
packages/core/src/asset_manager/index.ts

@ -37,7 +37,7 @@ import { ItemManagerModule } from '../abstract/Module';
import { AddOptions, RemoveOptions } from '../common';
import EditorModel from '../editor/model/Editor';
import { ProjectData } from '../storage_manager';
import defaults, { AssetManagerConfig } from './config/config';
import defConfig, { AssetManagerConfig } from './config/config';
import Asset from './model/Asset';
import Assets from './model/Assets';
import AssetsEvents, { AssetOpenOptions } from './types';
@ -66,7 +66,7 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
*/
constructor(em: EditorModel) {
// @ts-ignore
super(em, 'AssetManager', new Assets([], em), AssetsEvents, defaults);
super(em, 'AssetManager', new Assets([], em), AssetsEvents, defConfig());
const { all, config } = this;
// @ts-ignore
this.assetsVis = new Assets([]);

4
packages/core/src/block_manager/config/config.ts

@ -38,11 +38,11 @@ export interface BlockManagerConfig {
custom?: boolean;
}
const config: BlockManagerConfig = {
const config: () => BlockManagerConfig = () => ({
appendTo: '',
blocks: [],
appendOnClick: false,
custom: false,
};
});
export default config;

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

@ -31,7 +31,7 @@ import { ItemManagerModule } from '../abstract/Module';
import FrameView from '../canvas/view/FrameView';
import Component from '../dom_components/model/Component';
import EditorModel from '../editor/model/Editor';
import defaults, { BlockManagerConfig } from './config/config';
import defConfig, { BlockManagerConfig } from './config/config';
import Block, { BlockProperties } from './model/Block';
import Blocks from './model/Blocks';
import Categories from '../abstract/ModuleCategories';
@ -61,7 +61,7 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
storageKey = '';
constructor(em: EditorModel) {
super(em, 'BlockManager', new Blocks(em.config.blockManager?.blocks || [], { em }), BlocksEvents, defaults);
super(em, 'BlockManager', new Blocks(em.config.blockManager?.blocks || [], { em }), BlocksEvents, defConfig());
// Global blocks collection
this.blocks = this.all;

4
packages/core/src/canvas/config/config.ts

@ -91,7 +91,7 @@ export interface CanvasConfig {
infiniteCanvas?: boolean;
}
const config: CanvasConfig = {
const config: () => CanvasConfig = () => ({
stylePrefix: 'cv-',
scripts: [],
styles: [],
@ -107,6 +107,6 @@ const config: CanvasConfig = {
`,
notTextable: ['button', 'a', 'input[type=checkbox]', 'input[type=radio]'],
allowExternalDrop: true,
};
});
export default config;

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

@ -35,7 +35,7 @@ import Component from '../dom_components/model/Component';
import ComponentView from '../dom_components/view/ComponentView';
import EditorModel from '../editor/model/Editor';
import { getElement, getViewEl } from '../utils/mixins';
import defaults, { CanvasConfig } from './config/config';
import defConfig, { CanvasConfig } from './config/config';
import Canvas from './model/Canvas';
import CanvasSpot, { CanvasSpotBuiltInTypes, CanvasSpotProps } from './model/CanvasSpot';
import CanvasSpots from './model/CanvasSpots';
@ -76,7 +76,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
* @private
*/
constructor(em: EditorModel) {
super(em, 'Canvas', defaults);
super(em, 'Canvas', defConfig());
this.canvas = new Canvas(this);
this.spots = new CanvasSpots(this);
@ -499,7 +499,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
* @return {Object}
* @private
*/
getMouseRelativeCanvas(ev: MouseEvent, opts: any) {
getMouseRelativeCanvas(ev: MouseEvent | { clientX: number; clientY: number }, opts: any) {
const zoom = this.getZoomDecimal();
const { top = 0, left = 0 } = this.getCanvasView().getPosition(opts) ?? {};

4
packages/core/src/code_manager/config/config.ts

@ -12,9 +12,9 @@ export interface CodeManagerConfig {
optsCodeViewer?: Record<string, any>;
}
const config: CodeManagerConfig = {
const config: () => CodeManagerConfig = () => ({
stylePrefix: 'cm-',
optsCodeViewer: {},
};
});
export default config;

4
packages/core/src/code_manager/index.ts

@ -18,7 +18,7 @@
* @module CodeManager
*/
import { isUndefined } from 'underscore';
import defaults, { CodeManagerConfig } from './config/config';
import defConfig, { CodeManagerConfig } from './config/config';
import gHtml from './model/HtmlGenerator';
import gCss from './model/CssGenerator';
import gJson from './model/JsonGenerator';
@ -39,7 +39,7 @@ export default class CodeManagerModule extends Module<CodeManagerConfig & { pSty
EditorView = CodeEditorView;
constructor(em: EditorModel) {
super(em, 'CodeManager', defaults);
super(em, 'CodeManager', defConfig());
const { config } = this;
const ppfx = config.pStylePrefix;
if (ppfx) config.stylePrefix = ppfx + config.stylePrefix;

4
packages/core/src/commands/config/config.ts

@ -21,10 +21,10 @@ export interface CommandsConfig {
strict?: boolean;
}
const config: CommandsConfig = {
const config: () => CommandsConfig = () => ({
stylePrefix: 'com-',
defaults: {},
strict: true,
};
});
export default config;

4
packages/core/src/commands/index.ts

@ -37,7 +37,7 @@
import { isFunction, includes } from 'underscore';
import CommandAbstract, { Command, CommandOptions, CommandObject, CommandFunction } from './view/CommandAbstract';
import defaults, { CommandsConfig } from './config/config';
import defConfig, { CommandsConfig } from './config/config';
import { Module } from '../abstract';
import Component, { eventDrag } from '../dom_components/model/Component';
import Editor from '../editor/model/Editor';
@ -107,7 +107,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* @private
*/
constructor(em: Editor) {
super(em, 'Commands', defaults);
super(em, 'Commands', defConfig());
const { config } = this;
const ppfx = config.pStylePrefix;
const { defaultCommands } = this;

9
packages/core/src/commands/view/SelectPosition.ts

@ -13,7 +13,7 @@ export default {
const utils = this.em.Utils;
const container = sourceElements[0].ownerDocument.body;
if (utils && !this.sorter)
if (utils)
this.sorter = new utils.ComponentSorter({
em: this.em,
treeClass: CanvasComponentNode,
@ -36,13 +36,6 @@ export default {
});
if (opts.onStart) this.sorter.eventHandlers.legacyOnStartSort = opts.onStart;
this.em.on(
'frame:scroll',
((...agrs: any[]) => {
const canvasScroll = this.canvas.getCanvasView().frame === agrs[0].frame;
if (canvasScroll) this.sorter.recalculateTargetOnScroll();
}).bind(this),
);
sourceElements &&
sourceElements.length > 0 &&
this.sorter.startSort(sourceElements.map((element) => ({ element })));

4
packages/core/src/css_composer/config/config.ts

@ -11,9 +11,9 @@ export interface CssComposerConfig {
rules?: Array<string>; // TODO
}
const config: CssComposerConfig = {
const config: () => CssComposerConfig = () => ({
stylePrefix: 'css-',
rules: [],
};
});
export default config;

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

@ -31,7 +31,7 @@ import { isArray, isString, isUndefined } from 'underscore';
import { isObject } from '../utils/mixins';
import Selectors from '../selector_manager/model/Selectors';
import Selector from '../selector_manager/model/Selector';
import defaults, { CssComposerConfig } from './config/config';
import defConfig, { CssComposerConfig } from './config/config';
import CssRule, { CssRuleJSON, CssRuleProperties } from './model/CssRule';
import CssRules from './model/CssRules';
import CssRulesView from './view/CssRulesView';
@ -85,7 +85,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
* @private
*/
constructor(em: EditorModel) {
super(em, 'CssComposer', null, {}, defaults);
super(em, 'CssComposer', null, {}, defConfig());
const { config } = this;
const ppfx = config.pStylePrefix;

4
packages/core/src/device_manager/config/config.ts

@ -25,7 +25,7 @@ export interface DeviceManagerConfig {
devices?: DeviceProperties[];
}
const config: DeviceManagerConfig = {
const config: () => DeviceManagerConfig = () => ({
default: '',
devices: [
{
@ -52,6 +52,6 @@ const config: DeviceManagerConfig = {
widthMedia: '480px',
},
],
};
});
export default config;

4
packages/core/src/device_manager/index.ts

@ -35,7 +35,7 @@
import { isString } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import EditorModel from '../editor/model/Editor';
import defaults, { DeviceManagerConfig } from './config/config';
import defConfig, { DeviceManagerConfig } from './config/config';
import Device, { DeviceProperties } from './model/Device';
import Devices from './model/Devices';
import DevicesView from './view/DevicesView';
@ -74,7 +74,7 @@ export default class DeviceManager extends ItemManagerModule<
storageKey = '';
constructor(em: EditorModel) {
super(em, 'DeviceManager', new Devices(), deviceEvents, defaults);
super(em, 'DeviceManager', new Devices(), deviceEvents, defConfig());
this.devices = this.all;
this.config.devices?.forEach((device) => this.add(device, { silent: true }));
this.select(this.config.default || this.devices.at(0));

6
packages/core/src/dom_components/config/config.ts

@ -62,7 +62,7 @@ export interface DomComponentsConfig {
useFrameDoc?: boolean;
}
export default {
const config: () => DomComponentsConfig = () => ({
stylePrefix: 'comp-',
components: [],
draggableComponents: true,
@ -87,4 +87,6 @@ export default {
'track',
'wbr',
],
} as DomComponentsConfig;
});
export default config;

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

@ -55,10 +55,10 @@
*/
import { debounce, isArray, isBoolean, isEmpty, isFunction, isString, isSymbol, result } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import { AddOptions, ObjectAny } from '../common';
import { ObjectAny } from '../common';
import EditorModel from '../editor/model/Editor';
import { isComponent } from '../utils/mixins';
import defaults, { DomComponentsConfig } from './config/config';
import defConfig, { DomComponentsConfig } from './config/config';
import Component, { IComponent, keyUpdate, keyUpdateInside } from './model/Component';
import ComponentComment from './model/ComponentComment';
import ComponentFrame from './model/ComponentFrame';
@ -334,7 +334,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
* @private
*/
constructor(em: EditorModel) {
super(em, 'DomComponents', new Components(undefined, { em }));
super(em, 'DomComponents', new Components(undefined, { em }), ComponentsEvents, defConfig());
const { config } = this;
this.symbols = new Symbols([], { em, config, domc: this });
@ -343,11 +343,6 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
this.config.components = em.config.components || this.config.components;
}
for (let name in defaults) {
//@ts-ignore
if (!(name in this.config)) this.config[name] = defaults[name];
}
const ppfx = this.config.pStylePrefix;
if (ppfx) this.config.stylePrefix = ppfx + this.config.stylePrefix;

75
packages/core/src/dom_components/model/Component.ts

@ -20,13 +20,13 @@ import Selectors from '../../selector_manager/model/Selectors';
import Traits from '../../trait_manager/model/Traits';
import EditorModel from '../../editor/model/Editor';
import {
AddComponentsOption,
ComponentAdd,
ComponentDefinition,
ComponentDefinitionDefined,
ComponentOptions,
ComponentProperties,
DragMode,
ResetComponentsOptions,
SymbolToUpOptions,
ToHTMLOptions,
} from './types';
@ -213,6 +213,16 @@ export default class Component extends StyleableModel<ComponentProperties> {
return this.get('locked');
}
get frame() {
return this.opt.frame;
}
get page() {
return this.frame?.getPage();
}
preInit() {}
/**
* Hook method, called once the model is created
*/
@ -238,7 +248,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
views!: ComponentView[];
view?: ComponentView;
viewLayer?: ItemView;
frame?: Frame;
rule?: CssRule;
prevColl?: Components;
__hasUm?: boolean;
@ -275,13 +284,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
opt.em = em;
this.opt = opt;
this.em = em!;
this.frame = opt.frame;
this.config = opt.config || {};
this.set('attributes', {
...(result(this, 'defaults').attributes || {}),
...(this.get('attributes') || {}),
});
this.ccid = Component.createId(this, opt);
this.preInit();
this.initClasses();
this.initComponents();
this.initTraits();
@ -999,7 +1008,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
components<T extends ComponentAdd | undefined>(
components?: T,
opts: AddComponentsOption = {},
opts: ResetComponentsOptions = {},
): undefined extends T ? Components : Component[] {
const coll = this.get('components')!;
@ -1428,19 +1437,43 @@ export default class Component extends StyleableModel<ComponentProperties> {
* // -> <span title="Custom attribute"></span>
*/
toHTML(opts: ToHTMLOptions = {}): string {
const model = this;
const attrs = [];
const customTag = opts.tag;
const tag = customTag || model.get('tagName');
const sTag = model.get('void');
const tag = customTag || this.get('tagName');
delete opts.tag;
const attr = this.__attrToString(opts);
const attrString = attr ? ` ${attr}` : '';
const inner = this.getInnerHTML(opts);
const skipEndTag = !inner && this.get('void');
let code = `<${tag}${attrString}${skipEndTag ? '/' : ''}>${inner}`;
!skipEndTag && (code += `</${tag}>`);
return code;
}
/**
* Get inner HTML of the component
* @param {Object} [opts={}] Same options of `toHTML`
* @returns {String} HTML string
*/
getInnerHTML(opts?: ToHTMLOptions) {
return this.__innerHTML(opts);
}
__innerHTML(opts: ToHTMLOptions = {}) {
const cmps = this.components();
return !cmps.length ? this.content : cmps.map((c) => c.toHTML(opts)).join('');
}
__attrToString(opts: ToHTMLOptions = {}) {
const attrs = [];
const customAttr = opts.attributes;
let attributes = this.getAttrToHTML(opts);
delete opts.tag;
// Get custom attributes if requested
if (customAttr) {
if (isFunction(customAttr)) {
attributes = customAttr(model, attributes) || {};
attributes = customAttr(this, attributes) || {};
} else if (isObject(customAttr)) {
attributes = customAttr;
}
@ -1448,7 +1481,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (opts.withProps) {
const props = this.toJSON();
forEach(props, (value, key) => {
const skipProps = ['classes', 'attributes', 'components'];
if (key[0] !== '_' && skipProps.indexOf(key) < 0) {
@ -1478,26 +1510,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
}
}
const attrString = attrs.length ? ` ${attrs.join(' ')}` : '';
const inner = model.getInnerHTML(opts);
let code = `<${tag}${attrString}${sTag ? '/' : ''}>${inner}`;
!sTag && (code += `</${tag}>`);
return code;
}
/**
* Get inner HTML of the component
* @param {Object} [opts={}] Same options of `toHTML`
* @returns {String} HTML string
*/
getInnerHTML(opts?: ToHTMLOptions) {
return this.__innerHTML(opts);
}
__innerHTML(opts: ToHTMLOptions = {}) {
const cmps = this.components();
return !cmps.length ? this.content : cmps.map((c) => c.toHTML(opts)).join('');
return attrs.join(' ');
}
/**

13
packages/core/src/dom_components/model/ComponentWrapper.ts

@ -30,19 +30,16 @@ export default class ComponentWrapper extends Component {
};
}
constructor(...args: ConstructorParameters<typeof Component>) {
super(...args);
const props = args[0] || {};
const opts = args[1];
const cmp = opts?.em?.Components;
preInit() {
const { opt, attributes: props } = this;
const cmp = this.em?.Components;
const CmpHead = cmp?.getType(typeHead)?.model;
const CmpDef = cmp?.getType('default').model;
if (CmpHead) {
this.set(
{
head: new CmpHead({ ...props.head }, opts),
docEl: new CmpDef({ tagName: 'html', ...props.docEl }, opts),
head: new CmpHead({ ...props.head }, opt),
docEl: new CmpDef({ tagName: 'html', ...props.docEl }, opt),
},
{ silent: true },
);

5
packages/core/src/dom_components/model/Components.ts

@ -254,12 +254,13 @@ Component> {
parseString(value: string, opt: ParseStringOptions = {}) {
const { em, domc, parent } = this;
const asDocument = opt.asDocument && parent?.is('wrapper');
const isWrapper = parent?.is('wrapper');
const asDocument = opt.asDocument && isWrapper;
const cssc = em.Css;
const parsed = em.Parser.parseHtml(value, { asDocument, ...opt.parserOptions });
let components = parsed.html;
if (asDocument) {
if (isWrapper && parsed.doctype) {
const root = parent as ComponentWrapper;
const { components: bodyCmps, ...restBody } = (parsed.html as ComponentDefinitionDefined) || {};
const { components: headCmps, ...restHead } = parsed.head || {};

6
packages/core/src/dom_components/model/types.ts

@ -19,6 +19,12 @@ export type DraggableDroppableFn = (source: Component, target: Component, index?
export interface AddComponentsOption extends AddOptions, OptionAsDocument {}
export interface ResetComponentsOptions extends AddComponentsOption {
previousModels?: Component[];
keepIds?: string[];
skipDomReset?: boolean;
}
interface ComponentWithCheck<C extends Component> {
new (props: any, opt: ComponentOptions): C;
isComponent(node: HTMLElement, opts?: ParseNodeOptions): ComponentDefinitionDefined | undefined | boolean;

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

@ -7,6 +7,7 @@ import Component from '../model/Component';
import ComponentView from './ComponentView';
import FrameView from '../../canvas/view/FrameView';
import Components from '../model/Components';
import { ResetComponentsOptions } from '../model/types';
export default class ComponentsView extends View {
opts!: any;
@ -124,9 +125,12 @@ export default class ComponentsView extends View {
return rendered;
}
resetChildren(models: Components, { previousModels = [] } = {}) {
this.parentEl!.innerHTML = '';
previousModels.forEach((md) => this.removeChildren(md, this.collection));
resetChildren(models: Components, opts: ResetComponentsOptions = {}) {
const { previousModels } = opts;
if (!opts.skipDomReset) {
this.parentEl!.innerHTML = '';
}
previousModels?.forEach((md) => this.removeChildren(md, this.collection));
models.each((model) => this.addToCollection(model));
}

4
packages/core/src/editor/config/config.ts

@ -432,7 +432,7 @@ export interface EditorConfig {
export type EditorConfigKeys = keyof EditorConfig;
const config: EditorConfig = {
const config: () => EditorConfig = () => ({
stylePrefix: 'gjs-',
components: '',
style: '',
@ -506,6 +506,6 @@ const config: EditorConfig = {
textViewCode: 'Code',
keepUnusedStyles: false,
customUI: false,
};
});
export default config;

3
packages/core/src/editor/index.ts

@ -89,7 +89,7 @@ import TraitManager from '../trait_manager';
import UndoManagerModule from '../undo_manager';
import UtilsModule from '../utils';
import html from '../utils/html';
import defaults, { EditorConfig, EditorConfigKeys } from './config/config';
import defConfig, { EditorConfig, EditorConfigKeys } from './config/config';
import EditorModel, { EditorLoadOptions } from './model/Editor';
import EditorView from './view/EditorView';
@ -132,6 +132,7 @@ export default class Editor implements IBaseModule<EditorConfig> {
config: EditorConfigType;
constructor(config: EditorConfig = {}, opts: any = {}) {
const defaults = defConfig();
this.config = {
...defaults,
...config,

1
packages/core/src/editor/model/Editor.ts

@ -365,6 +365,7 @@ export default class EditorModel extends Model {
storageManager: false,
undoManager: false,
});
shallow.set({ isShallow: true });
// We only need to load a few modules
shallow.Pages.onLoad();
shallow.Canvas.postLoad();

4
packages/core/src/i18n/config.ts

@ -37,13 +37,13 @@ export interface I18nConfig {
messagesAdd?: Record<string, any>;
}
const config: I18nConfig = {
const config: () => I18nConfig = () => ({
locale: 'en',
localeFallback: 'en',
detectLocale: true,
debug: false,
messages: { en },
messagesAdd: undefined,
};
});
export default config;

4
packages/core/src/i18n/index.ts

@ -27,7 +27,7 @@ import { isUndefined, isString } from 'underscore';
import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import { hasWin, deepMerge } from '../utils/mixins';
import defaults, { I18nConfig } from './config';
import defConfig, { I18nConfig } from './config';
import I18nEvents, { Messages } from './types';
export default class I18nModule extends Module<I18nConfig & { stylePrefix?: string }> {
@ -39,7 +39,7 @@ export default class I18nModule extends Module<I18nConfig & { stylePrefix?: stri
* @private
*/
constructor(em: EditorModel) {
super(em, 'I18n', defaults);
super(em, 'I18n', defConfig());
const add = this.config.messagesAdd;
add && this.addMessages(add);

4
packages/core/src/keymaps/config.ts

@ -24,7 +24,7 @@ export interface KeymapsConfig {
defaults?: Record<string, Omit<Keymap, 'id'> & { opts?: KeymapOptions }>;
}
const config: KeymapsConfig = {
const config: () => KeymapsConfig = () => ({
defaults: {
'core:undo': {
keys: '⌘+z, ctrl+z',
@ -66,6 +66,6 @@ const config: KeymapsConfig = {
opts: { prevent: true },
},
},
};
});
export default config;

4
packages/core/src/keymaps/index.ts

@ -48,7 +48,7 @@ import { hasWin } from '../utils/mixins';
import keymaster from '../utils/keymaster';
import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import defaults, { Keymap, KeymapOptions, KeymapsConfig } from './config';
import defConfig, { Keymap, KeymapOptions, KeymapsConfig } from './config';
export type KeymapEvent = 'keymap:add' | 'keymap:remove' | 'keymap:emit' | `keymap:emit:${string}`;
@ -59,7 +59,7 @@ export default class KeymapsModule extends Module<KeymapsConfig & { name?: strin
keymaps: Record<string, Keymap>;
constructor(em: EditorModel) {
super(em, 'Keymaps', defaults);
super(em, 'Keymaps', defConfig());
this.keymaps = {};
}

4
packages/core/src/modal_dialog/config/config.ts

@ -26,13 +26,13 @@ export interface ModalConfig {
extend?: Record<string, any>;
}
const config: ModalConfig = {
const config: () => ModalConfig = () => ({
stylePrefix: 'mdl-',
title: '',
content: '',
backdrop: true,
custom: false,
extend: {},
};
});
export default config;

4
packages/core/src/modal_dialog/index.ts

@ -38,7 +38,7 @@ import { Module } from '../abstract';
import EditorView from '../editor/view/EditorView';
import EditorModel from '../editor/model/Editor';
import { createText } from '../utils/dom';
import defaults, { ModalConfig } from './config/config';
import defConfig, { ModalConfig } from './config/config';
import ModalM from './model/Modal';
import ModalView from './view/ModalView';
import { EventHandler } from '../common';
@ -54,7 +54,7 @@ export default class ModalModule extends Module<ModalConfig> {
* @private
*/
constructor(em: EditorModel) {
super(em, 'Modal', defaults);
super(em, 'Modal', defConfig());
this.model = new ModalM(this);
this.model.on('change:open', (m: ModalM, enable: boolean) => {

4
packages/core/src/navigator/config/config.ts

@ -108,7 +108,7 @@ export interface LayerManagerConfig {
extend?: Record<string, any>;
}
const config: LayerManagerConfig = {
const config: () => LayerManagerConfig = () => ({
stylePrefix: '',
appendTo: '',
sortable: true,
@ -124,6 +124,6 @@ const config: LayerManagerConfig = {
onInit: () => {},
onRender: () => {},
extend: {},
};
});
export default config;

4
packages/core/src/navigator/index.ts

@ -45,7 +45,7 @@ import Module from '../abstract/Module';
import Component from '../dom_components/model/Component';
import EditorModel from '../editor/model/Editor';
import { hasWin, isComponent, isDef } from '../utils/mixins';
import defaults, { LayerManagerConfig } from './config/config';
import defConfig, { LayerManagerConfig } from './config/config';
import View from './view/ItemView';
import { ComponentsEvents } from '../dom_components/types';
@ -91,7 +91,7 @@ export default class LayerManager extends Module<LayerManagerConfig> {
events = events;
constructor(em: EditorModel) {
super(em, 'LayerManager', defaults);
super(em, 'LayerManager', defConfig());
bindAll(this, 'componentChanged', '__onRootChange', '__onComponent');
this.model = new ModuleModel(this, { opened: {} });
// @ts-ignore

4
packages/core/src/panels/config/config.ts

@ -34,7 +34,7 @@ export interface PanelsConfig {
defaults?: PanelProps[];
}
const config: PanelsConfig = {
const config: () => PanelsConfig = () => ({
stylePrefix: 'pn-',
defaults: [
{
@ -109,6 +109,6 @@ const config: PanelsConfig = {
],
},
],
};
});
export default config;

9
packages/core/src/panels/index.ts

@ -27,7 +27,7 @@
*/
import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import defaults, { PanelsConfig } from './config/config';
import defConfig, { PanelsConfig } from './config/config';
import Panel, { PanelProperties } from './model/Panel';
import Panels from './model/Panels';
import PanelsView from './view/PanelsView';
@ -42,13 +42,8 @@ export default class PanelManager extends Module<PanelsConfig> {
* @private
*/
constructor(em: EditorModel) {
super(em, 'Panels', defaults);
super(em, 'Panels', defConfig());
this.panels = new Panels(this, this.config.defaults!);
for (var name in defaults) {
//@ts-ignore
if (!(name in this.config)) this.config[name] = defaults[name];
}
return this;
}
/**

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

@ -60,6 +60,11 @@ export interface HTMLParserOptions extends OptionAsDocument {
*/
keepEmptyTextNodes?: boolean;
/**
* Indicate if or how to detect if the passed HTML string should be parsed as a document.
*/
detectDocument?: boolean | ((html: string) => boolean);
/**
* Custom transformer to run before passing the input HTML to the parser.
* A common use case might be to sanitize the input string.
@ -105,7 +110,7 @@ export interface ParserConfig {
optionsHtml?: HTMLParserOptions;
}
const config: ParserConfig = {
const config: () => ParserConfig = () => ({
textTags: ['br', 'b', 'i', 'u', 'a', 'ul', 'ol'],
textTypes: ['text', 'textnode', 'comment'],
parserCss: undefined,
@ -117,6 +122,6 @@ const config: ParserConfig = {
allowUnsafeAttrValue: false,
keepEmptyTextNodes: false,
},
};
});
export default config;

4
packages/core/src/parser/index.ts

@ -26,7 +26,7 @@
*/
import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import defaults, { HTMLParserOptions, ParserConfig } from './config/config';
import defConfig, { HTMLParserOptions, ParserConfig } from './config/config';
import ParserCss from './model/ParserCss';
import ParserHtml from './model/ParserHtml';
@ -35,7 +35,7 @@ export default class ParserModule extends Module<ParserConfig & { name?: string
parserCss: ReturnType<typeof ParserCss>;
constructor(em: EditorModel) {
super(em, 'Parser', defaults);
super(em, 'Parser', defConfig());
const { config } = this;
this.parserCss = ParserCss(em, config);
this.parserHtml = ParserHtml(em, config);

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

@ -5,6 +5,7 @@ import EditorModel from '../../editor/model/Editor';
import { HTMLParseResult, HTMLParserOptions, ParseNodeOptions, ParserConfig } from '../config/config';
import BrowserParserHtml from './BrowserParserHtml';
import { doctypeToString } from '../../utils/dom';
import { isDef } from '../../utils/mixins';
const modelAttrStart = 'data-gjs-';
const event = 'parse:html';
@ -313,12 +314,16 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
const conf = em?.get('Config') || {};
const res: HTMLParseResult = { html: [] };
const cf = { ...config, ...opts };
const options = {
const preOptions = {
...config.optionsHtml,
// @ts-ignore Support previous `configParser.htmlType` option
htmlType: config.optionsHtml?.htmlType || config.htmlType,
...opts,
};
const options = {
...preOptions,
asDocument: this.__checkAsDocument(str, preOptions),
};
const { preParser, asDocument } = options;
const input = isFunction(preParser) ? preParser(str, { editor: em?.getEditor()! }) : str;
const parseRes = isFunction(cf.parserHtml) ? cf.parserHtml(input, options) : BrowserParserHtml(input, options);
@ -374,7 +379,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
}
res.html = resHtml;
em?.trigger(event, { input, output: res });
em?.trigger(event, { input, output: res, options });
return res;
},
@ -392,6 +397,16 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
toRemove.map((name) => node.removeAttribute(name));
each(nodes, (node) => this.__sanitizeNode(node as HTMLElement, opts));
},
__checkAsDocument(str: string, opts: HTMLParserOptions) {
if (isDef(opts.asDocument)) {
return opts.asDocument;
} else if (isFunction(opts.detectDocument)) {
return !!opts.detectDocument(str);
} else if (opts.detectDocument) {
return str.toLowerCase().trim().startsWith('<!doctype');
}
},
};
};

4
packages/core/src/rich_text_editor/config/config.ts

@ -80,11 +80,11 @@ export interface RichTextEditorConfig {
custom?: boolean;
}
const config: RichTextEditorConfig = {
const config: () => RichTextEditorConfig = () => ({
stylePrefix: 'rte-',
adjustToolbar: true,
actions: ['bold', 'italic', 'underline', 'strikethrough', 'link', 'wrap'],
custom: false,
};
});
export default config;

4
packages/core/src/rich_text_editor/index.ts

@ -44,7 +44,7 @@ import ComponentView from '../dom_components/view/ComponentView';
import EditorModel from '../editor/model/Editor';
import { createEl, cx, on, removeEl } from '../utils/dom';
import { hasWin, isDef } from '../utils/mixins';
import defaults, { CustomRTE, RichTextEditorConfig } from './config/config';
import defConfig, { CustomRTE, RichTextEditorConfig } from './config/config';
import RichTextEditor, { RichTextEditorAction } from './model/RichTextEditor';
import CanvasEvents from '../canvas/types';
import { ComponentsEvents } from '../dom_components/types';
@ -87,7 +87,7 @@ export default class RichTextEditorModule extends Module<RichTextEditorConfig &
*/
constructor(em: EditorModel) {
super(em, 'RichTextEditor', defaults);
super(em, 'RichTextEditor', defConfig());
const { config } = this;
const ppfx = config.pStylePrefix;

4
packages/core/src/selector_manager/config/config.ts

@ -132,7 +132,7 @@ export interface SelectorManagerConfig {
custom?: boolean;
}
const config: SelectorManagerConfig = {
const config: () => SelectorManagerConfig = () => ({
stylePrefix: 'clm-',
appendTo: '',
selectors: [],
@ -148,6 +148,6 @@ const config: SelectorManagerConfig = {
'<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path></svg>',
componentFirst: false,
custom: false,
};
});
export default config;

5
packages/core/src/selector_manager/index.ts

@ -76,7 +76,7 @@
import { isString, debounce, isObject, isArray, bindAll } from 'underscore';
import { isComponent, isRule } from '../utils/mixins';
import { Model, Collection, RemoveOptions, SetOptions, Debounced } from '../common';
import defaults, { SelectorManagerConfig } from './config/config';
import defConfig, { SelectorManagerConfig } from './config/config';
import Selector from './model/Selector';
import Selectors from './model/Selectors';
import State from './model/State';
@ -86,7 +86,6 @@ import Component from '../dom_components/model/Component';
import { ItemManagerModule } from '../abstract/Module';
import { StyleModuleParam } from '../style_manager';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import CssRule from '../css_composer/model/CssRule';
import { ComponentsEvents } from '../dom_components/types';
export type SelectorEvent = 'selector:add' | 'selector:remove' | 'selector:update' | 'selector:state' | 'selector';
@ -138,7 +137,7 @@ export default class SelectorManager extends ItemManagerModule<SelectorManagerCo
*/
constructor(em: EditorModel) {
super(em, 'SelectorManager', new Selectors([]), selectorEvents, defaults, { skipListen: true });
super(em, 'SelectorManager', new Selectors([]), selectorEvents, defConfig(), { skipListen: true });
bindAll(this, '__updateSelectedByComponents');
const { config } = this;
const ppfx = config.pStylePrefix;

4
packages/core/src/storage_manager/config/config.ts

@ -78,7 +78,7 @@ export interface StorageManagerConfig {
};
}
const config: StorageManagerConfig = {
const config: () => StorageManagerConfig = () => ({
id: 'gjs-',
type: 'local',
autosave: true,
@ -103,6 +103,6 @@ const config: StorageManagerConfig = {
onLoad: (result) => result,
},
},
};
});
export default config;

4
packages/core/src/storage_manager/index.ts

@ -42,7 +42,7 @@
import { isEmpty, isFunction } from 'underscore';
import { Module } from '../abstract';
import defaults, { StorageManagerConfig } from './config/config';
import defConfig, { StorageManagerConfig } from './config/config';
import LocalStorage from './model/LocalStorage';
import RemoteStorage from './model/RemoteStorage';
import EditorModel from '../editor/model/Editor';
@ -65,7 +65,7 @@ export default class StorageManager extends Module<
events = StorageEvents;
constructor(em: EditorModel) {
super(em, 'StorageManager', defaults);
super(em, 'StorageManager', defConfig());
const { config } = this;
if (config._disable) config.type = undefined;
this.storages = {};

6
packages/core/src/style_manager/config/config.ts

@ -66,7 +66,7 @@ export interface StyleManagerConfig {
pStylePrefix?: string;
}
export default {
const config: () => StyleManagerConfig = () => ({
sectors: [
{
name: 'General',
@ -128,4 +128,6 @@ export default {
showComputed: true,
clearProperties: true,
avoidComputed: ['width', 'height'],
} as StyleManagerConfig;
});
export default config;

4
packages/core/src/style_manager/index.ts

@ -66,7 +66,7 @@
import { isUndefined, isArray, isString, debounce, bindAll } from 'underscore';
import { isComponent } from '../utils/mixins';
import { AddOptions, Debounced, Model } from '../common';
import defaults, { StyleManagerConfig } from './config/config';
import defConfig, { StyleManagerConfig } from './config/config';
import Sector, { SectorProperties } from './model/Sector';
import Sectors from './model/Sectors';
import Properties from './model/Properties';
@ -157,7 +157,7 @@ export default class StyleManager extends ItemManagerModule<
* @private
*/
constructor(em: EditorModel) {
super(em, 'StyleManager', new Sectors([], { em }), stylesEvents, defaults);
super(em, 'StyleManager', new Sectors([], { em }), stylesEvents, defConfig());
bindAll(this, '__clearStateTarget');
const c = this.config;
const ppfx = c.pStylePrefix;

4
packages/core/src/trait_manager/config/config.ts

@ -22,11 +22,11 @@ export interface TraitManagerConfig {
optionsTarget?: Record<string, any>[];
}
const config: TraitManagerConfig = {
const config: () => TraitManagerConfig = () => ({
stylePrefix: 'trt-',
appendTo: '',
optionsTarget: [{ value: false }, { value: '_blank' }],
custom: false,
};
});
export default config;

4
packages/core/src/trait_manager/index.ts

@ -33,7 +33,7 @@ import { Module } from '../abstract';
import { Model } from '../common';
import Component from '../dom_components/model/Component';
import EditorModel from '../editor/model/Editor';
import defaults from './config/config';
import defConfig from './config/config';
import {
CustomTrait,
TraitCustomData,
@ -81,7 +81,7 @@ export default class TraitManager extends Module<TraitManagerConfigModule> {
* @private
*/
constructor(em: EditorModel) {
super(em, 'TraitManager', defaults as any);
super(em, 'TraitManager', defConfig() as any);
const { state, config, events } = this;
const ppfx = config.pStylePrefix;
ppfx && (config.stylePrefix = `${ppfx}${config.stylePrefix}`);

4
packages/core/src/undo_manager/config.ts

@ -11,9 +11,9 @@ export interface UndoManagerConfig {
trackSelection?: boolean;
}
const config: UndoManagerConfig = {
const config: () => UndoManagerConfig = () => ({
maximumStackLength: 500,
trackSelection: true,
};
});
export default config;

4
packages/core/src/undo_manager/index.ts

@ -28,7 +28,7 @@ import UndoManager from 'backbone-undo';
import { isArray, isBoolean, isEmpty, unique, times } from 'underscore';
import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import defaults, { UndoManagerConfig } from './config';
import defConfig, { UndoManagerConfig } from './config';
export interface UndoGroup {
index: number;
@ -47,7 +47,7 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
um: any;
constructor(em: EditorModel) {
super(em, 'UndoManager', defaults);
super(em, 'UndoManager', defConfig());
if (this.config._disable) {
this.config.maximumStackLength = 0;

14
packages/core/src/utils/Droppable.ts

@ -29,6 +29,7 @@ export default class Droppable {
dragStop?: DragStop;
draggedNode?: CanvasNewComponentNode;
sorter!: ComponentSorter<CanvasNewComponentNode>;
setAbsoluteDragContent?: (cnt: any) => any;
constructor(em: EditorModel, rootEl?: HTMLElement) {
this.em = em;
@ -123,7 +124,6 @@ export default class Droppable {
// any not empty element
let content = dragSourceOrigin?.content || '<br>';
let dragStop: DragStop;
let dragSource;
em.stopDefault();
// Select the right drag provider
@ -137,7 +137,7 @@ export default class Droppable {
target,
onEnd: (ev: any, dragger: any, { cancelled }: any) => {
let comp;
if (!cancelled) {
if (!cancelled && !!content) {
comp = wrapper.append(content)[0];
const canvasOffset = canvas.getOffset();
const { top, left, position } = target.getStyle() as ObjectStrings;
@ -156,7 +156,7 @@ export default class Droppable {
},
});
dragStop = (cancel?: boolean) => dragger.stop(ev, { cancel });
dragSource = (cnt: any) => (content = cnt);
this.setAbsoluteDragContent = (cnt: any) => (content = cnt);
} else {
const sorter = new utils.ComponentSorter({
em,
@ -187,13 +187,6 @@ export default class Droppable {
sorter.eventHandlers.legacyOnEnd = sorterOptions.legacyOnEnd;
sorter.containerContext.customTarget = sorterOptions.customTarget;
}
this.em.on(
'frame:scroll',
((...agrs: any[]) => {
const canvasScroll = this.canvas.getCanvasView().frame === agrs[0].frame;
if (canvasScroll) sorter.recalculateTargetOnScroll();
}).bind(this),
);
let dropModel = this.getTempDropModel(content);
const el = dropModel.view?.el;
const sources = el ? [{ element: el, dragSource: dragSourceOrigin }] : [];
@ -262,6 +255,7 @@ export default class Droppable {
if (this.draggedNode) {
this.draggedNode.content = content;
}
this.setAbsoluteDragContent?.(content);
this.endDrop(!content, ev);
}

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

@ -8,6 +8,8 @@ import { SortableTreeNode } from './SortableTreeNode';
* Subclasses must implement the `view` and `element` methods.
*/
export abstract class BaseComponentNode extends SortableTreeNode<Component> {
private displayCache: Map<Component, boolean> = new Map();
/**
* Get the list of child components.
* @returns {BaseComponentNode[] | null} - The list of children wrapped in
@ -19,20 +21,37 @@ export abstract class BaseComponentNode extends SortableTreeNode<Component> {
/**
* Get the list of displayed children, i.e., components that have a valid HTML element.
* Cached values are used to avoid recalculating the display status unnecessarily.
* @returns {BaseComponentNode[] | null} - The list of displayed children wrapped in
* BaseComponentNode, or null if there are no displayed children.
*/
private getDisplayedChildren(): BaseComponentNode[] | null {
const children = this.model.components();
const displayedChildren = children.filter((child) => {
const element = child.getEl();
return isDisplayed(element);
});
const displayedChildren = children.filter((child) => this.isChildDisplayed(child));
return displayedChildren.map((comp: Component) => new (this.constructor as any)(comp));
}
/**
* Check if a child is displayed, using cached value if available.
* @param {Component} child - The child component to check.
* @returns {boolean} - Whether the child is displayed.
*/
private isChildDisplayed(child: Component): boolean {
// Check if display status is cached
if (this.displayCache.has(child)) {
return this.displayCache.get(child)!;
}
const element = child.getEl();
const displayed = isDisplayed(element);
// Cache the display status
this.displayCache.set(child, displayed);
return displayed;
}
/**
* Get the parent component of this node.
* @returns {BaseComponentNode | null} - The parent wrapped in BaseComponentNode,
@ -114,8 +133,7 @@ export abstract class BaseComponentNode extends SortableTreeNode<Component> {
for (let i = 0; i < children.length; i++) {
const child = children.at(i);
const element = child.getEl();
const displayed = isDisplayed(element);
const displayed = this.isChildDisplayed(child);
if (displayed) displayedCount++;
if (displayedCount === index + 1) return i;
@ -208,8 +226,13 @@ export abstract class BaseComponentNode extends SortableTreeNode<Component> {
}
}
function isDisplayed(element: HTMLElement | undefined) {
if (!!!element) return false;
/**
* Function to check if an element is displayed in the DOM.
* @param {HTMLElement | undefined} element - The element to check.
* @returns {boolean} - Whether the element is displayed.
*/
function isDisplayed(element: HTMLElement | undefined): boolean {
if (!element) return false;
return (
element instanceof HTMLElement &&
window.getComputedStyle(element).display !== 'none' &&

5
packages/core/src/utils/sorter/CanvasComponentNode.ts

@ -1,6 +1,11 @@
import { BaseComponentNode } from './BaseComponentNode';
export default class CanvasComponentNode extends BaseComponentNode {
protected _dropAreaConfig = {
ratio: 0.8,
minUndroppableDimension: 1, // In px
maxUndroppableDimension: 15, // In px
};
/**
* Get the associated view of this component.
* @returns The view associated with the component, or undefined if none.

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

@ -3,6 +3,7 @@ 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;
@ -13,13 +14,14 @@ export default class CanvasNewComponentNode extends CanvasComponentNode {
const canMoveSymbol = !symbolModel || !this.isSourceSameSymbol(symbolModel);
const sourceContent: CanMoveSource = (isFunction(content) ? dragDef : content) || source.model;
if (Array.isArray(sourceContent)) {
return (
sourceContent.every((contentItem, i) => this.canMoveSingleContent(contentItem, realIndex + i)) && canMoveSymbol
canMoveSymbol && sourceContent.every((contentItem, i) => this.canMoveSingleContent(contentItem, realIndex + i))
);
}
return this.canMoveSingleContent(sourceContent, realIndex) && canMoveSymbol;
return canMoveSymbol && this.canMoveSingleContent(sourceContent, realIndex);
}
private canMoveSingleContent(contentItem: ContentElement | Component, index: number): boolean {
@ -39,6 +41,13 @@ export default class CanvasNewComponentNode extends CanvasComponentNode {
return this.addSingleChild(content, index, insertingTextableIntoText);
}
/**
* Adds a single content item to the current node.
* @param {ContentType} content - The content to add.
* @param {number} index - The index where the content is to be added.
* @param {boolean} insertingTextableIntoText - Whether the operation involves textable content.
* @returns {CanvasNewComponentNode} - The newly added node.
*/
private addSingleChild(
content: ContentType,
index: number,

31
packages/core/src/utils/sorter/ComponentSorter.ts

@ -1,3 +1,4 @@
import { bindAll } from 'underscore';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import Component from '../../dom_components/model/Component';
import EditorModel from '../../editor/model/Editor';
@ -62,6 +63,8 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
},
},
});
bindAll(this, 'handleScrollEvent');
}
private onStartSort() {
@ -69,6 +72,22 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
this.setAutoCanvasScroll(true);
}
protected bindDragEventHandlers() {
this.em.on('frame:scroll', this.handleScrollEvent);
super.bindDragEventHandlers();
}
protected cleanupEventListeners(): void {
this.em.off('frame:scroll', this.handleScrollEvent);
super.cleanupEventListeners();
}
handleScrollEvent(...agrs: any[]) {
const frame = agrs?.[0]?.frame;
const canvasScroll = this.em.Canvas.getCanvasView().frame === frame;
if (canvasScroll) this.recalculateTargetOnScroll();
}
private onMouseMove = (mouseEvent: MouseEvent) => {
const insertingTextableIntoText = this.targetIsText && this.sourceNodes?.some((node) => node.isTextable());
if (insertingTextableIntoText) {
@ -110,8 +129,9 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
return sourceNodes.reduce((addedNodes, sourceNode) => {
if (!targetNode.canMove(sourceNode, index)) return addedNodes;
if (this.isPositionChanged(targetNode, sourceNode, index)) {
const addedNode = this.moveNode(targetNode, sourceNode, index);
const { index: lastIndex, addedNode } = this.moveNode(targetNode, sourceNode, index);
addedNodes.push(addedNode);
index = lastIndex;
}
index++; // Increment the index
return addedNodes;
@ -146,9 +166,9 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
* @param targetNode - The node where the source node will be moved.
* @param sourceNode - The node being moved.
* @param index - The index at which to move the source node.
* @returns The node that was moved and added, or null if it couldn't be moved.
* @returns An object containing the added node and its new index, or null if it couldn't be moved.
*/
private moveNode(targetNode: NodeType, sourceNode: NodeType, index: number): NodeType {
private moveNode(targetNode: NodeType, sourceNode: NodeType, index: number) {
const parent = sourceNode.getParent();
if (parent) {
const initialSourceIndex = parent.indexOfChild(sourceNode);
@ -161,7 +181,7 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
const addedNode = targetNode.addChildAt(sourceNode, index, { action: 'move-component' }) as NodeType;
this.triggerEndMoveEvent(addedNode);
return addedNode;
return { addedNode, index };
}
/**
@ -193,8 +213,10 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
private onTargetChange = (oldTargetNode: NodeType | undefined, newTargetNode: NodeType | undefined) => {
oldTargetNode?.restNodeState();
const { Canvas } = this.em;
if (!newTargetNode) {
this.placeholder.hide();
Canvas.removeSpots(spotTarget);
return;
}
newTargetNode?.setSelectedParentState();
@ -207,7 +229,6 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
this.placeholder.show();
}
const { Canvas } = this.em;
const { Select, Hover, Spacing } = CanvasSpotBuiltInTypes;
[Select, Hover, Spacing].forEach((type) => Canvas.removeSpots({ type }));
Canvas.addSpot({ ...spotTarget, component: newTargetNode.model });

158
packages/core/src/utils/sorter/Dimension.ts

@ -0,0 +1,158 @@
import CanvasModule from '../../canvas';
import { Placement, DroppableZoneConfig } from './types';
/**
* A class representing dimensions of an element, including position, size, offsets, and other metadata.
* Provides functionality to calculate differences between current and previous dimensions and update them.
*/
export default class Dimension {
public top: number;
public left: number;
public height: number;
public width: number;
public offsets: ReturnType<CanvasModule['getElementOffsets']>;
public dir?: boolean;
/**
* Initializes the DimensionCalculator with the given initial dimensions.
*
* @param initialDimensions - The initial dimensions containing `top`, `left`, `height`, `width`, and other properties.
*/
constructor(initialDimensions: {
top: number;
left: number;
height: number;
width: number;
offsets: ReturnType<CanvasModule['getElementOffsets']>;
dir?: boolean;
el?: HTMLElement;
indexEl?: number;
}) {
this.top = initialDimensions.top;
this.left = initialDimensions.left;
this.height = initialDimensions.height;
this.width = initialDimensions.width;
this.offsets = initialDimensions.offsets;
this.dir = initialDimensions.dir;
}
/**
* Calculates the difference between the current and previous dimensions.
* If there are no previous dimensions, it will return zero differences.
*
* @returns An object containing the differences in `top` and `left` positions.
*/
public calculateDimensionDifference(dimension: Dimension): { topDifference: number; leftDifference: number } {
const topDifference = dimension.top - this.top;
const leftDifference = dimension.left - this.left;
return { topDifference, leftDifference };
}
/**
* Updates the current dimensions by adding the given differences to the `top` and `left` values.
*
* @param topDifference - The difference to add to the current `top` value.
* @param leftDifference - The difference to add to the current `left` value.
*/
public adjustDimensions(difference: { topDifference: number; leftDifference: number }): Dimension {
this.top += difference.topDifference;
this.left += difference.leftDifference;
return this;
}
/**
* Determines the placement ('before' or 'after') based on the X and Y coordinates and center points.
*
* @param {number} mouseX X coordinate of the mouse
* @param {number} mouseY Y coordinate of the mouse
* @return {Placement} 'before' or 'after'
*/
public determinePlacement(mouseX: number, mouseY: number): Placement {
const xCenter = this.left + this.width / 2;
const yCenter = this.top + this.height / 2;
if (this.dir) {
return mouseY < yCenter ? 'before' : 'after';
} else {
return mouseX < xCenter ? 'before' : 'after';
}
}
/**
* Compares the current dimension object with another dimension to check equality.
*
* @param {Dimension} dimension - The dimension to compare against.
* @returns {boolean} True if the dimensions are equal, otherwise false.
*/
public equals(dimension: Dimension | undefined): boolean {
if (!dimension) return false;
return (
this.top === dimension.top &&
this.left === dimension.left &&
this.height === dimension.height &&
this.width === dimension.width &&
this.dir === dimension.dir &&
JSON.stringify(this.offsets) === JSON.stringify(dimension.offsets)
);
}
/**
* Creates a clone of the current Dimension object.
*
* @returns {Dimension} A new Dimension object with the same properties.
*/
public clone(): Dimension {
return new Dimension({
top: this.top,
left: this.left,
height: this.height,
width: this.width,
offsets: { ...this.offsets }, // Shallow copy of offsets
dir: this.dir,
});
}
public getDropArea(config: DroppableZoneConfig): Dimension {
const dropZone = this.clone();
// Adjust width
const { newSize: newWidth, newPosition: newLeft } = this.adjustDropDimension(this.width, this.left, config);
dropZone.width = newWidth;
dropZone.left = newLeft;
// Adjust height
const { newSize: newHeight, newPosition: newTop } = this.adjustDropDimension(this.height, this.top, config);
dropZone.height = newHeight;
dropZone.top = newTop;
return dropZone;
}
private adjustDropDimension(
size: number,
position: number,
config: DroppableZoneConfig,
): { newSize: number; newPosition: number } {
const { ratio, minUndroppableDimension: minUnDroppableDimension, maxUndroppableDimension } = config;
let undroppableDimension = (size * (1 - ratio)) / 2;
undroppableDimension = Math.max(undroppableDimension, minUnDroppableDimension);
undroppableDimension = Math.min(undroppableDimension, maxUndroppableDimension);
const newSize = size - undroppableDimension * 2;
const newPosition = position + undroppableDimension;
return { newSize, newPosition };
}
/**
* Checks if the given coordinates are within the bounds of this dimension instance.
*
* @param {number} x - The X coordinate to check.
* @param {number} y - The Y coordinate to check.
* @returns {boolean} - True if the coordinates are within bounds, otherwise false.
*/
public isWithinBounds(x: number, y: number): boolean {
return x >= this.left && x <= this.left + this.width && y >= this.top && y <= this.top + this.height;
}
}

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

@ -3,17 +3,11 @@ import { $, View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import { isTextNode, off, on } from '../dom';
import { SortableTreeNode } from './SortableTreeNode';
import {
Dimension,
Placement,
PositionOptions,
DragDirection,
SorterEventHandlers,
CustomTarget,
DragSource,
} from './types';
import { Placement, PositionOptions, DragDirection, SorterEventHandlers, CustomTarget, DragSource } from './types';
import { bindAll, each } from 'underscore';
import { matches, findPosition, offset, isInFlow } from './SorterUtils';
import { matches, findPosition, offset, isStyleInFlow } from './SorterUtils';
import { RateLimiter } from './RateLimiter';
import Dimension from './Dimension';
type ContainerContext = {
container: HTMLElement;
@ -35,19 +29,19 @@ interface DropLocationDeterminerOptions<T, NodeType extends SortableTreeNode<T>>
* Represents the data related to the last move event during drag-and-drop sorting.
* This type is discriminated by the presence or absence of a valid target node.
*/
type LastMoveData<NodeType> = {
type lastMoveData<NodeType> = {
/** The target node under the mouse pointer during the last move. */
lastTargetNode?: NodeType;
targetNode?: NodeType;
/** The node under the mouse pointer during this move*/
hoveredNode?: NodeType;
/** The index where the placeholder or dragged element should be inserted. */
lastIndex?: number;
index?: number;
/** Placement relative to the target ('before' or 'after'). */
lastPlacement?: Placement;
/** The dimensions of the target node. */
lastTargetDimensions?: Dimension;
/** The dimensions of the child elements within the target node. */
lastChildrenDimensions?: Dimension[];
placement?: Placement;
/** The mouse event, used if we want to move placeholder with scrolling. */
lastMouseEvent?: MouseEvent;
mouseEvent?: MouseEvent;
placeholderDimensions?: Dimension;
};
export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> extends View {
@ -60,11 +54,13 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
eventHandlers: SorterEventHandlers<NodeType>;
sourceNodes: NodeType[] = [];
lastMoveData!: LastMoveData<NodeType>;
lastMoveData!: lastMoveData<NodeType>;
containerOffset = {
top: 0,
left: 0,
};
private moveThreshold: number = 20;
private rateLimiter: RateLimiter<MouseEvent>; // Rate limiter for onMove
constructor(options: DropLocationDeterminerOptions<T, NodeType>) {
super();
@ -77,6 +73,7 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
bindAll(this, 'endDrag', 'cancelDrag', 'recalculateTargetOnScroll', 'startSort', 'onDragStart', 'onMove');
this.restLastMoveData();
this.rateLimiter = new RateLimiter<MouseEvent>(this.moveThreshold);
}
/**
@ -101,75 +98,113 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
* to determine the new target.
*/
recalculateTargetOnScroll(): void {
const { lastTargetNode, lastMouseEvent } = this.lastMoveData;
// recalculate dimensions when the canvas is scrolled
this.restLastMoveData();
this.lastMoveData.lastTargetNode = lastTargetNode;
const { mouseEvent: lastMouseEvent } = this.lastMoveData;
if (!lastMouseEvent) {
return;
}
this.onMove(lastMouseEvent);
this.lastMoveData.lastMouseEvent = lastMouseEvent;
}
private onMove(mouseEvent: MouseEvent): void {
this.rateLimiter.updateArgs(mouseEvent);
this.rateLimiter.execute(this.handleMove.bind(this));
}
private handleMove(mouseEvent: MouseEvent): void {
this.adjustForScroll();
const { targetNode: lastTargetNode } = this.lastMoveData;
this.eventHandlers.onMouseMove?.(mouseEvent);
const { mouseXRelativeToContainer: mouseX, mouseYRelativeToContainer: mouseY } =
this.getMousePositionRelativeToContainer(mouseEvent);
const { mouseXRelative: mouseX, mouseYRelative: mouseY } = this.getMousePositionRelativeToContainer(
mouseEvent.clientX,
mouseEvent.clientY,
);
const targetNode = this.getTargetNode(mouseEvent);
const targetChanged = !targetNode?.equals(lastTargetNode);
if (targetChanged) {
this.eventHandlers.onTargetChange?.(lastTargetNode, targetNode);
}
if (!targetNode) {
this.triggerLegacyOnMoveCallback(mouseEvent, 0);
this.triggerMoveEvent(mouseX, mouseY);
this.restLastMoveData();
return;
}
// Handle movement over the valid target node
const index = this.handleMovementOnTarget(targetNode, mouseX, mouseY);
const { index, placement, placeholderDimensions } = this.getDropPosition(targetNode, mouseX, mouseY);
const placeHolderMoved =
!placeholderDimensions.equals(this.lastMoveData.placeholderDimensions) ||
placement !== this.lastMoveData.placement;
if (placeHolderMoved) {
this.eventHandlers.onPlaceholderPositionChange?.(placeholderDimensions!, placement!);
}
this.lastMoveData = {
...this.lastMoveData,
targetNode,
mouseEvent,
index,
placement,
placeholderDimensions,
};
this.triggerMoveEvent(mouseX, mouseY);
this.triggerLegacyOnMoveCallback(mouseEvent, index);
this.lastMoveData.lastMouseEvent = mouseEvent;
}
private adjustForScroll() {
const lastTargetNode = this.lastMoveData.targetNode;
if (lastTargetNode?.element) {
const dims = this.getDim(lastTargetNode?.element);
const diff = lastTargetNode.nodeDimensions?.calculateDimensionDifference(dims);
if (diff) {
lastTargetNode.adjustDimensions(diff);
}
}
const lastHoveredNode = this.lastMoveData.hoveredNode;
if (lastHoveredNode?.element) {
const dims = this.getDim(lastHoveredNode?.element);
const diff = lastHoveredNode.nodeDimensions?.calculateDimensionDifference(dims);
if (diff) {
lastHoveredNode.adjustDimensions(diff);
}
}
}
private restLastMoveData() {
this.lastMoveData = {
lastTargetNode: undefined,
lastIndex: undefined,
lastPlacement: undefined,
lastTargetDimensions: undefined,
lastChildrenDimensions: undefined,
lastMouseEvent: undefined,
targetNode: undefined,
index: undefined,
placement: undefined,
mouseEvent: undefined,
};
}
private triggerLegacyOnMoveCallback(mouseEvent: MouseEvent, index: number) {
private triggerLegacyOnMoveCallback(mouseEvent: MouseEvent, index?: number) {
// For backward compatibility, leave it to a single node
const model = this.sourceNodes[0]?.model;
this.eventHandlers.legacyOnMoveClb?.({
event: mouseEvent,
target: model,
parent: this.lastMoveData.lastTargetNode?.model,
parent: this.lastMoveData.targetNode?.model,
index: index,
});
}
private triggerMoveEvent(mouseX: number, mouseY: number) {
const {
lastTargetNode: targetNode,
lastPlacement: placement,
lastIndex: index,
lastChildrenDimensions: childrenDimensions,
} = this.lastMoveData;
const { targetNode: targetNode, placement: placement, index: index } = this.lastMoveData;
const legacyIndex = index ? index + (placement === 'after' ? -1 : 0) : 0;
this.em.trigger('sorter:drag', {
target: targetNode?.element || null,
targetModel: this.lastMoveData.lastTargetNode?.model,
targetModel: this.lastMoveData.targetNode?.model,
sourceModel: this.sourceNodes[0].model,
dims: childrenDimensions || [],
dims: targetNode?.childrenDimensions || [],
pos: {
index: legacyIndex,
indexEl: legacyIndex,
@ -184,64 +219,104 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
* Handles the movement of the dragged element over a target node.
* Updates the placeholder position and triggers relevant events when necessary.
*
* @param hoveredNode - The node currently being hovered over.
* @param node - The node currently being hovered over.
* @param mouseX - The x-coordinate of the mouse relative to the container.
* @param mouseY - The y-coordinate of the mouse relative to the container.
* @returns The index at which the placeholder should be positioned.
*/
private handleMovementOnTarget(hoveredNode: NodeType, mouseX: number, mouseY: number): number {
const { lastTargetNode, lastChildrenDimensions } = this.lastMoveData;
const targetChanged = !hoveredNode.equals(lastTargetNode);
if (targetChanged) {
this.eventHandlers.onTargetChange?.(lastTargetNode, hoveredNode);
}
let placeholderDimensions, index, placement: Placement;
const children = hoveredNode.getChildren();
private getDropPosition(node: NodeType, mouseX: number, mouseY: number) {
let { nodeDimensions, childrenDimensions } = node;
const children = node.getChildren();
const nodeHasChildren = children && children.length > 0;
const hoveredNodeDimensions = this.getDim(hoveredNode.element!);
const childrenDimensions =
targetChanged || !!!lastChildrenDimensions ? this.getChildrenDim(hoveredNode) : lastChildrenDimensions;
nodeDimensions = !nodeDimensions ? this.getDim(node.element!) : nodeDimensions;
node.nodeDimensions = nodeDimensions;
childrenDimensions = !childrenDimensions ? this.getChildrenDim(node) : childrenDimensions;
node.childrenDimensions = childrenDimensions;
let placeholderDimensions = nodeDimensions.clone(),
index = 0,
placement = 'inside' as Placement;
if (nodeHasChildren) {
({ index, placement } = findPosition(childrenDimensions, mouseX, mouseY));
placeholderDimensions = childrenDimensions[index];
} else {
placeholderDimensions = hoveredNodeDimensions;
index = 0;
placement = 'inside';
placeholderDimensions = childrenDimensions[index].clone();
index = index + (placement == 'after' ? 1 : 0);
}
index = index + (placement == 'after' ? 1 : 0);
this.eventHandlers.onPlaceholderPositionChange?.(placeholderDimensions, placement);
this.lastMoveData = {
lastTargetNode: hoveredNode,
lastTargetDimensions: hoveredNodeDimensions,
lastChildrenDimensions: childrenDimensions,
lastIndex: index,
lastPlacement: placement,
return {
index,
placement,
placeholderDimensions,
};
return index;
}
private getTargetNode(mouseEvent: MouseEvent) {
const customTarget = this.containerContext.customTarget;
/**
* 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.
*
* @param hoveredModel - The model corresponding to the hovered element.
* @returns The new or reused hovered node.
*/
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;
return newHoveredNode;
}
/**
* Checks if the target node has changed and returns the last one if they are identical.
*
* @param targetNode - The newly calculated target node.
* @returns The new or reused target node.
*/
private getOrReuseTargetNode(targetNode?: NodeType): NodeType | undefined {
const lastTargetNode = this.lastMoveData.targetNode;
return targetNode?.equals(lastTargetNode) ? lastTargetNode : targetNode;
}
private getMouseTargetElement(mouseEvent: MouseEvent) {
const customTarget = this.containerContext.customTarget;
let mouseTarget = this.containerContext.document.elementFromPoint(
mouseEvent.clientX,
mouseEvent.clientY,
) as HTMLElement;
let mouseTargetEl: HTMLElement | null = customTarget ? customTarget({ event: mouseEvent }) : mouseTarget;
const targetEl = this.getFirstElementWithAModel(mouseTargetEl);
if (!targetEl) return;
const targetModel = $(targetEl)?.data('model');
const mouseTargetNode = new this.treeClass(targetModel);
const targetNode = this.getValidParentNode(mouseTargetNode);
return targetNode;
return mouseTargetEl;
}
private onDragStart(mouseEvent: MouseEvent): void {
@ -253,7 +328,7 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
}
cancelDrag() {
const { lastTargetNode } = this.lastMoveData;
const { targetNode: lastTargetNode } = this.lastMoveData;
this.eventHandlers.onTargetChange?.(lastTargetNode, undefined);
this.finalizeMove();
}
@ -264,16 +339,17 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
this.eventHandlers.onEnd?.();
this.eventHandlers.legacyOnEnd?.();
this.restLastMoveData();
this.rateLimiter.clearTimeout();
}
private dropDragged() {
const { lastTargetNode, lastIndex } = this.lastMoveData;
const { targetNode: lastTargetNode, index: lastIndex } = this.lastMoveData;
this.eventHandlers.onDrop?.(lastTargetNode, this.sourceNodes, lastIndex);
this.finalizeMove();
}
private triggerOnDragEndEvent() {
const { lastTargetNode: targetNode } = this.lastMoveData;
const { targetNode: targetNode } = this.lastMoveData;
// For backward compatibility, leave it to a single node
const firstSourceNode = this.sourceNodes[0];
@ -317,25 +393,59 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
return null;
}
private getValidParentNode(targetNode: NodeType) {
let finalNode = targetNode;
while (finalNode !== null) {
const canMove = this.sourceNodes.some((node) => finalNode.canMove(node, 0));
// For backward compatibility, leave it to a single node
const firstSource = this.sourceNodes[0];
this.em.trigger('sorter:drag:validation', {
valid: canMove,
src: firstSource?.element,
srcModel: firstSource?.model,
trg: finalNode.element,
trgModel: finalNode.model,
});
if (canMove) break;
finalNode = finalNode.getParent()! as NodeType;
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!);
if (!targetNode.isWithinDropBounds(mouseX, mouseY)) {
return this.handleParentTraversal(targetNode, mouseX, mouseY);
}
return finalNode;
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;
return this.handleParentTraversal(targetNode, mouseX, mouseY);
}
private handleParentTraversal(targetNode: NodeType, mouseX: number, mouseY: number): NodeType | undefined {
const parent = targetNode.getParent() as NodeType;
if (!parent) return;
const indexInParent = this.getIndexInParent(parent, targetNode, targetNode.nodeDimensions!, mouseX, mouseY);
return this.getValidParent(parent, indexInParent, mouseX, mouseY);
}
private getIndexInParent(
parent: NodeType,
targetNode: NodeType,
nodeDimensions: Dimension,
mouseX: number,
mouseY: number,
) {
let indexInParent = parent?.indexOfChild(targetNode);
nodeDimensions.dir = this.getDirection(targetNode.element!, parent.element!);
indexInParent = indexInParent + (nodeDimensions.determinePlacement(mouseX, mouseY) == 'after' ? 1 : 0);
return indexInParent;
}
private triggerDragValidation(canMove: boolean, targetNode: NodeType) {
const firstSource = this.sourceNodes[0];
this.em.trigger('sorter:drag:validation', {
valid: canMove,
src: firstSource?.element,
srcModel: firstSource?.model,
trg: targetNode.element,
trgModel: targetNode.model,
});
return firstSource;
}
/**
@ -352,6 +462,25 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
off(this.containerContext.document, 'mouseup dragend touchend', this.endDrag);
}
/**
* Determines if an element is in the normal flow of the document.
* This checks whether the element is not floated or positioned in a way that removes it from the flow.
*
* @param {HTMLElement} el - The element to check.
* @param {HTMLElement} [parent=document.body] - The parent element for additional checks (defaults to `document.body`).
* @return {boolean} Returns `true` if the element is in flow, otherwise `false`.
* @private
*/
private getDirection(el: HTMLElement, parent: HTMLElement): boolean {
let dirValue: boolean;
if (this.dragDirection === DragDirection.Vertical) dirValue = true;
else if (this.dragDirection === DragDirection.Horizontal) dirValue = false;
else dirValue = isStyleInFlow(el, parent);
return dirValue;
}
/**
* Get children dimensions
* @param {NodeType} el Element root
@ -378,14 +507,7 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
}
const dim = this.getDim(el);
let dir = this.dragDirection;
let dirValue: boolean;
if (dir === DragDirection.Vertical) dirValue = true;
else if (dir === DragDirection.Horizontal) dirValue = false;
else dirValue = isInFlow(el, targetElement);
dim.dir = dirValue;
dim.dir = this.getDirection(el, targetElement);
dims.push(dim);
});
@ -395,27 +517,26 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
/**
* Gets the mouse position relative to the container, adjusting for scroll and canvas relative options.
*
* @param {MouseEvent} mouseEvent - The current mouse event.
* @return {{ mouseXRelativeToContainer: number, mouseYRelativeToContainer: number }} - The mouse X and Y positions relative to the container.
* @private
*/
private getMousePositionRelativeToContainer(mouseEvent: MouseEvent): {
mouseXRelativeToContainer: number;
mouseYRelativeToContainer: number;
private getMousePositionRelativeToContainer(
mouseX: number,
mouseY: number,
): {
mouseXRelative: number;
mouseYRelative: number;
} {
const { em } = this;
let mouseYRelativeToContainer =
mouseEvent.pageY - this.containerOffset.top + this.containerContext.container.scrollTop;
let mouseXRelativeToContainer =
mouseEvent.pageX - this.containerOffset.left + this.containerContext.container.scrollLeft;
if (this.positionOptions.canvasRelative && !!em) {
const mousePos = em.Canvas.getMouseRelativeCanvas(mouseEvent, { noScroll: 1 });
mouseXRelativeToContainer = mousePos.x;
mouseYRelativeToContainer = mousePos.y;
let mouseYRelative = mouseY - this.containerOffset.top + this.containerContext.container.scrollTop;
let mouseXRelative = mouseX - this.containerOffset.left + this.containerContext.container.scrollLeft;
if (this.positionOptions.canvasRelative) {
const mousePos = this.em.Canvas.getMouseRelativeCanvas({ clientX: mouseX, clientY: mouseY }, { noScroll: 1 });
mouseXRelative = mousePos.x;
mouseYRelative = mousePos.y;
}
return { mouseXRelativeToContainer, mouseYRelativeToContainer };
return { mouseXRelative, mouseYRelative };
}
/**
@ -463,6 +584,6 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> ext
width = el.offsetWidth;
}
return { top, left, height, width, offsets };
return new Dimension({ top, left, height, width, offsets });
}
}

33
packages/core/src/utils/sorter/PlaceholderClass.ts

@ -1,5 +1,12 @@
import { View } from '../../common';
import { Dimension, Placement } from './types';
import { Placement } from './types';
import Dimension from './Dimension';
import { RateLimiter } from './RateLimiter';
type PlaceHolderPosition = {
elementDimension: Dimension;
placement: Placement;
};
export class PlaceholderClass extends View {
pfx: string;
@ -10,6 +17,8 @@ export class PlaceholderClass extends View {
top: number;
left: number;
};
private moveLimiter: RateLimiter<PlaceHolderPosition>;
constructor(options: {
container: HTMLElement;
pfx?: string;
@ -29,6 +38,9 @@ export class PlaceholderClass extends View {
top: options.offset.top || 0,
left: options.offset.left || 0,
};
// Initialize the RateLimiter with the moveThreshold
this.moveLimiter = new RateLimiter<PlaceHolderPosition>(100);
}
show() {
@ -36,15 +48,28 @@ export class PlaceholderClass extends View {
}
hide() {
this.moveLimiter.clearTimeout();
this.el.style.display = 'none';
}
/**
* Updates the position of the placeholder.
* Updates the position of the placeholder with a movement threshold.
* @param {Dimension} elementDimension element dimensions.
* @param {Position} placement either before or after the target.
* @param {Placement} placement either before or after the target.
*/
move(elementDimension: Dimension, placement: Placement) {
const position: PlaceHolderPosition = { elementDimension, placement };
// Update the position arguments in the RateLimiter
this.moveLimiter.updateArgs(position);
// Execute the callback with a threshold
this.moveLimiter.execute(({ elementDimension, placement }) => {
this._move(elementDimension, placement);
});
}
private _move(elementDimension: Dimension, placement: Placement) {
const marginOffset = 0;
const unit = 'px';
let top = 0;
@ -70,7 +95,7 @@ export class PlaceholderClass extends View {
const borderWidth = borderLeftWidth + borderRightWidth;
top = elTop + paddingTop + borderTopWidth;
left = elLeft + paddingLeft + borderLeftWidth;
width = elWidth - paddingLeft * 2 - borderWidth + 'px';
width = Math.max(elWidth - paddingLeft * 2 - borderWidth, 1) + 'px';
height = 'auto';
} else {
if (!dir) {

32
packages/core/src/utils/sorter/RateLimiter.ts

@ -0,0 +1,32 @@
export class RateLimiter<T> {
private threshold: number;
private lastArgs: T | undefined;
private timeout: NodeJS.Timeout | null = null;
constructor(threshold: number) {
this.threshold = threshold;
}
updateArgs(args: T) {
this.lastArgs = args;
}
execute(callback: (args: T) => void) {
if (!this.timeout) {
this.timeout = setTimeout(() => {
if (this.lastArgs) {
callback(this.lastArgs);
}
this.timeout = null;
}, this.threshold);
}
}
clearTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
delete this.lastArgs;
}
}

37
packages/core/src/utils/sorter/SortableTreeNode.ts

@ -1,4 +1,6 @@
import { View } from '../../common';
import Dimension from './Dimension';
import { DroppableZoneConfig } from './types';
import { DragSource } from './types';
/**
@ -9,6 +11,15 @@ import { DragSource } from './types';
export abstract class SortableTreeNode<T> {
protected _model: T;
protected _dragSource: DragSource<T>;
protected _dropAreaConfig: DroppableZoneConfig = {
ratio: 1,
minUndroppableDimension: 0, // In px
maxUndroppableDimension: 0, // In px
};
/** The dimensions of the node. */
public nodeDimensions?: Dimension;
/** The dimensions of the child elements within the target node. */
public childrenDimensions?: Dimension[];
constructor(model: T, dragSource: DragSource<T> = {}) {
this._model = model;
this._dragSource = dragSource;
@ -87,7 +98,31 @@ export abstract class SortableTreeNode<T> {
return this._dragSource;
}
equals(node?: SortableTreeNode<T>): boolean {
get dropArea(): Dimension | undefined {
// If no parent, there's no reason to reduce the drop zone
if (!this.getParent()) return this.nodeDimensions?.clone();
return this.nodeDimensions?.getDropArea(this._dropAreaConfig);
}
/**
* Checks if the given coordinates are within the bounds of this node.
*
* @param {number} x - The X coordinate to check.
* @param {number} y - The Y coordinate to check.
* @returns {boolean} - True if the coordinates are within bounds, otherwise false.
*/
public isWithinDropBounds(x: number, y: number): boolean {
return !!this.dropArea && this.dropArea.isWithinBounds(x, y);
}
equals(node?: SortableTreeNode<T>): node is SortableTreeNode<T> {
return !!node?._model && this._model === node._model;
}
adjustDimensions(diff: { topDifference: number; leftDifference: number }) {
if (diff.topDifference === 0 && diff.leftDifference === 0) return;
this.nodeDimensions?.adjustDimensions(diff);
this.childrenDimensions?.forEach((dims) => dims.adjustDimensions(diff));
}
}

8
packages/core/src/utils/sorter/Sorter.ts

@ -12,9 +12,9 @@ import {
PositionOptions,
SorterDragBehaviorOptions,
SorterEventHandlers,
Dimension,
Placement,
} from './types';
import Dimension from './Dimension';
import { SorterOptions } from './types';
export default class Sorter<T, NodeType extends SortableTreeNode<T>> {
@ -107,7 +107,7 @@ export default class Sorter<T, NodeType extends SortableTreeNode<T>> {
/**
* This method is should be called when the user scrolls within the container.
*/
recalculateTargetOnScroll(): void {
protected recalculateTargetOnScroll(): void {
this.dropLocationDeterminer.recalculateTargetOnScroll();
}
@ -183,7 +183,7 @@ export default class Sorter<T, NodeType extends SortableTreeNode<T>> {
return sourceElement;
}
private bindDragEventHandlers() {
protected bindDragEventHandlers() {
on(this.containerContext.document, 'keydown', this.rollback);
}
@ -196,7 +196,7 @@ export default class Sorter<T, NodeType extends SortableTreeNode<T>> {
*
* @private
*/
private cleanupEventListeners(): void {
protected cleanupEventListeners(): void {
off(this.containerContext.document, 'keydown', this.rollback);
}

17
packages/core/src/utils/sorter/SorterUtils.ts

@ -3,7 +3,8 @@ import EditorModel from '../../editor/model/Editor';
import { isTextNode } from '../dom';
import { matches as matchesMixin } from '../mixins';
import { SortableTreeNode } from './SortableTreeNode';
import { Dimension, Placement, DragDirection, SorterOptions } from './types';
import { Placement, DragDirection, SorterOptions } from './types';
import Dimension from './Dimension';
/**
* Find the position based on passed dimensions and coordinates
@ -133,18 +134,6 @@ export function closest(el: HTMLElement, selector: string): HTMLElement | undefi
elem = elem.parentNode;
}
}
/**
* Determines if an element is in the normal flow of the document.
* This checks whether the element is not floated or positioned in a way that removes it from the flow.
*
* @param {HTMLElement} el - The element to check.
* @param {HTMLElement} [parent=document.body] - The parent element for additional checks (defaults to `document.body`).
* @return {boolean} Returns `true` if the element is in flow, otherwise `false`.
* @private
*/
export function isInFlow(el: HTMLElement, parent: HTMLElement = document.body): boolean {
return !!el && isStyleInFlow(el, parent);
}
/**
* Checks if an element has styles that keep it in the document flow.
@ -155,7 +144,7 @@ export function isInFlow(el: HTMLElement, parent: HTMLElement = document.body):
* @return {boolean} Returns `true` if the element is styled to be in flow, otherwise `false`.
* @private
*/
function isStyleInFlow(el: HTMLElement, parent: HTMLElement): boolean {
export function isStyleInFlow(el: HTMLElement, parent: HTMLElement): boolean {
if (isTextNode(el)) return false;
const elementStyles = el.style || {};

19
packages/core/src/utils/sorter/types.ts

@ -1,6 +1,6 @@
import CanvasModule from '../../canvas';
import { ComponentDefinition } from '../../dom_components/model/types';
import EditorModel from '../../editor/model/Editor';
import Dimension from './Dimension';
import { SortableTreeNode } from './SortableTreeNode';
export type ContentElement = string | ComponentDefinition;
@ -24,19 +24,14 @@ export type DragSource<T> = DraggableContent & {
model?: T;
};
export interface Dimension {
top: number;
left: number;
height: number;
width: number;
offsets: ReturnType<CanvasModule['getElementOffsets']>;
dir?: boolean;
el?: HTMLElement;
indexEl?: number;
}
export type Placement = 'inside' | 'before' | 'after';
export type DroppableZoneConfig = {
ratio: number;
minUndroppableDimension: number; // In px
maxUndroppableDimension: number; // In px
};
export enum DragDirection {
Vertical = 'Vertical',
Horizontal = 'Horizontal',

11
packages/core/test/common.ts

@ -47,6 +47,17 @@ export function setupTestEditor(opts?: { withCanvas?: boolean; config?: Partial<
return { editor, em, dsm, cmpRoot, fixtures: fixtures as HTMLElement };
}
export function fixJsDom(editor: Editor) {
fixJsDomIframe(editor);
}
export const fixJsDomIframe = (em: EditorModel | Editor) => {
em.on(CanvasEvents.frameLoad, ({ el, view }) => {
// this seems to fix the issue of the loop
el.onload = null;
});
};
export function waitEditorEvent(em: Editor | EditorModel, event: string) {
return new Promise((resolve) => em.once(event, resolve));
}

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

@ -428,7 +428,6 @@ describe('Css Composer', () => {
`;
const result = obj.addCollection(cssRule);
const [rule1, rule2, rule3] = result;
console.log({ result: JSON.parse(JSON.stringify(rule1)) });
expect(result.length).toEqual(3);
expect(obj.getAll().length).toEqual(3);

49
packages/core/test/specs/dom_components/index.ts

@ -319,21 +319,21 @@ describe('DOM Components', () => {
describe('Custom components with styles', () => {
test('canMove returns false if source is not provided', () => {
const target = obj.addComponent({ type: 'div' }) as Component;
const target = obj.addComponent({ tagName: 'div' }) as Component;
const result = obj.canMove(target);
expect(result.result).toBe(false);
expect(result.reason).toBe(CanMoveReason.InvalidSource);
});
test('canMove returns false if target is not provided', () => {
const source = obj.addComponent({ type: 'div' }) as Component;
const source = obj.addComponent({ tagName: 'div' }) as Component;
const result = obj.canMove(undefined as any, source);
expect(result.result).toBe(false);
expect(result.reason).toBe(CanMoveReason.InvalidSource);
});
test('canMove returns false when source and target have the same main symbol', () => {
const component = obj.addComponent({ type: 'div' }) as Component;
const component = obj.addComponent({ tagName: 'div' }) as Component;
const mainSymbol = obj.addSymbol(component) as Component;
const target = obj.addSymbol(mainSymbol) as Component;
const source = obj.addSymbol(mainSymbol) as Component;
@ -356,31 +356,31 @@ describe('DOM Components', () => {
});
test('canMove returns false when source is not draggable in the target', () => {
const target = obj.addComponent({ type: 'div', droppable: true }) as Component;
const source = obj.addComponent({ type: 'span', draggable: false }) as Component;
const target = obj.addComponent({ tagName: 'div', droppable: true }) as Component;
const source = obj.addComponent({ tagName: 'span', draggable: false }) as Component;
const result = obj.canMove(target, source);
expect(result.result).toBe(false);
expect(result.reason).toBe(CanMoveReason.SourceReject);
});
test('canMove returns false when target does not accept the source', () => {
const target = obj.addComponent({ type: 'div', droppable: false }) as Component;
const source = obj.addComponent({ type: 'span', draggable: true }) as Component;
const target = obj.addComponent({ tagName: 'div', droppable: false }) as Component;
const source = obj.addComponent({ tagName: 'span', draggable: true }) as Component;
const result = obj.canMove(target, source);
expect(result.result).toBe(false);
expect(result.reason).toBe(CanMoveReason.TargetReject);
});
test('canMove returns true when source is draggable and target is droppable', () => {
const target = obj.addComponent({ type: 'div', droppable: true }) as Component;
const source = obj.addComponent({ type: 'span', draggable: true }) as Component;
const target = obj.addComponent({ tagName: 'div', droppable: true }) as Component;
const source = obj.addComponent({ tagName: 'span', draggable: true }) as Component;
const result = obj.canMove(target, source);
expect(result.result).toBe(true);
});
test('canMove returns false when target is inside the source', () => {
const source = obj.addComponent({ type: 'div' }) as Component;
const target = source.append({ type: 'span' })[0];
const source = obj.addComponent({ tagName: 'div' }) as Component;
const target = source.append({ tagName: 'span' })[0];
const result = obj.canMove(target, source);
expect(result.result).toBe(false);
expect(result.reason).toBe(CanMoveReason.TargetReject);
@ -491,6 +491,33 @@ describe('DOM Components', () => {
expect(newRoot.toHTML()).toBe(flattenHTML(docHtml));
});
test('load projectData containing HTML document, without document detect', () => {
editor.loadProjectData({
pages: [{ component: docHtml }],
});
const newRoot = editor.getWrapper()!;
const { head, doctype } = newRoot;
// Components from the head are moved inside the body
expect(head.components().length).toBe(0);
expect(doctype).toBe('');
expect(newRoot.components().length).toBe(2);
});
test('load projectData containing HTML document, with document detect', () => {
editor.Parser.getConfig().optionsHtml!.detectDocument = true;
editor.loadProjectData({
pages: [{ component: docHtml }],
});
const newRoot = editor.getWrapper()!;
const { head, doctype } = newRoot;
expect(head.components().length).toBe(1);
expect(doctype).toBe('<!DOCTYPE html>');
expect(newRoot.toHTML()).toBe(flattenHTML(docHtml));
});
});
});
});

150
packages/core/test/specs/grapesjs/index.ts

@ -1,6 +1,9 @@
import grapesjs, { Component, Editor, usePlugin } from '../../../src';
import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper';
import { EditorConfig } from '../../../src/editor/config/config';
import type { Plugin } from '../../../src/plugin_manager';
import { StorageManagerConfig } from '../../../src/storage_manager/config/config';
import { fixJsDom, fixJsDomIframe, waitEditorEvent } from '../../common';
type TestPlugin = Plugin<{ cVal: string }>;
@ -9,7 +12,7 @@ describe('GrapesJS', () => {
let fixture: HTMLElement;
let editorName = '';
let htmlString = '';
let config: any;
let config: Partial<EditorConfig>;
let cssString = '';
let documentEl = '';
@ -24,6 +27,16 @@ describe('GrapesJS', () => {
},
};
const initTestEditor = (config: Partial<EditorConfig>) => {
const editor = grapesjs.init({
...config,
plugins: [fixJsDom, ...(config.plugins || [])],
});
fixJsDomIframe(editor.getModel().shallow);
return editor;
};
beforeAll(() => {
editorName = 'editor-fixture';
});
@ -37,9 +50,9 @@ describe('GrapesJS', () => {
config = {
container: '#' + editorName,
storageManager: {
autoload: 0,
autosave: 0,
type: 0,
autoload: false,
autosave: false,
type: '',
},
};
document.body.innerHTML = `<div id="fixtures"><div id="${editorName}"></div></div>`;
@ -86,23 +99,33 @@ describe('GrapesJS', () => {
expect(editor.getStyle().length).toEqual(0);
});
test.skip('Editor canvas baseCSS can be overwritten', () => {
test('Editor canvas baseCSS can be overwritten', (done) => {
config.components = htmlString;
config.baseCss = '#wrapper { background-color: #eee; }';
config.protectedCss = '';
const editor = grapesjs.init(config);
const body = editor.Canvas.getBody();
expect(body.outerHTML).toContain(config.baseCss);
expect(body.outerHTML.replace(/\s+/g, ' ')).not.toContain('body { margin: 0;');
const editor = initTestEditor({
...config,
components: htmlString,
baseCss: '#wrapper { background-color: #eee; }',
protectedCss: '',
});
editor.onReady(() => {
const body = editor.Canvas.getBody();
expect(body.outerHTML).toContain(config.baseCss);
expect(body.outerHTML.replace(/\s+/g, ' ')).not.toContain('body { margin: 0;');
done();
});
});
test.skip('Editor canvas baseCSS defaults to sensible values if not defined', () => {
test('Editor canvas baseCSS defaults to sensible values if not defined', (done) => {
config.components = htmlString;
config.protectedCss = '';
grapesjs.init(config);
expect(window.frames[0].document.documentElement.outerHTML.replace(/\s+/g, ' ')).toContain(
'body { background-color: #fff',
);
const editor = initTestEditor(config);
editor.onReady(() => {
const htmlEl = editor.Canvas.getDocument().documentElement;
expect(htmlEl.outerHTML.replace(/\s+/g, ' ')).toContain('body { background-color: #fff');
done();
});
});
test('Init editor with html', () => {
@ -122,8 +145,8 @@ describe('GrapesJS', () => {
});
test('Init editor from element', () => {
config.fromElement = 1;
config.storageManager = { type: 0 };
config.fromElement = true;
config.storageManager = false;
fixture.innerHTML = documentEl;
const editor = grapesjs.init(config);
const html = editor.getHtml();
@ -138,8 +161,8 @@ describe('GrapesJS', () => {
});
test('Init editor from element with multiple font-face at-rules', () => {
config.fromElement = 1;
config.storageManager = { type: 0 };
config.fromElement = true;
config.storageManager = false;
fixture.innerHTML =
`
<style>
@ -262,8 +285,8 @@ describe('GrapesJS', () => {
});
test('Keep unused css classes/selectors option for getCSS method', () => {
config.fromElement = 1;
config.storageManager = { type: 0 };
config.fromElement = true;
config.storageManager = false;
fixture.innerHTML = documentEl;
const editor = grapesjs.init(config);
const css = editor.getCss({ keepUnusedStyles: true });
@ -276,8 +299,8 @@ describe('GrapesJS', () => {
cssString =
'.test2{color:red}.test3{color:blue} @media only screen and (max-width: 620px) { .notused { color: red; } } ';
documentEl = '<style>' + cssString + '</style>' + htmlString;
config.fromElement = 1;
config.storageManager = { type: 0 };
config.fromElement = true;
config.storageManager = false;
fixture.innerHTML = documentEl;
const editor = grapesjs.init(config);
const css = editor.getCss({ keepUnusedStyles: true });
@ -289,10 +312,10 @@ describe('GrapesJS', () => {
});
test('Keep unused css classes/selectors option for init method', () => {
config.fromElement = 1;
config.storageManager = { type: 0 };
config.fromElement = true;
config.storageManager = false;
fixture.innerHTML = documentEl;
const editor = grapesjs.init({ ...config, keepUnusedStyles: 1 });
const editor = grapesjs.init({ ...config, keepUnusedStyles: true });
const css = editor.getCss();
const protCss = editor.getConfig().protectedCss;
expect(editor.getStyle().length).toEqual(2);
@ -300,25 +323,21 @@ describe('GrapesJS', () => {
});
describe('Plugins', () => {
test.skip('Adds new storage as plugin and store data there', (done) => {
const pluginName = storageId + '-p2';
grapesjs.plugins.add(pluginName, (e) => e.StorageManager.add(storageId, storageMock));
config.storageManager.type = storageId;
config.plugins = [pluginName];
const editor = grapesjs.init(config);
test('Adds new storage as plugin and store data there', async () => {
(config.storageManager as StorageManagerConfig).type = storageId;
config.plugins = [(e) => e.StorageManager.add(storageId, storageMock)];
const editor = initTestEditor(config);
editor.setComponents(htmlString);
editor.store(() => {
editor.load((data: any) => {
expect(data.html).toEqual(htmlString);
done();
});
});
const projectData = editor.getProjectData();
await editor.store();
const data = await editor.load();
expect(data).toEqual(projectData);
});
test.skip('Adds a new storage and fetch correctly data from it', async () => {
test('Adds a new storage and fetch correctly data from it', async () => {
fixture.innerHTML = documentEl;
const styleResult = { color: 'white', display: 'block' };
const style = [
const styles = [
{
selectors: [{ name: 'sclass1' }],
style: { color: 'green' },
@ -333,21 +352,19 @@ describe('GrapesJS', () => {
},
];
storage = {
css: '* { box-sizing: border-box; } body {margin: 0;}',
styles: JSON.stringify(style),
styles,
pages: [{}],
};
const pluginName = storageId + '-p';
grapesjs.plugins.add(pluginName, (e) => e.StorageManager.add(storageId, storageMock));
config.fromElement = true;
config.storageManager.type = storageId;
config.plugins = [pluginName];
config.storageManager.autoload = 1;
const editor = grapesjs.init(config);
const cc = editor.CssComposer;
expect(cc.getAll().length).toEqual(style.length);
expect(cc.getClassRule('test2')!.getStyle()).toEqual(styleResult);
config.plugins = [(e) => e.StorageManager.add(storageId, storageMock)];
const configStorage = config.storageManager as StorageManagerConfig;
configStorage.type = storageId;
configStorage.autoload = true;
const editor = initTestEditor(config);
await waitEditorEvent(editor, 'load');
const { Css } = editor;
expect(Css.getAll().length).toEqual(styles.length);
expect(Css.getClassRule('test2')!.getStyle()).toEqual(styleResult);
});
test('Execute plugins with custom options', () => {
@ -459,25 +476,32 @@ describe('GrapesJS', () => {
});
});
describe.skip('Component selection', () => {
describe('Component selection', () => {
let editor: Editor;
let wrapper: ComponentWrapper;
let el1: Component;
let el2: Component;
let el3: Component;
beforeEach(() => {
config.storageManager = { type: 0 };
config.components = `<div>
<div id="el1"></div>
<div id="el2"></div>
<div id="el3"></div>
</div>`;
editor = grapesjs.init(config);
beforeEach((done) => {
editor = grapesjs.init({
container: `#${editorName}`,
storageManager: false,
plugins: [fixJsDom],
components: `<div>
<div id="el1"></div>
<div id="el2"></div>
<div id="el3"></div>
</div>`,
});
fixJsDomIframe(editor.getModel().shallow);
wrapper = editor.DomComponents.getWrapper()!;
el1 = wrapper.find('#el1')[0];
el2 = wrapper.find('#el2')[0];
el3 = wrapper.find('#el3')[0];
editor.onReady(() => {
el1 = wrapper.find('#el1')[0];
el2 = wrapper.find('#el2')[0];
el3 = wrapper.find('#el3')[0];
done();
});
});
test('Select a single component', () => {

14
packages/core/test/specs/navigator/view/ItemView.js → packages/core/test/specs/navigator/view/ItemView.ts

@ -1,11 +1,11 @@
import ItemView from 'navigator/view/ItemView';
import config from 'navigator/config/config';
import EditorModel from 'editor/model/Editor';
import defConfig from '../../../../src/navigator/config/config';
import EditorModel from '../../../../src/editor/model/Editor';
import ItemView from '../../../../src/navigator/view/ItemView';
describe('ItemView', () => {
let itemView;
let itemView: ItemView;
const isVisible = (itemView) => {
const isVisible = (itemView: ItemView) => {
return itemView.module.isVisible(itemView.model);
};
@ -16,8 +16,8 @@ describe('ItemView', () => {
itemView = new ItemView({
model: new defCmp({}, { em }),
module: em.get('LayerManager'),
config: { ...config, em },
});
config: { ...defConfig(), em },
} as any);
});
describe('.isVisible', () => {
Loading…
Cancel
Save