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
359 changes: 229 additions & 130 deletions app/assets/javascript/lexxy.js

Large diffs are not rendered by default.

Binary file modified app/assets/javascript/lexxy.js.br
Binary file not shown.
Binary file modified app/assets/javascript/lexxy.js.gz
Binary file not shown.
6 changes: 3 additions & 3 deletions app/assets/javascript/lexxy.min.js

Large diffs are not rendered by default.

Binary file modified app/assets/javascript/lexxy.min.js.br
Binary file not shown.
Binary file modified app/assets/javascript/lexxy.min.js.gz
Binary file not shown.
59 changes: 59 additions & 0 deletions app/assets/stylesheets/lexxy-content.css
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,62 @@
--lexxy-attachment-text-color: var(--lexxy-color-ink-inverted);
}
}

/* Attachment galleries
/* ------------------------------------------------------------------------ */

:where(.attachment-gallery) {
display: grid;
column-gap: 1rem;
row-gap: 3rem;
margin-block: var(--lexxy-content-margin);
margin-inline: auto;
max-inline-size: 100%;

/* Make action-text-attachment elements proper grid items */
action-text-attachment {
display: block;
}

/* Two images: side by side */
&.attachment-gallery--2 {
grid-template-columns: repeat(2, 1fr);
}

/* Three images: all in a row */
&.attachment-gallery--3 {
grid-template-columns: repeat(3, 1fr);
}

/* Four images: 2x2 grid */
&.attachment-gallery--4 {
grid-template-columns: repeat(2, 1fr);
}

/* Five or more images: 3 column grid */
&.attachment-gallery--5,
&.attachment-gallery--6,
&.attachment-gallery--7,
&.attachment-gallery--8,
&.attachment-gallery--9 {
grid-template-columns: repeat(3, 1fr);
}

/* Attachments in galleries have no individual margin */
.attachment {
margin-block: 0;
}

/* Images in galleries should fill their grid cell */
.attachment--preview img {
block-size: auto;
inline-size: 100%;
max-block-size: 32rem;
object-fit: contain;
}

/* Captions in galleries need proper spacing */
.attachment__caption {
margin-block-start: 1ch;
}
}
9 changes: 2 additions & 7 deletions src/editor/command_dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,7 @@ export class CommandDispatcher {
const files = Array.from(target.files)
if (!files.length) return

for (const file of files) {
this.contents.uploadFile(file)
}
this.contents.uploadFiles(files)
}
})

Expand Down Expand Up @@ -238,10 +236,7 @@ export class CommandDispatcher {
const files = Array.from(dataTransfer.files)
if (!files.length) return

for (const file of files) {
this.contents.uploadFile(file)
}

this.contents.uploadFiles(files)
this.editor.focus()
}
}
Expand Down
118 changes: 111 additions & 7 deletions src/editor/contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { $toggleLink, $createLinkNode } from "@lexical/link"
import { dispatch, parseHtml } from "../helpers/html_helper"
import { $isListItemNode, $isListNode } from "@lexical/list"
import { getNearestListItemNode } from "../helpers/lexical_helper"
import { nextFrame } from "../helpers/timing_helpers.js";
import { nextFrame } from "../helpers/timing_helpers.js"
import { $createAttachmentGalleryNode } from "../nodes/attachment_gallery_node"

