Skip to content

Commit 5f51f2b

Browse files
committed
floating page breaks
1 parent cfa3a66 commit 5f51f2b

11 files changed

Lines changed: 178 additions & 14 deletions

File tree

package-lock.json

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/super-editor/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@
7373
"tippy.js": "^6.3.7",
7474
"vite-plugin-node-polyfills": "^0.22.0",
7575
"y-prosemirror": "^1.2.12",
76-
"yjs": "13.6.19"
76+
"yjs": "13.6.19",
77+
"@floating-ui/dom": "^1.7.0"
7778
},
7879
"devDependencies": {
7980
"@playwright/test": "^1.51.0",
@@ -87,6 +88,7 @@
8788
"vite": "^5.4.12",
8889
"vitest": "^1.6.1",
8990
"vue-draggable-next": "^2.2.1",
90-
"which": "^5.0.0"
91+
"which": "^5.0.0",
92+
"@floating-ui/dom": "^1.7.0"
9193
}
9294
}

packages/super-editor/src/assets/styles/extensions/pagination.css

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
:root {
2+
--sd-editor-separator-height: 18px;
3+
}
4+
15
.pagination-section-header {
26
cursor: default;
37
}
@@ -23,15 +27,27 @@
2327
.pagination-separator {
2428
position: relative;
2529
display: block;
26-
height: 18px;
27-
min-height: 18px;
30+
height: var(--sd-editor-separator-height);
31+
min-height: var(--sd-editor-separator-height);
2832
min-width: 100%;
2933
width: 100%;
3034
border-top: 1px solid #DBDBDB;
3135
border-bottom: 1px solid #DBDBDB;
3236
cursor: default;
3337
}
3438

39+
.pagination-separator--table {
40+
border: 0;
41+
}
42+
43+
.pagination-separator-floating {
44+
position: fixed;
45+
height: var(--sd-editor-separator-height);
46+
border-top: 1px solid #DBDBDB;
47+
border-bottom: 1px solid #DBDBDB;
48+
pointer-events: none;
49+
}
50+
3551
.pagination-inner {
3652
position: absolute;
3753
top: 0;

packages/super-editor/src/components/pagination-helpers.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export function adjustPaginationBreaks(editorElem, editor) {
2323
if (!firstLeft) firstLeft = left;
2424
if (left !== firstLeft) {
2525
const diff = left - firstLeft;
26+
// Note: elements with "position: fixed" do not work correctly with transform style.
27+
// node.style.left = `${diff}px`;
2628
node.style.transform = `translateX(${diff}px)`;
2729
}
2830
});
29-
};
31+
};

