How do you add a custom button to the grapesjs toolbar?
I have followed the instructions on this github issue and written the code below, but the button doesn't appear in the toolbar as expected.
What am I missing?
initToolbar() {
const { em } = this;
const model = this;
const ppfx = (em && em.getConfig('stylePrefix')) || '';
if (!model.get('toolbar')) {
var tb = [];
if (model.collection) {
tb.push({
attributes: { class: 'fa fa-arrow-up' },
command: ed => ed.runCommand('core:component-exit', { force: 1 })
});
}
if (model.get('draggable')) {
tb.push({
attributes: {
class: `fa fa-arrows ${ppfx}no-touch-actions`,
draggable: true
},
//events: hasDnd(this.em) ? { dragstart: 'execCommand' } : '',
command: 'tlb-move'
});
}
if (model.get('schedule')) {
tb.push({
attributes: { class: 'fa fa-clock', },
command: 'tlb-settime'
});
}
if (model.get('copyable')) {
tb.push({
attributes: { class: 'fa fa-clone' },
command: 'tlb-clone'
});
}
if (model.get('removable')) {
tb.push({
attributes: { class: 'fa fa-trash-o' },
command: 'tlb-delete'
});
}
model.set('toolbar', tb);
}
},
One way to add new toolbar icons is to add the button as each component is selected.
// define this event handler after editor is defined
// like in const editor = grapesjs.init({ ...config });
editor.on('component:selected', () => {
// whenever a component is selected in the editor
// set your command and icon here
const commandToAdd = 'tlb-settime';
const commandIcon = 'fa fa-clock';
// get the selected componnet and its default toolbar
const selectedComponent = editor.getSelected();
const defaultToolbar = selectedComponent.get('toolbar');
// check if this command already exists on this component toolbar
const commandExists = defaultToolbar.some(item => item.command === commandToAdd);
// if it doesn't already exist, add it
if (!commandExists) {
selectedComponent.set({
toolbar: [ ...defaultToolbar, { attributes: {class: commandIcon}, command: commandToAdd }]
});
}
});
If it's important to you that only components with the "schedule" attribute have this toolbar option show up, as in your example, you can access and check this from selectedComponent:
const selectedComponent = editor.getSelected();
const defaultToolbar = selectedComponent.get('toolbar');
const commandExists = defaultToolbar.some(item => item.command === commandToAdd);
// add this
const hasScheduleAttribute = selectedComponent.attributes.schedule;
if (!commandExists && hasScheduleAttribute) { // ...set toolbar code
Related
As the title suggest I am trying to apply a class/styling to a specific element (in this case an SVG path) based on its corresponding button.
I have a map of the UK that is split up into regions and corresponding buttons to those regions, and I am trying to style a region based on hovering over a button.
So far I have a list of buttons and regions:
// England Buttons
const grBtn = document.querySelector(".eng-gr");
const seBtn = document.querySelector(".eng-se");
const eeBtn = document.querySelector(".eng-ee");
const emBtn = document.querySelector(".eng-em");
const yhBtn = document.querySelector(".eng-yh");
const neBtn = document.querySelector(".eng-ne");
const nwBtn = document.querySelector(".eng-nw");
const wmBtn = document.querySelector(".eng-wm");
const swBtn = document.querySelector(".eng-sw");
// England Region
const grRgn = document.querySelector(".greater-london");
const seRgn = document.querySelector(".south-east-england");
const eeRgn = document.querySelector(".east-england");
const emRgn = document.querySelector(".east-midlands");
const yhRgn = document.querySelector(".yorkshire-humber");
const neRgn = document.querySelector(".north-east-england");
const nwRgn = document.querySelector(".north-west-england");
const wmRgn = document.querySelector(".west-midlands");
const swRgn = document.querySelector(".south-west-england");
I know I can manually use a function for each button/region like so:
grBtn.addEventListener("mouseover", function () {
grRgn.classList.add("rgn-hover");
});
grBtn.addEventListener("mouseout", function () {
grRgn.classList.remove("rgn-hover");
});
But I am trying to figure out how I can do it with one (or a few) functions instead of each button/region (i will eventually be adding the rest of the UK).
Codepen of Project: https://codepen.io/MartynMc/pen/OJZWWer
Just an idea but if you can edit the HTML part and trasform this:
Greater London
Into this
Greater London
So for each button you specify the SVG class to highlight, you can just use event delegation on the buttons container like this:
let container = document.querySelector(".buttons");
container.addEventListener("mouseover", function (e) {
let ref = e.target.dataset.highlight;
if (ref) {
document.querySelector("." + ref).classList.add("rgn-hover");
}
});
container.addEventListener("mouseout", function (e) {
let ref = e.target.dataset.highlight;
if (ref) {
document.querySelector("." + ref).classList.remove("rgn-hover");
}
});
And that's all the JS code you need.
The parent receives the event from its children and if the child contains a data-highlight attribute, it finds a node with that class name and adds/removes the css class.
try this:
let arr = [
{ btn: grBtn, rgn: grRgn },
{ btn: seBtn, rgn: seRgn },
{ btn: eeBtn, rgn: eeRgn },
{ btn: emBtn, rgn: emRgn },
{ btn: yhBtn, rgn: yhRgn },
{ btn: neBtn, rgn: neRgn },
{ btn: nwBtn, rgn: nwRgn },
{ btn: wmBtn, rgn: wmRgn },
{ btn: swBtn, rgn: swRgn },
];
arr.forEach((item) => {
item.btn.addEventListener("mouseover", function () {
item.rgn.classList.add("rgn-hover");
});
item.btn.addEventListener("mouseout", function () {
item.rgn.classList.remove("rgn-hover");
});
});
I have been trying to create a simple auto complete using Quasar's select but I'm not sure if this is a bug or if I'm doing something wrong.
Problem
Whenever I click the QSelect component, it doesn't show the dropdown where I can pick the options from.
video of the problem
As soon as I click on the QSelect component, I make a request to fetch a list of 50 tags, then I populate the tags to my QSelect but the dropdown doesn't show.
Code
import type { PropType } from "vue";
import { defineComponent, h, ref } from "vue";
import type { TagCodec } from "#/services/api/resources/tags/codec";
import { list } from "#/services/api/resources/tags/actions";
import { QSelect } from "quasar";
export const TagAutoComplete = defineComponent({
name: "TagAutoComplete",
props: {
modelValue: { type: Array as PropType<TagCodec[]> },
},
emits: ["update:modelValue"],
setup(props, context) {
const loading = ref(false);
const tags = ref<TagCodec[]>([]);
// eslint-disable-next-line #typescript-eslint/ban-types
const onFilterTest = (val: string, doneFn: (update: Function) => void) => {
const parameters = val === "" ? {} : { title: val };
doneFn(async () => {
loading.value = true;
const response = await list(parameters);
if (val) {
const needle = val.toLowerCase();
tags.value = response.data.data.filter(
(tag) => tag.title.toLowerCase().indexOf(needle) > -1
);
} else {
tags.value = response.data.data;
}
loading.value = false;
});
};
const onInput = (values: TagCodec[]) => {
context.emit("update:modelValue", values);
};
return function render() {
return h(QSelect, {
modelValue: props.modelValue,
multiple: true,
options: tags.value,
dense: true,
optionLabel: "title",
optionValue: "id",
outlined: true,
useInput: true,
useChips: true,
placeholder: "Start typing to search",
onFilter: onFilterTest,
"onUpdate:modelValue": onInput,
loading: loading.value,
});
};
},
});
What I have tried
I have tried to use the several props that is available for the component but nothing seemed to work.
My understanding is that whenever we want to create an AJAX request using QSelect we should use the onFilter event emitted by QSelect and handle the case from there.
Questions
Is this the way to create a Quasar AJAX Autocomplete? (I have tried to search online but all the answers are in Quasar's forums that are currently returning BAD GATEWAY)
What am I doing wrong that it is not displaying the dropdown as soon as I click on the QSelect?
It seems updateFn may not allow being async. Shift the async action a level up to solve the issue.
const onFilterTest = async (val, update /* abort */) => {
const parameters = val === '' ? {} : { title: val };
loading.value = true;
const response = await list(parameters);
let list = response.data.data;
if (val) {
const needle = val.toLowerCase();
list = response.data.data.filter((x) => x.title.toLowerCase()
.includes(needle));
}
update(() => {
tags.value = list;
loading.value = false;
});
};
I tested it by the following code and mocked values.
// import type { PropType } from 'vue';
import { defineComponent, h, ref } from 'vue';
// import type { TagCodec } from "#/services/api/resources/tags/codec";
// import { list } from "#/services/api/resources/tags/actions";
import { QSelect } from 'quasar';
export const TagAutoComplete = defineComponent({
name: 'TagAutoComplete',
props: {
modelValue: { type: [] },
},
emits: ['update:modelValue'],
setup(props, context) {
const loading = ref(false);
const tags = ref([]);
const onFilterTest = async (val, update /* abort */) => {
// const parameters = val === '' ? {} : { title: val };
loading.value = true;
const response = await new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
data: [
{
id: 1,
title: 'Vue',
},
{
id: 2,
title: 'Vuex',
},
{
id: 3,
title: 'Nuxt',
},
{
id: 4,
title: 'SSR',
},
],
},
});
}, 3000);
});
let list = response.data.data;
if (val) {
const needle = val.toLowerCase();
list = response.data.data.filter((x) => x.title.toLowerCase()
.includes(needle));
}
update(() => {
tags.value = list;
loading.value = false;
});
};
const onInput = (values) => {
context.emit('update:modelValue', values);
};
return function render() {
return h(QSelect, {
modelValue: props.modelValue,
multiple: true,
options: tags.value,
dense: true,
optionLabel: 'title',
optionValue: 'id',
outlined: true,
useInput: true,
useChips: true,
placeholder: 'Start typing to search',
onFilter: onFilterTest,
'onUpdate:modelValue': onInput,
loading: loading.value,
});
};
},
});
I have anuglar 11 application. And I am using an icon to load a graph. But the time that the graph is loaded when the icon is triggered takes a long time. So to prevent that a user triggers many times the icon. I want to disable the icon till the graph is loaded.
So this is what I have for the icon:
<span (click)="createChartFromMap(selectedSensor.sensor.charts[0],selectedSensor.properties['key'],selectedSensor.properties['name'] )"
class="ml-auto " >
<fa-icon [icon]="selectedSensor.sensor.icon" [styles]="{'color': '#BF0404'}" size="lg" class="menu-list-item">
</fa-icon>
</span>
and this is the method:
createChartFromMap(element: string, node: string, name: string) {
const chartParams: ChartParams = new ChartParamsObj(
node,
DateTime.utc().startOf('day').toISO(),
DateTime.utc().endOf('day').toISO(),
'P1D'
);
const el = {
config: {
label: `${name}`,
xrange: [
DateTime.local().startOf('day').toFormat('yyyy-LL-dd HH:mm:ss'),
DateTime.local().endOf('day').toFormat('yyyy-LL-dd HH:mm:ss')
],
yrange:[0, 10]
},
type: element,
paramObj: chartParams
};
this.mapRegistryService.components.load(el.type, el.config, el.paramObj);
}
and the service that loads the data looks like this:
$blockButtonGraph: Observable<boolean>;
components = {
'area-chart':
{
component: AreaChartComponent,
config: {
grid: {
style: 'area-chart',
},
call: (params): Observable<WifiDensityDto[]> => {
return this.wifiDensityService.getWifiDensities(
DateTime.utc(params.start).startOf('day').toISO(),
DateTime.utc(params.end).endOf('day').toISO(),
params.node)
},
}
},
'line-chart':
{
component: LineChartComponent,
config: {
grid: {
style: 'line-chart'
},
call: (params) => {
return this.cameraValuesService.cameraDataInInterval(
params.start,
params.end,
params.node)
}
}
},
load: (comp, config, paramObj?) => {
const cmp =JSON.parse(JSON.stringify(this.components[comp]));
cmp.config.grid.label = config.label;
cmp.config.grid.id = this.components.createUnId();
},
createUnId: () => {
const id = new Date().getTime();
return id;
},
register: (comp: any, injector: Injector) => {
const factory = new WidgetFactory(
this.components[comp.config.grid.name].component,
{
element: comp.config.grid.name,
config: comp.config
}
);
}
};
So I made a $blockButtonGraph observable.
But how to use now that observable?
Thank you
use [disabled] in your template. I'm not sure, but you might have to change your <span> to a <button> but it would look something like this:
<button
(click)=createChartFromMap(...)
[disabled]=$blockButtonGraph | async>
</button>
I am developing a chatbot using peekabot (link provided below for documentation and official example), and want to be able to open a URL in a new tab (as you would have the option to do using plain html) when a user clicks on an option. Linked with this is the fact that the URL button that is generated is styled wrong (shows up as blank without the label text on mouse-hover-over).
I have included the whole script below for context, but the bit I am referring to is:
6: {
text: 'You should check it out on test preview',
options: [{
text: "PREVIEW THIS LINK",
url: "www.google.com"
}]
},
The above is option 6 and on clicking it the user should go to the URL (e.g. google.com) but open in a new tab and also not be blank (the button link is blank on hover over for some reason)
I have tried: url: "www.google.com" target="_blank (see below) but this breaks the whole javascript code.
6: {
text: 'You should check it out on test preview',
options: [{
text: "PREVIEW THIS LINK",
url: "www.google.com" target="_blank
}]
},
I have also tried:
url: "www.google.com target="_blank"
and
url: "www.google.com target=_blank"
neither works.
For an answer:
Solution for opening the URL in a new tab
Correction so that the button link (for the URL) is not BLANK on hover-over. (when you move the mouse away from the button link, the text appears)
This is the official site - https://peekobot.github.io/peekobot/ but irritatingly even in their example - the url to GitHub is opened in the same tab.
The whole script below for context:
</script>
<script type="text/javascript" src="<?php echo base_url(''); ?>js/bot.js>v=1"></script>
<script>
const chat = {
1: {
text: 'Some Text here',
options: [{
text: 'More Text here',
next: 101
},
{
text: '??',
next: 201
}
,
{
text: '?????',
next: 301
}
]
},
101: {
text: 'Some information here,
options: [{
text: 'That is great, thanks!',
next: 102
},
{
text: 'I would like some more info please.',
next: 103
}]
},
102: {
text: 'Thanks and Goodbye',
url: siteurl + "index.php/student/test",
options: [{
text: 'Bye and thank you.',
next: 3
},
{
text: 'I would like some more info please.',
next: 104
}]
},
103: {
text: 'Info here',
options: [{
text: 'That is great, thanks!',
next: 103
},
{
text: 'I would like some more info please.',
next: 104
}]
},
5: {
text: 'Aah, you\'re missing out!',
next: 6
},
6: {
text: 'You should check it out on test preview',
options: [{
text: "Go to PRIVIEW",
url: "www.google.com"
}]
},
};
const bot = function () {
const peekobot = document.getElementById('peekobot');
const container = document.getElementById('peekobot-container');
const inner = document.getElementById('peekobot-inner');
let restartButton = null;
const sleep = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
};
const scrollContainer = function () {
inner.scrollTop = inner.scrollHeight;
};
const insertNewChatItem = function (elem) {
//container.insertBefore(elem, peekobot);
peekobot.appendChild(elem);
scrollContainer();
//debugger;
elem.classList.add('activated');
};
const printResponse = async function (step) {
const response = document.createElement('div');
response.classList.add('chat-response');
response.innerHTML = step.text;
insertNewChatItem(response);
await sleep(1500);
if (step.options) {
const choices = document.createElement('div');
choices.classList.add('choices');
step.options.forEach(function (option) {
const button = document.createElement(option.url ? 'a' : 'button');
button.classList.add('choice');
button.innerHTML = option.text;
if (option.url) {
button.href = option.url;
} else {
button.dataset.next = option.next;
}
choices.appendChild(button);
});
insertNewChatItem(choices);
} else if (step.next) {
printResponse(chat[step.next]);
}
};
const printChoice = function (choice) {
const choiceElem = document.createElement('div');
choiceElem.classList.add('chat-ask');
choiceElem.innerHTML = choice.innerHTML;
insertNewChatItem(choiceElem);
};
const disableAllChoices = function () {
const choices = document.querySelectorAll('.choice');
choices.forEach(function (choice) {
choice.disabled = 'disabled';
});
return;
};
const handleChoice = async function (e) {
if (!e.target.classList.contains('choice') || 'A' === e.target.tagName) {
// Target isn't a button, but could be a child of a button.
var button = e.target.closest('#peekobot-container .choice');
if (button !== null) {
button.click();
}
return;
}
e.preventDefault();
const choice = e.target;
disableAllChoices();
printChoice(choice);
scrollContainer();
await sleep(1500);
if (choice.dataset.next) {
printResponse(chat[choice.dataset.next]);
}
// Need to disable buttons here to prevent multiple choices
};
const handleRestart = function () {
startConversation();
}
const startConversation = function () {
printResponse(chat[1]);
}
const init = function () {
container.addEventListener('click', handleChoice);
restartButton = document.createElement('button');
restartButton.innerText = "Restart";
restartButton.classList.add('restart');
restartButton.addEventListener('click', handleRestart);
container.appendChild(restartButton);
startConversation();
};
init();
}
bot();
</script>
UPDATE:
Based on a suggestion below, I've tried:
if (step.options) {
const choices = document.createElement('div');
choices.classList.add('choices');
step.options.forEach(function (option) {
const button = document.createElement(option.url ? 'a' : 'button');
button.classList.add('choice');
button.innerHTML = option.text;
if (option.url) {
button.href = option.url;
if (option.target) {
button.target = option.target;
} else {
button.dataset.next = option.next;
}
choices.appendChild(button);
});
insertNewChatItem(choices);
} else if (step.next) {
printResponse(chat[step.next]);
}
};
This breaks the whole code so the chatbot doesn't run at all.
What I'm thinking is you would have to modify the code or get the author to modify the code for you.
I'm looking at the main js code here: https://github.com/Peekobot/peekobot/blob/master/peekobot.js
This snippet is what I am looking at:
step.options.forEach(function (option) {
const button = document.createElement(option.url ? 'a' : 'button');
button.classList.add('choice');
button.innerHTML = option.text;
if (option.url) {
button.href = option.url;
} else {
button.dataset.next = option.next;
}
choices.appendChild(button);
});
That last part would get changed to something like this, I would think.
if (option.url) {
button.href = option.url;
if (option.target) {
button.target = option.target;
}
} else {
...
I am making a custom Plugin but I found that if I return an html structure to generate a custom icon, it does not interpret the html.
enter image description here
class Cloud extends Plugin {
constructor (uppy, opts)
{
super(uppy, opts)
this.id = opts.id || 'Cloud'
this.title = opts.title || 'Cloud'
this.type = 'acquirer'
//this.prepareUpload = this.prepareUpload.bind(this)
this.icon = () => {
//aria-controls="uppy-DashboardContent-panel--Cloud"
console.log('[ pluginCloud ] icon',this);
let html = `<div>Hola</div>`;
return html;
}
this.defaultLocale = {
strings: {}
}
console.log('[ pluginCloud ] constructor', this);
this.i18nInit();
this.install = this.install.bind(this)
//this.setPluginState = this.setPluginState.bind(this)
this.render = this.render.bind(this)
}
prepareUpload (fileIDs) {
console.log('[ pluginCloud ] prepareUpload');
return Promise.resolve()
}
install () {
console.log('[ pluginCloud ] install');
//this.uppy.addPreProcessor(this.prepareUpload)
const target = this.opts.target
if (target) {
this.mount(target, this)
}
}
uninstall () {
console.log('[ pluginCloud ] uninstall');
//this.uppy.removePreProcessor(this.prepareUpload)
this.unmount()
}
i18nInit () {
console.log('[ pluginCloud ] Translate');
}
}
let uppy = Uppy.Core();
// Setting dashboard
let dashboard =
{
id:'video',
inline: false,
target: 'body',
trigger: '#open_modal_video',
locale: {strings:UploadFile.LANG_ES},
metaFields : [
{ id : 'name' , name : 'Nombre' , placeholder : 'file name' },
{ id : 'description' , name : 'DescripciĆ³n' , placeholder : '' }
],
}
// Dashboard
uppy.use(Uppy.Dashboard, dashboard);
//Custom plugin
uppy.use(Cloud,{
target: Uppy.Dashboard,
companionUrl: 'http://cloud.localhost'
});
I have also noticed that every time I click the button it is rendered again.
I have tried to do add html directly with JS but it does not solve the problem correctly
I have been able to solve it by adding additional css to display the icon
[aria-controls="uppy-DashboardContent-panel--Cloud"]::before
{
content: '';
background: #00BCD4;
width: 30px;
height: 30px;
border-radius: 30px;
overflow: hidden;
}
I would need to add an image in the 'after' field to show it.