Skip to content

Commit 93b5785

Browse files
authored
Merge pull request #531 from Harbour-Enterprises/nick/har-9643-custom-buttons
HAR-9643 - Adds third-party dev custom button support
2 parents 5d7307b + e98c3f0 commit 93b5785

7 files changed

Lines changed: 105 additions & 14 deletions

File tree

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

examples/vue-custom-mark/src/App.vue

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,70 @@ import { SuperDoc } from '@harbour-enterprises/superdoc';
55
import UploadFile from '../../shared/vue/UploadFile/UploadFile.vue';
66
import sampleDocument from '../../shared/data/sample-document.docx?url';
77
8+
// These custom SVG icons are in our public folder as an example. You should import in the correct way for your project.
9+
import headphonesSVG from '/headphones-solid.svg?raw';
10+
import chevronDownSVG from '/circle-chevron-down-solid.svg?raw';
11+
812
// This is our custom mark that we are creating for this example
913
import { CustomMark } from './custom-mark.js';
1014
15+
16+
/** Customization of toolbar buttons with custom button, icon */
17+
const customButton = {
18+
type: 'button',
19+
name: 'insertCustomMark',
20+
21+
// Since this command is already in editor.commands (from the custom-mark extension), we can use the command name directly
22+
command: 'setMyCustomMark',
23+
24+
tooltip: 'Insert Custom Mark',
25+
group: 'center',
26+
icon: headphonesSVG, // You can use a custom icon here
27+
};
28+
29+
const customButtonUsingCustomFunction = {
30+
type: 'button',
31+
name: 'insertCustomMark',
32+
33+
// We can also pass in a function as the command
34+
command: () => {
35+
const id = Math.random().toString(36).substring(2, 7);
36+
return superdoc.value?.activeEditor?.commands.setMyCustomMark(id);
37+
},
38+
39+
tooltip: 'Insert Custom Mark',
40+
group: 'center',
41+
icon: headphonesSVG, // You can use a custom icon here
42+
};
43+
44+
const customDropDown = {
45+
type: 'dropdown',
46+
name: 'customDropdown',
47+
command: ({ item, option }) => {
48+
if (!item || !option) return; // Case where the dropdown is being expanded or collapsed but no option selected
49+
50+
const { key, label } = option; // This is from the options array defined below
51+
52+
// Do something with the selected option here
53+
// For example, we can call a command or a custom function based on the key
54+
console.log(`[customDropDown Selected option: ${label} (key: ${key})`);
55+
if (key === 'custom-mark') {
56+
superdoc.value?.activeEditor?.commands.setMyCustomMark();
57+
} else if (key === 'export-docx') {
58+
exportDocx();
59+
}
60+
},
61+
tooltip: 'Custom Dropdown',
62+
group: 'center',
63+
icon: chevronDownSVG,
64+
hasCaret: true,
65+
suppressActiveHighlight: true,
66+
options: [
67+
{ label: 'Insert Custom Mark', key: 'custom-mark', },
68+
{ label: 'Export to DOCX', key: 'export-docx', },
69+
],
70+
};
71+
1172
const superdoc = shallowRef(null);
1273
const init = (fileToLoad) => {
1374
superdoc.value = new SuperDoc({
@@ -18,12 +79,25 @@ const init = (fileToLoad) => {
1879
// Load the document if provided, otherwise load the sample document
1980
document: fileToLoad ? { data: fileToLoad } : sampleDocument,
2081
21-
// Initialize the toolbar
22-
toolbar: '#toolbar',
23-
toolbarGroups: ['center'],
24-
2582
editorExtensions: [CustomMark],
2683
onReady: myCustomOnReady,
84+
85+
modules: {
86+
87+
// Customize the toolbar
88+
toolbar: {
89+
selector: '#toolbar',
90+
toulbarGroups: ['center'],
91+
92+
// You can pass in custom buttons here
93+
customButtons: [
94+
customButton,
95+
customButtonUsingCustomFunction,
96+
customDropDown,
97+
],
98+
}
99+
100+
}
27101
});
28102
};
29103
@@ -38,7 +112,6 @@ const myCustomOnReady = () => {
38112
});
39113
}
40114
41-
42115
/**
43116
* This is an example of how to export the content of the editor to a DOCX file.
44117
* @param { Editor } editor - The editor instance.

examples/vue-custom-mark/src/custom-mark.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const CustomMark = Extensions.Mark.create({
3535
addCommands() {
3636
return {
3737
setMyCustomMark: (id) => ({ commands, tr }) => {
38-
if (!id) throw new Error('id is required for my custom mark');
38+
if (!id) id = Math.random().toString(36).substring(2, 7);
3939

4040
// set Mark accepts the attributes that are defined in the addAttributes method
4141
commands.setMark(this.name, { 'data-custom-id': id });

packages/super-editor/src/components/toolbar/ButtonGroup.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const closeDropdowns = () => {
7171
const handleSelect = (item, option) => {
7272
closeDropdowns();
7373
const value = item.dropdownValueKey.value ? option[item.dropdownValueKey.value] : option.label;
74-
emit('command', { item, argument: value });
74+
emit('command', { item, argument: value, option });
7575
};
7676
7777
const handleClickOutside = (e) => {

packages/super-editor/src/components/toolbar/Toolbar.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ const onWindowResized = async () => {
4646
};
4747
const onResizeThrottled = throttle(onWindowResized, 300);
4848
49-
const handleCommand = ({ item, argument }) => {
50-
proxy.$toolbar.emitCommand({ item, argument });
49+
const handleCommand = ({ item, argument, option }) => {
50+
proxy.$toolbar.emitCommand({ item, argument, option });
5151
};
5252
</script>
5353

packages/super-editor/src/components/toolbar/super-toolbar.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { toolbarIcons } from './toolbarIcons.js';
1212
import { getQuickFormatList } from '@extensions/linked-styles/linked-styles.js';
1313
import { getAvailableColorOptions, makeColorOption, renderColorOptions } from './color-dropdown-helpers.js';
1414
import { isInTable } from '@helpers/isInTable.js';
15+
import { useToolbarItem } from '@components/toolbar/use-toolbar-item';
1516

1617
/**
1718
* @typedef {Object} ToolbarConfig
@@ -26,6 +27,7 @@ import { isInTable } from '@helpers/isInTable.js';
2627
* @property {Object} [editor=null] - The editor instance
2728
* @property {string} [aiApiKey=null] - API key for AI integration
2829
* @property {string} [aiEndpoint=null] - Endpoint for AI integration
30+
* @property {ToolbarItem[]} [customButtons=[]] - Custom buttons to add to the toolbar
2931
*/
3032

