feat: Add HTML export option (#4056)
* tidy * Add title to HTML export * fix: Add compatability for documents without collab state * Add HTML download option to UI * docs * fix nodes that required document to render * Refactor to allow for styling of HTML export * div>article for easier programatic content extraction
This commit is contained in:
@@ -179,12 +179,12 @@ export const unsubscribeDocument = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadDocument = createAction({
|
||||
name: ({ t, isContextMenu }) =>
|
||||
isContextMenu ? t("Download") : t("Download document"),
|
||||
export const downloadDocumentAsHTML = createAction({
|
||||
name: ({ t }) => t("HTML"),
|
||||
section: DocumentSection,
|
||||
keywords: "html export",
|
||||
icon: <DownloadIcon />,
|
||||
keywords: "export",
|
||||
iconInContextMenu: false,
|
||||
visible: ({ activeDocumentId, stores }) =>
|
||||
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
|
||||
perform: ({ activeDocumentId, stores }) => {
|
||||
@@ -193,10 +193,37 @@ export const downloadDocument = createAction({
|
||||
}
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
document?.download();
|
||||
document?.download("text/html");
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadDocumentAsMarkdown = createAction({
|
||||
name: ({ t }) => t("Markdown"),
|
||||
section: DocumentSection,
|
||||
keywords: "md markdown export",
|
||||
icon: <DownloadIcon />,
|
||||
iconInContextMenu: false,
|
||||
visible: ({ activeDocumentId, stores }) =>
|
||||
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
|
||||
perform: ({ activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
document?.download("text/markdown");
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadDocument = createAction({
|
||||
name: ({ t, isContextMenu }) =>
|
||||
isContextMenu ? t("Download") : t("Download document"),
|
||||
section: DocumentSection,
|
||||
icon: <DownloadIcon />,
|
||||
keywords: "export",
|
||||
children: [downloadDocumentAsHTML, downloadDocumentAsMarkdown],
|
||||
});
|
||||
|
||||
export const duplicateDocument = createAction({
|
||||
name: ({ t, isContextMenu }) =>
|
||||
isContextMenu ? t("Duplicate") : t("Duplicate document"),
|
||||
|
||||
@@ -2,10 +2,10 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import { breakpoints } from "@shared/styles";
|
||||
import GlobalStyles from "@shared/styles/globals";
|
||||
import { dark, light, lightMobile, darkMobile } from "@shared/styles/theme";
|
||||
import useMediaQuery from "~/hooks/useMediaQuery";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import GlobalStyles from "~/styles/globals";
|
||||
|
||||
const Theme: React.FC = ({ children }) => {
|
||||
const { ui } = useStores();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import { EditorState, Selection, Plugin, Transaction } from "prosemirror-state";
|
||||
import { Decoration, EditorView } from "prosemirror-view";
|
||||
import * as React from "react";
|
||||
import { DefaultTheme, ThemeProps } from "styled-components";
|
||||
import EditorContainer from "@shared/editor/components/Styles";
|
||||
import { EmbedDescriptor } from "@shared/editor/embeds";
|
||||
import Extension, { CommandFactory } from "@shared/editor/lib/Extension";
|
||||
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
|
||||
@@ -40,7 +41,6 @@ import EmojiMenu from "./components/EmojiMenu";
|
||||
import { SearchResult } from "./components/LinkEditor";
|
||||
import LinkToolbar from "./components/LinkToolbar";
|
||||
import SelectionToolbar from "./components/SelectionToolbar";
|
||||
import EditorContainer from "./components/Styles";
|
||||
import WithTheme from "./components/WithTheme";
|
||||
|
||||
export { default as Extension } from "@shared/editor/lib/Extension";
|
||||
|
||||
@@ -296,6 +296,7 @@ function DocumentMenu({
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
actionToMenuItem(downloadDocument, context),
|
||||
{
|
||||
type: "route",
|
||||
title: t("History"),
|
||||
@@ -305,7 +306,6 @@ function DocumentMenu({
|
||||
visible: canViewHistory,
|
||||
icon: <HistoryIcon />,
|
||||
},
|
||||
actionToMenuItem(downloadDocument, context),
|
||||
{
|
||||
type: "button",
|
||||
title: t("Print"),
|
||||
|
||||
@@ -2,10 +2,10 @@ import { addDays, differenceInDays } from "date-fns";
|
||||
import { floor } from "lodash";
|
||||
import { action, autorun, computed, observable, set } from "mobx";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import unescape from "@shared/utils/unescape";
|
||||
import DocumentsStore from "~/stores/DocumentsStore";
|
||||
import User from "~/models/User";
|
||||
import type { NavigationNode } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import Storage from "~/utils/Storage";
|
||||
import ParanoidModel from "./ParanoidModel";
|
||||
import View from "./View";
|
||||
@@ -419,21 +419,18 @@ export default class Document extends ParanoidModel {
|
||||
};
|
||||
}
|
||||
|
||||
download = async () => {
|
||||
// Ensure the document is upto date with latest server contents
|
||||
await this.fetch();
|
||||
const body = unescape(this.text);
|
||||
const blob = new Blob([`# ${this.title}\n\n${body}`], {
|
||||
type: "text/markdown",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
// Firefox support requires the anchor tag be in the DOM to trigger the dl
|
||||
if (document.body) {
|
||||
document.body.appendChild(a);
|
||||
}
|
||||
a.href = url;
|
||||
a.download = `${this.titleWithDefault}.md`;
|
||||
a.click();
|
||||
download = async (contentType: "text/html" | "text/markdown") => {
|
||||
await client.post(
|
||||
`/documents.export`,
|
||||
{
|
||||
id: this.id,
|
||||
},
|
||||
{
|
||||
download: true,
|
||||
headers: {
|
||||
accept: contentType,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import styledNormalize from "styled-normalize";
|
||||
import { breakpoints, depths } from "@shared/styles";
|
||||
|
||||
export default createGlobalStyle`
|
||||
${styledNormalize}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
body,
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: ${(props) => props.theme.text};
|
||||
overscroll-behavior-y: none;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
@media (min-width: ${breakpoints.tablet}px) {
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ${breakpoints.tablet}px) and (display-mode: standalone) {
|
||||
body:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.titleBarDivider};
|
||||
z-index: ${depths.titleBarDivider};
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${(props) => props.theme.link};
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
h1 { font-size: 2.25em; }
|
||||
h2 { font-size: 1.5em; }
|
||||
h3 { font-size: 1.25em; }
|
||||
h4 { font-size: 1em; }
|
||||
h5 { font-size: 0.875em; }
|
||||
h6 { font-size: 0.75em; }
|
||||
|
||||
p,
|
||||
dl,
|
||||
ol,
|
||||
ul,
|
||||
pre,
|
||||
blockquote {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid ${(props) => props.theme.divider};
|
||||
}
|
||||
|
||||
.js-focus-visible :focus:not(.focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.js-focus-visible .focus-visible {
|
||||
outline-color: ${(props) => props.theme.primary};
|
||||
outline-offset: -1px;
|
||||
}
|
||||
`;
|
||||
@@ -25,6 +25,7 @@ type Options = {
|
||||
|
||||
type FetchOptions = {
|
||||
download?: boolean;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
const fetchWithRetry = retry(fetch);
|
||||
@@ -81,6 +82,7 @@ class ApiClient {
|
||||
"cache-control": "no-cache",
|
||||
"x-editor-version": EDITOR_VERSION,
|
||||
pragma: "no-cache",
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
// for multipart forms or other non JSON requests fetch
|
||||
|
||||
@@ -67,7 +67,6 @@ export default function download(
|
||||
if ("download" in a) {
|
||||
a.href = url;
|
||||
a.setAttribute("download", fn);
|
||||
a.innerHTML = "downloading…";
|
||||
D.body && D.body.appendChild(a);
|
||||
setTimeout(function () {
|
||||
a.click();
|
||||
|
||||
Reference in New Issue
Block a user