Skip to content

Commit b1ca9c6

Browse files
authored
feat(YfmTable): add ghost when dragging rows and columns (#849)
1 parent 2482d89 commit b1ca9c6

File tree

3 files changed

+309
-18
lines changed

3 files changed

+309
-18
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import type {EditorView} from '#pm/view';
2+
import type {TableDescBinded} from 'src/table-utils/table-desc';
3+
4+
type Event = Pick<MouseEvent, 'clientX' | 'clientY' | 'target'>;
5+
6+
type BuildGhostResult = {
7+
domElement: HTMLElement;
8+
shiftX: number;
9+
shiftY: number;
10+
};
11+
12+
export type YfmTableDnDGhostParams = {
13+
initial: Event;
14+
type: 'row' | 'column';
15+
rangeIdx: number;
16+
tableDesc: TableDescBinded;
17+
};
18+
19+
export class YfmTableDnDGhost {
20+
private _x: number;
21+
private _y: number;
22+
23+
private readonly _dndBackgroundElem: HTMLElement;
24+
private readonly _ghostTable: HTMLElement;
25+
private readonly _ghostButton: HTMLElement | null = null;
26+
27+
private readonly _tblShiftX: number;
28+
private readonly _tblShiftY: number;
29+
30+
private readonly _btnShiftX: number = 0;
31+
private readonly _btnShiftY: number = 0;
32+
33+
private _rafId: number;
34+
35+
constructor(view: EditorView, params: YfmTableDnDGhostParams) {
36+
this._x = params.initial.clientX;
37+
this._y = params.initial.clientY;
38+
39+
const document = view.dom.ownerDocument;
40+
41+
this._dndBackgroundElem = document.createElement('div');
42+
this._dndBackgroundElem.classList.add('g-md-yfm-table-dnd-cursor-background');
43+
44+
{
45+
const res = this._buildGhostButton(params);
46+
if (res) {
47+
this._ghostButton = res.domElement;
48+
this._btnShiftX = res.shiftX;
49+
this._btnShiftY = res.shiftY;
50+
this._dndBackgroundElem.appendChild(this._ghostButton);
51+
}
52+
}
53+
54+
{
55+
const {domElement, shiftX, shiftY} =
56+
params.type === 'row'
57+
? this._buildRowGhost(view, params)
58+
: this._buildColumnGhost(view, params);
59+
60+
this._ghostTable = domElement;
61+
this._tblShiftX = shiftX;
62+
this._tblShiftY = shiftY;
63+
this._dndBackgroundElem.appendChild(this._ghostTable);
64+
}
65+
66+
this._updatePositions();
67+
68+
this._rafId = requestAnimationFrame(() => {
69+
document.body.append(this._dndBackgroundElem);
70+
this._startAnimation();
71+
});
72+
}
73+
74+
move(event: Event) {
75+
this._x = event.clientX;
76+
this._y = event.clientY;
77+
}
78+
79+
destroy() {
80+
cancelAnimationFrame(this._rafId);
81+
this._dndBackgroundElem.remove();
82+
}
83+
84+
private _startAnimation() {
85+
const self = this;
86+
let last = {x: self._x, y: self._y};
87+
88+
self._rafId = requestAnimationFrame(function update() {
89+
if (self._x !== last.x || self._y !== last.y) {
90+
last = {x: self._x, y: self._y};
91+
self._updatePositions();
92+
}
93+
self._rafId = requestAnimationFrame(update);
94+
});
95+
}
96+
97+
private _updatePositions() {
98+
{
99+
const tx = this._x + this._tblShiftX;
100+
const ty = this._y + this._tblShiftY;
101+
this._ghostTable.style.transform = `translate(${tx}px, ${ty}px)`;
102+
}
103+
104+
if (this._ghostButton) {
105+
const tx = this._x + this._btnShiftX;
106+
const ty = this._y + this._btnShiftY;
107+
this._ghostButton.style.transform = `translate(${tx}px, ${ty}px)`;
108+
}
109+
}
110+
111+
private _buildRowGhost(
112+
view: EditorView,
113+
{tableDesc, rangeIdx}: YfmTableDnDGhostParams,
114+
): BuildGhostResult {
115+
let shiftX = 0;
116+
let shiftY = 0;
117+
118+
const document = view.dom.ownerDocument;
119+
const container = this._buildGhostContainer(view);
120+
121+
const table = container.appendChild(document.createElement('table'));
122+
const tbody = table.appendChild(document.createElement('tbody'));
123+
124+
{
125+
const tablePos = tableDesc.pos;
126+
const tableNode = view.domAtPos(tablePos + 1).node;
127+
const rect = (tableNode as Element).getBoundingClientRect();
128+
table.style.width = rect.width + 'px';
129+
}
130+
131+
const range = tableDesc.base.getRowRanges()[rangeIdx];
132+
for (let rowIdx = range.startIdx; rowIdx <= range.endIdx; rowIdx++) {
133+
const tr = tbody.appendChild(document.createElement('tr'));
134+
135+
for (let colIdx = 0; colIdx < tableDesc.cols; colIdx++) {
136+
const cellPos = tableDesc.getPosForCell(rowIdx, colIdx);
137+
if (cellPos.type === 'real') {
138+
const origNode = view.domAtPos(cellPos.from + 1).node as HTMLElement;
139+
const cloned = tr.appendChild(origNode.cloneNode(true));
140+
141+
const rect = origNode.getBoundingClientRect();
142+
(cloned as HTMLElement).style.width = rect.width + 'px';
143+
(cloned as HTMLElement).style.height = rect.height + 'px';
144+
145+
if (rowIdx === range.startIdx && colIdx === 0) {
146+
shiftX = rect.left - this._x;
147+
shiftY = rect.top - this._y;
148+
}
149+
}
150+
}
151+
}
152+
153+
removeIdAttributes(table);
154+
155+
return {domElement: container, shiftX, shiftY};
156+
}
157+
158+
private _buildColumnGhost(
159+
view: EditorView,
160+
{tableDesc, rangeIdx}: YfmTableDnDGhostParams,
161+
): BuildGhostResult {
162+
let shiftX = 0;
163+
let shiftY = 0;
164+
165+
const document = view.dom.ownerDocument;
166+
const container = this._buildGhostContainer(view);
167+
168+
{
169+
const tablePos = tableDesc.pos;
170+
const table = view.domAtPos(tablePos + 1).node;
171+
const rect = (table as Element).getBoundingClientRect();
172+
container.style.height = rect.height + 'px';
173+
}
174+
175+
const table = container.appendChild(document.createElement('table'));
176+
const tbody = table.appendChild(document.createElement('tbody'));
177+
178+
const range = tableDesc.base.getColumnRanges()[rangeIdx];
179+
for (let rowIdx = 0; rowIdx < tableDesc.rows; rowIdx++) {
180+
const tr = tbody.appendChild(document.createElement('tr'));
181+
182+
for (let colIdx = range.startIdx; colIdx <= range.endIdx; colIdx++) {
183+
const cellPos = tableDesc.getPosForCell(rowIdx, colIdx);
184+
if (cellPos.type === 'real') {
185+
const origNode = view.domAtPos(cellPos.from + 1).node as HTMLElement;
186+
const cloned = tr.appendChild(origNode.cloneNode(true));
187+
188+
const rect = origNode.getBoundingClientRect();
189+
(cloned as HTMLElement).style.width = rect.width + 'px';
190+
(cloned as HTMLElement).style.height = rect.height + 'px';
191+
192+
if (rowIdx === 0 && colIdx === range.startIdx) {
193+
container.style.minWidth = rect.width + 'px';
194+
195+
shiftX = rect.left - this._x;
196+
shiftY = rect.top - this._y;
197+
}
198+
}
199+
}
200+
}
201+
202+
removeIdAttributes(table);
203+
204+
return {domElement: container, shiftX, shiftY};
205+
}
206+
207+
private _buildGhostButton({
208+
initial: {target},
209+
}: YfmTableDnDGhostParams): BuildGhostResult | null {
210+
if (!(target instanceof Element)) return null;
211+
212+
const button = target.closest('.g-button');
213+
if (!button) return null;
214+
215+
const rect = button.getBoundingClientRect();
216+
const cloned = button.cloneNode(true) as HTMLElement;
217+
218+
removeIdAttributes(cloned);
219+
cloned.style.cursor = '';
220+
cloned.classList.add('g-md-yfm-table-dnd-ghost-button');
221+
222+
return {
223+
domElement: cloned,
224+
shiftX: rect.left - this._x,
225+
shiftY: rect.top - this._y,
226+
};
227+
}
228+
229+
private _buildGhostContainer(view: EditorView): HTMLElement {
230+
const container = view.dom.ownerDocument.createElement('div');
231+
container.setAttribute('aria-hidden', 'true');
232+
233+
const yfmClasses = Array.from(view.dom.classList).filter((val) => val.startsWith('yfm_'));
234+
container.classList.add('g-md-yfm-table-dnd-ghost', 'yfm', ...yfmClasses);
235+
236+
return container;
237+
}
238+
}
239+
240+
function removeIdAttributes(elem: HTMLElement) {
241+
elem.removeAttribute('id');
242+
elem.querySelectorAll('[id]').forEach((el) => el.removeAttribute('id'));
243+
}

src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,37 @@
1010
background: transparent;
1111
}
1212

13+
.yfm.g-md-yfm-table-dnd-ghost,
14+
.g-button.g-md-yfm-table-dnd-ghost-button {
15+
position: fixed;
16+
17+
cursor: grabbing;
18+
pointer-events: none;
19+
20+
transition: none;
21+
will-change: transform;
22+
}
23+
24+
.yfm.g-md-yfm-table-dnd-ghost {
25+
& > table {
26+
border-color: var(--g-color-line-brand);
27+
box-shadow: 0 8px 20px 1px var(--g-color-sfx-shadow);
28+
29+
& > tbody > tr > td {
30+
border-color: var(--g-color-line-brand);
31+
}
32+
}
33+
}
34+
35+
.g-button.g-md-yfm-table-dnd-ghost-button {
36+
--g-button-background-color-hover: var(--g-color-base-background);
37+
--g-button-background-color: var(--g-color-base-background);
38+
--g-button-border-color: var(--g-color-line-brand);
39+
--g-button-text-color: var(--g-color-text-brand);
40+
41+
z-index: 2;
42+
}
43+
1344
.yfm.ProseMirror {
1445
.g-md-yfm-table-dnd-dragged-row,
1546
.g-md-yfm-table-dnd-dragged-column-cell {

0 commit comments

Comments
 (0)