const myPlugin = (editor) => { const category1 = { id: 'first', label: 'First category' }; const category2 = { id: 'second', label: 'Second category' }; // Demo component with some traits editor.Components.addType('demo-cmp', { model: { defaults: { traits: [ { type: 'date', name: 'date-trait', label: 'Date trait', placeholder: 'Insert date', }, { type: 'text', name: 'text-trait', label: false, placeholder: 'Insert text', category: category1, }, { type: 'select', name: 'select-trait', label: 'Select 1', category: category1, default: 'opt1', options: [ { id: 'opt1', name: 'Option 1'}, { id: 'opt2', name: 'Option 2'}, { id: 'opt3', name: 'Option 3'}, ] }, { type: 'select', name: 'select-trait2', label: 'Select 2', category: category1, default: 'opt2', options: [ { id: 'opt1', name: 'Option 1'}, { id: 'opt2', name: 'Option 2'}, { id: 'opt3', name: 'Option 3'}, ] }, { type: 'number', name: 'number-trait', placeholder: '0-100', min: 0, max: 100, step: 5, }, { type: 'color', label: 'Color trait', name: 'color-trait', }, { type: 'checkbox', label: 'Checkbox trait', name: 'checkbox-trait', valueTrue: 'YES', valueFalse: 'NO', }, { type: 'checkbox', name: 'open', category: category2, }, { type: 'button', label: 'Button trait', labelButton: 'Alert', name: 'button-trait', category: category2, full: true, command: () => alert('hello'), }, { type: 'button', label: false, full: true, category: category2, labelButton: 'Open code', name: 'button-trait2', command: 'core:open-code', }, ], }, }, }); }; const editor = grapesjs.init({ container: '#gjs', height: '100%', storageManager: false, noticeOnUnload: false, fromElement: true, plugins: ['gjs-blocks-basic', myPlugin], traitManager: { custom: true } }); Vue.component('trait-field', { props: { trait: Object }, template: '#trait-field', computed: { inputValue() { return this.trait.getValue({ useType: true }); }, type() { return this.trait.getType(); }, placeholder() { return this.trait.get('placeholder'); }, toOptions() { const { trait } = this; return trait.getOptions().map(o => ({ value: trait.getOptionId(o), text: trait.getOptionLabel(o) })) }, isTypeNeedLabel() { return !['checkbox', 'button'].includes(this.type); } }, methods: { handleChange(value) { this.trait.setValue(value); }, handleInput(value) { this.trait.setValue(value, { partial: true }); }, } }); const app = new Vue({ el: '.vue-app', vuetify: new Vuetify({ theme: { dark: true } }), data: { traitCategories: [], panels: [], }, mounted() { editor.on('trait:custom', this.onTraitCustom); }, destroyed() { editor.off('trait:custom', this.onTraitCustom); }, methods: { onTraitCustom(props) { const { container } = props; if (container && !container.contains(this.$el)) { container.appendChild(this.$el); } const traitsByCategory = editor.Traits.getTraitsByCategory(); const noCategoryIndex = traitsByCategory.findIndex(trc => !trc.category) || 0; // Keep items without categories open if (!this.panels.includes(noCategoryIndex)) { this.panels.push(noCategoryIndex); } this.traitCategories = traitsByCategory; }, categoryId(traitCategory) { return traitCategory.category?.id || 'none'; }, } });
<div id="gjs"> <div style="padding: 25px" data-gjs-type="demo-cmp"> Custom Trait Manager (select me and check my traits) </div> </div> <div style="display: none;"> <!-- Vue app --> <div class="vue-app"> <v-app> <v-main> <v-expansion-panels accordion multiple v-model="panels"> <v-expansion-panel v-for="trc in traitCategories"> <v-expansion-panel-header v-if="trc.category"> {{ trc.category.getLabel() }} </v-expansion-panel-header> <v-expansion-panel-header class="no-cat-header" v-else></v-expansion-panel-header> <v-expansion-panel-content> <v-row> <trait-field v-for="trait in trc.items" :key="trait.id" :trait="trait"/> </v-row> </v-expansion-panel-content> </v-expansion-panel> </v-expansion-panels> </v-main> </v-app> </div> <!-- Trait Field template --> <div id="trait-field" style="display: none;"> <v-col :class="['py-0 px-1 mb-1', trait.get('full') && 'mb-3']" :cols="12"> <v-row class="flex-nowrap" v-if="isTypeNeedLabel"> <v-col cols="auto pr-0">{{ trait.getLabel() }}</v-col> </v-row> <div v-if="type === 'number'"> <v-text-field :placeholder="placeholder" :value="inputValue" @change="handleChange" outlined dense/> </div> <div v-else-if="type === 'checkbox'"> <v-checkbox :label="trait.getLabel()" :input-value="inputValue" @change="handleChange"></v-checkbox> </div> <div v-else-if="type === 'select'"> <v-select :items="toOptions" :value="inputValue" @change="handleChange" outlined dense/> </div> <div v-else-if="type === 'color'"> <v-text-field :placeholder="placeholder" :value="inputValue" @change="handleChange" outlined dense> <template v-slot:append> <div :style="{ backgroundColor: inputValue || placeholder }" class="trait-color-prv"> <input class="trait-input-color" type="color" :value="inputValue || placeholder" @change="(ev) => handleChange(ev.target.value)" @input="(ev) => handleInput(ev.target.value)" /> </div> </template> </v-text-field> </div> <div v-else-if="type === 'button'"> <v-btn block @click="trait.runCommand()"> <v-row> <v-col>{{ trait.get('labelButton') }}</v-col> </v-row> </v-btn> </div> <div v-else> <v-text-field :placeholder="placeholder" :type="type" :value="inputValue" @change="handleChange" outlined dense/> </div> </v-col> </div> </div>
body, html { margin: 0; height: 100%; } .trait-input-color { width: 16px !important; height: 15px !important; opacity: 0 !important; } /* Vuetify overrides */ .v-application { background: transparent !important; } .v-application--wrap { min-height: auto; } .v-input__slot { font-size: 12px; min-height: 10px !important; color-scheme: dark; } .v-select__selections { flex-wrap: nowrap; } .v-text-field .v-input__slot { padding: 0 10px !important; } .v-input--selection-controls { margin-top: 0; } .v-text-field__details, .v-messages { display: none; } .no-cat-header { opacity: 0; padding: 10px; max-height: 10px; pointer-events: none; min-height: auto !important; } .trait-color-prv { border: 1px solid rgba(255,255,255,0.5); border-radius: 3px; }