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:
Tom Moor
2022-09-07 13:34:39 +02:00
committed by GitHub
parent eb5126335c
commit e8a6de3f18
30 changed files with 1756 additions and 1790 deletions

View File

@@ -0,0 +1,8 @@
import styled from "styled-components";
import style, { Props } from "../../styles/editor";
const EditorContainer = styled.div<Props>`
${style};
`;
export default EditorContainer;

View File

@@ -33,12 +33,16 @@ export default class CheckboxItem extends Node {
},
],
toDOM: (node) => {
const input = document.createElement("span");
input.tabIndex = -1;
input.className = "checkbox";
input.setAttribute("aria-checked", node.attrs.checked.toString());
input.setAttribute("role", "checkbox");
input.addEventListener("click", this.handleClick);
const checked = node.attrs.checked.toString();
let input;
if (typeof document !== "undefined") {
input = document.createElement("span");
input.tabIndex = -1;
input.className = "checkbox";
input.setAttribute("aria-checked", checked);
input.setAttribute("role", "checkbox");
input.addEventListener("click", this.handleClick);
}
return [
"li",
@@ -51,7 +55,9 @@ export default class CheckboxItem extends Node {
{
contentEditable: "false",
},
input,
...(input
? [input]
: [["span", { class: "checkbox", "aria-checked": checked }]]),
],
["div", 0],
];

View File

@@ -122,44 +122,53 @@ export default class CodeFence extends Node {
},
],
toDOM: (node) => {
const button = document.createElement("button");
button.innerText = this.options.dictionary.copy;
button.type = "button";
button.addEventListener("click", this.handleCopyToClipboard);
let actions;
if (typeof document !== "undefined") {
const button = document.createElement("button");
button.innerText = this.options.dictionary.copy;
button.type = "button";
button.addEventListener("click", this.handleCopyToClipboard);
const select = document.createElement("select");
select.addEventListener("change", this.handleLanguageChange);
const select = document.createElement("select");
select.addEventListener("change", this.handleLanguageChange);
const actions = document.createElement("div");
actions.className = "code-actions";
actions.appendChild(select);
actions.appendChild(button);
actions = document.createElement("div");
actions.className = "code-actions";
actions.appendChild(select);
actions.appendChild(button);
this.languageOptions.forEach(([key, label]) => {
const option = document.createElement("option");
const value = key === "none" ? "" : key;
option.value = value;
option.innerText = label;
option.selected = node.attrs.language === value;
select.appendChild(option);
});
this.languageOptions.forEach(([key, label]) => {
const option = document.createElement("option");
const value = key === "none" ? "" : key;
option.value = value;
option.innerText = label;
option.selected = node.attrs.language === value;
select.appendChild(option);
});
// For the Mermaid language we add an extra button to toggle between
// source code and a rendered diagram view.
if (node.attrs.language === "mermaidjs") {
const showSourceButton = document.createElement("button");
showSourceButton.innerText = this.options.dictionary.showSource;
showSourceButton.type = "button";
showSourceButton.classList.add("show-source-button");
showSourceButton.addEventListener("click", this.handleToggleDiagram);
actions.prepend(showSourceButton);
// For the Mermaid language we add an extra button to toggle between
// source code and a rendered diagram view.
if (node.attrs.language === "mermaidjs") {
const showSourceButton = document.createElement("button");
showSourceButton.innerText = this.options.dictionary.showSource;
showSourceButton.type = "button";
showSourceButton.classList.add("show-source-button");
showSourceButton.addEventListener(
"click",
this.handleToggleDiagram
);
actions.prepend(showSourceButton);
const showDiagramButton = document.createElement("button");
showDiagramButton.innerText = this.options.dictionary.showDiagram;
showDiagramButton.type = "button";
showDiagramButton.classList.add("show-digram-button");
showDiagramButton.addEventListener("click", this.handleToggleDiagram);
actions.prepend(showDiagramButton);
const showDiagramButton = document.createElement("button");
showDiagramButton.innerText = this.options.dictionary.showDiagram;
showDiagramButton.type = "button";
showDiagramButton.classList.add("show-digram-button");
showDiagramButton.addEventListener(
"click",
this.handleToggleDiagram
);
actions.prepend(showDiagramButton);
}
}
return [
@@ -168,7 +177,7 @@ export default class CodeFence extends Node {
class: "code-block",
"data-language": node.attrs.language,
},
["div", { contentEditable: "false" }, actions],
...(actions ? [["div", { contentEditable: "false" }, actions]] : []),
["pre", ["code", { spellCheck: "false" }, 0]],
];
},

View File

@@ -50,23 +50,28 @@ export default class Heading extends Node {
contentElement: ".heading-content",
})),
toDOM: (node) => {
const anchor = document.createElement("button");
anchor.innerText = "#";
anchor.type = "button";
anchor.className = "heading-anchor";
anchor.addEventListener("click", (event) => this.handleCopyLink(event));
let anchor, fold;
if (typeof document !== "undefined") {
anchor = document.createElement("button");
anchor.innerText = "#";
anchor.type = "button";
anchor.className = "heading-anchor";
anchor.addEventListener("click", (event) =>
this.handleCopyLink(event)
);
const fold = document.createElement("button");
fold.innerText = "";
fold.innerHTML =
'<svg fill="currentColor" width="12" height="24" viewBox="6 0 12 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.23823905,10.6097108 L11.207376,14.4695888 L11.207376,14.4695888 C11.54411,14.907343 12.1719566,14.989236 12.6097108,14.652502 C12.6783439,14.5997073 12.7398293,14.538222 12.792624,14.4695888 L15.761761,10.6097108 L15.761761,10.6097108 C16.0984949,10.1719566 16.0166019,9.54410997 15.5788477,9.20737601 C15.4040391,9.07290785 15.1896811,9 14.969137,9 L9.03086304,9 L9.03086304,9 C8.47857829,9 8.03086304,9.44771525 8.03086304,10 C8.03086304,10.2205442 8.10377089,10.4349022 8.23823905,10.6097108 Z" /></svg>';
fold.type = "button";
fold.className = `heading-fold ${
node.attrs.collapsed ? "collapsed" : ""
}`;
fold.addEventListener("mousedown", (event) =>
this.handleFoldContent(event)
);
fold = document.createElement("button");
fold.innerText = "";
fold.innerHTML =
'<svg fill="currentColor" width="12" height="24" viewBox="6 0 12 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.23823905,10.6097108 L11.207376,14.4695888 L11.207376,14.4695888 C11.54411,14.907343 12.1719566,14.989236 12.6097108,14.652502 C12.6783439,14.5997073 12.7398293,14.538222 12.792624,14.4695888 L15.761761,10.6097108 L15.761761,10.6097108 C16.0984949,10.1719566 16.0166019,9.54410997 15.5788477,9.20737601 C15.4040391,9.07290785 15.1896811,9 14.969137,9 L9.03086304,9 L9.03086304,9 C8.47857829,9 8.03086304,9.44771525 8.03086304,10 C8.03086304,10.2205442 8.10377089,10.4349022 8.23823905,10.6097108 Z" /></svg>';
fold.type = "button";
fold.className = `heading-fold ${
node.attrs.collapsed ? "collapsed" : ""
}`;
fold.addEventListener("mousedown", (event) =>
this.handleFoldContent(event)
);
}
return [
`h${node.attrs.level + (this.options.offset || 0)}`,
@@ -78,8 +83,7 @@ export default class Heading extends Node {
node.attrs.collapsed ? "collapsed" : ""
}`,
},
anchor,
fold,
...(anchor ? [anchor, fold] : []),
],
[
"span",

View File

@@ -52,40 +52,43 @@ export default class Notice extends Node {
},
],
toDOM: (node) => {
const select = document.createElement("select");
select.addEventListener("change", this.handleStyleChange);
let icon, actions;
if (typeof document !== "undefined") {
const select = document.createElement("select");
select.addEventListener("change", this.handleStyleChange);
this.styleOptions.forEach(([key, label]) => {
const option = document.createElement("option");
option.value = key;
option.innerText = label;
option.selected = node.attrs.style === key;
select.appendChild(option);
});
this.styleOptions.forEach(([key, label]) => {
const option = document.createElement("option");
option.value = key;
option.innerText = label;
option.selected = node.attrs.style === key;
select.appendChild(option);
});
const actions = document.createElement("div");
actions.className = "notice-actions";
actions.appendChild(select);
actions = document.createElement("div");
actions.className = "notice-actions";
actions.appendChild(select);
let component;
let component;
if (node.attrs.style === "tip") {
component = <StarredIcon color="currentColor" />;
} else if (node.attrs.style === "warning") {
component = <WarningIcon color="currentColor" />;
} else {
component = <InfoIcon color="currentColor" />;
if (node.attrs.style === "tip") {
component = <StarredIcon color="currentColor" />;
} else if (node.attrs.style === "warning") {
component = <WarningIcon color="currentColor" />;
} else {
component = <InfoIcon color="currentColor" />;
}
icon = document.createElement("div");
icon.className = "icon";
ReactDOM.render(component, icon);
}
const icon = document.createElement("div");
icon.className = "icon";
ReactDOM.render(component, icon);
return [
"div",
{ class: `notice-block ${node.attrs.style}` },
icon,
["div", { contentEditable: "false" }, actions],
...(icon ? [icon] : []),
["div", { contentEditable: "false" }, ...(actions ? [actions] : [])],
["div", { class: "content" }, 0],
];
},