Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/assets/stylesheets/lexxy-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,33 @@
pointer-events: auto;
}

.lexxy-node-action {
align-items: center;
aspect-ratio: 1;
background: transparent;
block-size: var(--button-size);
border-radius: var(--floating-tools-radius);
color: var(--lexxy-color-ink-inverted);
display: flex;
justify-content: center;
min-block-size: var(--button-size);
min-inline-size: var(--button-size);
text-decoration: none;

svg {
block-size: 1.125em;
inline-size: 1.125em;
fill: currentColor;
opacity: 0.8;
}

&:hover {
background: var(--lexxy-color-ink-medium);

svg { opacity: 1; }
}
}

.lexxy-node-delete:hover {
background-color: var(--lexxy-color-red);
}
Expand Down
2 changes: 1 addition & 1 deletion src/config/dom_purify.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Lexxy from "./lexxy"
const ALLOWED_HTML_TAGS = [ "a", "b", "blockquote", "br", "code", "div", "em",
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "u", "ul", "table", "tbody", "tr", "th", "td" ]

const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
const ALLOWED_HTML_ATTRIBUTES = [ "alt", "blob-url", "caption", "class", "collapsed", "content", "content-type", "contenteditable",
"data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
"previewable", "sgid", "src", "style", "title", "url", "width" ]

Expand Down
130 changes: 128 additions & 2 deletions src/elements/node_delete_button.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,34 @@ const DELETE_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"
<path d="M11.2041 1.01074C12.2128 1.113 13 1.96435 13 3V4H15L15.1025 4.00488C15.6067 4.05621 16 4.48232 16 5C16 5.55228 15.5523 6 15 6H14.8457L14.1416 15.1533C14.0614 16.1953 13.1925 17 12.1475 17H5.85254L5.6582 16.9902C4.76514 16.9041 4.03607 16.2296 3.88184 15.3457L3.8584 15.1533L3.1543 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H5V3C5 1.89543 5.89543 1 7 1H11L11.2041 1.01074ZM5.85254 15H12.1475L12.8398 6H5.16016L5.85254 15ZM7 4H11V3H7V4Z"/>
</svg>`

const PREVIEW_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2H5C3.89543 2 3 2.89543 3 4V14C3 15.1046 3.89543 16 5 16H13C14.1046 16 15 15.1046 15 14V7H12C10.8954 7 10 6.10457 10 5V2ZM12 2.41421L14.5858 5H12V2.41421ZM5 1C3.34315 1 2 2.34315 2 4V14C2 15.6569 3.34315 17 5 17H13C14.6569 17 16 15.6569 16 14V6.41421C16 6.01639 15.842 5.63486 15.5607 5.35355L11.6464 1.43934C11.3651 1.15804 10.9836 1 10.5858 1H5Z"/>
</svg>`

const EDIT_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5858 1.58579C14.3668 0.804738 15.6332 0.804738 16.4142 1.58579C17.1953 2.36683 17.1953 3.63317 16.4142 4.41421L6.41421 14.4142C6.14935 14.6791 5.82269 14.8735 5.46487 14.9793L2.28306 15.9193C1.81048 16.059 1.32955 15.8584 1.09763 15.4527C0.916167 15.1359 0.940981 14.7489 1.13617 14.4638L1.02073 14.717L2.02073 12.5352C2.12653 12.1774 2.32089 11.8507 2.58579 11.5858L12.5858 1.58579ZM15 3L14.4142 2.41421L4 12.8284L3.28306 14.717L5.17157 14L15.5858 3.58579L15 3Z"/>
</svg>`

const DOWNLOAD_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M9 1C9.55228 1 10 1.44772 10 2V9.58579L12.2929 7.29289C12.6834 6.90237 13.3166 6.90237 13.7071 7.29289C14.0976 7.68342 14.0976 8.31658 13.7071 8.70711L9.70711 12.7071C9.31658 13.0976 8.68342 13.0976 8.29289 12.7071L4.29289 8.70711C3.90237 8.31658 3.90237 7.68342 4.29289 7.29289C4.68342 6.90237 5.31658 6.90237 5.70711 7.29289L8 9.58579V2C8 1.44772 8.44772 1 9 1ZM3 14C3 13.4477 2.55228 13 2 13C1.44772 13 1 13.4477 1 14V15C1 16.1046 1.89543 17 3 17H15C16.1046 17 17 16.1046 17 15V14C17 13.4477 16.5523 13 16 13C15.4477 13 15 13.4477 15 14V15H3V14Z"/>
</svg>`