export default class Contents {
constructor(editorElement) {
Expand Down Expand Up @@ -245,32 +246,61 @@ export default class Contents {
}

uploadFile(file) {
this.uploadFiles([file])
}

uploadFiles(files) {
if (!this.editorElement.supportsAttachments) {
console.warn("This editor does not supports attachments (it's configured with [attachments=false])")
return
}

if (!this.#shouldUploadFile(file)) {
return
}
const validFiles = files.filter(file => this.#shouldUploadFile(file))
if (validFiles.length === 0) return

const uploadUrl = this.editorElement.directUploadUrl
const blobUrlTemplate = this.editorElement.blobUrlTemplate

this.editor.update(() => {
const uploadedImageNode = new ActionTextAttachmentUploadNode({ file: file, uploadUrl: uploadUrl, blobUrlTemplate: blobUrlTemplate, editor: this.editor })
this.insertAtCursor(uploadedImageNode)
const selection = $getSelection()
const anchorNode = selection?.anchor.getNode()
const currentParagraph = anchorNode?.getTopLevelElement()

const uploadNodes = validFiles.map(file =>
new ActionTextAttachmentUploadNode({
file: file,
uploadUrl: uploadUrl,
blobUrlTemplate: blobUrlTemplate,
editor: this.editor
})
)

// Wrap multiple files in a gallery, insert single files directly
const nodeToInsert = uploadNodes.length >= 2
? this.#createGalleryWithNodes(uploadNodes)
: uploadNodes[0]

this.#insertNode(nodeToInsert, currentParagraph)

// If we inserted a gallery, move cursor outside of it
if (uploadNodes.length >= 2) {
this.#selectOutsideNode(nodeToInsert)
}
}, { tag: HISTORY_MERGE_TAG })
}

async deleteSelectedNodes() {
let focusNode = null
let parentWasGallery = false

this.editor.update(() => {
if ($isNodeSelection(this.#selection.current)) {
const nodesToRemove = this.#selection.current.getNodes()
if (nodesToRemove.length === 0) return

const parent = nodesToRemove[0]?.getParent()
parentWasGallery = parent?.getType() === 'attachment_gallery'

focusNode = this.#findAdjacentNodeTo(nodesToRemove)
this.#deleteNodes(nodesToRemove)
}
Expand All @@ -279,7 +309,20 @@ export default class Contents {
await nextFrame()

this.editor.update(() => {
this.#selectAfterDeletion(focusNode)
if (parentWasGallery) {
// Get the gallery node from the focus node
const galleryNode = focusNode?.getParent?.()?.getType?.() === 'attachment_gallery'
? focusNode.getParent()
: focusNode

if (galleryNode?.getType?.() === 'attachment_gallery') {
this.#selectOutsideNode(galleryNode)
} else {
this.#selectAfterDeletion(focusNode)
}
} else {
this.#selectAfterDeletion(focusNode)
}
this.#selection.clear()
this.editor.focus()
})
Expand Down Expand Up @@ -420,17 +463,29 @@ export default class Contents {
// Use splice() instead of node.remove() for proper removal and
// reconciliation. Would have issues with removing unintended decorator nodes
// with node.remove()
const galleriesToUpdate = new Map()

nodes.forEach((node) => {
const parent = node.getParent()
if (!$isElementNode(parent)) return

// Track gallery parents that need DOM updates
if (parent.getType() === 'attachment_gallery') {
galleriesToUpdate.set(parent.getKey(), parent)
}

const children = parent.getChildren()
const index = children.indexOf(node)

if (index >= 0) {
parent.splice(index, 1, [])
}
})

// Update gallery DOM and remove if empty/single child
galleriesToUpdate.forEach((gallery, galleryKey) => {
this.#updateOrRemoveGallery(gallery, galleryKey)
})
}

#findAdjacentNodeTo(nodes) {
Expand Down Expand Up @@ -625,4 +680,53 @@ export default class Contents {
#shouldUploadFile(file) {
return dispatch(this.editorElement, 'lexxy:file-accept', { file }, true)
}

#createGalleryWithNodes(uploadNodes) {
const gallery = $createAttachmentGalleryNode()
uploadNodes.forEach(node => gallery.append(node))
return gallery
}

#insertNode(node, currentParagraph) {
if (currentParagraph && $isParagraphNode(currentParagraph) && currentParagraph.getChildrenSize() === 0) {
currentParagraph.replace(node)
// If we're replacing an empty paragraph with a gallery, ensure there's a paragraph after for cursor placement
if (node.getType() === 'attachment_gallery') {
const newParagraph = $createParagraphNode()
node.insertAfter(newParagraph)
}
} else if (currentParagraph && $isElementNode(currentParagraph)) {
currentParagraph.insertAfter(node)
} else {
$insertNodes([node])
}
}

#selectOutsideNode(node) {
const nextNode = node.getNextSibling()
if (nextNode && ($isTextNode(nextNode) || $isParagraphNode(nextNode))) {
nextNode.selectStart()
} else {
// If there's no suitable next node, create a new paragraph after the gallery
const newParagraph = $createParagraphNode()
node.insertAfter(newParagraph)
newParagraph.selectStart()
}
}

#updateOrRemoveGallery(gallery, galleryKey) {
const childCount = gallery.getChildrenSize()

// If gallery is empty or has only one child, unwrap it
if (childCount <= 1) {
gallery.getChildren().forEach(child => gallery.insertBefore(child))
gallery.remove()
} else {
// Update the gallery's className to reflect new child count
const dom = this.editor.getElementByKey(galleryKey)
if (dom) {
dom.className = `attachment-gallery attachment-gallery--${childCount}`
}
}
}
}
3 changes: 2 additions & 1 deletion src/elements/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { registerHistory, createEmptyHistoryState } from '@lexical/history'
import theme from "../config/theme"
import { ActionTextAttachmentNode } from "../nodes/action_text_attachment_node"
import { ActionTextAttachmentUploadNode } from "../nodes/action_text_attachment_upload_node"
import { AttachmentGalleryNode } from "../nodes/attachment_gallery_node"
import { HorizontalDividerNode } from "../nodes/horizontal_divider_node"
import { CommandDispatcher } from "../editor/command_dispatcher"
import Selection from "../editor/selection"
Expand Down Expand Up @@ -190,7 +191,7 @@ export default class LexicalEditorElement extends HTMLElement {
]

if (this.supportsAttachments) {
nodes.push(ActionTextAttachmentNode, ActionTextAttachmentUploadNode)
nodes.push(ActionTextAttachmentNode, ActionTextAttachmentUploadNode, AttachmentGalleryNode)
}

return nodes
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/html_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const VISUALLY_RELEVANT_ELEMENTS_SELECTOR = [
"form", "input", "textarea", "select", "button", "code", "blockquote", "hr"
].join(",")

const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "div", "em",
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol", "p", "pre", "q", "s", "strong", "ul" ]

const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
Expand Down
73 changes: 73 additions & 0 deletions src/nodes/attachment_gallery_node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ElementNode } from "lexical"

export class AttachmentGalleryNode extends ElementNode {
static getType() {
return "attachment_gallery"
}

static clone(node) {
return new AttachmentGalleryNode(node.__key)
}

static importJSON(serializedNode) {
return new AttachmentGalleryNode()
}

static importDOM() {
return {
div: (domNode) => {
if (domNode.classList.contains('attachment-gallery')) {
return {
conversion: () => ({ node: new AttachmentGalleryNode() }),
priority: 1
}
}
return null
}
}
}

constructor(key) {
super(key)
}

createDOM() {
const element = document.createElement("div")
element.className = this.getGalleryClassName()
return element
}

updateDOM(prevNode, dom) {
const className = this.getGalleryClassName()
if (dom.className !== className) {
dom.className = className
}
return false
}

getGalleryClassName() {
const count = this.getChildrenSize()
return `attachment-gallery attachment-gallery--${count}`
}

isInline() {
return false
}

exportDOM(editor) {
const element = document.createElement("div")
element.className = this.getGalleryClassName()
return { element }
}

exportJSON() {
return {
type: "attachment_gallery",
version: 1
}
}
}

export function $createAttachmentGalleryNode() {
return new AttachmentGalleryNode()
}
1 change: 1 addition & 0 deletions test/dummy/config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
pin "prismjs"
pin "prettier"
pin "prettier/parser-html", to: "prettier--parser-html.js"
pin "marked", to: "https://ga.jspm.io/npm:[email protected]/lib/marked.esm.js"
Binary file added test/fixtures/files/example2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading