]> BookStack Code Mirror - bookstack/blob - resources/js/editor/schema-nodes.js
Crawled forward slightly on table resizing
[bookstack] / resources / js / editor / schema-nodes.js
1 import {orderedList, bulletList, listItem} from "prosemirror-schema-list";
2 import {tableNodes} from "prosemirror-tables";
3
4 /**
5  * @param {HTMLElement} node
6  * @return {string|null}
7  */
8 function getAlignAttrFromDomNode(node) {
9     const classList = node.classList;
10     const styles = node.style || {};
11     const alignments = ['right', 'left', 'center', 'justify'];
12     for (const alignment of alignments) {
13         if (classList.contains('align-' + alignment) || styles.textAlign === alignment) {
14             return alignment;
15         }
16     }
17     return null;
18 }
19
20 /**
21  * @param node
22  * @param {Object} attrs
23  * @return {Object}
24  */
25 function addAlignmentAttr(node, attrs) {
26     const positions = ['right', 'left', 'center', 'justify'];
27     for (const position of positions) {
28         if (node.attrs.align === position) {
29             return addClassToAttrs('align-' + position, attrs);
30         }
31     }
32     return attrs;
33 }
34
35 function getAttrsParserForAlignment(node) {
36     return {
37         align: getAlignAttrFromDomNode(node),
38     };
39 }
40
41 /**
42  * @param {String} className
43  * @param {Object} attrs
44  * @return {Object}
45  */
46 function addClassToAttrs(className, attrs) {
47     return Object.assign({}, attrs, {
48         class: attrs.class ? attrs.class + ' ' + className : className,
49     });
50 }
51
52 /**
53  * @param {String[]} attrNames
54  * @return {function(Element): {}}
55  */
56 function domAttrsToAttrsParser(attrNames) {
57     return function (node) {
58         const attrs = {};
59         for (const attr of attrNames) {
60             attrs[attr] = node.hasAttribute(attr) ? node.getAttribute(attr) : null;
61         }
62         return attrs;
63     };
64 }
65
66 /**
67  * @param {PmNode} node
68  * @param {String[]} attrNames
69  */
70 function extractAttrsForDom(node, attrNames) {
71     const domAttrs = {};
72     for (const attr of attrNames) {
73         if (node.attrs[attr]) {
74             domAttrs[attr] = node.attrs[attr];
75         }
76     }
77     return domAttrs;
78 }
79
80 const doc = {
81     content: "block+",
82 };
83
84 const paragraph = {
85     content: "inline*",
86     group: "block",
87     parseDOM: [
88         {
89             tag: "p",
90             getAttrs: getAttrsParserForAlignment,
91         }
92     ],
93     attrs: {
94         align: {
95             default: null,
96         }
97     },
98     toDOM(node) {
99         return ["p", addAlignmentAttr(node, {}), 0];
100     }
101 };
102
103 const blockquote = {
104     content: "block+",
105     group: "block",
106     defining: true,
107     parseDOM: [{tag: "blockquote", getAttrs: getAttrsParserForAlignment}],
108     attrs: {
109         align: {
110             default: null,
111         }
112     },
113     toDOM(node) {
114         return ["blockquote", addAlignmentAttr(node, {}), 0];
115     }
116 };
117
118 const horizontal_rule = {
119     group: "block",
120     parseDOM: [{tag: "hr"}],
121     toDOM() {
122         return ["hr"];
123     }
124 };
125
126
127 const headingParseGetAttrs = (level) => {
128     return function (node) {
129         return {level, align: getAlignAttrFromDomNode(node)};
130     };
131 };
132 const heading = {
133     attrs: {level: {default: 1}, align: {default: null}},
134     content: "inline*",
135     group: "block",
136     defining: true,
137     parseDOM: [
138         {tag: "h1", getAttrs: headingParseGetAttrs(1)},
139         {tag: "h2", getAttrs: headingParseGetAttrs(2)},
140         {tag: "h3", getAttrs: headingParseGetAttrs(3)},
141         {tag: "h4", getAttrs: headingParseGetAttrs(4)},
142         {tag: "h5", getAttrs: headingParseGetAttrs(5)},
143         {tag: "h6", getAttrs: headingParseGetAttrs(6)},
144     ],
145     toDOM(node) {
146         return ["h" + node.attrs.level, addAlignmentAttr(node, {}), 0]
147     }
148 };
149
150 const code_block = {
151     content: "text*",
152     marks: "",
153     group: "block",
154     code: true,
155     defining: true,
156     parseDOM: [{tag: "pre", preserveWhitespace: "full"}],
157     toDOM() {
158         return ["pre", ["code", 0]];
159     }
160 };
161
162 const text = {
163     group: "inline"
164 };
165
166 const image = {
167     inline: true,
168     attrs: {
169         src: {},
170         alt: {default: null},
171         title: {default: null},
172         height: {default: null},
173         width: {default: null},
174     },
175     group: "inline",
176     draggable: true,
177     parseDOM: [{
178         tag: "img[src]", getAttrs: function getAttrs(dom) {
179             return {
180                 src: dom.getAttribute("src"),
181                 title: dom.getAttribute("title"),
182                 alt: dom.getAttribute("alt"),
183                 height: dom.getAttribute("height"),
184                 width: dom.getAttribute("width"),
185             }
186         }
187     }],
188     toDOM: function toDOM(node) {
189         const ref = node.attrs;
190         const src = ref.src;
191         const alt = ref.alt;
192         const title = ref.title;
193         const width = ref.width;
194         const height = ref.height;
195         return ["img", {src, alt, title, width, height}]
196     }
197 };
198
199 const iframe = {
200     attrs: {
201         src: {},
202         height: {default: null},
203         width: {default: null},
204         title: {default: null},
205         allow: {default: null},
206         sandbox: {default: null},
207     },
208     group: "block",
209     draggable: true,
210     parseDOM: [{
211         tag: "iframe",
212         getAttrs: domAttrsToAttrsParser(["src", "width", "height", "title", "allow", "sandbox"]),
213     }],
214     toDOM(node) {
215         const attrs = extractAttrsForDom(node, ["src", "width", "height", "title", "allow", "sandbox"])
216         return ["iframe", attrs];
217     }
218 };
219
220 const hard_break = {
221     inline: true,
222     group: "inline",
223     selectable: false,
224     parseDOM: [{tag: "br"}],
225     toDOM() {
226         return ["br"];
227     }
228 };
229
230
231 const calloutParseGetAttrs = (type) => {
232     return function (node) {
233         return {type, align: getAlignAttrFromDomNode(node)};
234     };
235 };
236 const callout = {
237     attrs: {
238         type: {default: 'info'},
239         align: {default: null},
240     },
241     content: "inline*",
242     group: "block",
243     defining: true,
244     parseDOM: [
245         {tag: 'p.callout.info', getAttrs: calloutParseGetAttrs('info'), priority: 75},
246         {tag: 'p.callout.success', getAttrs: calloutParseGetAttrs('success'), priority: 75},
247         {tag: 'p.callout.danger', getAttrs: calloutParseGetAttrs('danger'), priority: 75},
248         {tag: 'p.callout.warning', getAttrs: calloutParseGetAttrs('warning'), priority: 75},
249         {tag: 'p.callout', getAttrs: calloutParseGetAttrs('info'), priority: 75},
250     ],
251     toDOM(node) {
252         const type = node.attrs.type || 'info';
253         return ['p', addAlignmentAttr(node, {class: 'callout ' + type}), 0];
254     }
255 };
256
257 const ordered_list = Object.assign({}, orderedList, {content: "list_item+", group: "block"});
258 const bullet_list = Object.assign({}, bulletList, {content: "list_item+", group: "block"});
259 const list_item = Object.assign({}, listItem, {content: 'paragraph block*'});
260
261 const table = {
262     content: "table_row+",
263     attrs: {
264         style: {default: null},
265     },
266     tableRole: "table",
267     isolating: true,
268     group: "block",
269     parseDOM: [{tag: "table", getAttrs: domAttrsToAttrsParser(['style'])}],
270     toDOM(node) {
271         return ["table", extractAttrsForDom(node, ['style']), ["tbody", 0]]
272     }
273 };
274
275 const table_row = {
276     content: "(table_cell | table_header)*",
277     tableRole: "row",
278     parseDOM: [{tag: "tr"}],
279     toDOM() { return ["tr", 0] }
280 };
281
282 let cellAttrs = {
283     colspan: {default: 1},
284     rowspan: {default: 1},
285     width: {default: null},
286     height: {default: null},
287 };
288
289 function getCellAttrs(dom) {
290     return {
291         colspan: Number(dom.getAttribute("colspan") || 1),
292         rowspan: Number(dom.getAttribute("rowspan") || 1),
293         width: dom.style.width || null,
294         height: dom.style.height || null,
295     };
296 }
297
298 function setCellAttrs(node) {
299     let attrs = {};
300
301     const styles = [];
302     if (node.attrs.colspan != 1) attrs.colspan = node.attrs.colspan;
303     if (node.attrs.rowspan != 1) attrs.rowspan = node.attrs.rowspan;
304     if (node.attrs.width) styles.push(`width: ${node.attrs.width}`);
305     if (node.attrs.height) styles.push(`height: ${node.attrs.height}`);
306     if (styles) {
307         attrs.style = styles.join(';');
308     }
309
310     return attrs
311 }
312
313 const table_cell = {
314     content: "block+",
315     attrs: cellAttrs,
316     tableRole: "cell",
317     isolating: true,
318     parseDOM: [{tag: "td", getAttrs: dom => getCellAttrs(dom)}],
319     toDOM(node) { return ["td", setCellAttrs(node), 0] }
320 };
321
322 const table_header = {
323     content: "block+",
324     attrs: cellAttrs,
325     tableRole: "header_cell",
326     isolating: true,
327     parseDOM: [{tag: "th", getAttrs: dom => getCellAttrs(dom)}],
328     toDOM(node) { return ["th", setCellAttrs(node), 0] }
329 };
330
331 const nodes = {
332     doc,
333     paragraph,
334     blockquote,
335     horizontal_rule,
336     heading,
337     code_block,
338     text,
339     image,
340     iframe,
341     hard_break,
342     callout,
343     ordered_list,
344     bullet_list,
345     list_item,
346     table,
347     table_row,
348     table_cell,
349     table_header,
350 };
351
352 export default nodes;