UNPKG

@silexlabs/grapesjs-symbols

Version:
789 lines (775 loc) 38.9 kB
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">&nbsp;<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; })();