Skip to content

Commit

Permalink
Float images in Tiptap editor, closes #94
Browse files Browse the repository at this point in the history
  • Loading branch information
tobifra committed Apr 10, 2024
1 parent 98ebb99 commit 222d028
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 17 deletions.
73 changes: 56 additions & 17 deletions frontend/src/components/admin/Editor/Editor.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="rounded-lg p-2">
<div class="rounded-lg pt-2">
<div v-if="editor" class="flex justify-between">
<div class="flex flex-col">
<div
Expand Down Expand Up @@ -28,13 +28,10 @@
</EditorButton>
</div>
<div class="flex">
<EditorButton @click.self.prevent class="rounded-l-lg border-l">
<EditorColorPicker v-model="textColor" />
</EditorButton>

<EditorButton
@click="editor.chain().focus().toggleBold().run()"
:active="editor.isActive('bold')"
class="rounded-l-lg border-l"
>
<font-awesome-icon class="size-5" :icon="icons.faBold" />
</EditorButton>
Expand All @@ -53,10 +50,12 @@
<EditorButton
@click="editor.chain().focus().toggleStrike().run()"
:active="editor.isActive('strike')"
class="rounded-r-lg"
>
<font-awesome-icon class="size-5" :icon="icons.faStrikethrough" />
</EditorButton>
<EditorButton @click.self.prevent class="rounded-r-lg">
<EditorColorPicker v-model="textColor" />
</EditorButton>
</div>
<div class="flex">
<EditorButton
Expand Down Expand Up @@ -97,22 +96,34 @@
</div>
<div class="flex">
<EditorButton
@click="editor.chain().focus().setTextAlign('left').run()"
:active="editor.isActive({ textAlign: 'left' })"
@click="handleAlign('left', 'float-left')"
:active="
editor.isActive({ textAlign: 'left' }) ||
(editor.isActive('image') &&
editor.getAttributes('image').class === 'float-left')
"
class="rounded-l-lg border-l"
>
<font-awesome-icon class="size-5" :icon="icons.faAlignLeft" />
</EditorButton>

<EditorButton
@click="editor.chain().focus().setTextAlign('center').run()"
:active="editor.isActive({ textAlign: 'center' })"
@click="handleAlign('center', 'block mx-auto')"
:active="
editor.isActive({ textAlign: 'center' }) ||
(editor.isActive('image') &&
editor.getAttributes('image').class === 'block mx-auto')
"
>
<font-awesome-icon class="size-5" :icon="icons.faAlignCenter" />
</EditorButton>
<EditorButton
@click="editor.chain().focus().setTextAlign('right').run()"
:active="editor.isActive({ textAlign: 'right' })"
@click="handleAlign('right', 'float-right')"
:active="
editor.isActive({ textAlign: 'right' }) ||
(editor.isActive('image') &&
editor.getAttributes('image').class === 'float-right')
"
class="rounded-r-lg"
>
<font-awesome-icon class="size-5" :icon="icons.faAlignRight" />
Expand Down Expand Up @@ -152,7 +163,7 @@
<textarea
v-else
v-model="inputModel"
class="appearance-none border-0 w-full prose prose-sm sm:prose lg:prose-lg xl:prose-2xl my-2 focus:outline-none bg-white rounded-lg p-2 h-48 overflow-scroll font-mono"
class="appearance-none w-full prose prose-sm sm:prose lg:prose-lg xl:prose-2xl my-2 focus:outline-none bg-white rounded-lg p-2 h-48 overflow-scroll font-mono focus:ring-0 border border-gray-700 focus:border-gray-700"
>
</textarea>
</div>
Expand Down Expand Up @@ -184,19 +195,19 @@
import { Editor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import HeadingExtension from "@tiptap/extension-heading";
import ImageResize from "tiptap-extension-resize-image";
import Image from "@tiptap/extension-image";
import LinkExtension from "@tiptap/extension-link";
import { Color } from "@tiptap/extension-color";
import TextStyle from "@tiptap/extension-text-style";
import TextAlign from "@tiptap/extension-text-align";
import { mergeAttributes } from "@tiptap/core";
import Placeholder from "@tiptap/extension-placeholder";
import HardBreak from "@tiptap/extension-hard-break";
import Table from "@tiptap/extension-table";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import TableRow from "@tiptap/extension-table-row";
import Underline from "@tiptap/extension-underline";
import Resizable from "./resizable-image";
import EditorButton from "./EditorButton.vue";
import {
faArrowRotateLeft,
Expand Down Expand Up @@ -300,6 +311,19 @@ export default {
.setImage({ src: this.backendURL + selected[0].path })
.run();
},
handleAlign(text, imageClass) {
this.editor.chain().focus();
if (this.editor.isActive("image")) {
console.log("image");
this.editor
.chain()
.updateAttributes("image", { class: imageClass })
.run();
} else {
console.log("text");
this.editor.chain().setTextAlign(text).run();
}
},
},
watch: {
modelValue(value) {
Expand Down Expand Up @@ -394,7 +418,22 @@ export default {
}),
TextStyle,
Color,
ImageResize,
Image.configure({
inline: true,
}),
Resizable.configure({
types: ["image", "video"],
handlerStyle: {
// handler point style
width: "8px",
height: "8px",
background: "#111827",
},
layerStyle: {
border: "2px dashed #111827",
},
}),
TextAlign.configure({
types: ["heading", "paragraph"],
}),
Expand All @@ -418,7 +457,7 @@ export default {
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl my-2 focus:outline-none bg-white rounded-lg p-2 min-h-[12rem] overflow-scroll",
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl my-2 focus:outline-none bg-white rounded-lg p-2 min-h-[12rem] overflow-scroll border border-gray-700",
},
},
});
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/admin/Editor/resizable-image/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import resizable from "./resizable";
export { resizable };
export default resizable;
167 changes: 167 additions & 0 deletions frontend/src/components/admin/Editor/resizable-image/resizable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Extension } from "@tiptap/core";
import { throttle } from "lodash";

export default Extension.create({
name: "resizable",
priority: 1000,
addOptions() {
return {
types: ["image"],
handlerStyle: {
width: "8px",
height: "8px",
background: "#07c160",
},
layerStyle: {
border: "2px solid #07c160",
},
};
},

addStorage() {
return {
resizeElement: null,
resizeNode: null,
};
},

addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
width: {
default: null,
parseHTML: (element) => element.style.width,
renderHTML: (attributes) => {
if (!attributes.width) return {};
return { style: `width: ${attributes.width}` };
},
},
class: {
default: "",
parseHTML: (element) => element.className,
renderHTML: (attributes) => {
if (!attributes.class) return {};
return {
class: attributes.class
.split(" ")
.filter((name) => name !== "selectednode")
.join(" "),
};
},
},
},
},
];
},

onCreate({ editor }) {
const element = editor.options.element;
element.style.position = "relative";

const resizeLayer = document.createElement("div");
resizeLayer.className = "resize-layer";
resizeLayer.style.display = "none";
resizeLayer.style.position = "absolute";

Object.entries(this.options.layerStyle).forEach(([key, value]) => {
resizeLayer.style[key] = value;
});

resizeLayer.addEventListener("mousedown", (e) => {
e.preventDefault();
const resizeElement = this.storage.resizeElement;
const resizeNode = this.storage.resizeNode;
if (!resizeElement) return;
if (/bottom/.test(e.target.className)) {
let startX = e.screenX;
const dir = e.target.classList.contains("bottom-left") ? -1 : 1;
const mousemoveHandle = (e) => {
const width = resizeElement.clientWidth;
const distanceX = e.screenX - startX;
const total = width + dir * distanceX;

resizeElement.style.width = total + "px";
resizeNode.attrs.width = total + "px";

const clientWidth = resizeElement.clientWidth;
const clientHeight = resizeElement.clientHeight;
const pos = getRelativePosition(resizeElement, element);
resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = clientWidth + "px";
resizeLayer.style.height = clientHeight + "px";
startX = e.screenX;
};
document.addEventListener("mousemove", mousemoveHandle);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", mousemoveHandle);
});
}
});
const handlers = ["top-left", "top-right", "bottom-left", "bottom-right"];
const fragment = document.createDocumentFragment();
for (let name of handlers) {
const item = document.createElement("div");
item.className = `handler ${name} rounded-full`;
item.style.position = "absolute";
Object.entries(this.options.handlerStyle).forEach(([key, value]) => {
item.style[key] = value;
});
const dir = name.split("-");
item.style[dir[0]] = parseInt(item.style.width) / -2 + "px";
item.style[dir[1]] = parseInt(item.style.height) / -2 + "px";
if (name === "bottom-left") item.style.cursor = "sw-resize";
if (name === "bottom-right") item.style.cursor = "se-resize";
fragment.appendChild(item);
}
resizeLayer.appendChild(fragment);
editor.resizeLayer = resizeLayer;
element.appendChild(resizeLayer);
},

onTransaction: throttle(function ({ editor }) {
const resizeLayer = editor.resizeLayer;
if (resizeLayer && resizeLayer.style.display === "block") {
const dom = this.storage.resizeElement;
const element = editor.options.element;
const pos = getRelativePosition(dom, element);
resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = dom.clientWidth + "px";
resizeLayer.style.height = dom.clientHeight + "px";
}
}, 240),

onSelectionUpdate: function ({ editor, transaction }) {
const element = editor.options.element;
const node = transaction.curSelection.node;
const resizeLayer = editor.resizeLayer;

if (node && this.options.types.includes(node.type.name)) {
resizeLayer.style.display = "block";
let dom = editor.view.domAtPos(transaction.curSelection.from).node;
dom = dom.querySelector(".ProseMirror-selectednode");
this.storage.resizeElement = dom;
this.storage.resizeNode = node;
const pos = getRelativePosition(dom, element);
resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = dom.clientWidth + "px";
resizeLayer.style.height = dom.clientHeight + "px";
} else {
resizeLayer.style.display = "none";
}
},
});

function getRelativePosition(element, ancestor) {
const elementRect = element.getBoundingClientRect();
const ancestorRect = ancestor.getBoundingClientRect();
const relativePosition = {
top: parseInt(elementRect.top - ancestorRect.top + ancestor.scrollTop),
left: parseInt(elementRect.left - ancestorRect.left + ancestor.scrollLeft),
};
return relativePosition;
}

0 comments on commit 222d028

Please sign in to comment.