const COLLAPSE_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4C2.44772 4 2 4.44772 2 5C2 5.55228 2.44772 6 3 6H15C15.5523 6 16 5.55228 16 5C16 4.44772 15.5523 4 15 4H3ZM3 8C2.44772 8 2 8.44772 2 9C2 9.55228 2.44772 10 3 10H15C15.5523 10 16 9.55228 16 9C16 8.44772 15.5523 8 15 8H3ZM2 13C2 12.4477 2.44772 12 3 12H10C10.5523 12 11 12.4477 11 13C11 13.5523 10.5523 14 10 14H3C2.44772 14 2 13.5523 2 13Z"/>
</svg>`

const EXPAND_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3C2 2.44772 2.44772 2 3 2H6C6.55228 2 7 2.44772 7 3V6C7 6.55228 6.55228 7 6 7H3C2.44772 7 2 6.55228 2 6V3ZM4 4V5H5V4H4ZM9 3C9 2.44772 9.44772 2 10 2H15C15.5523 2 16 2.44772 16 3C16 3.55228 15.5523 4 15 4H10C9.44772 4 9 3.55228 9 3ZM9 6C9 5.44772 9.44772 5 10 5H13C13.5523 5 14 5.44772 14 6C14 6.55228 13.5523 7 13 7H10C9.44772 7 9 6.55228 9 6ZM2 11C2 10.4477 2.44772 10 3 10H6C6.55228 10 7 10.4477 7 11V14C7 14.5523 6.55228 15 6 15H3C2.44772 15 2 14.5523 2 14V11ZM4 12V13H5V12H4ZM10 10C9.44772 10 9 10.4477 9 11C9 11.5523 9.44772 12 10 12H15C15.5523 12 16 11.5523 16 11C16 10.4477 15.5523 10 15 10H10ZM9 14C9 13.4477 9.44772 13 10 13H13C13.5523 13 14 13.4477 14 14C14 14.5523 13.5523 15 13 15H10C9.44772 15 9 14.5523 9 14Z"/>
</svg>`

export class NodeDeleteButton extends HTMLElement {
connectedCallback() {
this.editorElement = this.closest("lexxy-editor")
this.editor = this.editorElement.editor
this.classList.add("lexxy-floating-controls")

if (!this.querySelector(".lexxy-node-delete")) {
this.#attachDeleteButton()
this.#attachButtons()
}
}

Expand All @@ -21,15 +41,100 @@ export class NodeDeleteButton extends HTMLElement {
this.editorElement = null
}

#attachDeleteButton() {
#attachButtons() {
const container = createElement("div", { className: "lexxy-floating-controls__group" })
const fileUrl = this.dataset.fileUrl
const fileName = this.dataset.fileName
const contentType = this.dataset.contentType
const caption = this.dataset.caption

if (fileUrl) {
const previewButton = createElement("button", {
type: "button",
className: "lexxy-node-action",
"aria-label": "Open"
})
previewButton.tabIndex = -1
previewButton.dataset.tooltip = "Open"
previewButton.dataset.tooltipPosition = "below"
previewButton.innerHTML = PREVIEW_ICON
previewButton.addEventListener("click", (e) => {
e.stopPropagation()
this.#dispatchPreviewEvent(fileUrl, fileName, contentType, caption)
})
container.appendChild(previewButton)

if (this.#isEditable(contentType)) {
const editButton = createElement("button", {
type: "button",
className: "lexxy-node-action",
"aria-label": "Edit"
})
editButton.tabIndex = -1
editButton.dataset.tooltip = "Edit"
editButton.dataset.tooltipPosition = "below"
editButton.innerHTML = EDIT_ICON
editButton.addEventListener("click", (e) => {
e.stopPropagation()
this.#dispatchEditEvent(fileUrl, fileName, contentType, caption)
})
container.appendChild(editButton)
}

const downloadLink = createElement("a", {
href: fileUrl,
download: fileName || "",
className: "lexxy-node-action",
"aria-label": "Download"
})
downloadLink.tabIndex = -1
downloadLink.dataset.tooltip = "Download"
downloadLink.dataset.tooltipPosition = "below"
downloadLink.innerHTML = DOWNLOAD_ICON
downloadLink.addEventListener("click", (e) => e.stopPropagation())
container.appendChild(downloadLink)

if (this.dataset.previewable === "true") {
const isCollapsed = this.closest("figure.attachment")?.classList.contains("attachment--collapsed")
const toggleButton = createElement("button", {
type: "button",
className: "lexxy-node-action",
"aria-label": isCollapsed ? "Show preview" : "Collapse to card"
})
toggleButton.tabIndex = -1
toggleButton.dataset.tooltip = isCollapsed ? "Show preview" : "Collapse"
toggleButton.dataset.tooltipPosition = "below"
toggleButton.innerHTML = isCollapsed ? EXPAND_ICON : COLLAPSE_ICON
toggleButton.addEventListener("click", (e) => {
e.stopPropagation()
const figure = this.closest("figure.attachment")
if (figure) {
figure.classList.toggle("attachment--collapsed")
const nowCollapsed = figure.classList.contains("attachment--collapsed")
toggleButton.innerHTML = nowCollapsed ? EXPAND_ICON : COLLAPSE_ICON
toggleButton.setAttribute("aria-label", nowCollapsed ? "Show preview" : "Collapse to card")
toggleButton.dataset.tooltip = nowCollapsed ? "Show preview" : "Collapse"

this.editor.update(() => {
const node = $getNearestNodeFromDOMNode(this)
if (node) {
node.getWritable().collapsed = nowCollapsed
}
})
}
})
container.appendChild(toggleButton)
}
}

this.deleteButton = createElement("button", {
className: "lexxy-node-delete",
type: "button",
"aria-label": "Remove"
})
this.deleteButton.tabIndex = -1
this.deleteButton.dataset.tooltip = "Remove"
this.deleteButton.dataset.tooltipPosition = "below"
this.deleteButton.innerHTML = DELETE_ICON

this.handleDeleteClick = () => this.#deleteNode()
Expand All @@ -39,6 +144,27 @@ export class NodeDeleteButton extends HTMLElement {
this.appendChild(container)
}

#isEditable(contentType) {
if (!contentType) return false
return contentType.startsWith("text/") ||
contentType === "application/json" ||
contentType === "application/csv"
}

