Path: blob/master/src/packages/frontend/editors/slate/elements/image/editable.tsx
1698 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { useEffect, useRef, useState } from "react";6import { register } from "../register";7import { useFocused, useProcessLinks, useSelected, useSlate } from "../hooks";8import { useSetElement } from "../set-element";9import { FOCUSED_COLOR } from "../../util";10import { Resizable } from "re-resizable";11import { Image } from "./index";12import { useFileContext } from "@cocalc/frontend/lib/file-context";1314// NOTE: We do NOT use maxWidth:100% since that ends up15// making the resizing screw up the aspect ratio.1617function toFloat(s: number | string | undefined): number | undefined {18if (s == null) return s;19if (typeof s == "string" && s.endsWith("%")) return undefined;20return typeof s == "number" ? s : parseFloat(s);21}2223register({24slateType: "image",2526fromSlate: ({ node }) => {27// 28let src = node.src ?? "";29let alt = node.alt ?? "";30let title = node.title ?? "";31let width = node.width;32let height = node.height;33if (!width && !height && !src.match(/\s/)) {34// no width, no height and src has no spaces in it!35// Our markdown processes doesn't work when the36// image url has a space (or at least it is a pain37// to escape it), and using quotes doesn't easily38// workaround that so we just use an img take when39// src has whitespace (below).40if (title.length > 0) {41title = ` \"${title}\"`;42}43return ``;44} else {45// width or height require using html instead, unfortunately...46width = width ? ` width="${width}"` : "";47height = height ? ` height="${height}"` : "";48title = title ? ` title="${title}"` : "";49src = src ? `src="${src}"` : "";50alt = alt ? ` alt="${alt}"` : "";51// Important: this **must** start with '<img ' right now52// due to our fairly naive parsing code for html blocks.53//54// Also, object-fit: allows us to specify the55// width and height and have a general CSS max-width,56// without screwing up the aspect ratio.57return `<img ${src} ${alt} ${width} ${height} ${title} style="object-fit:cover"/>`;58}59},6061Element: ({ attributes, children, element }) => {62const node = element as Image;63const { src, alt, title } = node;6465const [width, setWidth] = useState<number | undefined>(toFloat(node.width));66const [height, setHeight] = useState<number | undefined>(67toFloat(node.height)68);6970useEffect(() => {71if (node.width && width != toFloat(node.width)) {72setWidth(toFloat(node.width));73}74if (node.height && height != toFloat(node.height)) {75setHeight(toFloat(node.height));76}77}, [element]);7879const focused = useFocused();80const selected = useSelected();81const border = `2px solid ${82focused && selected ? FOCUSED_COLOR : "transparent"83}`;8485const ref = useProcessLinks([src]);86const imageRef = useRef<any>(null);8788const editor = useSlate();89const setElement = useSetElement(editor, element);90const { urlTransform } = useFileContext();9192return (93<span {...attributes}>94<span ref={ref} contentEditable={false}>95<Resizable96style={{97display: "inline-block",98border,99maxWidth: "100%",100}}101lockAspectRatio={true}102size={103width != null && height != null104? {105width,106height,107}108: undefined109}110onResizeStop={(_e, _direction, _ref, d) => {111if (width == null || height == null) return;112const new_width = width + d.width;113const new_height = height + d.height;114setElement({115height: `${new_height}px`,116width: `${new_width}px`,117});118editor.saveValue();119setWidth(new_width);120setHeight(new_height);121}}122>123<img124onLoad={() => {125const elt = $(imageRef.current);126const width = elt.width() ?? 0;127const height = elt.height() ?? 0;128setWidth(width);129setHeight(height);130}}131ref={imageRef}132src={urlTransform?.(src, 'img') ?? src}133alt={alt}134title={title}135style={{136height: height ?? node.height,137width: width ?? node.width,138maxWidth: "100%",139maxHeight: "100%",140objectFit: "cover",141}}142/>143</Resizable>144</span>145{children}146</span>147);148},149});150151152