尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GrapesJS 复合属性(PropertyComposite)完全指南:从 margin 简写到自定义样式分组控件

GrapesJS 复合属性(PropertyComposite)完全指南:从 margin 简写到自定义样式分组控件 GrapesJS 复合属性PropertyComposite完全指南从 margin 简写到自定义样式分组控件【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs复合属性composite类型是 GrapesJS 样式管理器Style Manager中用于把多个子属性组织成单个逻辑控件的核心机制。以margin为例编辑器界面显示为margin一个控件内部却由margin-top、margin-right、margin-bottom、margin-left四个子属性组成当你切换detached模式时最终生成的 CSS 会从margin: 10px 20px 30px 40px这样的简写形式自动拆分为四条独立的声明。读完本文你将掌握PropertyComposite的全部配置参数与 API 方法理解其值拆分、合并、简写解析的底层原理并能用它和自定义fromStyle/toStyle回调打造符合业务需求的复合样式控件。本文基于 docs/api/property_composite.md 整理并结合仓库源码 PropertyComposite.ts 深入展开。一、什么是 PropertyCompositePropertyComposite是样式管理器中Property模型的一个子类type: composite用于表示由一组子属性sub properties组成的属性。它扩展自 Property因此天然继承了基类的全部能力如getValue、upValue、clear、getStyle、isVisible等同时增加了组合/拆分值这一层逻辑。从源码看PropertyComposite类的声明位于 PropertyComposite.ts#L87export default class PropertyCompositeT extends Recordstring, any PropertyCompositeProps extends PropertyT {其默认配置在 PropertyComposite.ts#L88-L99 中定义defaults() { return { ...Property.getDefaults(), detached: false, properties: [], separator: , join: null, fromStyle: null, toStyle: null, full: true, }; }注意其中的full: true复合属性在 UI 中默认占满整行宽度区别于其他默认半行宽度的属性。在实际使用中你不需要直接new PropertyComposite()而是通过type: composite声明由属性工厂自动实例化对应的类。GrapesJS 内置的margin、padding、border、border-radius、background、transform等都是复合或基于复合扩展的 stack类型属性的典型代表具体注册见 PropertyFactory.ts。二、核心配置参数详解PropertyComposite在Property基类配置id、property、default、label、onChange等见 property.md之上新增了以下专有参数参数类型默认值说明propertiesArrayObject[]子属性数组例如[{ type: number, property: margin-top }, ...]。每个子属性可以指定完整的属性定义含type也可以引用内置属性定义detachedBooleanfalse最终 CSS 属性是否被拆分。true时输出独立声明如margin-top: X; margin-right: Y; ...false时合并为简写形式如margin: X Y ...;separatorString \| RegExp 用于把简写值拆分成各子属性值的分隔符空格joinStringnull实际回退到separator用于把各子属性值合并成简写值时的连接符。为null时使用separator的值fromStyleFunctionnull自定义从目标 style 对象提取各子属性值的逻辑toStyleFunctionnull自定义由子属性值生成要应用到目标的 CSS style 对象的逻辑properties定义子属性properties是复合属性的骨架。官方文档给出的最小示例{ type: composite, properties: [ { type: number, property: margin-top }, // ... ], }GrapesJS 内置的复合属性正是这样构建的。以margin为例见 PropertyFactory.ts#L298-L309[ margin, { type: this.typeComposite, // composite properties: this.__sub([ { extend: margin-top, id: margin-top-sub }, { extend: margin-right, id: margin-right-sub }, { extend: margin-bottom, id: margin-bottom-sub }, { extend: margin-left, id: margin-left-sub }, ]), }, ],这里的__sub是属性工厂提供的辅助函数通过extend引用已注册的内置属性定义margin-top等再覆盖其id为xxx-sub作为子属性标识。从源码结构看padding、border、border-radius都沿用了margin这个模板from: margin只是替换了各自的子属性列表PropertyFactory.ts#L310-L356。detached拆分还是合并detached决定最终写入目标CSS Rule的样式形式detached: false默认——合并模式产出 CSS 简写margin: 10px 20px 30px 40px;detached: true——拆分模式产出独立声明margin-top: 10px; margin-right: 20px; margin-bottom: 30px; margin-left: 40px;内置属性中background就是一个detached: true的复合属性PropertyFactory.ts#L409-L412因为background简写形式非常复杂编辑器选择直接拆分成background-image、background-repeat、background-position、background-attachment、background-size等独立子属性来管理。separator 与 join值的拆分与合并separator默认 读取目标样式中的简写值时用它把字符串切成多段。源码中通过getSplitSeparator()生成正则new RegExp(\${this.get(separator)}(?![^\(]\)))[PropertyComposite.ts#L244-L246](https://gitcode.com/GitHub_Trending/gr/grapesjs/blob/5b647bc3228513de32121302823a43543cb01e7e/packages/core/src/style_manager/model/PropertyComposite.ts?utm_sourcegitcode_repo_files#L244-L246)这个正则特意用了后向否定断言(?![^\(]\))确保**不会在括号内部的空格处拆分**——例如translate(10px, 20px) 这种函数值内部的空格不会被误切。join默认null合并子属性值时的连接符。__getJoin()的实现是如果join是字符串则用它否则回退到separatorPropertyComposite.ts#L286-L289。fromStyle 与 toStyle自定义双向映射当默认的按空格拆分/合并逻辑无法满足需求时例如要解析margin简写与四边值之间的对应关系可以自定义转换逻辑。官方文档给出了完整示例// 从 style 对象中提取子属性值 fromStyle: (style) { const margins parseMarginShorthand(style.margin); return { margin-top: margins.top, // ... }; } // 由子属性值生成最终 CSS style 对象 toStyle: (values) { const top values[margin-top] || 0; const right values[margin-right] || 0; // ... return { margin: ${top} ${right} ..., }; }从源码实现看fromStyle/toStyle的签名比文档展示的更完整PropertyComposite.ts#L16-L22export type FromStyle (style: StyleProps, data: FromStyleData) PropValues; export type FromStyleData { property: Property; name: string; separator: RegExp }; export type ToStyle (values: PropValues, data: ToStyleData) StyleProps; export type ToStyleData { join: string; name: string; property: Property };即回调会额外收到{ property, name, separator }/{ join, name, property }上下文方便你在转换逻辑中访问当前复合属性、属性名和分隔符。fromStyle在 __getPropsFromStyle 中被调用当目标 style 命中该属性时优先走自定义提取逻辑。toStyle在 getStyleFromProps 中被调用生成最终要应用到目标的 style 对象。内置的transformstack 类型就是使用这两个回调的活例子——它通过fromStyle把transform: rotateZ(45deg)解析成{ transform-type: rotateZ, transform-value: 45deg }再通过toStyle反向拼回PropertyFactory.ts#L433-L452。三、API 方法详解PropertyComposite在原文档中共公开 7 个实例方法下面逐一说明其行为与底层实现。getProperties()获取全部子属性property.getProperties(); // [Property, Property, ...]源码实现PropertyComposite.ts#L124-L127直接返回内部Properties集合的模型数组副本getProperties(): Property[] { return [...this.get(properties).models]; }返回类型为ArrayProperty。getProperty(id)按 id 获取子属性。注意源码PropertyComposite.ts#L134-L136会同时匹配子属性的id与 CSS 属性名namegetProperty(id: string): Property | undefined { return this.properties.filter((prop) prop.getId() id || prop.getName() id)[0]; }这意味着property.getProperty(margin-top-sub)和property.getProperty(margin-top)都能命中同一个子属性。找不到时返回null文档签名或undefinedTS 类型使用时注意判空。getPropertyAt(index)按下标获取子属性property.getPropertyAt(0); // 第一个子属性源码通过this.get(properties).at(index)实现PropertyComposite.ts#L143-L146越界时返回null。isDetached()判断当前复合属性是否处于拆分模式等价于读取detached配置PropertyComposite.ts#L152-L154isDetached() { return !!this.get(detached); }返回Boolean。getValues(opts)获取所有子属性的当前值返回一个对象。官方示例// 假设该属性是 margin子属性为 margin-top、margin-right 等 console.log(property.getValues()); // { margin-top: 10px, margin-right: 20px, ... };可选参数opts.byName默认false为true时用子属性的CSS 属性名margin-top作为返回对象的键为false时用子属性的idmargin-top-sub作为键。源码实现PropertyComposite.ts#L166-L175getValues({ byName }: { byName?: boolean } {}) { return this.getProperties().reduce( (res, prop) { const key byName ? prop.getName() : prop.getId(); res[key] ${prop.__getFullValue()}; return res; }, {} as Recordstring, any, ); }getSeparator()获取属性分隔符返回类型为RegExp不是字符串。这是把separator配置包装成不会误拆函数内部空格的正则表达式getSeparator() { return this.getSplitSeparator(); } // new RegExp(${this.get(separator)}(?![^\\(]*\\)))实现见 PropertyComposite.ts#L181-L183 与 PropertyComposite.ts#L244-L246。getJoin()获取合并子属性值时使用的连接字符串getJoin() { return this.__getJoin(); }join未显式配置时返回separator的值PropertyComposite.ts#L189-L191。继承自 Property 的常用方法由于PropertyComposite extends Property你还可以直接使用基类能力详见 property.mdgetValue(opts)/upValue(value, opts)/clear(opts)读写与清除值并自动传播到选中目标getStyle(opts)返回 CSS style 对象复合属性重写为聚合子属性结果getParent()如果当前属性是子属性返回其父级PropertyCompositehasValue(opts)判断是否含有效值复合属性重写为任一子属性有值canClear()当前值是否直接来自选中目标从而可被清除isFull()是否在 UI 中占满整行复合属性默认为true。四、底层原理值如何被拆分与合并理解PropertyComposite的关键是弄清它如何在简写字符串与子属性值集合之间转换。整个流程由以下几个内部方法协作完成。1. 拆分__getSplitValue当目标 style 中是简写形式如margin: 10px 20px时__getSplitValue负责把它拆到各个子属性PropertyComposite.ts#L312-L335__getSplitValue(value: string | string[] , { byName }: OptionByName {}) { const props this.getProperties(); const props4Nums props.length 4 props.every((prop) isNumberType(prop.getType())); const values this.__splitValue(value, this.getSplitSeparator()); const result: StyleProps {}; props.forEach((prop, i) { const value values[i]; let res !isUndefined(value) ? value : ; // : prop.getDefaultValue(); if (props4Nums) { // Try to get value from a shorthand: // 11px - 11px 11px 11px 11xp // 11px 22px - 11px 22px 11px 22xp const len values.length; res values[i] || values[(i % len) (len ! 1 len % 2 ? 1 : 0)] || res; } const key byName ? prop.getName() : prop.getId(); result[key] res || ; }); return result; }值得注意的细节当恰好有 4 个数值型子属性props4Nums为真如 margin、padding、border-radius时会执行 CSS 简写的标准展开规则11px→ 四边都是11px11px 22px→ 上下11px、左右22px11px 22px 33px→ 上11px、左右22px、下33px11px 22px 33px 44px→ 依次对应。2. 合并getStyleFromProps反向的合并逻辑在getStyleFromProps中PropertyComposite.ts#L200-L242若配置了toStyle直接调用它生成 style 对象否则按detached分支处理detached: true直接把各子属性值铺成独立键值detached: false取各子属性完整值空值过滤后用join拼成一个简写字符串{ [name]: value }。随后还会做两件收尾工作为简写模式补充各子属性名对应的空键保证样式对象键的完整性并在opts.camelCase时把所有键转成驼峰形式。3. 双向桥接__getPropsFromStyle 与 __setProperties读取方向__getPropsFromStyle(style, opts)先判断 style 是否真的包含该复合属性__styleHasProps检查主属性名或任一子属性名命中且非空然后优先走fromStyle否则把主属性值按分隔符拆开再用各子属性名覆盖更精确的值PropertyComposite.ts#L337-L362。写入方向__setProperties(values, opts)遍历子属性仅当值变化时才调用prop.upValue(value, opts)把变更传播到目标同时记录一份拼接值用于clear()的变更追踪PropertyComposite.ts#L364-L375。4. 变更传播链子属性的任何变更都会通过change事件汇聚到__upProperties再经__upTargetsStyleProps计算出聚合后的 style最终调用基类的__upTargetsStyle→sm.addStyleTargets(style, opts)写入选中目标的 CSS RulePropertyComposite.ts#L248-L266。整个链路与 style_manager.md 中addStyleTargets的说明一致。五、视图层子属性如何渲染PropertyComposite对应视图 PropertyCompositeView.ts。它继承PropertyView在onRender时把子属性集合交给一个嵌套的PropertiesView渲染PropertyCompositeView.ts#L25-L46onRender() { const { pfx } this; const model this.model as PropertyComposite; const props model.get(properties)!; if (props.length !this.props) { const detached model.isDetached(); const propsView new PropertiesView({ config: { ...this.config, highlightComputed: detached, highlightChanged: detached, }, collection: props, parent: this, }); // ... } }两个细节值得注意嵌套渲染复合属性的 UI 是一个外层字段sm-field sm-composite内部再渲染一组子属性字段形成分组控件的视觉结构高亮策略联动highlightComputed与highlightChanged只在detached模式下开启。可以推断拆分模式下每个子属性对应独立的 CSS 声明因此需要各自高亮已计算/已修改状态而合并模式下只有一个聚合值高亮意义不大。六、与 PropertyStack 的关系PropertyStacktype: stack如transition、box-shadow、text-shadow继承自PropertyComposite在其基础上增加了多层layer能力——每个 layer 是一组子属性值多个 layer 用layerSeparator默认, 连接。其默认配置PropertyStack.ts#L107-L120正是...PropertyComposite.getDefaults()的扩展defaults() { return { ...PropertyComposite.getDefaults(), layers: [], emptyValue: unset, layerSeparator: , , layerJoin: , prepend: 0, preview: false, layerLabel: null, selectedLayer: null, }; }从源码结构看box-shadow就是stack 复合属性的组合它有 6 个子属性水平偏移、垂直偏移、模糊、扩散、颜色、类型layerLabel会把它们拼成X Y blur spread的预览标签PropertyFactory.ts#L373-L394。因此掌握PropertyComposite是理解PropertyStack的前置条件。关于 stack 的完整 API 见 property_stack.md。七、实战自定义一个复合属性下面组合本文知识通过styleManager.addProperty见 style_manager.md在Dimension分组中添加一个自定义的复合属性gap网格/弹性布局间距并自定义fromStyle/toStyle处理简写解析editor.StyleManager.addProperty(Dimension, { id: gap-custom, type: composite, property: gap, label: Gap, separator: , join: , properties: [ { id: row-gap-sub, property: row-gap, type: number, default: 0, units: [px, em, rem] }, { id: column-gap-sub, property: column-gap, type: number, default: 0, units: [px, em, rem] }, ], // 可选当 style 中只有 gap 简写时显式拆分行列值 fromStyle: (style, { name, separator }) { const values String(style[name] || ).split(separator).filter(Boolean); return { row-gap-sub: values[0] || , column-gap-sub: values[1] || values[0] || , }; }, // 可选把行、列值合并回 gap 简写 toStyle: (values, { name }) { const row values[row-gap-sub] || 0; const column values[column-gap-sub] || row; return { [name]: ${row} ${column} }; }, });之后选中任意组件该复合属性会在 Style Manager 中显示为一个包含两个数值输入框的分组控件修改任一子属性时经__upProperties→toStyle聚合为gap: 10px 20px写入选中目标的 CSS Rule切换组件时fromStyle会把目标的gap简写还原到两个子输入框。如需完全接管解析逻辑也可不配置fromStyle/toStyle让默认的空格拆分 4 数值简写展开机制工作——对于gap这类简单双值属性默认机制已经足够。八、相关文档与验证材料API 参考property_composite.md、property.md、property_stack.md、style_manager.md模型实现PropertyComposite.ts、PropertyStack.ts、PropertyFactory.ts内置复合属性定义视图实现PropertyCompositeView.ts默认分组配置config.tsDimension分组含margin、paddingDecorations含border、border-radius、background等复合属性测试用例Properties.ts、PropertyFactory.ts、PropertyCompositeView.ts可验证上述拆分、合并与渲染行为【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表