#dispatchEditEvent(fileUrl, fileName, contentType, caption) {
this.editorElement.dispatchEvent(new CustomEvent("lexxy:edit-attachment", {
bubbles: true,
detail: { fileUrl, fileName, contentType, caption }
}))
}

#dispatchPreviewEvent(fileUrl, fileName, contentType, caption) {
this.editorElement.dispatchEvent(new CustomEvent("lexxy:preview-attachment", {
bubbles: true,
detail: { fileUrl, fileName, contentType, caption }
}))
}

#deleteNode() {
this.editor.update(() => {
const node = $getNearestNodeFromDOMNode(this)
Expand Down
87 changes: 76 additions & 11 deletions src/nodes/action_text_attachment_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export class ActionTextAttachmentNode extends DecoratorNode {
node: new ActionTextAttachmentNode({
sgid: attachment.getAttribute("sgid"),
src: attachment.getAttribute("url"),
blobUrl: attachment.getAttribute("blob-url"),
previewable: attachment.getAttribute("previewable"),
collapsed: attachment.getAttribute("collapsed"),
altText: attachment.getAttribute("alt"),
caption: attachment.getAttribute("caption"),
contentType: attachment.getAttribute("content-type"),
Expand Down Expand Up @@ -79,13 +81,15 @@ export class ActionTextAttachmentNode extends DecoratorNode {
return Lexxy.global.get("attachmentTagName")
}

constructor({ tagName, sgid, src, previewable, altText, caption, contentType, fileName, fileSize, width, height }, key) {
constructor({ tagName, sgid, src, blobUrl, previewable, collapsed, altText, caption, contentType, fileName, fileSize, width, height }, key) {
super(key)

this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME
this.sgid = sgid
this.src = src
this.blobUrl = blobUrl
this.previewable = parseBoolean(previewable)
this.collapsed = collapsed != null ? parseBoolean(collapsed) : this.#defaultCollapsed(contentType)
this.altText = altText || ""
this.caption = caption || ""
this.contentType = contentType || ""
Expand All @@ -97,12 +101,31 @@ export class ActionTextAttachmentNode extends DecoratorNode {
this.editor = $getEditor()
}

get fileUrl() {
return this.blobUrl || this.src
}

#defaultCollapsed(contentType) {
return contentType === "application/pdf"
}

createDOM() {
const figure = this.createAttachmentFigure()

if (this.isPreviewableAttachment) {
figure.appendChild(this.#createDOMForImage())
figure.appendChild(this.#createEditableCaption())
if (this.collapsed) {
figure.classList.add("attachment--collapsed")
}

const previewView = createElement("div", { className: "attachment__preview-view" })
previewView.appendChild(this.#createDOMForImage())
previewView.appendChild(this.#createEditableCaption())
figure.appendChild(previewView)

const cardView = createElement("div", { className: "attachment__card-view" })
cardView.appendChild(this.#createDOMForFile())
cardView.appendChild(this.#createDOMForNotImage())
figure.appendChild(cardView)
} else {
figure.appendChild(this.#createDOMForFile())
figure.appendChild(this.#createDOMForNotImage())
Expand All @@ -117,6 +140,24 @@ export class ActionTextAttachmentNode extends DecoratorNode {
caption.value = this.caption
}

const cardView = dom.querySelector(".attachment__card-view")
if (cardView) {
const captionText = cardView.querySelector(".attachment__caption-text")
if (this.caption) {
if (captionText) {
captionText.textContent = this.caption
} else {
const meta = cardView.querySelector(".attachment__meta")
if (meta) {
const newCaption = createElement("span", { className: "attachment__caption-text", textContent: this.caption })
meta.prepend(newCaption)
}
}
} else if (captionText) {
captionText.remove()
}
}

return false
}

Expand All @@ -132,7 +173,9 @@ export class ActionTextAttachmentNode extends DecoratorNode {
const attachment = createElement(this.tagName, {
sgid: this.sgid,
previewable: this.previewable || null,
collapsed: this.collapsed ? "true" : null,
url: this.src,
"blob-url": this.blobUrl || null,
alt: this.altText,
caption: this.caption,
"content-type": this.contentType,
Expand All @@ -153,7 +196,9 @@ export class ActionTextAttachmentNode extends DecoratorNode {
tagName: this.tagName,
sgid: this.sgid,
src: this.src,
blobUrl: this.blobUrl,
previewable: this.previewable,
collapsed: this.collapsed,
altText: this.altText,
caption: this.caption,
contentType: this.contentType,
Expand All @@ -173,8 +218,17 @@ export class ActionTextAttachmentNode extends DecoratorNode {
figure.draggable = true
figure.dataset.lexicalNodeKey = this.__key

const deleteButton = createElement("lexxy-node-delete-button")
figure.appendChild(deleteButton)
const controls = createElement("lexxy-node-delete-button")
if (this.fileUrl) {
controls.dataset.fileUrl = this.fileUrl
controls.dataset.fileName = this.fileName || ""
controls.dataset.contentType = this.contentType || ""
controls.dataset.caption = this.caption || ""
}
if (this.isPreviewableAttachment) {
controls.dataset.previewable = "true"
}
figure.appendChild(controls)

return figure
}
Expand Down Expand Up @@ -223,23 +277,34 @@ export class ActionTextAttachmentNode extends DecoratorNode {
}
}

static FILE_TYPE_LABELS = { md: "M↓", png: "IMG", jpg: "IMG", jpeg: "IMG", gif: "IMG", webp: "IMG", svg: "IMG", xls: "XLS", xlsx: "XLS" }

#createDOMForFile() {
const extension = this.fileName ? this.fileName.split(".").pop().toLowerCase() : "unknown"
return createElement("span", { className: "attachment__icon", textContent: `${extension}` })
const extension = this.fileName ? this.fileName.split(".").pop().toLowerCase() : "?"
const label = ActionTextAttachmentNode.FILE_TYPE_LABELS[extension] || extension.toUpperCase()
return createElement("span", { className: "attachment__icon", textContent: label })
}

#createDOMForNotImage() {
const figcaption = createElement("figcaption", { className: "attachment__caption" })

const nameTag = createElement("strong", { className: "attachment__name", textContent: this.caption || this.fileName })

const nameTag = createElement("strong", { className: "attachment__name", textContent: this.fileName })
figcaption.appendChild(nameTag)

const metaRow = createElement("span", { className: "attachment__meta" })

if (this.caption) {
const captionTag = createElement("span", { className: "attachment__caption-text", textContent: this.caption })
metaRow.appendChild(captionTag)
}

if (this.fileSize) {
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) })
figcaption.appendChild(sizeSpan)
const subtitle = createElement("span", { className: "attachment__subtitle", textContent: bytesToHumanSize(this.fileSize) })
metaRow.appendChild(subtitle)
}

figcaption.appendChild(metaRow)

return figcaption
}

Expand Down
3 changes: 2 additions & 1 deletion src/nodes/action_text_attachment_upload_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ class AttachmentNodeConversion {
return new ActionTextAttachmentNode({
...this.uploadNode,
...this.#propertiesFromBlob,
src: this.#src
src: this.#src,
blobUrl: this.#blobSrc
})
}

Expand Down
Loading
Loading