diff --git a/src/common/index.ts b/src/common/index.ts index 63ccacb2a..8b82cbcbf 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -25,6 +25,13 @@ export type ObjectStrings = Record; export type Nullable = undefined | null | false; +export interface OptionAsDocument { + /** + * Treat the HTML string as document (option valid on the root component, eg. will include ``, ``, etc.). + */ + asDocument?: boolean; +} + // https://github.com/microsoft/TypeScript/issues/29729#issuecomment-1483854699 export type LiteralUnion = T | (U & NOOP); diff --git a/src/dom_components/model/ComponentWrapper.ts b/src/dom_components/model/ComponentWrapper.ts index ba9f66f4f..ed2958fc6 100644 --- a/src/dom_components/model/ComponentWrapper.ts +++ b/src/dom_components/model/ComponentWrapper.ts @@ -1,3 +1,4 @@ +import { attrToString } from '../../utils/dom'; import Component from './Component'; import ComponentHead, { type as typeHead } from './ComponentHead'; import { ToHTMLOptions } from './types'; @@ -15,6 +16,7 @@ export default class ComponentWrapper extends Component { traits: [], doctype: '', head: null, + docEl: null, stylable: [ 'background', 'background-color', @@ -30,10 +32,13 @@ export default class ComponentWrapper extends Component { constructor(...args: ConstructorParameters) { super(...args); const opts = args[1]; - const CmpHead = opts?.em?.Components.getType(typeHead)?.model; + 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), }); } } @@ -42,12 +47,19 @@ export default class ComponentWrapper extends Component { return this.get('head'); } + get docEl(): Component { + return this.get('docEl'); + } + toHTML(opts: ToHTMLOptions = {}) { const { asDocument } = opts; + const { head, docEl } = this; const { doctype = '' } = this.attributes; const body = super.toHTML(opts); - const head = (asDocument && this.head?.toHTML(opts)) || ''; - return asDocument ? `${doctype}${head}${body}` : body; + const headStr = (asDocument && head?.toHTML(opts)) || ''; + const docElAttr = (asDocument && attrToString(docEl?.getAttrToHTML())) || ''; + const docElAttrStr = docElAttr ? ` ${docElAttr}` : ''; + return asDocument ? `${doctype}${headStr}${body}` : body; } __postAdd() { diff --git a/src/dom_components/model/Components.ts b/src/dom_components/model/Components.ts index 780b49f10..f211c54bc 100644 --- a/src/dom_components/model/Components.ts +++ b/src/dom_components/model/Components.ts @@ -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 ComponentText from './ComponentText'; +import ComponentWrapper from './ComponentWrapper'; export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => { if (!cmp) return []; @@ -228,12 +229,23 @@ 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; + components = (parsed.html as any).components; + root.head.set(parsed.head as any, 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; @@ -243,7 +255,7 @@ Component> { }); } - return parsed.html; + return components; } /** @ts-ignore */ diff --git a/src/dom_components/model/types.ts b/src/dom_components/model/types.ts index f8139e0e2..87d22d950 100644 --- a/src/dom_components/model/types.ts +++ b/src/dom_components/model/types.ts @@ -1,5 +1,5 @@ import Frame from '../../canvas/model/Frame'; -import { Nullable } from '../../common'; +import { Nullable, OptionAsDocument } from '../../common'; import EditorModel from '../../editor/model/Editor'; import Selectors from '../../selector_manager/model/Selectors'; import { TraitProperties } from '../../trait_manager/types'; @@ -264,7 +264,7 @@ type ComponentAddType = Component | ComponentDefinition | ComponentDefinitionDef export type ComponentAdd = ComponentAddType | ComponentAddType[]; -export interface ToHTMLOptions { +export interface ToHTMLOptions extends OptionAsDocument { /** * Custom tagName. */ @@ -280,11 +280,6 @@ export interface ToHTMLOptions { */ altQuoteAttr?: boolean; - /** - * Return the HTML string as document (option valid on the root component, eg. will include the ). - */ - asDocument?: 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. diff --git a/src/utils/dom.ts b/src/utils/dom.ts index aae8b0a5c..4eaa540e0 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -214,6 +214,12 @@ export const doctypeToString = (dt?: DocumentType | null) => { return ``; }; +export const attrToString = (attrs: ObjectAny = {}) => { + const res: string[] = []; + each(attrs, (value, key) => res.push(`${key}="${value}"`)); + return res.join(' '); +}; + export const on = ( el: EventTarget | EventTarget[], ev: string,