This commit is contained in:
Martin Kleinschrodt 2022-07-16 19:28:39 +02:00
parent 9c5c09df81
commit 8f64945bdd
9 changed files with 3706 additions and 5858 deletions

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@
"@simplewebauthn/typescript-types": "4.0.0",
"@tiptap/core": "2.0.0-beta.182",
"@tiptap/starter-kit": "2.0.0-beta.191",
"@types/autosize": "4.0.1",
"@types/dompurify": "2.3.1",
"@types/marked": "4.0.3",
"@types/papaparse": "5.2.5",
@ -37,7 +38,7 @@
"@types/workbox-window": "4.3.3",
"@types/zxcvbn": "4.4.1",
"@webcomponents/webcomponentsjs": "2.5.0",
"autosize": "5.0.0",
"autosize": "5.0.1",
"date-fns": "2.22.1",
"dompurify": "2.3.3",
"event-target-shim": "6.0.2",

View File

@ -200,6 +200,7 @@ export abstract class BaseInput extends LitElement {
.input-container {
display: flex;
align-self: stretch;
}
.input-element {
@ -260,7 +261,7 @@ export abstract class BaseInput extends LitElement {
const { focused, value, placeholder } = this;
return html`
${this._renderAbove()}
<div class="horizontal center-aligning layout">
<div class="horizontal center-aligning layout fill-vertically">
${this._renderBefore()}
<div class="input-container stretch">

View File

@ -70,7 +70,7 @@ export class FieldElement extends LitElement {
if (this.field.type === FieldType.Note) {
actions.push({
icon: "expand",
label: "Fullscreen",
label: $l("Expand"),
action: () => this._editNoteFullscreen(),
});
} else {

View File

@ -645,6 +645,10 @@ export class PlIcon extends LitElement {
:host([icon="collapse"]) > div::before {
content: "\\f326";
}
:host([icon="markdown"]) > div::before {
content: "\\f354";
}
`,
];

View File

@ -1,4 +1,4 @@
import { VaultItem, Field, Tag, AuditType } from "@padloc/core/src/item";
import { VaultItem, Field, Tag, AuditType, FieldType } from "@padloc/core/src/item";
import { Vault, VaultID } from "@padloc/core/src/vault";
import { translate as $l } from "@padloc/locale/src/translate";
import { debounce, wait, escapeRegex, truncate } from "@padloc/core/src/util";
@ -22,6 +22,8 @@ import { cache } from "lit/directives/cache.js";
import { Button } from "./button";
import "./item-icon";
import { iconForAudit, noItemsTextForAudit, titleTextForAudit } from "../lib/audit";
import { singleton } from "../lib/singleton";
import { NoteDialog } from "./note-dialog";
export interface ListItem {
item: VaultItem;
@ -87,6 +89,9 @@ export class VaultItemListItem extends LitElement {
@query(".move-right-button")
private _moveRightButton: Button;
@singleton("pl-note-dialog")
private _noteDialog: NoteDialog;
updated() {
this._scroll();
}
@ -119,6 +124,16 @@ export class VaultItemListItem extends LitElement {
e.stopPropagation();
const field = item.fields[index];
if (field.type === FieldType.Note) {
const value = await this._noteDialog.show(field.value);
if (value !== field.value) {
field.value = value;
await app.updateItem(item, { fields: item.fields });
}
return;
}
setClipboard(await field.transform(), `${item.name} / ${field.name}`);
const fieldEl = e.target as HTMLElement;
fieldEl.classList.add("copied");

View File

@ -1,4 +1,4 @@
import { css, customElement, html, LitElement, property } from "lit-element";
import { css, customElement, html, LitElement, property, query } from "lit-element";
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import { shared, content } from "../styles";
@ -7,29 +7,47 @@ import "./icon";
import "./list";
import "./popover";
import { htmlToMarkdown, markdownToHtml } from "../lib/markdown";
import { $l } from "@padloc/locale/src/translate";
import "./textarea";
import { Textarea } from "./textarea";
import "./select";
@customElement("pl-rich-input")
export class RichInput extends LitElement {
get value() {
const html = this._editor.getHTML();
const md = htmlToMarkdown(html);
return md;
return this._markdownInput?.value || "";
}
set value(content: string) {
const html = markdownToHtml(content).replace(/\n/g, "");
this._editor.commands.clearContent();
this._editor.commands.insertContent(html);
(async () => {
await this.updateComplete;
console.log("markdowninput", this._markdownInput);
this._markdownInput.value = content;
})();
}
@property()
mode: "wysiwyg" | "markdown" = "wysiwyg";
@property({ type: Boolean })
isFullscreen = false;
@query("pl-textarea")
_markdownInput: Textarea;
private _editor = new Editor({
extensions: [StarterKit],
onTransaction: () => {
this.requestUpdate();
this.dispatchEvent(new CustomEvent("input"));
if (this.mode === "wysiwyg") {
if (this._markdownInput) {
this._markdownInput.value = htmlToMarkdown(this._editor.getHTML());
}
this.dispatchEvent(new CustomEvent("input"));
this.requestUpdate();
}
},
onFocus: () => this.classList.add("focused"),
onBlur: () => this.classList.remove("focused"),
@ -44,6 +62,22 @@ export class RichInput extends LitElement {
this._editor.commands.focus();
}
private async _toggleMarkdown() {
if (this.mode === "markdown") {
this.mode = "wysiwyg";
await this.updateComplete;
const html = markdownToHtml(this._markdownInput.value).replace(/\n/g, "");
this._editor.commands.clearContent();
this._editor.commands.insertContent(html);
this._editor.commands.focus();
} else {
this.mode = "markdown";
await this.updateComplete;
this._markdownInput.updated();
this._markdownInput.focus();
}
}
static styles = [
shared,
content,
@ -60,124 +94,170 @@ export class RichInput extends LitElement {
:host(.focused) {
border-color: var(--color-highlight);
}
pl-textarea {
border: none;
--input-padding: calc(2 * var(--spacing));
font-family: var(--font-family-mono);
font-size: 0.9em;
}
`,
];
render() {
return html`
<div class="small padded half-spacing wrapping horizontal layout border-bottom">
<pl-button class="transparent slim">
${this._editor?.isActive("heading", { level: 1 })
? html` <pl-icon icon="heading-1"></pl-icon> `
: this._editor?.isActive("heading", { level: 2 })
? html` <pl-icon icon="heading-2"></pl-icon> `
: this._editor?.isActive("heading", { level: 3 })
? html` <pl-icon icon="heading-3"></pl-icon> `
: html` <pl-icon icon="text"></pl-icon> `}
<div class="small padded double-spacing horizontal layout border-bottom">
<div class="half-spacing wrapping horizontal layout stretch">
<pl-button class="transparent slim" title="${$l("Text Mode")}">
${this._editor?.isActive("heading", { level: 1 })
? html` <pl-icon icon="heading-1"></pl-icon> `
: this._editor?.isActive("heading", { level: 2 })
? html` <pl-icon icon="heading-2"></pl-icon> `
: this._editor?.isActive("heading", { level: 3 })
? html` <pl-icon icon="heading-3"></pl-icon> `
: html` <pl-icon icon="text"></pl-icon> `}
<pl-icon class="small" icon="dropdown"></pl-icon>
</pl-button>
<pl-icon class="small" icon="dropdown"></pl-icon>
</pl-button>
<pl-popover hide-on-click>
<pl-list>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setHeading({ level: 1 }).run()}
>
<pl-icon icon="heading-1"></pl-icon>
</div>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setHeading({ level: 2 }).run()}
>
<pl-icon icon="heading-2"></pl-icon>
</div>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setHeading({ level: 3 }).run()}
>
<pl-icon icon="heading-3"></pl-icon>
</div>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setParagraph().run()}
>
<pl-icon icon="text"></pl-icon>
</div>
</pl-list>
</pl-popover>
<pl-popover hide-on-click>
<pl-list>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setHeading({ level: 1 }).run()}
>
<pl-icon icon="heading-1"></pl-icon>
</div>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setHeading({ level: 2 }).run()}
>
<pl-icon icon="heading-2"></pl-icon>
</div>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setHeading({ level: 3 }).run()}
>
<pl-icon icon="heading-3"></pl-icon>
</div>
<div
class="small double-padded centering horizontal layout list-item hover click"
@click=${() => this._editor.chain().focus().setParagraph().run()}
>
<pl-icon icon="text"></pl-icon>
</div>
</pl-list>
</pl-popover>
<div class="border-left"></div>
<div class="border-left"></div>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("bold")}
@click=${() => this._editor.chain().focus().toggleBold().run()}
>
<pl-icon icon="bold"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("bold")}
@click=${() => this._editor.chain().focus().toggleBold().run()}
title="${$l("Bold")}"
>
<pl-icon icon="bold"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("italic")}
@click=${() => this._editor.chain().focus().toggleItalic().run()}
>
<pl-icon icon="italic"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("italic")}
@click=${() => this._editor.chain().focus().toggleItalic().run()}
title="${$l("Italic")}"
>
<pl-icon icon="italic"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("strike")}
@click=${() => this._editor.chain().focus().toggleStrike().run()}
>
<pl-icon icon="strikethrough"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("strike")}
@click=${() => this._editor.chain().focus().toggleStrike().run()}
title="${$l("Strikethrough")}"
>
<pl-icon icon="strikethrough"></pl-icon>
</pl-button>
<div class="border-left"></div>
<div class="border-left"></div>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("bulletList")}
@click=${() => this._editor.chain().focus().toggleBulletList().run()}
>
<pl-icon icon="list"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("bulletList")}
@click=${() => this._editor.chain().focus().toggleBulletList().run()}
title="${$l("Unordered List")}"
>
<pl-icon icon="list"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("orderedList")}
@click=${() => this._editor.chain().focus().toggleOrderedList().run()}
>
<pl-icon icon="list-ol"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("orderedList")}
@click=${() => this._editor.chain().focus().toggleOrderedList().run()}
title="${$l("Ordered List")}"
>
<pl-icon icon="list-ol"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("blockquote")}
@click=${() => this._editor.chain().focus().toggleBlockquote().run()}
>
<pl-icon icon="blockquote"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
.toggled=${this._editor?.isActive("blockquote")}
@click=${() => this._editor.chain().focus().toggleBlockquote().run()}
title="${$l("Blockquote")}"
>
<pl-icon icon="blockquote"></pl-icon>
</pl-button>
<div class="border-left"></div>
<div class="border-left"></div>
<pl-button
class="transparent slim"
@click=${() => this._editor.chain().focus().setHorizontalRule().run()}
>
<pl-icon icon="line"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
@click=${() => this._editor.chain().focus().setHorizontalRule().run()}
title="${$l("Insert Horizontal Line")}"
>
<pl-icon icon="line"></pl-icon>
</pl-button>
</div>
<div class="half-spacing left-padded horizontal layout border-left">
<pl-select
class="slim"
.value=${this.mode as any}
.options=${[
{
label: "WYSIWYG",
value: "wysiwyg",
},
{
label: "Markdown",
value: "markdown",
},
]}
hidden
></pl-select>
<div class="stretch"></div>
<pl-button
class="transparent slim"
style="line-height: 1.2em"
@click=${() => this._toggleMarkdown()}
.toggled=${this.mode === "markdown"}
>
<div>M</div>
<pl-icon icon="markdown"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
@click=${() => this.dispatchEvent(new CustomEvent("toggle-fullscreen"))}
>
<pl-icon icon="${this.isFullscreen ? "collapse" : "expand"}"></pl-icon>
</pl-button>
<pl-button
class="transparent slim"
@click=${() => this.dispatchEvent(new CustomEvent("toggle-fullscreen"))}
>
<pl-icon icon="${this.isFullscreen ? "cancel" : "expand"}"></pl-icon>
</pl-button>
</div>
</div>
<div class="double-padded container scroller stretch" @click=${(e: Event) => e.stopPropagation()}></div>
<div
class="double-padded container scroller stretch"
@click=${(e: Event) => e.stopPropagation()}
?hidden=${this.mode !== "wysiwyg"}
></div>
<pl-textarea autosize class="stretch" ?hidden=${this.mode !== "markdown"}></pl-textarea>
`;
}
}

View File

@ -1,5 +1,4 @@
// @ts-ignore
import autosize from "autosize/src/autosize";
import autosize from "autosize";
import { css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { BaseInput } from "./base-input";
@ -12,13 +11,17 @@ export class Textarea extends BaseInput {
updated() {
if (this.autosize) {
setTimeout(() => autosize(this._inputElement));
setTimeout(() => autosize.update(this._inputElement));
}
}
connectedCallback() {
async connectedCallback() {
super.connectedCallback();
this.addEventListener("keydown", (e: KeyboardEvent) => this._keydown(e));
if (this.autosize) {
await this.updateComplete;
autosize(this._inputElement);
}
}
static styles = [

View File

@ -5,14 +5,13 @@ import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { html } from "lit";
const turndown = new TurnDown({
// blankReplacement: (_content, node) => {
// return node.nodeName === "P" ? `<p>&nbsp;</p>\n` : "";
// },
headingStyle: "atx",
bulletListMarker: "-",
});
turndown.addRule("p", {
filter: "p",
replacement: (content, node) => {
if (node.nextSibling) {
if (node.nextSibling && !["OL", "UL"].includes(node.nextSibling.nodeName)) {
content = content + "\n\n";
}
if (node.previousSibling) {
@ -42,12 +41,24 @@ turndown.addRule("li", {
if (parent?.nodeName === "OL") {
var start = parent.getAttribute("start");
var index = Array.prototype.indexOf.call(parent.children, node);
prefix = (start ? Number(start) + index : index + 1) + ". ";
prefix = (start ? Number(start) + index : index + 1) + ". ";
}
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? "\n" : "");
},
});
// turndown.addRule("lists", {
// filter: ["ul", "ol"],
// replacement: (content, node) => {
// const parent = node.parentNode;
// if (parent.nodeName === "LI" && parent.lastElementChild === node) {
// return "\n" + content;
// } else {
// return "\n\n" + content + "\n\n";
// }
// },
// });
// Add a hook to make all links open a new window
addHook("afterSanitizeAttributes", function (node) {
// set all elements owning target to target=_blank