@silexlabs/grapesjs-symbols
Version:
Symbols for GrapesJS
789 lines (775 loc) • 38.9 kB
JavaScript
var GrapesJSSymbols = (function () {
'use strict';
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$1=globalThis,i$1=t$1.trustedTypes,s=i$1?i$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,e$2="$lit$",h=`lit$${Math.random().toFixed(9).slice(2)}$`,o$1="?"+h,n=`<${o$1}>`,r=document,l=()=>r.createComment(""),c=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,u=t=>a(t)||"function"==typeof t?.[Symbol.iterator],d="[ \t\n\f\r]",f=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,_=/>/g,m=RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),p=/'/g,g=/"/g,$=/^(?:script|style|textarea|title)$/i,y=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=y(1),T=Symbol.for("lit-noChange"),E=Symbol.for("lit-nothing"),A=new WeakMap,C=r.createTreeWalker(r,129);function P(t,i){if(!a(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s?s.createHTML(i):i}const V=(t,i)=>{const s=t.length-1,o=[];let r,l=2===i?"<svg>":3===i?"<math>":"",c=f;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,y=0;for(;y<s.length&&(c.lastIndex=y,u=c.exec(s),null!==u);)y=c.lastIndex,c===f?"!--"===u[1]?c=v:void 0!==u[1]?c=_:void 0!==u[2]?($.test(u[2])&&(r=RegExp("</"+u[2],"g")),c=m):void 0!==u[3]&&(c=m):c===m?">"===u[0]?(c=r??f,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?m:'"'===u[3]?g:p):c===g||c===p?c=m:c===v||c===_?c=f:(c=m,r=void 0);const x=c===m&&t[i+1].startsWith("/>")?" ":"";l+=c===f?s+n:d>=0?(o.push(a),s.slice(0,d)+e$2+s.slice(d)+h+x):s+h+(-2===d?i:x);}return [P(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),o]};class N{constructor({strings:t,_$litType$:s},n){let r;this.parts=[];let c=0,a=0;const u=t.length-1,d=this.parts,[f,v]=V(t,s);if(this.el=N.createElement(f,n),C.currentNode=this.el.content,2===s||3===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=C.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(e$2)){const i=v[a++],s=r.getAttribute(t).split(h),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:c,name:e[2],strings:s,ctor:"."===e[1]?H:"?"===e[1]?I:"@"===e[1]?L:k}),r.removeAttribute(t);}else t.startsWith(h)&&(d.push({type:6,index:c}),r.removeAttribute(t));if($.test(r.tagName)){const t=r.textContent.split(h),s=t.length-1;if(s>0){r.textContent=i$1?i$1.emptyScript:"";for(let i=0;i<s;i++)r.append(t[i],l()),C.nextNode(),d.push({type:2,index:++c});r.append(t[s],l());}}}else if(8===r.nodeType)if(r.data===o$1)d.push({type:2,index:c});else {let t=-1;for(;-1!==(t=r.data.indexOf(h,t+1));)d.push({type:7,index:c}),t+=h.length-1;}c++;}}static createElement(t,i){const s=r.createElement("template");return s.innerHTML=t,s}}function S(t,i,s=t,e){if(i===T)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=c(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(false),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=S(t,h._$AS(t,i.values),h,e)),i}class M{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??r).importNode(i,true);C.currentNode=e;let h=C.nextNode(),o=0,n=0,l=s[0];for(;void 0!==l;){if(o===l.index){let i;2===l.type?i=new R(h,h.nextSibling,this,t):1===l.type?i=new l.ctor(h,l.name,l.strings,this,t):6===l.type&&(i=new z(h,this,t)),this._$AV.push(i),l=s[++n];}o!==l?.index&&(h=C.nextNode(),o++);}return C.currentNode=r,e}p(t){let i=0;for(const s of this._$AV) void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++;}}class R{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=E,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??true;}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=S(this,t,i),c(t)?t===E||null==t||""===t?(this._$AH!==E&&this._$AR(),this._$AH=E):t!==this._$AH&&t!==T&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):u(t)?this.k(t):this._(t);}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t));}_(t){this._$AH!==E&&c(this._$AH)?this._$AA.nextSibling.data=t:this.T(r.createTextNode(t)),this._$AH=t;}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=N.createElement(P(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new M(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=A.get(t.strings);return void 0===i&&A.set(t.strings,i=new N(t)),i}k(t){a(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new R(this.O(l()),this.O(l()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e);}_$AR(t=this._$AA.nextSibling,i){for(this._$AP?.(false,true,i);t&&t!==this._$AB;){const i=t.nextSibling;t.remove(),t=i;}}setConnected(t){ void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class k{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=E,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=E;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=S(this,t,i,0),o=!c(t)||t!==this._$AH&&t!==T,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=S(this,e[s+n],i,n),r===T&&(r=this._$AH[n]),o||=!c(r)||r!==this._$AH[n],r===E?t=E:t!==E&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===E?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class H extends k{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===E?void 0:t;}}class I extends k{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==E);}}class L extends k{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=S(this,t,i,0)??E)===T)return;const s=this._$AH,e=t===E&&s!==E||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==E&&(s===E||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t;}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t);}}class z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(t){S(this,t);}}const j=t$1.litHtmlPolyfillSupport;j?.(N,R),(t$1.litHtmlVersions??=[]).push("3.3.0");const B=(t,i,s)=>{const e=i;let h=e._$litPart$;if(void 0===h){const t=null;e._$litPart$=h=new R(i.insertBefore(l(),t),t,void 0,{});}return h._$AI(t),h};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t={CHILD:2},e$1=t=>(...e)=>({_$litDirective$:t,values:e});class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i;}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/class e extends i{constructor(i){if(super(i),this.it=E,i.type!==t.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(r){if(r===E||null==r)return this._t=void 0,this.it=r;if(r===T)return r;if("string"!=typeof r)throw Error(this.constructor.directiveName+"() called with a non-string value");if(r===this.it)return this._t;this.it=r;const s=[r];return s.raw=s,this._t={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName="unsafeHTML",e.resultType=1;const o=e$1(e);
/**
* Make sure we can drop a symbol instance
*/
function allowDrop(editor, component) {
if (!component) {
throw new Error('No component provided to check drop permission.');
}
// Check if the component or one of its parents is a symbol instance
let current = component;
do {
const info = editor.Components.getSymbolInfo(current);
if (info === null || info === void 0 ? void 0 : info.isSymbol) {
return false;
}
current = current.parent();
} while (current);
// If no symbol instance is found, allow the drop
return true;
}
/**
* Get all the symbols
*/
function getSymbols(editor) {
return editor.Components.getSymbols().map(symbol => {
return editor.Components.getSymbolInfo(symbol);
});
// const symbols = editor.Components.getSymbols()
// return symbols.map(symbol => {
// const info = editor.Components.getSymbolInfo(symbol)
// return {
// symbol,
// info,
// }
// })
}
/**
* Get a symbol by its ID
*/
function getSymbol(editor, component) {
const info = editor.Components.getSymbolInfo(component);
if (info === null || info === void 0 ? void 0 : info.isSymbol) {
return info;
}
return null;
}
/**
* Create a new symbol from a component
*/
function createSymbol(editor, component) {
if (!component) {
throw new Error('No component provided to create a symbol.');
}
const symbol = editor.Components.addSymbol(component);
if (symbol) {
return editor.Components.getSymbolInfo(symbol);
}
else {
throw new Error('Failed to create symbol from the provided component.');
}
}
/**
* Bind a symbol instance to a component
*/
function unbindSymbolInstance(editor, component) {
const info = editor.Components.getSymbolInfo(component);
if (info === null || info === void 0 ? void 0 : info.isInstance) {
editor.Components.detachSymbol(component); // Detach instance from its symbol
}
else {
throw new Error('Component is not a symbol instance.');
}
}
/**
* Delete a symbol instance and all its references
* This will remove the main symbol but leave the instances
*/
function deleteSymbol(editor, symbol) {
const symbolInfo = editor.Components.getSymbolInfo(symbol);
if (!symbolInfo) {
throw new Error(`Can not delete symbol: symbol info not found for ${(symbol === null || symbol === void 0 ? void 0 : symbol.getId()) || 'unknown'}`);
}
// Delete the symbol
symbol === null || symbol === void 0 ? void 0 : symbol.remove();
}
function closestHtml(child, attr) {
let ptr = child;
while (ptr && !ptr.getAttribute(attr)) {
ptr = ptr.parentElement;
}
return ptr;
}
function confirmDialog({ editor, content: main, title, primaryLabel, secondaryLabel = 'Cancel', cbk, lsKey, }) {
// Check if the user has already been asked
if (localStorage.getItem(lsKey) === 'on') {
cbk();
}
else {
const content = document.createElement('div');
editor.Modal.open({
title,
content,
});
let remember = 'off';
B(x `<main>
${o(main)}
</main><footer style="
display: flex;
justify-content: space-between;
margin-top: 30px;
">
<div>
<label class="gjs-field gjs-field-checkbox" style="
float: left;
margin-right: 10px;
">
<input type="checkbox" id="remember" @click=${({ target: rememberCheckbox }) => remember = rememberCheckbox === null || rememberCheckbox === void 0 ? void 0 : rememberCheckbox.value}>
<i class="gjs-chk-icon"></i>
</label>
<label for="remember">Don't ask me again</label>
</div>
<div>
<button
class="gjs-btn-prim"
@click=${() => editor.Modal.close()}
style="
margin-left: auto;
background: transparent;
margin-right: 10px;
">${secondaryLabel}</button>
<button class="gjs-btn-prim" @click=${() => {
cbk();
localStorage.setItem(lsKey, remember);
editor.Modal.close();
}}>${primaryLabel}</button>
</div>
</footer>`, content);
}
}
class SymbolsView {
constructor(options) {
this.options = options;
this.lastPos = null;
this.lastTarget = null;
// listen to redraw UI
options.editor.on('component:selected', () => this.render());
// listen to drag event in order to have access to the drop target
options.editor.on('sorter:drag', (event) => {
this.lastPos = event.pos;
this.lastTarget = event.target;
});
// Listen to events on `symbol`
options.editor.on('symbol', () => {
// Redraw the UI
this.render();
});
// list wrapper
this.el = document.createElement('div');
this.el.classList.add('symbols__wrapper');
document.querySelector(options.appendTo)
.appendChild(this.el);
// first render
this.render();
}
render() {
const symbols = getSymbols(this.options.editor);
symbols.forEach(symbolInfo => {
if (!symbolInfo.main) {
console.warn('Symbol has no main component:', symbolInfo);
return;
}
});
const selected = this.options.editor.getSelected();
B(x `
<style>
.symbols__symbol-selected {
border: 1px solid ${this.options.highlightColor};
}
.symbols__symbol {
position: relative;
}
.symbols__num {
font-size: xx-small;
}
.symbols__empty {
padding: 10px;
text-align: center;
width: 100%;
}
.symbols__remove {
position: absolute;
top: 0; right: 0;
width: 20px;
line-height: 1;
cursor: pointer;
}
</style>
<main class="symbols__list" @dragend=${(event) => this.onDrop(event)}>
<div class="gjs-blocks-c">
${
// keep the same structure as the layers panel
symbols
.filter(s => s.main)
.map(symbolData => x `
<div
class="gjs-block gjs-one-bg gjs-four-color-h symbols__symbol
${!!selected && symbolData.instances.includes(selected) ? 'symbols__symbol-selected' : ''}
${symbolData.main.get('icon') || 'fa fa-diamond'}
"
title="" draggable="true"
data-symbol-id=${symbolData.main.getId()}
>
<div title="Unlink all instances and delete Symbol" class="symbols__remove" @click=${(event) => this.onRemove(event)}>
<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>
</div>
<div class="gjs-block-label">
${symbolData.main.getName()}
<div class="symbols__num">
${symbolData.instances.length} instances
</div>
</div>
</div>
`)}
${symbols.length ? '' : x `<div class="symbols__empty">
${this.options.emptyText}
</div>`}
</div>
</main>
`, this.el);
return this;
}
onDrop(event) {
var _a;
const symbolId = event.target.dataset.symbolId;
if (symbolId) {
const symbolInfo = getSymbol(this.options.editor, this.options.editor.Components.getById(symbolId));
if (symbolInfo) {
const parentId = (_a = this.lastTarget) === null || _a === void 0 ? void 0 : _a.id;
if (!parentId)
throw new Error('Can not create the symbol: missing param id');
const parent = this.lastTarget ? this.options.editor.Components.getById(parentId) : null;
if (parent) {
// Make sure we have a target and position
this.lastTarget = this.lastTarget || this.options.editor.Canvas.getBody();
this.lastPos = this.lastPos || { placement: 'after', index: 0 };
// Get the parent component from the HTML element
const parentId = this.lastTarget.getAttribute('id');
if (!parentId)
throw new Error('Can not create the symbol: missing param id');
const parent = this.options.editor.Components.allById()[parentId];
// Check if we can drop the symbol there
if (allowDrop(this.options.editor, parent)) {
// create the new component
const { instances } = createSymbol(this.options.editor, symbolInfo.main);
// Last one is the added one
const instance = instances[instances.length - 1];
const [c] = this.lastPos.placement === 'after' ? parent.append(instance) :
parent.append(instance, { at: this.lastPos.index });
// Select the new component
// Break unit tests? editor.select(c, { scroll: true })
return c;
}
else {
throw new Error('Can not create the symbol: one of the parent is in the symbol');
}
}
else {
throw new Error('Can not create the symbol: parent not found');
}
}
else {
throw new Error(`Could not create an instance of symbol ${symbolId}: symbol not found`);
}
}
return null;
}
onRemove({ target: deleteButton }) {
// Warn the user
confirmDialog({
editor: this.options.editor,
title: 'Delete Symbol',
content: `
<p>Are you sure you want to delete this symbol?</p>
<p>Deleting this symbol <em>will not</em> delete its instances, just disconnects them. Confirm to proceed or cancel to maintain the current link.</p>
`,
primaryLabel: 'Delete',
cbk: () => {
this.onRemoveConfirm(deleteButton);
},
lsKey: 'delete-symbol',
});
}
onRemoveConfirm(target) {
var _a;
const symbolId = (_a = closestHtml((target), 'data-symbol-id')) === null || _a === void 0 ? void 0 : _a.dataset.symbolId;
if (!symbolId) {
throw new Error('Can not delete symbol: missing param symbolId');
}
const symbol = this.options.editor.Components.getSymbols()
.find(symbol => symbol.getId() === symbolId);
if (!symbol) {
throw new Error('Can not delete symbol: symbol not found');
}
deleteSymbol(this.options.editor, symbol);
}
}
const cmdAdd = 'symbols:add';
const cmdRemove = 'symbols:remove';
const cmdUnlink = 'symbols:unlink';
const cmdCreate = 'symbols:create';
const cmdList = 'symbols:list';
// Same signature as a grapesjs plugin
function initCommands (editor) {
editor.Commands.add(cmdList, {
run() {
return getSymbols(editor);
},
});
editor.Commands.add(cmdAdd, _addSymbol);
editor.Commands.add(cmdRemove, _removeSymbol);
editor.Commands.add(cmdUnlink, _unlinkSymbolInstance);
editor.Commands.add(cmdCreate, _createSymbolInstance);
}
// Symbol management functions
// These are exported for unit tests
function displayError(editor, title, message) {
const content = document.createElement('div');
editor.Modal.open({
title,
content,
});
B(x `<main>
<p>${o(message)}</p>
</main><footer style="
display: flex;
justify-content: space-between;
margin-top: 30px;
">
<div></div>
<button class="gjs-btn-prim" @click=${() => editor.Modal.close()}>Close</button>
</footer>`, content);
}
/**
* Create a new symbol
* @param options.component - the component which will become the first instance of the symbol
* @returns {Symbol}
*/
function _addSymbol(editor, _, { component = editor.getSelected(), label = component === null || component === void 0 ? void 0 : component.getName(), icon }) {
if (!component || !label) {
throw new Error('Can not create the symbol: missing required param');
}
// Give the component a name
component.setName(label);
if (icon)
component.set('icon', icon);
// add the symbol
const s = createSymbol(editor, component);
// return the symbol to the caller
return s;
}
/**
* Delete a symbol
* @param {symbolId: string} - object containing the symbolId
*/
function _removeSymbol(editor, _, { symbolId }) {
if (!symbolId) {
throw new Error('Can not delete symbol: missing param symbolId');
}
const symbol = editor.Components.getSymbols()
.find(symbol => symbol.getId() === symbolId);
if (!symbol) {
throw new Error('Can not delete symbol: symbol not found');
}
deleteSymbol(editor, symbol);
}
function _unlinkSymbolInstance(editor, _, { component }) {
if (!component) {
throw new Error('Can not unlink the component: missing param component');
}
unbindSymbolInstance(editor, component);
}
/**
* @param {{index, indexEl, method}} pos Where to insert the component, as [defined by the Sorter](https://github.com/artf/grapesjs/blob/0842df7c2423300f772e9e6cdc88c6ae8141c732/src/utils/Sorter.js#L871)
*/
function _createSymbolInstance(editor, _, { symbol, pos, target }) {
if (!symbol || !pos || !target) {
throw new Error('Can not create the symbol: missing param symbol or pos or target');
}
pos = pos || {};
if (symbol && pos && target) {
const isHtmlElement = target instanceof HTMLElement;
const isComponent = !isHtmlElement;
const parentId = isComponent ? undefined : target.getAttribute('id');
if (!parentId && !isComponent) {
throw new Error('Can not create the symbol: missing parentId or target component');
}
const parent = isComponent ? target : editor.Components.allById()[parentId];
// Check that it is a valid parent
if (parent) {
if (allowDrop(editor, parent)) {
// create the new component
const symbolInfo = createSymbol(editor, symbol);
if (!symbolInfo)
throw new Error('Can not create the symbol: symbol creation failed');
const { instances } = symbolInfo;
// Last one is the added one
const instance = instances[instances.length - 1];
const [c] = pos.placement === 'after' ? parent.append(instance) :
parent.append(instance, { at: pos.index });
// Select the new component
// Break unit tests? editor.select(c, { scroll: true })
return c;
}
else {
// Cancel and notify the user
displayError(editor, 'Error: can not create the symbol', '<p>One of the parent is in the symbol.</p><p>Please remove the parent from the symbol and try again.</p>');
throw new Error('Can not create the symbol: one of the parent is in the symbol');
}
}
else {
throw new Error('Can not create the symbol: parent not found');
}
}
else {
throw new Error('Can not create the symbol: missing param symbol or pos or target');
}
}
function registerCapabilities(addCapability) {
addCapability({
id: cmdList,
command: cmdList,
description: 'List all symbols',
readOnly: true,
tags: ['symbols'],
});
addCapability({
id: cmdAdd,
command: cmdAdd,
description: 'Create symbol from element',
inputSchema: {
type: 'object',
properties: {
label: { type: 'string' },
icon: { type: 'string' },
},
},
tags: ['symbols'],
});
addCapability({
id: cmdRemove,
command: cmdRemove,
description: 'Delete a symbol',
destructive: true,
inputSchema: {
type: 'object',
required: ['symbolId'],
properties: {
symbolId: { type: 'string' },
},
},
tags: ['symbols'],
});
addCapability({
id: cmdUnlink,
command: cmdUnlink,
description: 'Unlink element from symbol',
tags: ['symbols'],
});
addCapability({
id: cmdCreate,
command: cmdCreate,
description: 'Create symbol instance',
tags: ['symbols'],
});
}
var SymbolEvents;
(function (SymbolEvents) {
SymbolEvents["CREATE"] = "symbols:create";
SymbolEvents["REMOVE"] = "symbols:remove";
SymbolEvents["LINK"] = "symbols:link";
SymbolEvents["UNLINK"] = "symbols:unlink";
SymbolEvents["CREATE_INSTANCE"] = "symbols:create:instance";
SymbolEvents["DELETE_INSTANCE"] = "symbols:delete:instance";
})(SymbolEvents || (SymbolEvents = {}));
// Same signature as a grapesjs plugin
function initTraits (editor, options) {
function unlink(component) {
// FIXME: Handle the case when the component is the main symbol
confirmDialog({
editor,
title: 'Unlink from symbol',
content: `
<p>Are you sure you want to unlink this component from the symbol? This is a <em>definitive action<em></p>
<p>Unlinking this component <em>will not</em> delete the symbol, just disconnects it. Confirm to proceed or cancel to maintain the current link.</p>
`,
primaryLabel: 'Unlink',
cbk: () => {
unbindSymbolInstance(editor, component);
},
lsKey: 'unlink-symbol',
});
}
function updateUi(el, component) {
var _a, _b;
const symbolInfo = component && getSymbol(editor, component);
if (symbolInfo) {
el.style.display = 'initial';
B(x `<fieldset class="gjs-trt-trait__wrp gjs-trt-trait__wrp-title" style="
border-color: ${options.primaryColor};
padding: 10px;
">
<legend class="fa fa-ban on fa-diamond"> <em>${(_b = (_a = symbolInfo.main) === null || _a === void 0 ? void 0 : _a.getName()) !== null && _b !== void 0 ? _b : 'symbol'}</em></legend>
<div class="gjs-field">
<button
class="gjs-btn-prim gjs-btn--full"
style="
margin: 10px 0;
border: 1px solid ${options.primaryColor};
"
@click=${() => unlink(component)}>Unlink</button>
</div>
</fieldset>`, el);
}
else {
el.style.display = 'none';
}
}
// Create a new trait type
editor.TraitManager.addType('symbol-trait', {
noLabel: true,
createInput() {
// Create a new element container and add some content
const el = document.createElement('div');
// update the UI when a new symbol is selected or created/deleted etc
Object.values(SymbolEvents).forEach(event => editor.on(event, () => updateUi(el)));
// Initial ui update
updateUi(el);
// this will be the element passed to onEvent and onUpdate
return el;
},
// Update the component based on UI changes
// `elInput` is the result HTMLElement you get from `createInput`
//onEvent({ elInput, component, event }) {
//},
// Update UI on the component change
onUpdate({ elInput, component }) {
updateUi(elInput, component);
},
});
// Add trait to symbols when the user selects one
editor.on('component:selected symbol', () => {
var _a, _b;
const component = editor.getSelected();
const symbolId = component && ((_b = (_a = getSymbol(editor, component)) === null || _a === void 0 ? void 0 : _a.main) === null || _b === void 0 ? void 0 : _b.getId());
if (symbolId) {
component.addTrait([{
type: 'symbol-trait',
name: 'Symbol',
}]);
}
});
// Add the new trait to all component types
editor.DomComponents.getTypes().forEach(type => {
editor.DomComponents.addType(type.id, {
//isComponent: el => isComponent(el),
model: {
defaults: {
traits: [
// Keep the type original traits
...editor.DomComponents.getType(type.id).model.prototype.defaults.traits,
// Add the new trait
{
type: 'symbol-trait',
name: 'Symbol',
}
]
}
}
});
});
}
/**
* Attributes that can contain references to element IDs.
* These should be updated when IDs are made unique.
*/
const ID_REFERENCE_ATTRIBUTES = [
'for', // <label for="...">
'aria-labelledby', // ARIA: labels
'aria-describedby', // ARIA: descriptions
'aria-controls', // ARIA: controls
'aria-owns', // ARIA: ownership
'aria-activedescendant', // ARIA: active descendant
'aria-flowto', // ARIA: flow
'aria-errormessage', // ARIA: error message
'list', // <input list="..."> for datalist
'form', // <input form="..."> for form association
'headers', // <td headers="..."> for table headers
];
/**
* Attributes that contain ID references prefixed with #
*/
const HASH_ID_ATTRIBUTES = [
'href', // <a href="#id">
'data-target', // Bootstrap 4: data-target="#id"
'data-bs-target', // Bootstrap 5: data-bs-target="#id"
];
/**
* Helper to get all components under a root component in depth-first order.
*/
function flattenComponents(root) {
const components = [];
root.onAll((c) => components.push(c));
return components;
}
/**
* Build a map from main symbol component IDs to instance component IDs
* Uses component order to correlate since both have the same structure
*/
function buildIdMap(mainSymbol, instance) {
const idMap = new Map();
const mainComponents = flattenComponents(mainSymbol);
const instanceComponents = flattenComponents(instance);
const n = Math.min(mainComponents.length, instanceComponents.length);
for (let i = 0; i < n; i++) {
const mainId = mainComponents[i].getAttributes().id;
const instanceId = instanceComponents[i].getAttributes().id;
if (mainId && instanceId) {
idMap.set(mainId, instanceId);
}
}
return idMap;
}
/**
* Collect all ID reference values from the main symbol.
* Returns a map of: referenced ID -> list of {component index, attribute name, isHash}
*/
function collectIdReferences(mainSymbol) {
const refs = new Map();
flattenComponents(mainSymbol).forEach((component, index) => {
const attrs = component.getAttributes();
// Check direct ID references
for (const attr of ID_REFERENCE_ATTRIBUTES) {
const value = attrs[attr];
if (typeof value === 'string' && value) {
const ids = value.split(/\s+/);
ids.forEach(id => {
if (!refs.has(id))
refs.set(id, []);
refs.get(id).push({ index, attr, isHash: false });
});
}
}
// Check hash-prefixed ID references
for (const attr of HASH_ID_ATTRIBUTES) {
const value = attrs[attr];
if (typeof value === 'string' && value.startsWith('#')) {
const id = value.substring(1);
if (!refs.has(id))
refs.set(id, []);
refs.get(id).push({ index, attr, isHash: true });
}
}
});
return refs;
}
/**
* Update ID references in a symbol instance to match the new unique IDs.
*
* This function:
* 1. Gets the main symbol for this instance
* 2. Builds a map of main symbol IDs -> instance IDs
* 3. Finds all ID references in the main symbol (for, aria-*, href="#...", etc.)
* 4. Updates those references in the instance to use the new IDs
*
* @param editor - The GrapesJS editor instance
* @param instance - The root component of the symbol instance
*/
function makeInstanceIdsUnique(editor, instance) {
const symbolInfo = editor.Components.getSymbolInfo(instance);
if (!(symbolInfo === null || symbolInfo === void 0 ? void 0 : symbolInfo.main))
return;
const mainSymbol = symbolInfo.main;
const idMap = buildIdMap(mainSymbol, instance);
if (idMap.size === 0)
return;
const refs = collectIdReferences(mainSymbol);
const instanceComponents = flattenComponents(instance);
refs.forEach((refList, referencedId) => {
// Prefer exact match, fallback to prefix match (e.g., for GrapesJS suffixes)
let newId;
idMap.forEach((instanceId, mainId) => {
if (mainId === referencedId || mainId.startsWith(referencedId + '-')) {
newId = instanceId;
}
});
if (!newId)
return;
refList.forEach(ref => {
const component = instanceComponents[ref.index];
if (!component)
return;
const attrs = component.getAttributes();
const currentValue = attrs[ref.attr];
let newValue;
if (ref.isHash) {
newValue = `#${newId}`;
}
else if (typeof currentValue === 'string' && currentValue.includes(' ')) {
// Handle space-separated list of IDs
newValue = currentValue
.split(/\s+/)
.map(id => id === referencedId ? newId : id)
.join(' ');
}
else {
newValue = newId;
}
// Temporarily add symbol override for attributes, then set attribute, then restore overrides
const overrides = component.getSymbolOverride() || [];
const overridesArray = Array.isArray(overrides) ? overrides : [];
const hasAttrOverride = overridesArray.includes('attributes');
if (!hasAttrOverride)
component.setSymbolOverride([...overridesArray, 'attributes']);
component.setAttributes({ [ref.attr]: newValue });
if (!hasAttrOverride)
component.setSymbolOverride(overrides);
});
});
}
var index = (editor, opts = {}) => {
const options = { ...{
appendTo: '#symbols',
emptyText: 'No symbol yet.',
primaryColor: '#b9a5a6',
secondaryColor: '#463a3c',
highlightColor: '#d97aa6',
}, ...opts };
editor.on('load', () => {
// Display symbols
new SymbolsView({ ...options, editor });
// Commands to create/delete symbols
initCommands(editor);
// Add traits like a plugin
initTraits(editor, options);
// When a symbol instance is created (by any method: drop, duplicate, API, etc.),
// update ID references to match the new unique IDs generated by GrapesJS
editor.on('symbol:instance:add', ({ component }) => {
makeInstanceIdsUnique(editor, component);
});
});
// Register AI capabilities if grapesjs-ai-capabilities is loaded
editor.on('ai-capabilities:ready', (addCapability) => {
registerCapabilities(addCapability);
});
};
return index;
})();