3133
/**
@@ -136,6 +138,7 @@ export class SuperToolbar extends EventEmitter {
136138
editor: null,
137139
aiApiKey: null,
138140
aiEndpoint: null,
141+
customButtons: [],
139142
};
140143

141144
/**
@@ -526,7 +529,7 @@ export class SuperToolbar extends EventEmitter {
526529
* @returns {ToolbarItem[]} An array of toolbar items in the specified group
527530
*/
528531
getToolbarItemByGroup(groupName) {
529-
return this.toolbarItems.filter((item) => item.group.value === groupName);
532+
return this.toolbarItems.filter((item) => (item.group?.value || 'center') === groupName);
530533
}
531534

532535
/**
@@ -549,6 +552,10 @@ export class SuperToolbar extends EventEmitter {
549552
#makeToolbarItems(superToolbar, icons, isDev = false) {
550553
const documentWidth = document.documentElement.clientWidth; // take into account the scrollbar
551554
const { defaultItems, overflowItems } = makeDefaultItems(superToolbar, isDev, documentWidth, this.role, icons);
555+
const customItems = this.config.customButtons || [];
556+
if (customItems.length) {
557+
defaultItems.push(...customItems.map((item) => useToolbarItem({...item})));
558+
};
552559

553560
let allConfigItems = [
554561
...defaultItems.map((item) => item.name.value),
@@ -738,15 +745,15 @@ export class SuperToolbar extends EventEmitter {
738745
* @param {*} [params.argument] - The argument passed to the command
739746
* @returns {*} The result of the executed command, undefined if no result is returned
740747
*/
741-
emitCommand({ item, argument }) {
748+
emitCommand({ item, argument, option }) {
742749
this.activeEditor?.focus();
743750
const { command } = item;
744751

745752
if (!command) {
746753
return;
747754
}
748755

749-
this.log('(emmitCommand) Command:', command, item, argument);
756+
this.log('(emmitCommand) Command:', command, '\n\titem:', item, '\n\targument:', argument, '\n\toption:', option);
750757

751758
// Check if we have a custom or overloaded command defined
752759
if (command in this.#interceptedCommands) {
@@ -755,10 +762,19 @@ export class SuperToolbar extends EventEmitter {
755762

756763
if (command in this.activeEditor?.commands) {
757764
this.activeEditor.commands[command](argument);
758-
this.updateToolbarState();
759-
} else {
765+
}
766+
767+
// If the command is a function, call it with the argument
768+
else if (typeof command === 'function') {
769+
command({ item, argument, option });
770+
}
771+
772+
// If we don't know what to do with this command, throw an error
773+
else {
760774
throw new Error(`[super-toolbar 🎨] Command not found: ${command}`);
761775
}
776+
777+
this.updateToolbarState();
762778
}
763779

764780
/**

0 commit comments

Comments
 (0)