@fiduswriter/editor 0.2.6 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/figure.css +88 -0
- package/css/tracking.css +45 -0
- package/dist/copyright_dialog/templates.d.ts.map +1 -1
- package/dist/copyright_dialog/templates.js +11 -31
- package/dist/copyright_dialog/templates.js.map +1 -1
- package/dist/dialogs/epub_export.d.ts +15 -0
- package/dist/dialogs/epub_export.d.ts.map +1 -0
- package/dist/dialogs/epub_export.js +43 -0
- package/dist/dialogs/epub_export.js.map +1 -0
- package/dist/dialogs/html_export.d.ts +3 -0
- package/dist/dialogs/html_export.d.ts.map +1 -1
- package/dist/dialogs/html_export.js +4 -3
- package/dist/dialogs/html_export.js.map +1 -1
- package/dist/dialogs/index.d.ts +2 -0
- package/dist/dialogs/index.d.ts.map +1 -1
- package/dist/dialogs/index.js +2 -0
- package/dist/dialogs/index.js.map +1 -1
- package/dist/dialogs/template_export.d.ts +13 -0
- package/dist/dialogs/template_export.d.ts.map +1 -0
- package/dist/dialogs/template_export.js +46 -0
- package/dist/dialogs/template_export.js.map +1 -0
- package/dist/dialogs/templates.d.ts +9 -0
- package/dist/dialogs/templates.d.ts.map +1 -1
- package/dist/dialogs/templates.js +54 -12
- package/dist/dialogs/templates.js.map +1 -1
- package/dist/document_template/index.d.ts.map +1 -1
- package/dist/document_template/index.js +23 -4
- package/dist/document_template/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/menus/headerbar/model.d.ts.map +1 -1
- package/dist/menus/headerbar/model.js +31 -10
- package/dist/menus/headerbar/model.js.map +1 -1
- package/dist/state_plugins/clipboard.d.ts.map +1 -1
- package/dist/state_plugins/clipboard.js +67 -3
- package/dist/state_plugins/clipboard.js.map +1 -1
- package/dist/track/accept.d.ts.map +1 -1
- package/dist/track/accept.js +21 -4
- package/dist/track/accept.js.map +1 -1
- package/dist/track/reject.d.ts.map +1 -1
- package/dist/track/reject.js +17 -5
- package/dist/track/reject.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/locale/en/messages.json +18 -1
- package/locale/en_US/messages.json +18 -1
- package/package.json +6 -6
- package/src/copyright_dialog/templates.ts +35 -31
- package/src/dialogs/epub_export.ts +66 -0
- package/src/dialogs/html_export.ts +11 -3
- package/src/dialogs/index.ts +2 -0
- package/src/dialogs/template_export.ts +66 -0
- package/src/dialogs/templates.ts +117 -14
- package/src/document_template/index.ts +23 -4
- package/src/index.ts +1 -3
- package/src/menus/headerbar/model.ts +39 -10
- package/src/state_plugins/clipboard.ts +89 -5
- package/src/track/accept.ts +29 -4
- package/src/track/reject.ts +23 -5
- package/src/types.ts +0 -1
- package/src/images/edit_dialog/model.ts +0 -5
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {Dialog} from "fwtoolkit"
|
|
2
|
-
import {htmlExportDialogTemplate} from "./templates.js"
|
|
2
|
+
import {getExportTrackChangesValue, htmlExportDialogTemplate} from "./templates.js"
|
|
3
3
|
|
|
4
4
|
export interface HtmlExportDialogResult {
|
|
5
5
|
/** Render `equation`/`figure_equation` nodes as SVG images (MathJax)
|
|
6
6
|
instead of MathML. */
|
|
7
7
|
svgMath: boolean
|
|
8
|
+
/** Resolve all tracked changes (accept all) before exporting. When false,
|
|
9
|
+
the tracked changes are kept and rendered in the export. */
|
|
10
|
+
resolveTrackChanges: boolean
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export class HtmlExportDialog {
|
|
@@ -28,8 +31,13 @@ export class HtmlExportDialog {
|
|
|
28
31
|
".html-svg-math"
|
|
29
32
|
) as HTMLInputElement
|
|
30
33
|
)?.checked
|
|
34
|
+
const resolveTrackChanges =
|
|
35
|
+
getExportTrackChangesValue(
|
|
36
|
+
dialogEl,
|
|
37
|
+
"html-track-changes"
|
|
38
|
+
) !== "include"
|
|
31
39
|
;(this.dialog as Dialog).close()
|
|
32
|
-
return resolve({svgMath: !!svgMath})
|
|
40
|
+
return resolve({svgMath: !!svgMath, resolveTrackChanges})
|
|
33
41
|
}
|
|
34
42
|
})
|
|
35
43
|
|
|
@@ -46,7 +54,7 @@ export class HtmlExportDialog {
|
|
|
46
54
|
this.dialog = new Dialog({
|
|
47
55
|
title: gettext("HTML export options"),
|
|
48
56
|
body: htmlExportDialogTemplate(),
|
|
49
|
-
height:
|
|
57
|
+
height: 330,
|
|
50
58
|
width: 420,
|
|
51
59
|
buttons,
|
|
52
60
|
restoreActiveElement: false
|
package/src/dialogs/index.ts
CHANGED
|
@@ -8,5 +8,7 @@ export {TableDialog, TableConfigurationDialog} from "./table.js"
|
|
|
8
8
|
export {RevisionDialog} from "./revision.js"
|
|
9
9
|
export {PdfExportDialog} from "./pdf_export.js"
|
|
10
10
|
export {HtmlExportDialog} from "./html_export.js"
|
|
11
|
+
export {EpubExportDialog} from "./epub_export.js"
|
|
12
|
+
export {TemplateExportDialog} from "./template_export.js"
|
|
11
13
|
export {OrderedListStartDialog} from "./ordered_list.js"
|
|
12
14
|
export {CodeBlockDialog} from "./code_block.js"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {Dialog} from "fwtoolkit"
|
|
2
|
+
import {
|
|
3
|
+
exportTrackChangesTemplate,
|
|
4
|
+
getExportTrackChangesValue
|
|
5
|
+
} from "./templates.js"
|
|
6
|
+
|
|
7
|
+
export interface TemplateExportDialogResult {
|
|
8
|
+
/** Resolve all tracked changes (accept all) before exporting. When false,
|
|
9
|
+
the tracked changes are kept and rendered in the exported file. */
|
|
10
|
+
resolveTrackChanges: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Ask how tracked changes should be handled before a DOCX/ODT template export. */
|
|
14
|
+
export class TemplateExportDialog {
|
|
15
|
+
dialog: Dialog | false
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this.dialog = false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
init(fileType: "docx" | "odt"): Promise<TemplateExportDialogResult | false> {
|
|
22
|
+
const title =
|
|
23
|
+
fileType === "docx"
|
|
24
|
+
? gettext("DOCX export options")
|
|
25
|
+
: gettext("ODT export options")
|
|
26
|
+
const buttons: Array<Record<string, unknown>> = []
|
|
27
|
+
const dialogDonePromise = new Promise<TemplateExportDialogResult | false>(
|
|
28
|
+
resolve => {
|
|
29
|
+
buttons.push({
|
|
30
|
+
text: gettext("Export"),
|
|
31
|
+
classes: "fw-dark",
|
|
32
|
+
click: () => {
|
|
33
|
+
const dialogEl = (this.dialog as Dialog).dialogEl
|
|
34
|
+
const resolveTrackChanges =
|
|
35
|
+
getExportTrackChangesValue(
|
|
36
|
+
dialogEl,
|
|
37
|
+
"template-track-changes"
|
|
38
|
+
) !== "include"
|
|
39
|
+
;(this.dialog as Dialog).close()
|
|
40
|
+
return resolve({resolveTrackChanges})
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
buttons.push({
|
|
45
|
+
type: "cancel" as const,
|
|
46
|
+
click: () => {
|
|
47
|
+
;(this.dialog as Dialog).close()
|
|
48
|
+
resolve(false)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
this.dialog = new Dialog({
|
|
55
|
+
title,
|
|
56
|
+
body: exportTrackChangesTemplate("template-track-changes"),
|
|
57
|
+
height: 220,
|
|
58
|
+
width: 420,
|
|
59
|
+
buttons,
|
|
60
|
+
restoreActiveElement: false
|
|
61
|
+
})
|
|
62
|
+
this.dialog.open()
|
|
63
|
+
|
|
64
|
+
return dialogDonePromise
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/dialogs/templates.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {CATS} from "@fiduswriter/document/schema/i18n"
|
|
2
|
-
import {escapeText} from "fwtoolkit"
|
|
2
|
+
import {escapeText, infoTooltip} from "fwtoolkit"
|
|
3
3
|
|
|
4
4
|
interface InternalTarget {
|
|
5
5
|
id: string
|
|
@@ -96,40 +96,143 @@ export const revisionDialogTemplate = ({dir}: {dir: string}): string =>
|
|
|
96
96
|
export const pdfExportDialogTemplate = (): string => `
|
|
97
97
|
<h4>${gettext("Tracked changes")}</h4>
|
|
98
98
|
<div class="fw-radio">
|
|
99
|
-
<input type="radio" name="pdf-track-changes" value="resolve" class="pdf-track-resolve" checked="">
|
|
99
|
+
<input type="radio" name="pdf-track-changes" value="resolve" class="pdf-track-resolve" checked="" aria-describedby="pdf-track-resolve-help">
|
|
100
100
|
<label class="pdf-track-resolve-label">${gettext("Resolve tracked changes (accept all)")}</label>
|
|
101
|
+
${infoTooltip(
|
|
102
|
+
gettext(
|
|
103
|
+
"Applies all tracked changes so the PDF shows the document as if every change had already been accepted."
|
|
104
|
+
),
|
|
105
|
+
"pdf-track-resolve-help"
|
|
106
|
+
)}
|
|
101
107
|
</div>
|
|
102
108
|
<div class="fw-radio">
|
|
103
|
-
<input type="radio" name="pdf-track-changes" value="include" class="pdf-track-include">
|
|
109
|
+
<input type="radio" name="pdf-track-changes" value="include" class="pdf-track-include" aria-describedby="pdf-track-include-help">
|
|
104
110
|
<label class="pdf-track-include-label">${gettext("Include tracked changes in the PDF")}</label>
|
|
111
|
+
${infoTooltip(
|
|
112
|
+
gettext(
|
|
113
|
+
"Prints the tracked changes so reviewers can see what was added, removed or altered."
|
|
114
|
+
),
|
|
115
|
+
"pdf-track-include-help"
|
|
116
|
+
)}
|
|
105
117
|
</div>
|
|
106
118
|
<p>
|
|
107
119
|
<label>
|
|
108
|
-
<input type="checkbox" class="pdf-embed-fidus">
|
|
120
|
+
<input type="checkbox" class="pdf-embed-fidus" aria-describedby="pdf-embed-fidus-help">
|
|
109
121
|
${gettext("Embed a Fidus Writer file of this document in the PDF")}
|
|
110
122
|
</label>
|
|
123
|
+
${infoTooltip(
|
|
124
|
+
gettext(
|
|
125
|
+
"Embeds the editable Fidus Writer document inside the PDF so it can be reopened for editing."
|
|
126
|
+
),
|
|
127
|
+
"pdf-embed-fidus-help"
|
|
128
|
+
)}
|
|
111
129
|
</p>
|
|
112
130
|
<h4>${gettext("Print production")}</h4>
|
|
113
131
|
<p>
|
|
114
|
-
<label><input type="checkbox" class="pdf-crop-marks"> ${gettext("Crop marks")}</label
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<label><input type="checkbox" class="pdf-
|
|
132
|
+
<label><input type="checkbox" class="pdf-crop-marks" aria-describedby="pdf-crop-marks-help"> ${gettext("Crop marks")}</label>
|
|
133
|
+
${infoTooltip(
|
|
134
|
+
gettext("Small lines at the corners of the page that show printers where to cut the paper."),
|
|
135
|
+
"pdf-crop-marks-help"
|
|
136
|
+
)}<br>
|
|
137
|
+
<label><input type="checkbox" class="pdf-trim-box" aria-describedby="pdf-trim-box-help"> ${gettext("Trim box")}</label>
|
|
138
|
+
${infoTooltip(
|
|
139
|
+
gettext("Marks the final size of the page after it has been cut."),
|
|
140
|
+
"pdf-trim-box-help"
|
|
141
|
+
)}<br>
|
|
142
|
+
<label><input type="checkbox" class="pdf-bleed-box" aria-describedby="pdf-bleed-box-help"> ${gettext("Bleed box")}</label>
|
|
143
|
+
${infoTooltip(
|
|
144
|
+
gettext("Marks the area beyond the final page size where images and colors must extend so no white edges appear after cutting."),
|
|
145
|
+
"pdf-bleed-box-help"
|
|
146
|
+
)}<br>
|
|
147
|
+
<label>${gettext("Bleed")}: <input type="number" class="pdf-bleed-mm fw-inline" value="3" min="0" step="0.5" aria-describedby="pdf-bleed-mm-help"> ${gettext("mm")}</label>
|
|
148
|
+
${infoTooltip(
|
|
149
|
+
gettext("How far, in millimeters, images and colors extend beyond the edge of the page."),
|
|
150
|
+
"pdf-bleed-mm-help"
|
|
151
|
+
)}<br>
|
|
152
|
+
<label><input type="checkbox" class="pdf-link-borders" aria-describedby="pdf-link-borders-help"> ${gettext("Show link annotation borders")}</label>
|
|
153
|
+
${infoTooltip(
|
|
154
|
+
gettext("Draws a visible border around hyperlinks in the PDF so they are easier to find."),
|
|
155
|
+
"pdf-link-borders-help"
|
|
156
|
+
)}<br>
|
|
157
|
+
<label><input type="checkbox" class="pdf-rasterize-svgs" aria-describedby="pdf-rasterize-svgs-help"> ${gettext("Rasterize SVG images")}</label>
|
|
158
|
+
${infoTooltip(
|
|
159
|
+
gettext("Converts SVG images to bitmap images in the PDF, which can help when PDF viewers or printers do not render SVG images correctly."),
|
|
160
|
+
"pdf-rasterize-svgs-help"
|
|
161
|
+
)}
|
|
120
162
|
</p>
|
|
121
163
|
`
|
|
122
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Radio group for choosing whether tracked changes are resolved (accepted) or
|
|
167
|
+
* kept in the exported file. `name` must be unique per dialog; the chosen value
|
|
168
|
+
* is read back with {@link getExportTrackChangesValue}.
|
|
169
|
+
*/
|
|
170
|
+
export const exportTrackChangesTemplate = (name: string): string => `
|
|
171
|
+
<h4>${gettext("Tracked changes")}</h4>
|
|
172
|
+
<div class="fw-radio">
|
|
173
|
+
<input type="radio" name="${name}" value="resolve" class="export-track-resolve" checked="" aria-describedby="${name}-resolve-help">
|
|
174
|
+
<label class="export-track-resolve-label">${gettext("Resolve tracked changes (accept all)")}</label>
|
|
175
|
+
${infoTooltip(
|
|
176
|
+
gettext(
|
|
177
|
+
"Applies all tracked changes so the exported file shows the document as if every change had already been accepted."
|
|
178
|
+
),
|
|
179
|
+
`${name}-resolve-help`
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
<div class="fw-radio">
|
|
183
|
+
<input type="radio" name="${name}" value="include" class="export-track-include" aria-describedby="${name}-include-help">
|
|
184
|
+
<label class="export-track-include-label">${gettext("Include tracked changes in the export")}</label>
|
|
185
|
+
${infoTooltip(
|
|
186
|
+
gettext(
|
|
187
|
+
"Keeps the tracked changes so reviewers can see what was added, removed or altered."
|
|
188
|
+
),
|
|
189
|
+
`${name}-include-help`
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
`
|
|
193
|
+
|
|
194
|
+
/** Read the tracked-changes radio group value ("resolve" or "include"). */
|
|
195
|
+
export const getExportTrackChangesValue = (
|
|
196
|
+
dialogEl: Element,
|
|
197
|
+
name: string
|
|
198
|
+
): string => {
|
|
199
|
+
return (
|
|
200
|
+
dialogEl.querySelector(
|
|
201
|
+
`input[name="${name}"]:checked`
|
|
202
|
+
) as HTMLInputElement | null
|
|
203
|
+
)?.value || "resolve"
|
|
204
|
+
}
|
|
205
|
+
|
|
123
206
|
export const htmlExportDialogTemplate = (): string => `
|
|
207
|
+
${exportTrackChangesTemplate("html-track-changes")}
|
|
208
|
+
<p>
|
|
209
|
+
<label>
|
|
210
|
+
<input type="checkbox" class="html-svg-math" aria-describedby="html-svg-math-help">
|
|
211
|
+
${gettext("Render formulas as SVG images instead of MathML")}
|
|
212
|
+
</label>
|
|
213
|
+
${infoTooltip(
|
|
214
|
+
gettext(
|
|
215
|
+
"SVG renders consistently across browsers; MathML keeps formulas as text that can be searched and copied, but only newer browsers support it."
|
|
216
|
+
),
|
|
217
|
+
"html-svg-math-help"
|
|
218
|
+
)}
|
|
219
|
+
</p>
|
|
220
|
+
`
|
|
221
|
+
|
|
222
|
+
export const epubExportDialogTemplate = (): string => `
|
|
223
|
+
${exportTrackChangesTemplate("epub-track-changes")}
|
|
124
224
|
<p>
|
|
125
225
|
<label>
|
|
126
|
-
<input type="checkbox" class="
|
|
226
|
+
<input type="checkbox" class="epub-svg-math" aria-describedby="epub-svg-math-help">
|
|
127
227
|
${gettext("Render formulas as SVG images instead of MathML")}
|
|
128
228
|
</label>
|
|
229
|
+
${infoTooltip(
|
|
230
|
+
gettext(
|
|
231
|
+
"SVG renders consistently across browsers; MathML keeps formulas as text that can be searched and copied, but only newer browsers support it."
|
|
232
|
+
),
|
|
233
|
+
"epub-svg-math-help"
|
|
234
|
+
)}
|
|
129
235
|
</p>
|
|
130
|
-
<p class="formula-note">${gettext(
|
|
131
|
-
"SVG renders consistently across browsers and is used in PDF export; MathML keeps formulas as text that can be searched and copied, but only newer browsers support it."
|
|
132
|
-
)}</p>
|
|
133
236
|
`
|
|
134
237
|
|
|
135
238
|
export const tableInsertTemplate = (): string => `
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
import type {Editor} from "../types.js"
|
|
16
16
|
import {SaveCopy} from "../exporter/native/index.js"
|
|
17
|
+
import {TemplateExportDialog} from "../dialogs/index.js"
|
|
17
18
|
|
|
18
19
|
export {serializeHelp} from "./schema.js"
|
|
19
20
|
|
|
@@ -388,8 +389,17 @@ export class ModDocumentTemplate {
|
|
|
388
389
|
action: (editor: Editor) => {
|
|
389
390
|
import(
|
|
390
391
|
"@fiduswriter/document/exporter/docx/index"
|
|
391
|
-
).then(({DOCXExporter}: any) => {
|
|
392
|
-
const
|
|
392
|
+
).then(async ({DOCXExporter}: any) => {
|
|
393
|
+
const dialog = new TemplateExportDialog()
|
|
394
|
+
const options = await dialog.init("docx")
|
|
395
|
+
if (!options) {
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
const doc = editor.getDoc({
|
|
399
|
+
changes: options.resolveTrackChanges
|
|
400
|
+
? "acceptAllNoInsertions"
|
|
401
|
+
: undefined
|
|
402
|
+
}) as any
|
|
393
403
|
const title = shortFileTitle(
|
|
394
404
|
doc.title,
|
|
395
405
|
doc.path || ""
|
|
@@ -426,8 +436,17 @@ export class ModDocumentTemplate {
|
|
|
426
436
|
),
|
|
427
437
|
action: (editor: Editor) => {
|
|
428
438
|
import("@fiduswriter/document/exporter/odt/index").then(
|
|
429
|
-
({ODTExporter}: any) => {
|
|
430
|
-
const
|
|
439
|
+
async ({ODTExporter}: any) => {
|
|
440
|
+
const dialog = new TemplateExportDialog()
|
|
441
|
+
const options = await dialog.init("odt")
|
|
442
|
+
if (!options) {
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
const doc = editor.getDoc({
|
|
446
|
+
changes: options.resolveTrackChanges
|
|
447
|
+
? "acceptAllNoInsertions"
|
|
448
|
+
: undefined
|
|
449
|
+
}) as any
|
|
431
450
|
const title = shortFileTitle(
|
|
432
451
|
doc.title,
|
|
433
452
|
doc.path || ""
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,6 @@ import {RequestAccessDialog} from "./documents/access_rights/request_access_dial
|
|
|
45
45
|
import {E2EESnapshotManager} from "./e2ee/snapshot-manager.js"
|
|
46
46
|
import {ExportFidusFile} from "./exporter/native/file.js"
|
|
47
47
|
import {ModFootnotes} from "./footnotes/index.js"
|
|
48
|
-
import {imageEditModel} from "./images/edit_dialog/model.js"
|
|
49
48
|
import {buildEditorKeymap} from "./keymap.js"
|
|
50
49
|
import {
|
|
51
50
|
codeBlockMenuModel,
|
|
@@ -215,7 +214,6 @@ export class Editor {
|
|
|
215
214
|
navigatorFilterModel: navigatorFilterModel(),
|
|
216
215
|
orderedListMenuModel: orderedListMenuModel(),
|
|
217
216
|
selectionMenuModel: selectionMenuModel(),
|
|
218
|
-
imageEditModel: imageEditModel(),
|
|
219
217
|
tableMenuModel: tableMenuModel(),
|
|
220
218
|
figureMenuModel: figureMenuModel(),
|
|
221
219
|
toolbarModel: toolbarModel(),
|
|
@@ -291,7 +289,7 @@ export class Editor {
|
|
|
291
289
|
staticUrl("css/bibliography/bibliography.css"),
|
|
292
290
|
staticUrl("css/editor/dot_menu.css"),
|
|
293
291
|
staticUrl("css/cropper.min.css"),
|
|
294
|
-
staticUrl("css/inline_tools.css"),
|
|
292
|
+
staticUrl("css/fwtoolkit/inline_tools.css"),
|
|
295
293
|
staticUrl("css/editor/e2ee.css")
|
|
296
294
|
])
|
|
297
295
|
new ModDocumentTemplate(this)
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
LanguageDialog,
|
|
15
15
|
PdfExportDialog,
|
|
16
16
|
RevisionDialog,
|
|
17
|
-
HtmlExportDialog
|
|
17
|
+
HtmlExportDialog,
|
|
18
|
+
EpubExportDialog
|
|
18
19
|
} from "../../dialogs/index.js"
|
|
19
20
|
import {E2EEKeyManager} from "fwtoolkit/e2ee/key-manager"
|
|
20
21
|
import {PassphraseManager} from "fwtoolkit/e2ee/passphrase-manager"
|
|
@@ -639,9 +640,20 @@ export const headerbarModel = () => ({
|
|
|
639
640
|
if (!options) {
|
|
640
641
|
return
|
|
641
642
|
}
|
|
642
|
-
const doc = getExportDoc(
|
|
643
|
-
|
|
644
|
-
|
|
643
|
+
const doc = getExportDoc(
|
|
644
|
+
editor,
|
|
645
|
+
options.resolveTrackChanges
|
|
646
|
+
? {changes: "acceptAllNoInsertions"}
|
|
647
|
+
: undefined
|
|
648
|
+
)
|
|
649
|
+
const converterOptions: Record<string, unknown> = {}
|
|
650
|
+
if (options.svgMath) {
|
|
651
|
+
converterOptions.mathOutput = "svg"
|
|
652
|
+
}
|
|
653
|
+
if (!options.resolveTrackChanges) {
|
|
654
|
+
// Keep the marks and render them in the output.
|
|
655
|
+
converterOptions.trackChanges = true
|
|
656
|
+
}
|
|
645
657
|
const exporter = new HTMLExporter(
|
|
646
658
|
doc,
|
|
647
659
|
db.bibDB,
|
|
@@ -649,7 +661,7 @@ export const headerbarModel = () => ({
|
|
|
649
661
|
editor.app.csl,
|
|
650
662
|
editor.docInfo.updated as Date,
|
|
651
663
|
getDocumentTemplate(editor).documentStyles,
|
|
652
|
-
|
|
664
|
+
converterOptions
|
|
653
665
|
)
|
|
654
666
|
exporter.progressCallback = exportProgress(doc)
|
|
655
667
|
exporter.init()
|
|
@@ -732,18 +744,35 @@ export const headerbarModel = () => ({
|
|
|
732
744
|
action: (editor: Editor) => {
|
|
733
745
|
import(
|
|
734
746
|
"@fiduswriter/document/exporter/epub/index"
|
|
735
|
-
).then(({EpubExporter}) => {
|
|
747
|
+
).then(async ({EpubExporter}) => {
|
|
736
748
|
const db = getDB(editor)
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
749
|
+
const dialog = new EpubExportDialog()
|
|
750
|
+
const options = await dialog.init()
|
|
751
|
+
if (!options) {
|
|
752
|
+
return
|
|
753
|
+
}
|
|
754
|
+
const doc = getExportDoc(
|
|
755
|
+
editor,
|
|
756
|
+
options.resolveTrackChanges
|
|
757
|
+
? {changes: "acceptAllNoInsertions"}
|
|
758
|
+
: undefined
|
|
759
|
+
)
|
|
760
|
+
const converterOptions: Record<string, unknown> = {}
|
|
761
|
+
if (options.svgMath) {
|
|
762
|
+
converterOptions.mathOutput = "svg"
|
|
763
|
+
}
|
|
764
|
+
if (!options.resolveTrackChanges) {
|
|
765
|
+
// Keep the marks and render them in the output.
|
|
766
|
+
converterOptions.trackChanges = true
|
|
767
|
+
}
|
|
740
768
|
const exporter = new EpubExporter(
|
|
741
769
|
doc,
|
|
742
770
|
db.bibDB,
|
|
743
771
|
db.imageDB,
|
|
744
772
|
editor.app.csl,
|
|
745
773
|
editor.docInfo.updated as Date,
|
|
746
|
-
getDocumentTemplate(editor).documentStyles
|
|
774
|
+
getDocumentTemplate(editor).documentStyles,
|
|
775
|
+
converterOptions
|
|
747
776
|
)
|
|
748
777
|
exporter.progressCallback = exportProgress(doc)
|
|
749
778
|
exporter.init()
|
|
@@ -3,7 +3,7 @@ import fixUTF8 from "fix-utf8"
|
|
|
3
3
|
import {Fragment, Slice} from "prosemirror-model"
|
|
4
4
|
import {Plugin, PluginKey, TextSelection} from "prosemirror-state"
|
|
5
5
|
import {ReplaceStep} from "prosemirror-transform"
|
|
6
|
-
import type {Node} from "prosemirror-model"
|
|
6
|
+
import type {Node, ResolvedPos} from "prosemirror-model"
|
|
7
7
|
import type {EditorState, Transaction} from "prosemirror-state"
|
|
8
8
|
import type {EditorView} from "prosemirror-view"
|
|
9
9
|
|
|
@@ -24,6 +24,70 @@ export const resetPasteRange = (tr: Transaction): void => {
|
|
|
24
24
|
tr.setMeta(key, {pasteRange: null})
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Document section nodes. When content is copied from within the same editor,
|
|
28
|
+
// prosemirror-view records these in the data-pm-slice attribute of the
|
|
29
|
+
// clipboard HTML and wraps the pasted content in them again while parsing it.
|
|
30
|
+
const sectionNodeTypes = new Set([
|
|
31
|
+
"richtext_part",
|
|
32
|
+
"heading_part",
|
|
33
|
+
"table_part",
|
|
34
|
+
"contributors_part",
|
|
35
|
+
"tags_part",
|
|
36
|
+
"table_of_contents",
|
|
37
|
+
"separator_part"
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
// Remove a section node wrapper from pasted content when it corresponds to an
|
|
41
|
+
// ancestor node at the paste position. Content copied within the same document
|
|
42
|
+
// arrives wrapped in such a wrapper. If it was not removed, the wrapper would
|
|
43
|
+
// be treated like foreign content and be flattened during filtering below,
|
|
44
|
+
// losing the structure and formatting of the copied content.
|
|
45
|
+
const unwrapSectionWrapper = (slice: Slice, $pos: ResolvedPos): Slice => {
|
|
46
|
+
let {content, openStart, openEnd} = slice
|
|
47
|
+
// The outermost wrapper node corresponds to this ancestor of the position.
|
|
48
|
+
let wrapperDepth = $pos.depth - openStart + 1
|
|
49
|
+
while (
|
|
50
|
+
openStart > 0 &&
|
|
51
|
+
openEnd > 0 &&
|
|
52
|
+
wrapperDepth >= 1 &&
|
|
53
|
+
wrapperDepth <= $pos.depth &&
|
|
54
|
+
content.childCount === 1 &&
|
|
55
|
+
sectionNodeTypes.has(content.firstChild!.type.name) &&
|
|
56
|
+
content.firstChild!.type.name === $pos.node(wrapperDepth).type.name
|
|
57
|
+
) {
|
|
58
|
+
content = content.firstChild!.content
|
|
59
|
+
openStart -= 1
|
|
60
|
+
openEnd -= 1
|
|
61
|
+
wrapperDepth += 1
|
|
62
|
+
}
|
|
63
|
+
return new Slice(content, openStart, openEnd)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The number of node levels that are actually traversable openly at one side
|
|
67
|
+
// of a fragment, limited to maxDepth. Filtering removes nodes from slices,
|
|
68
|
+
// which can make their openStart/openEnd exceed the nesting depth of the
|
|
69
|
+
// remaining content. Slices violating that constraint crash ProseMirror's
|
|
70
|
+
// Selection.replace (it walks into non-existing child nodes when calculating
|
|
71
|
+
// the insertion end position), so the open depths have to be reduced to fit.
|
|
72
|
+
const openChainDepth = (
|
|
73
|
+
fragment: Fragment,
|
|
74
|
+
maxDepth: number,
|
|
75
|
+
side: "start" | "end"
|
|
76
|
+
): number => {
|
|
77
|
+
let depth = 0
|
|
78
|
+
let node = side === "start" ? fragment.firstChild : fragment.lastChild
|
|
79
|
+
while (
|
|
80
|
+
node &&
|
|
81
|
+
!node.isLeaf &&
|
|
82
|
+
!node.type.spec.isolating &&
|
|
83
|
+
depth < maxDepth
|
|
84
|
+
) {
|
|
85
|
+
depth += 1
|
|
86
|
+
node = side === "start" ? node.firstChild : node.lastChild
|
|
87
|
+
}
|
|
88
|
+
return depth
|
|
89
|
+
}
|
|
90
|
+
|
|
27
91
|
// Filter a fragment to only include allowed nodes and marks
|
|
28
92
|
// parentNode is optional - used to check if converted nodes are valid children
|
|
29
93
|
const filterFragment = (
|
|
@@ -271,6 +335,14 @@ export const clipboardPlugin = (options: {editor: Editor; viewType: string}) =>
|
|
|
271
335
|
return slice
|
|
272
336
|
}
|
|
273
337
|
|
|
338
|
+
const $pos = view.state.selection.$from
|
|
339
|
+
|
|
340
|
+
// Content copied within the same editor arrives wrapped in a
|
|
341
|
+
// section node corresponding to an ancestor of the paste
|
|
342
|
+
// position. Unwrap it so that it is handled like other pasted
|
|
343
|
+
// content rather than being flattened during filtering.
|
|
344
|
+
slice = unwrapSectionWrapper(slice, $pos)
|
|
345
|
+
|
|
274
346
|
// Get allowed elements and marks at the current selection position
|
|
275
347
|
const {elements, marks} = getAllowedElementsAndMarks(view.state)
|
|
276
348
|
|
|
@@ -280,7 +352,6 @@ export const clipboardPlugin = (options: {editor: Editor; viewType: string}) =>
|
|
|
280
352
|
}
|
|
281
353
|
|
|
282
354
|
// Get the parent node at the selection position to check compatibility
|
|
283
|
-
const $pos = view.state.selection.$from
|
|
284
355
|
const parentNode = $pos.parent
|
|
285
356
|
|
|
286
357
|
// Filter the slice content
|
|
@@ -292,11 +363,24 @@ export const clipboardPlugin = (options: {editor: Editor; viewType: string}) =>
|
|
|
292
363
|
parentNode
|
|
293
364
|
)
|
|
294
365
|
|
|
295
|
-
//
|
|
366
|
+
// All content may have been removed by the filtering.
|
|
367
|
+
if (!filteredContent.size) {
|
|
368
|
+
return Slice.empty
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Return a new slice with the filtered content. Reduce the
|
|
372
|
+
// open depths to what the filtered content can actually
|
|
373
|
+
// provide, so that the slice remains insertable.
|
|
296
374
|
return new Slice(
|
|
297
375
|
filteredContent,
|
|
298
|
-
|
|
299
|
-
|
|
376
|
+
Math.min(
|
|
377
|
+
slice.openStart,
|
|
378
|
+
openChainDepth(filteredContent, slice.openStart, "start")
|
|
379
|
+
),
|
|
380
|
+
Math.min(
|
|
381
|
+
slice.openEnd,
|
|
382
|
+
openChainDepth(filteredContent, slice.openEnd, "end")
|
|
383
|
+
)
|
|
300
384
|
)
|
|
301
385
|
}
|
|
302
386
|
}
|
package/src/track/accept.ts
CHANGED
|
@@ -19,7 +19,12 @@ export const accept = (
|
|
|
19
19
|
return
|
|
20
20
|
}
|
|
21
21
|
const trackMark = nodeAtPos.marks.find(mark => mark.type.name === type)
|
|
22
|
-
|
|
22
|
+
// Block level track changes are stored in the node's track attribute
|
|
23
|
+
// rather than in marks (figures, tables, text blocks).
|
|
24
|
+
const blockTrack = (
|
|
25
|
+
nodeAtPos.attrs.track as Array<{type: string}> | undefined
|
|
26
|
+
)?.find(track => track.type === type)
|
|
27
|
+
if (!trackMark && !blockTrack) {
|
|
23
28
|
return
|
|
24
29
|
}
|
|
25
30
|
view.state.doc.nodesBetween(
|
|
@@ -34,13 +39,22 @@ export const accept = (
|
|
|
34
39
|
}
|
|
35
40
|
if (!node.isInline) {
|
|
36
41
|
reachedEnd = true
|
|
37
|
-
} else if (
|
|
42
|
+
} else if (
|
|
43
|
+
trackMark &&
|
|
44
|
+
!trackMark.isInSet(node.marks) &&
|
|
45
|
+
!(
|
|
46
|
+
node.attrs.track as Array<{type: string}> | undefined
|
|
47
|
+
)?.some(track => track.type === type)
|
|
48
|
+
) {
|
|
38
49
|
reachedEnd = true
|
|
39
50
|
return false
|
|
40
51
|
}
|
|
41
|
-
// Traverse only those nodes which have the track marks
|
|
52
|
+
// Traverse only those nodes which have the track marks or
|
|
53
|
+
// corresponding block level track entries.
|
|
42
54
|
if (
|
|
43
|
-
|
|
55
|
+
(node.attrs.track as Array<{type: string}> | undefined)?.some(
|
|
56
|
+
track => track.type === type
|
|
57
|
+
) ||
|
|
44
58
|
(trackMark && trackMark.isInSet(node.marks))
|
|
45
59
|
) {
|
|
46
60
|
if (type === "deletion") {
|
|
@@ -68,6 +82,11 @@ export const accept = (
|
|
|
68
82
|
reachedEnd = false
|
|
69
83
|
}
|
|
70
84
|
} else {
|
|
85
|
+
// The gate above guarantees the insertion mark is set
|
|
86
|
+
// on nodes without a track attribute.
|
|
87
|
+
if (!trackMark) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
71
90
|
tr.step(
|
|
72
91
|
new AddMarkStep(
|
|
73
92
|
map.map(nodePos),
|
|
@@ -81,6 +100,12 @@ export const accept = (
|
|
|
81
100
|
)
|
|
82
101
|
}
|
|
83
102
|
} else if (type === "format_change") {
|
|
103
|
+
// format_change only exists as a mark, so trackMark is
|
|
104
|
+
// guaranteed to be set here (the guard above would have
|
|
105
|
+
// returned otherwise).
|
|
106
|
+
if (!trackMark) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
84
109
|
tr.step(
|
|
85
110
|
new RemoveMarkStep(
|
|
86
111
|
map.map(nodePos),
|