]> BookStack Code Mirror - bookstack/blob - resources/js/editor/menu/index.js
Started menu dialog support
[bookstack] / resources / js / editor / menu / index.js
1 import {
2     MenuItem, Dropdown, DropdownSubmenu, renderGrouped, joinUpItem, liftItem, selectParentNodeItem,
3     undoItem, redoItem, wrapItem, blockTypeItem, setAttrItem, insertBlockBeforeItem,
4 } from "./menu"
5 import {icons} from "./icons";
6 import ColorPickerGrid from "./ColorPickerGrid";
7 import DialogBox from "./DialogBox";
8 import {toggleMark} from "prosemirror-commands";
9 import {menuBar} from "./menubar"
10 import schema from "../schema";
11 import {removeMarks} from "../commands";
12 import DialogForm from "./DialogForm";
13 import DialogInput from "./DialogInput";
14
15
16 function cmdItem(cmd, options) {
17     const passedOptions = {
18         label: options.title,
19         run: cmd
20     };
21     for (const prop in options) {
22         passedOptions[prop] = options[prop];
23     }
24     if ((!options.enable || options.enable === true) && !options.select) {
25         passedOptions[options.enable ? "enable" : "select"] = function (state) {
26             return cmd(state);
27         };
28     }
29
30     return new MenuItem(passedOptions)
31 }
32
33 function markActive(state, type) {
34     const ref = state.selection;
35     const from = ref.from;
36     const $from = ref.$from;
37     const to = ref.to;
38     const empty = ref.empty;
39     if (empty) {
40         return type.isInSet(state.storedMarks || $from.marks())
41     } else {
42         return state.doc.rangeHasMark(from, to, type)
43     }
44 }
45
46 function markItem(markType, options) {
47     const passedOptions = {
48         active: function active(state) {
49             return markActive(state, markType)
50         },
51         enable: true
52     };
53     for (const prop in options) {
54         passedOptions[prop] = options[prop];
55     }
56
57     return cmdItem(toggleMark(markType, passedOptions.attrs), passedOptions)
58 }
59
60 const inlineStyles = [
61     markItem(schema.marks.strong, {title: "Bold", icon: icons.strong}),
62     markItem(schema.marks.em, {title: "Italic", icon: icons.em}),
63     markItem(schema.marks.underline, {title: "Underline", icon: icons.underline}),
64     markItem(schema.marks.strike, {title: "Strikethrough", icon: icons.strike}),
65     markItem(schema.marks.superscript, {title: "Superscript", icon: icons.superscript}),
66     markItem(schema.marks.subscript, {title: "Subscript", icon: icons.subscript}),
67 ];
68
69 const formats = [
70     blockTypeItem(schema.nodes.heading, {
71         label: "Header Large",
72         attrs: {level: 2}
73     }),
74     blockTypeItem(schema.nodes.heading, {
75         label: "Header Medium",
76         attrs: {level: 3}
77     }),
78     blockTypeItem(schema.nodes.heading, {
79         label: "Header Small",
80         attrs: {level: 4}
81     }),
82     blockTypeItem(schema.nodes.heading, {
83         label: "Header Tiny",
84         attrs: {level: 5}
85     }),
86     blockTypeItem(schema.nodes.paragraph, {
87         label: "Paragraph",
88         attrs: {}
89     }),
90     markItem(schema.marks.code, {
91         label: "Inline Code",
92         attrs: {}
93     }),
94     new DropdownSubmenu([
95         blockTypeItem(schema.nodes.callout, {
96             label: "Info Callout",
97             attrs: {type: 'info'}
98         }),
99         blockTypeItem(schema.nodes.callout, {
100             label: "Danger Callout",
101             attrs: {type: 'danger'}
102         }),
103         blockTypeItem(schema.nodes.callout, {
104             label: "Success Callout",
105             attrs: {type: 'success'}
106         }),
107         blockTypeItem(schema.nodes.callout, {
108             label: "Warning Callout",
109             attrs: {type: 'warning'}
110         })
111     ], { label: 'Callouts' }),
112 ];
113
114 const alignments = [
115     setAttrItem('align', 'left', {
116         icon: icons.align_left
117     }),
118     setAttrItem('align', 'center', {
119         icon: icons.align_center
120     }),
121     setAttrItem('align', 'right', {
122         icon: icons.align_right
123     }),
124     setAttrItem('align', 'justify', {
125         icon: icons.align_justify
126     }),
127 ];
128
129 const colorOptions = ["#000000","#993300","#333300","#003300","#003366","#000080","#333399","#333333","#800000","#FF6600","#808000","#008000","#008080","#0000FF","#666699","#808080","#FF0000","#FF9900","#99CC00","#339966","#33CCCC","#3366FF","#800080","#999999","#FF00FF","#FFCC00","#FFFF00","#00FF00","#00FFFF","#00CCFF","#993366","#FFFFFF","#FF99CC","#FFCC99","#FFFF99","#CCFFCC","#CCFFFF","#99CCFF","#CC99FF"];
130
131 const colors = [
132     new DropdownSubmenu([
133         new ColorPickerGrid(schema.marks.text_color, 'color', colorOptions),
134     ], {icon: icons.text_color}),
135     new DropdownSubmenu([
136         new ColorPickerGrid(schema.marks.background_color, 'color', colorOptions),
137     ], {icon: icons.background_color}),
138 ];
139
140 const lists = [
141     wrapItem(schema.nodes.bullet_list, {
142         title: "Bullet List",
143         icon: icons.bullet_list,
144     }),
145     wrapItem(schema.nodes.ordered_list, {
146         title: "Ordered List",
147         icon: icons.ordered_list,
148     }),
149 ];
150
151 const inserts = [
152     insertBlockBeforeItem(schema.nodes.horizontal_rule, {
153         title: "Horizontal Rule",
154         icon: icons.horizontal_rule,
155     }),
156 ];
157
158 const utilities = [
159     new MenuItem({
160         title: 'Clear Formatting',
161         icon: icons.format_clear,
162         run: removeMarks(),
163         enable: state => true,
164     }),
165 ];
166
167 function getMarkAttribute(markType, attribute) {
168     return function(state) {
169         const marks = state.selection.$head.marks();
170         for (const mark of marks) {
171             if (mark.type === markType) {
172                 return mark.attrs[attribute];
173             }
174         }
175
176         return null;
177     };
178 }
179
180 let box = new DialogBox([
181     new DialogForm([
182         new DialogInput({
183             label: 'URL',
184             id: 'url',
185             value: getMarkAttribute(schema.marks.link, 'href'),
186         }),
187         new DialogInput({
188             label: 'Title',
189             id: 'title',
190             value: getMarkAttribute(schema.marks.link, 'title'),
191         })
192     ], {
193         canceler: () =>  box.close(),
194         action: (data) => console.log('submit', data),
195     }),
196 ], {label: 'Insert Link', closer: () => {console.log('close')}});
197
198 const menu = menuBar({
199     floating: false,
200     content: [
201         [undoItem, redoItem],
202         [new DropdownSubmenu(formats, { label: 'Formats' })],
203         inlineStyles,
204         colors,
205         alignments,
206         lists,
207         inserts,
208         utilities,
209         [box]
210     ],
211 });
212
213 export default menu;
214
215 // !! This module defines a number of building blocks for ProseMirror
216 // menus, along with a [menu bar](#menu.menuBar) implementation.
217
218 // MenuElement:: interface
219 // The types defined in this module aren't the only thing you can
220 // display in your menu. Anything that conforms to this interface can
221 // be put into a menu structure.
222 //
223 //   render:: (pm: EditorView) → {dom: dom.Node, update: (EditorState) → bool}
224 //   Render the element for display in the menu. Must return a DOM
225 //   element and a function that can be used to update the element to
226 //   a new state. The `update` function will return false if the
227 //   update hid the entire element.