]> BookStack Code Mirror - bookstack/blob - resources/js/editor/plugins/table-resizing.js
Started work on details/summary blocks
[bookstack] / resources / js / editor / plugins / table-resizing.js
1 /**
2  * This file originates from https://github.com/ProseMirror/prosemirror-tables
3  * and is hence subject to the MIT license found here:
4  * https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
5  * @copyright Marijn Haverbeke and others
6  */
7
8 import {Plugin, PluginKey} from "prosemirror-state"
9 import {Decoration, DecorationSet} from "prosemirror-view"
10 import {
11     cellAround,
12     pointsAtCell,
13     setAttr,
14     TableMap,
15 } from "prosemirror-tables";
16
17 export const key = new PluginKey("tableColumnResizing")
18
19 export function columnResizing(options = {}) {
20     const {
21         handleWidth, cellMinWidth, lastColumnResizable
22     } = Object.assign({
23         handleWidth: 5,
24         cellMinWidth: 25,
25         lastColumnResizable: true
26     }, options);
27
28     let plugin = new Plugin({
29         key,
30         state: {
31             init(_, state) {
32                 return new ResizeState(-1, false)
33             },
34             apply(tr, prev) {
35                 return prev.apply(tr)
36             }
37         },
38         props: {
39             attributes(state) {
40                 let pluginState = key.getState(state)
41                 return pluginState.activeHandle > -1 ? {class: "resize-cursor"} : null
42             },
43
44             handleDOMEvents: {
45                 mousemove(view, event) {
46                     handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable)
47                 },
48                 mouseleave(view) {
49                     handleMouseLeave(view)
50                 },
51                 mousedown(view, event) {
52                     handleMouseDown(view, event, cellMinWidth)
53                 }
54             },
55
56             decorations(state) {
57                 let pluginState = key.getState(state)
58                 if (pluginState.activeHandle > -1) return handleDecorations(state, pluginState.activeHandle)
59             },
60
61             nodeViews: {}
62         }
63     })
64     return plugin
65 }
66
67 class ResizeState {
68     constructor(activeHandle, dragging) {
69         this.activeHandle = activeHandle
70         this.dragging = dragging
71     }
72
73     apply(tr) {
74         let state = this, action = tr.getMeta(key)
75         if (action && action.setHandle != null)
76             return new ResizeState(action.setHandle, null)
77         if (action && action.setDragging !== undefined)
78             return new ResizeState(state.activeHandle, action.setDragging)
79         if (state.activeHandle > -1 && tr.docChanged) {
80             let handle = tr.mapping.map(state.activeHandle, -1)
81             if (!pointsAtCell(tr.doc.resolve(handle))) handle = null
82             state = new ResizeState(handle, state.dragging)
83         }
84         return state
85     }
86 }
87
88 function handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) {
89     let pluginState = key.getState(view.state)
90
91     if (!pluginState.dragging) {
92         let target = domCellAround(event.target), cell = -1
93         if (target) {
94             let {left, right} = target.getBoundingClientRect()
95             if (event.clientX - left <= handleWidth)
96                 cell = edgeCell(view, event, "left")
97             else if (right - event.clientX <= handleWidth)
98                 cell = edgeCell(view, event, "right")
99         }
100
101         if (cell != pluginState.activeHandle) {
102             if (!lastColumnResizable && cell !== -1) {
103                 let $cell = view.state.doc.resolve(cell)
104                 let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
105                 let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
106
107                 if (col == map.width - 1) {
108                     return
109                 }
110             }
111
112             updateHandle(view, cell)
113         }
114     }
115 }
116
117 function handleMouseLeave(view) {
118     let pluginState = key.getState(view.state)
119     if (pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1)
120 }
121
122 function handleMouseDown(view, event, cellMinWidth) {
123     let pluginState = key.getState(view.state)
124     if (pluginState.activeHandle == -1 || pluginState.dragging) return false
125
126     let cell = view.state.doc.nodeAt(pluginState.activeHandle)
127     let width = currentColWidth(view, pluginState.activeHandle, cell.attrs)
128     view.dispatch(view.state.tr.setMeta(key, {setDragging: {startX: event.clientX, startWidth: width}}))
129
130     function finish(event) {
131         window.removeEventListener("mouseup", finish)
132         window.removeEventListener("mousemove", move)
133         let pluginState = key.getState(view.state)
134         if (pluginState.dragging) {
135             updateColumnWidth(view, pluginState.activeHandle, draggedWidth(pluginState.dragging, event, cellMinWidth))
136             view.dispatch(view.state.tr.setMeta(key, {setDragging: null}))
137         }
138     }
139
140     function move(event) {
141         if (!event.which) return finish(event)
142         let pluginState = key.getState(view.state)
143         let dragged = draggedWidth(pluginState.dragging, event, cellMinWidth)
144         displayColumnWidth(view, pluginState.activeHandle, dragged, cellMinWidth)
145     }
146
147     window.addEventListener("mouseup", finish)
148     window.addEventListener("mousemove", move)
149     event.preventDefault()
150     return true
151 }
152
153 function currentColWidth(view, cellPos, {colspan, colwidth}) {
154     let width = colwidth && colwidth[colwidth.length - 1]
155     if (width) return width
156     let dom = view.domAtPos(cellPos)
157     let node = dom.node.childNodes[dom.offset]
158     let domWidth = node.offsetWidth, parts = colspan
159     if (colwidth) for (let i = 0; i < colspan; i++) if (colwidth[i]) {
160         domWidth -= colwidth[i]
161         parts--
162     }
163     return domWidth / parts
164 }
165
166 function domCellAround(target) {
167     while (target && target.nodeName != "TD" && target.nodeName != "TH")
168         target = target.classList.contains("ProseMirror") ? null : target.parentNode
169     return target
170 }
171
172 function edgeCell(view, event, side) {
173     let found = view.posAtCoords({left: event.clientX, top: event.clientY})
174     if (!found) return -1
175     let {pos} = found
176     let $cell = cellAround(view.state.doc.resolve(pos))
177     if (!$cell) return -1
178     if (side == "right") return $cell.pos
179     let map = TableMap.get($cell.node(-1)), start = $cell.start(-1)
180     let index = map.map.indexOf($cell.pos - start)
181     return index % map.width == 0 ? -1 : start + map.map[index - 1]
182 }
183
184 function draggedWidth(dragging, event, cellMinWidth) {
185     let offset = event.clientX - dragging.startX
186     return Math.max(cellMinWidth, dragging.startWidth + offset)
187 }
188
189 function updateHandle(view, value) {
190     view.dispatch(view.state.tr.setMeta(key, {setHandle: value}))
191 }
192
193 function updateColumnWidth(view, cell, width) {
194     let $cell = view.state.doc.resolve(cell);
195     let table = $cell.node(-1);
196     let map = TableMap.get(table);
197     let start = $cell.start(-1);
198     let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1;
199     let tr = view.state.tr;
200
201     for (let row = 0; row < map.height; row++) {
202         let mapIndex = row * map.width + col;
203         // Rowspanning cell that has already been handled
204         if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue
205         let pos = map.map[mapIndex]
206         let {attrs} = table.nodeAt(pos);
207         const newWidth = (attrs.colspan * width) + 'px';
208
209         tr.setNodeMarkup(start + pos, null, setAttr(attrs, "width",  newWidth));
210     }
211
212     if (tr.docChanged) view.dispatch(tr)
213 }
214
215 function displayColumnWidth(view, cell, width, cellMinWidth) {
216     const $cell = view.state.doc.resolve(cell)
217     const table = $cell.node(-1);
218     const start = $cell.start(-1);
219     const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
220     let dom = view.domAtPos($cell.start(-1)).node
221     while (dom.nodeName !== "TABLE") {
222         dom = dom.parentNode
223     }
224     updateColumnsOnResize(view, table, dom, cellMinWidth, col, width)
225 }
226
227
228 function updateColumnsOnResize(view, tableNode, tableDom, cellMinWidth, overrideCol, overrideValue) {
229     console.log({tableNode, tableDom, cellMinWidth, overrideCol, overrideValue});
230     let totalWidth = 0;
231     let fixedWidth = true;
232     const rows = tableDom.querySelectorAll('tr');
233
234     for (let y = 0; y < rows.length; y++) {
235         const row = rows[y];
236         const cell = row.children[overrideCol];
237         cell.style.width = `${overrideValue}px`;
238         if (y === 0) {
239             for (let x = 0; x < row.children.length; x++) {
240                 const cell = row.children[x];
241                 if (cell.style.width) {
242                     const width = Number(cell.style.width.replace('px', ''));
243                     totalWidth += width || cellMinWidth;
244                 } else {
245                     fixedWidth = false;
246                     totalWidth += cellMinWidth;
247                 }
248             }
249         }
250     }
251
252     console.log(totalWidth);
253     if (fixedWidth) {
254         tableDom.style.width = totalWidth + "px"
255         tableDom.style.minWidth = ""
256     } else {
257         tableDom.style.width = ""
258         tableDom.style.minWidth = totalWidth + "px"
259     }
260 }
261
262 function zeroes(n) {
263     let result = []
264     for (let i = 0; i < n; i++) result.push(0)
265     return result
266 }
267
268 function handleDecorations(state, cell) {
269     let decorations = []
270     let $cell = state.doc.resolve(cell)
271     let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
272     let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan
273     for (let row = 0; row < map.height; row++) {
274         let index = col + row * map.width - 1
275         // For positions that are have either a different cell or the end
276         // of the table to their right, and either the top of the table or
277         // a different cell above them, add a decoration
278         if ((col == map.width || map.map[index] != map.map[index + 1]) &&
279             (row == 0 || map.map[index - 1] != map.map[index - 1 - map.width])) {
280             let cellPos = map.map[index]
281             let pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1
282             let dom = document.createElement("div")
283             dom.className = "column-resize-handle"
284             decorations.push(Decoration.widget(pos, dom))
285         }
286     }
287     return DecorationSet.create(state.doc, decorations)
288 }