Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce HTML string document import #5895

Merged
merged 18 commits into from
May 21, 2024
20 changes: 20 additions & 0 deletions src/abstract/ModuleCategories.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { isArray, isString } from 'underscore';
import { AddOptions, Collection } from '../common';
import { normalizeKey } from '../utils/mixins';
import EditorModel from '../editor/model/Editor';
import Category, { CategoryProperties } from './ModuleCategory';

type CategoryCollectionParams = ConstructorParameters<typeof Collection<Category>>;

interface CategoryOptions {
events?: { update?: string };
em?: EditorModel;
}

export default class Categories extends Collection<Category> {
constructor(models?: CategoryCollectionParams[0], opts: CategoryOptions = {}) {
super(models, opts);
const { events, em } = opts;
const evUpdate = events?.update;
if (em) {
evUpdate &&
this.on('change', (category, options) =>
em.trigger(evUpdate, { category, changes: category.changedAttributes(), options })
);
}
}

/** @ts-ignore */
add(model: (CategoryProperties | Category)[] | CategoryProperties | Category, opts?: AddOptions) {
const models = isArray(model) ? model : [model];
Expand Down
5 changes: 4 additions & 1 deletion src/block_manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
// Global blocks collection
this.blocks = this.all;
this.blocksVisible = new Blocks(this.blocks.models, { em });
this.categories = new Categories();
this.categories = new Categories([], {
em,
events: { update: BlocksEvents.categoryUpdate },
});

// Setup the sync between the global and public collections
this.blocks.on('add', model => this.blocksVisible.add(model));
Expand Down
7 changes: 7 additions & 0 deletions src/block_manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ export enum BlocksEvents {
*/
dragEnd = 'block:drag:stop',

/**
* @event `block:category:update` Block category updated.
* @example
* editor.on('block:category:update', ({ category, changes }) => { ... });
*/
categoryUpdate = 'block:category:update',

/**
* @event `block:custom` Event to use in case of [custom Block Manager UI](https://grapesjs.com/docs/modules/Blocks.html#customization).
* @example
Expand Down
18 changes: 18 additions & 0 deletions src/canvas/view/FrameView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ModuleView } from '../../abstract';
import { BoxRect, ObjectAny } from '../../common';
import CssRulesView from '../../css_composer/view/CssRulesView';
import ComponentWrapperView from '../../dom_components/view/ComponentWrapperView';
import ComponentView from '../../dom_components/view/ComponentView';
import { type as typeHead } from '../../dom_components/model/ComponentHead';
import Droppable from '../../utils/Droppable';
import {
append,
Expand Down Expand Up @@ -40,6 +42,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
private jsContainer?: HTMLElement;
private tools: { [key: string]: HTMLElement } = {};
private wrapper?: ComponentWrapperView;
private headView?: ComponentView;
private frameWrapView?: FrameWrapView;

constructor(model: Frame, view?: FrameWrapView) {
Expand Down Expand Up @@ -333,6 +336,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
evOpts.window = this.getWindow();
em?.trigger(`${evLoad}:before`, evOpts); // deprecated
em?.trigger(CanvasEvents.frameLoad, evOpts);
this.renderHead();
appendScript([...canvas.get('scripts')]);
};
}
Expand Down Expand Up @@ -368,6 +372,20 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
appendVNodes(head, toAdd);
}

renderHead() {
const { model, em } = this;
const { root } = model;
const HeadView = em.Components.getType(typeHead)!.view;
this.headView = new HeadView({
el: this.getHead(),
model: root.head,
config: {
...root.config,
frameView: this,
},
}).render();
}

renderBody() {
const { config, em, model, ppfx } = this;
const doc = this.getDoc();
Expand Down
11 changes: 3 additions & 8 deletions src/code_manager/model/HtmlGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { Model } from '../../common';
import Component from '../../dom_components/model/Component';
import { ToHTMLOptions } from '../../dom_components/model/types';
import EditorModel from '../../editor/model/Editor';

export type HTMLGeneratorBuildOptions = {
export interface HTMLGeneratorBuildOptions extends ToHTMLOptions {
/**
* Remove unnecessary IDs (eg. those created automatically).
*/
cleanId?: boolean;

/**
* You can pass an object of custom attributes to replace with the current ones
* or you can even pass a function to generate attributes dynamically.
*/
attributes?: Record<string, any> | ((component: Component, attr: Record<string, any>) => Record<string, any>);
};
}

export default class HTMLGenerator extends Model {
build(model: Component, opts: HTMLGeneratorBuildOptions & { em?: EditorModel } = {}) {
Expand Down
7 changes: 7 additions & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export type ObjectStrings = Record<string, string>;

export type Nullable = undefined | null | false;

export interface OptionAsDocument {
/**
* Treat the HTML string as document (option valid on the root component, eg. will include doctype, html, head, etc.).
*/
asDocument?: boolean;
}

// https://github.com/microsoft/TypeScript/issues/29729#issuecomment-1483854699
export type LiteralUnion<T, U> = T | (U & NOOP);

Expand Down
12 changes: 9 additions & 3 deletions src/dom_components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import ComponentVideoView from './view/ComponentVideoView';
import ComponentView, { IComponentView } from './view/ComponentView';
import ComponentWrapperView from './view/ComponentWrapperView';
import ComponentsView from './view/ComponentsView';
import ComponentHead, { type as typeHead } from './model/ComponentHead';

export type ComponentEvent =
| 'component:create'
Expand Down Expand Up @@ -251,15 +252,20 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
view: ComponentTextNodeView,
},
{
id: 'text',
model: ComponentText,
view: ComponentTextView,
id: typeHead,
model: ComponentHead,
view: ComponentView,
},
{
id: 'wrapper',
model: ComponentWrapper,
view: ComponentWrapperView,
},
{
id: 'text',
model: ComponentText,
view: ComponentTextView,
},
{
id: 'default',
model: Component,
Expand Down
9 changes: 7 additions & 2 deletions src/dom_components/model/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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,
Expand Down Expand Up @@ -164,6 +165,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
};
}

get tagName() {
return this.get('tagName')!;
}

get classes() {
return this.get('classes')!;
}
Expand Down Expand Up @@ -1145,7 +1150,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
components<T extends ComponentAdd | undefined>(
components?: T,
opts: any = {}
opts: AddComponentsOption = {}
): undefined extends T ? Components : Component[] {
const coll = this.get('components')!;

Expand Down Expand Up @@ -2035,7 +2040,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
return result(this.prototype, 'defaults');
}

static isComponent(el: HTMLElement): ComponentDefinitionDefined | boolean | undefined {
static isComponent(el: HTMLElement, opts?: any): ComponentDefinitionDefined | boolean | undefined {
return { tagName: toLowerCase(el.tagName) };
}

Expand Down
24 changes: 24 additions & 0 deletions src/dom_components/model/ComponentHead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Component from './Component';
import { toLowerCase } from '../../utils/mixins';
import { DraggableDroppableFn } from './types';

export const type = 'head';
const droppable = ['title', 'style', 'base', 'link', 'meta', 'script', 'noscript'];

export default class ComponentHead extends Component {
get defaults() {
return {
// @ts-ignore
...super.defaults,
type,
tagName: type,
draggable: false,
highlightable: false,
droppable: (({ tagName }) => !tagName || droppable.includes(toLowerCase(tagName))) as DraggableDroppableFn,
};
}

static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === type;
}
}
47 changes: 47 additions & 0 deletions src/dom_components/model/ComponentWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { isUndefined } from 'underscore';
import { attrToString } from '../../utils/dom';
import Component from './Component';
import ComponentHead, { type as typeHead } from './ComponentHead';
import { ToHTMLOptions } from './types';

export default class ComponentWrapper extends Component {
get defaults() {
Expand All @@ -11,6 +15,9 @@ export default class ComponentWrapper extends Component {
draggable: false,
components: [],
traits: [],
doctype: '',
head: null,
docEl: null,
stylable: [
'background',
'background-color',
Expand All @@ -23,6 +30,46 @@ export default class ComponentWrapper extends Component {
};
}

constructor(...args: ConstructorParameters<typeof Component>) {
super(...args);
const opts = args[1];
const cmp = opts?.em?.Components;
const CmpHead = cmp?.getType(typeHead)?.model;
const CmpDef = cmp?.getType('default').model;
if (CmpHead) {
this.set(
{
head: new CmpHead({}, opts),
docEl: new CmpDef({ tagName: 'html' }, opts),
},
{ silent: true }
);
}
}

get head(): ComponentHead {
return this.get('head');
}

get docEl(): Component {
return this.get('docEl');
}

get doctype(): string {
return this.attributes.doctype || '';
}

toHTML(opts: ToHTMLOptions = {}) {
const { doctype } = this;
const asDoc = !isUndefined(opts.asDocument) ? opts.asDocument : !!doctype;
const { head, docEl } = this;
const body = super.toHTML(opts);
const headStr = (asDoc && head?.toHTML(opts)) || '';
const docElAttr = (asDoc && attrToString(docEl?.getAttrToHTML())) || '';
const docElAttrStr = docElAttr ? ` ${docElAttr}` : '';
return asDoc ? `${doctype}<html${docElAttrStr}>${headStr}${body}</html>` : body;
}

__postAdd() {
const um = this.em?.UndoManager;
!this.__hasUm && um?.add(this);
Expand Down
30 changes: 23 additions & 7 deletions src/dom_components/model/Components.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, keys } from 'underscore';
import Component from './Component';
import { AddOptions, Collection, ObjectAny } from '../../common';
import { AddOptions, Collection, ObjectAny, OptionAsDocument } from '../../common';
import { DomComponentsConfig } from '../config/config';
import EditorModel from '../../editor/model/Editor';
import ComponentManager from '..';
import CssRule from '../../css_composer/model/CssRule';
import { ComponentAdd, ComponentProperties } from './types';
import { ComponentAdd, ComponentDefinitionDefined, ComponentProperties } from './types';
import ComponentText from './ComponentText';
import ComponentWrapper from './ComponentWrapper';

export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => {
if (!cmp) return [];
Expand Down Expand Up @@ -228,12 +229,27 @@ Component> {
return new model(attrs, options) as Component;
}

parseString(value: string, opt: AddOptions & { temporary?: boolean; keepIds?: string[] } = {}) {
const { em, domc } = this;
parseString(value: string, opt: AddOptions & OptionAsDocument & { temporary?: boolean; keepIds?: string[] } = {}) {
const { em, domc, parent } = this;
const asDocument = opt.asDocument && parent?.is('wrapper');
const cssc = em.Css;
const parsed = em.Parser.parseHtml(value);
const parsed = em.Parser.parseHtml(value, { asDocument });
let components = parsed.html;

if (asDocument) {
const root = parent as ComponentWrapper;
const { components: bodyCmps, ...restBody } = (parsed.html as ComponentDefinitionDefined) || {};
const { components: headCmps, ...restHead } = parsed.head || {};
components = bodyCmps!;
root.set(restBody as any, opt);
root.head.set(restHead as any, opt);
root.head.components(headCmps, opt);
root.docEl.set(parsed.root as any, opt);
root.set({ doctype: parsed.doctype });
}

// We need this to avoid duplicate IDs
Component.checkId(parsed.html!, parsed.css, domc!.componentsById, opt);
Component.checkId(components, parsed.css, domc!.componentsById, opt);

if (parsed.css && cssc && !opt.temporary) {
const { at, ...optsToPass } = opt;
Expand All @@ -243,7 +259,7 @@ Component> {
});
}

return parsed.html;
return components;
}

/** @ts-ignore */
Expand Down
8 changes: 5 additions & 3 deletions src/dom_components/model/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Frame from '../../canvas/model/Frame';
import { Nullable } from '../../common';
import { AddOptions, Nullable, OptionAsDocument } from '../../common';
import EditorModel from '../../editor/model/Editor';
import Selectors from '../../selector_manager/model/Selectors';
import { TraitProperties } from '../../trait_manager/types';
Expand All @@ -15,6 +15,8 @@ export type DragMode = 'translate' | 'absolute' | '';

export type DraggableDroppableFn = (source: Component, target: Component, index?: number) => boolean | void;

export interface AddComponentsOption extends AddOptions, OptionAsDocument {}

export interface ComponentStackItem {
id: string;
model: typeof Component;
Expand Down Expand Up @@ -264,7 +266,7 @@ type ComponentAddType = Component | ComponentDefinition | ComponentDefinitionDef

export type ComponentAdd = ComponentAddType | ComponentAddType[];

export type ToHTMLOptions = {
export interface ToHTMLOptions extends OptionAsDocument {
/**
* Custom tagName.
*/
Expand All @@ -285,7 +287,7 @@ export type ToHTMLOptions = {
* or you can even pass a function to generate attributes dynamically.
*/
attributes?: Record<string, any> | ((component: Component, attr: Record<string, any>) => Record<string, any>);
};
}

export interface ComponentOptions {
em?: EditorModel;
Expand Down
Loading
Loading