packages/super-editor/src/core/Editor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export class Editor extends EventEmitter {
191191
lastSelection: null,
192192
suppressDefaultDocxStyles: false,
193193
jsonOverride: false,
194+
paginationFloatingClass: null,
194195
onBeforeCreate: () => null,
195196
onCreate: () => null,
196197
onUpdate: () => null,

packages/super-editor/src/dev/components/DeveloperPlayground.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ onMounted(async () => {
178178
</style>
179179
180180
<style scoped>
181+
.sd-toolbar {
182+
width: 100%;
183+
background: white;
184+
position: relative;
185+
z-index: 1;
186+
}
187+
181188
.page-spacer {
182189
height: 11in;
183190
width: 60px;
@@ -199,6 +206,8 @@ onMounted(async () => {
199206
}
200207
201208
.dev-app__layout {
209+
display: flex;
210+
flex-direction: column;
202211
width: 100%;
203212
height: 100vh;
204213
}

packages/super-editor/src/extensions/pagination/pagination.js

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import { ImagePlaceholderPluginKey } from '@extensions/image/imageHelpers/imageP
88
import { LinkedStylesPluginKey } from '@extensions/linked-styles/linked-styles.js';
99
import { findParentNodeClosestToPos } from '@core/helpers/findParentNodeClosestToPos.js';
1010
import { generateDocxRandomId } from '../../core/helpers/index.js';
11+
import { computePosition, autoUpdate, hide } from '@floating-ui/dom';
12+
13+
const SEPARATOR_CLASS = 'pagination-separator';
14+
const SEPARATOR_FLOATING_CLASS = 'pagination-separator-floating';
1115

1216
const isDebugging = false;
17+
const cleanupFunctions = new Set();
1318

1419
export const Pagination = Extension.create({
1520
name: 'pagination',
@@ -42,6 +47,7 @@ export const Pagination = Extension.create({
4247
*/
4348
addPmPlugins() {
4449
const editor = this.editor;
50+
4551
let isUpdating = false;
4652

4753
// Used to prevent unnecessary transactions
@@ -129,7 +135,7 @@ export const Pagination = Extension.create({
129135
let previousDecorations = DecorationSet.empty;
130136

131137
return {
132-
update: (view) => {
138+
update: (view, prevState) => {
133139
if (!shouldUpdate || isUpdating) return;
134140

135141
isUpdating = true;
@@ -141,6 +147,7 @@ export const Pagination = Extension.create({
141147
*/
142148
if (isDebugging) console.debug('--- Calling performUpdate ---')
143149
performUpdate(editor, view, previousDecorations);
150+
144151
isUpdating = false;
145152
shouldUpdate = false;
146153
},
@@ -155,6 +162,10 @@ export const Pagination = Extension.create({
155162

156163
return [paginationPlugin];
157164
},
165+
166+
onDestroy() {
167+
cleanupFloatingSeparators();
168+
},
158169
});
159170

160171
/**
@@ -218,8 +229,9 @@ const getHeaderFooterId = (currentPageNumber, sectionType, editor, node = null)
218229
* @returns {void}
219230
*/
220231
const performUpdate = (editor, view, previousDecorations) => {
221-
const sectionData = editor.storage.pagination.sectionData;
232+
const sectionData = editor.storage.pagination.sectionData;
222233
const newDecorations = calculatePageBreaks(view, editor, sectionData);
234+
const editorElement = editor.options.element;
223235

224236
// Skip updating if decorations haven't changed
225237
if (!previousDecorations.eq(newDecorations)) {
@@ -229,7 +241,18 @@ const performUpdate = (editor, view, previousDecorations) => {
229241
);
230242

231243
view.dispatch(updateTransaction);
232-
}
244+
245+
requestAnimationFrame(() => {
246+
requestAnimationFrame(() => {
247+
cleanupFloatingSeparators();
248+
const separators = [...editorElement.querySelectorAll(`.${SEPARATOR_CLASS}--table`)];
249+
separators.forEach((separator) => {
250+
const { cleanup } = createFloatingSeparator(separator, editor);
251+
cleanupFunctions.add(cleanup);
252+
});
253+
});
254+
});
255+
};
233256

234257
// Emit that pagination has been updated
235258
editor.emit('paginationUpdate');
@@ -370,8 +393,11 @@ function generateInternalPageBreaks(doc, view, editor, sectionData) {
370393

371394
if (isHardBreakNode || shouldAddPageBreak) {
372395
const $currentPos = view.state.doc.resolve(currentPos);
396+
const table = findParentNodeClosestToPos($currentPos, (node) => node.type.name === 'table');
373397
const tableRow = findParentNodeClosestToPos($currentPos, (node) => node.type.name === 'tableRow');
374398

399+
let isInTable = (table || tableRow) ? true : false;
400+
375401
if (tableRow) {
376402
// If the node is in a table cell, then split the entire row.
377403
currentNode = tableRow.node;
@@ -408,7 +434,7 @@ function generateInternalPageBreaks(doc, view, editor, sectionData) {
408434
const pageSpacer = Decoration.widget(breakPos, spacingNode, { key: 'stable-key' });
409435
decorations.push(pageSpacer);
410436

411-
const pageBreak = createPageBreak({ editor, header, footer });
437+
const pageBreak = createPageBreak({ editor, header, footer, isInTable });
412438
decorations.push(Decoration.widget(breakPos, pageBreak, { key: 'stable-key' }));
413439

414440
// Check if we have a hard page break node
@@ -637,7 +663,7 @@ const onHeaderFooterDblClick = (editor, currentFocusedSectionEditor) => {
637663
* @param {HTMLElement} param0.footer The footer element
638664
* @returns {HTMLElement} The page break element
639665
*/
640-
function createPageBreak({ editor, header, footer, footerBottom = null, isFirstHeader, isLastFooter }) {
666+
function createPageBreak({ editor, header, footer, footerBottom = null, isFirstHeader, isLastFooter, isInTable = false }) {
641667
const { pageSize, pageMargins } = editor.converter.pageStyles;
642668

643669
let sectionHeight = 0;
@@ -661,9 +687,11 @@ function createPageBreak({ editor, header, footer, footerBottom = null, isFirstH
661687
const separatorHeight = 20;
662688
sectionHeight += separatorHeight;
663689
const separator = document.createElement('div');
664-
separator.className = 'pagination-separator';
690+
separator.classList.add(SEPARATOR_CLASS);
691+
if (isInTable) {
692+
separator.classList.add(`${SEPARATOR_CLASS}--table`);
693+
}
665694
if (isDebugging) separator.style.backgroundColor = 'green';
666-
667695
innerDiv.appendChild(separator);
668696
}
669697

@@ -731,3 +759,75 @@ const onImageLoad = (editor) => {
731759
editor.view.dispatch(newTr);
732760
});
733761
};
762+
763+
function createFloatingSeparator(separator, editor) {
764+
const floatingSeparator = document.createElement('div');
765+
floatingSeparator.classList.add(SEPARATOR_FLOATING_CLASS);
766+
floatingSeparator.dataset.floatingSeparator = '';
767+
768+
const { paginationFloatingClass } = editor.options;
769+
if (paginationFloatingClass) {
770+
floatingSeparator.classList.add(paginationFloatingClass);
771+
}
772+
773+
document.body.append(floatingSeparator);
774+
775+
const updatePosition = () => {
776+
computePosition(separator, floatingSeparator, {
777+
strategy: 'fixed',
778+
placement: 'top-start',
779+
middleware: [
780+
hide({
781+
padding: {
782+
top: 21,
783+
bottom: 21,
784+
},
785+
}),
786+
{
787+
name: 'copy',
788+
fn: ({ elements }) => {
789+
const rect = elements.reference.getBoundingClientRect();
790+
return {
791+
x: rect.left,
792+
y: rect.top,
793+
data: {
794+
width: rect.width,
795+
height: rect.height,
796+
},
797+
};
798+
},
799+
},
800+
],
801+
}).then(({ x, y, middlewareData }) => {
802+
Object.assign(floatingSeparator.style, {
803+
top: `${y}px`,
804+
left: `${x}px`,
805+
width: `${middlewareData.copy.width}px`,
806+
height: `${middlewareData.copy.height}px`,
807+
visibility: middlewareData.hide?.referenceHidden ? 'hidden' : 'visible',
808+
});
809+
});
810+
};
811+
812+
const cleanup = autoUpdate(
813+
separator,
814+
floatingSeparator,
815+
updatePosition,
816+
// { animationFrame: true },
817+
);
818+
819+
const extendedCleanup = () => {
820+
floatingSeparator?.remove();
821+
cleanup();
822+
};
823+
824+
return {
825+
cleanup: extendedCleanup,
826+
updatePosition,
827+
};
828+
}
829+
830+
function cleanupFloatingSeparators() {
831+
cleanupFunctions.forEach((cleanup) => cleanup());
832+
cleanupFunctions.clear();
833+
}

packages/super-editor/vite.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default defineConfig(({ mode }) => {
2626
__APP_VERSION__: JSON.stringify(superdocVersion),
2727
},
2828
optimizeDeps: {
29-
exclude: ['yjs', 'tippy.js']
29+
exclude: ['yjs', 'tippy.js', '@floating-ui/dom']
3030
},
3131
build: {
3232
target: 'es2020',

packages/superdoc/src/SuperDoc.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ const onEditorException = ({ error, editor }) => {
257257
const editorOptions = (doc) => {
258258
const options = {
259259
pagination: proxy.$superdoc.config.pagination,
260+
paginationFloatingClass: proxy.$superdoc.config.paginationFloatingClass,
260261
documentId: doc.id,
261262
user: proxy.$superdoc.user,
262263
users: proxy.$superdoc.users,

packages/superdoc/src/core/SuperDoc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export class SuperDoc extends EventEmitter {
152152
title: 'SuperDoc',
153153
conversations: [],
154154
pagination: false, // Optional: Whether to show pagination in SuperEditors
155+
paginationFloatingClass: null,
155156
isInternal: false,
156157

157158
// toolbar config

0 commit comments

Comments
 (0)