Skip to content

Commit

Permalink
Clean up the code
Browse files Browse the repository at this point in the history
  • Loading branch information
Skalakid committed Aug 29, 2024
1 parent b08bc5a commit ade3501
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 166 deletions.
6 changes: 5 additions & 1 deletion src/styleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ function mergeMarkdownStyleWithDefault(input: PartialMarkdownStyle | undefined):
return output;
}

function parseStyleToNumber(style: string | null): number {
return style ? parseInt(style.replace('px', ''), 10) : 0;
}

export type {PartialMarkdownStyle};

export {mergeMarkdownStyleWithDefault};
export {mergeMarkdownStyleWithDefault, parseStyleToNumber};
168 changes: 168 additions & 0 deletions src/web/markdown/inlineImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type {HTMLMarkdownElement, MarkdownTextInputElement} from '../../MarkdownTextInput.web';
import {parseStyleToNumber} from '../../styleUtils';
import type {PartialMarkdownStyle} from '../../styleUtils';
import type {MarkdownRange} from '../utils/parserUtils';
import type {TreeNode} from '../utils/treeUtils';

function createLoadingIndicator(currentInput: MarkdownTextInputElement, url: string, markdownStyle: PartialMarkdownStyle) {
// Get current spinner animation progress if it exists
const currentSpinner = currentInput.querySelector(`[data-type="spinner"][data-url="${url}"]`)?.firstChild;
let currentTime: CSSNumberish = 0;
if (currentSpinner) {
const animation = (currentSpinner as HTMLMarkdownElement).getAnimations()[0];
if (animation) {
currentTime = animation.currentTime || 0;
}
}

const container = document.createElement('span');
container.contentEditable = 'false';

const spinner = document.createElement('span');
const spinnerStyles = markdownStyle.loadingIndicator;
if (spinnerStyles) {
const spinnerBorderWidth = spinnerStyles.borderWidth || 3;
Object.assign(spinner.style, {
border: `${spinnerBorderWidth}px solid ${String(spinnerStyles.secondaryColor)}`,
borderTop: `${spinnerBorderWidth}px solid ${String(spinnerStyles.primaryColor)}`,
borderRadius: '50%',
width: spinnerStyles.width || '20px',
height: spinnerStyles.height || '20px',
display: 'block',
animationPlayState: 'paused',
});
}

const containerStyles = markdownStyle.loadingIndicatorContainer;
Object.assign(container.style, {
...markdownStyle.loadingIndicatorContainer,
position: 'absolute',
bottom: '0',
left: '0',
width: containerStyles?.width || 'auto',
height: containerStyles?.height || 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
container.setAttribute('data-type', 'spinner');
container.setAttribute('data-url', url);
container.contentEditable = 'false';
container.appendChild(spinner);

const keyframes = [{transform: 'rotate(0deg)'}, {transform: 'rotate(360deg)'}];

const options = {
duration: 1000,
iterations: Infinity,
};
const animation2 = spinner.animate(keyframes, options);
animation2.currentTime = currentTime;
return container;
}

/* Replaces element in the tree, that is beeing builded, with the element from the currently rendered input (with previous state) */
function replaceElementInTreeNode(targetNode: TreeNode, newElement: HTMLMarkdownElement) {
// Clear newElement from its children
[...newElement.children].forEach((child) => {
child.remove();
});
newElement.remove();

// Move all children from targetNode to newElement
[...targetNode.element.children].forEach((child) => {
newElement.appendChild(child);
});
targetNode.element.remove();

targetNode.parentNode?.element.appendChild(newElement);
return {...targetNode, element: newElement};
}

function getImageMeta(url: string, callback: (err: string | Event | null, img?: HTMLImageElement) => void) {
const img = new Image();
img.onload = () => callback(null, img);
img.onerror = (err) => callback(err);
img.src = url;
}

/* The main function that adds inline image preview to the node */
function addInlineImagePreview(currentInput: MarkdownTextInputElement, targetNode: TreeNode, text: string, ranges: MarkdownRange[], markdownStyle: PartialMarkdownStyle) {
const linkRange = ranges.find((r) => r.type === 'link');
let imageHref = '';
if (linkRange) {
imageHref = text.substring(linkRange.start, linkRange.start + linkRange.length);
}

// If the inline image markdown with the same href is already loaded, replace the targetNode with the already loaded preview
const alreadyLoadedPreview = currentInput.querySelector(`[data-image-href="${imageHref}"]`);
if (alreadyLoadedPreview) {
return replaceElementInTreeNode(targetNode, alreadyLoadedPreview as HTMLMarkdownElement);
}

// Add a loading spinner
const spinner = createLoadingIndicator(currentInput, imageHref, markdownStyle);
if (spinner) {
targetNode.element.appendChild(spinner);
}

const maxWidth = parseStyleToNumber(`${markdownStyle.inlineImage?.maxWidth}`);
const maxHeight = parseStyleToNumber(`${markdownStyle.inlineImage?.maxHeight}`);
const imageMarginTop = parseStyleToNumber(`${markdownStyle.inlineImage?.marginTop}`);
const imageMarginBottom = parseStyleToNumber(`${markdownStyle.inlineImage?.marginBottom}`);

Object.assign(targetNode.element.style, {
display: 'block',
marginBottom: `${imageMarginBottom}px`,
paddingBottom: markdownStyle.loadingIndicatorContainer?.height || markdownStyle.loadingIndicator?.height || (!!markdownStyle.loadingIndicator && '30px') || undefined,
});

getImageMeta(imageHref, (_err, img) => {
if (!img || _err) {
return;
}

// Verify if the current spinner is for the loaded image. If not, it means that the response came after the user changed the image url
const currentSpinner = currentInput.querySelector('[data-type="spinner"]');
if (currentSpinner !== spinner) {
return;
}

// Remove the spinner
if (currentSpinner) {
currentSpinner.remove();
}

targetNode.element.setAttribute('data-image-href', imageHref);

// Calcate the image preview size and apply styles
const {naturalWidth, naturalHeight} = img;
let width: number | null = null;
let height: number | null = null;

let paddingValue = 0;
if (naturalWidth > naturalHeight) {
width = Math.min(maxWidth, naturalWidth);
paddingValue = (width / naturalWidth) * naturalHeight;
} else {
height = Math.min(maxHeight, naturalHeight);
paddingValue = height;
}

const widthSize = width ? `${width}px` : 'auto';
const heightSize = height ? `${height}px` : 'auto';

Object.assign(targetNode.element.style, {
backgroundImage: `url("${imageHref}")`,
backgroundPosition: `bottom left`,
backgroundSize: `${widthSize} ${heightSize}`,
backgroundRepeat: `no-repeat`,
paddingBottom: `${imageMarginTop + paddingValue}px`,
});
});

return targetNode;
}

// eslint-disable-next-line import/prefer-default-export
export {addInlineImagePreview};
166 changes: 4 additions & 162 deletions src/web/utils/blockUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import type {HTMLMarkdownElement} from '../../MarkdownTextInput.web';
import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
import type {PartialMarkdownStyle} from '../../styleUtils';
import {addInlineImagePreview} from '../markdown/inlineImages';
import type {MarkdownRange} from './parserUtils';
import type {NodeType, TreeNode} from './treeUtils';

function parseStyleToNumber(style: string | null) {
return style ? parseInt(style.replace('px', ''), 10) : 0;
}

function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownStyle: PartialMarkdownStyle) {
const node = targetElement;
switch (type) {
Expand Down Expand Up @@ -83,63 +80,6 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
}
}

function createLoadingIndicator(url: string, markdownStyle: PartialMarkdownStyle) {
// Get current spinner animation progress if it exists
const currentSpinner = document.querySelector('[data-type="spinner"]')?.firstChild;
let currentTime: CSSNumberish = 0;
if (currentSpinner) {
const animation = (currentSpinner as HTMLMarkdownElement).getAnimations()[0];
if (animation) {
currentTime = animation.currentTime || 0;
}
}

const container = document.createElement('span');
container.contentEditable = 'false';

const spinner = document.createElement('span');
const spinnerStyles = markdownStyle.loadingIndicator;
if (spinnerStyles) {
const spinnerBorderWidth = spinnerStyles.borderWidth || 3;
Object.assign(spinner.style, {
border: `${spinnerBorderWidth}px solid ${String(spinnerStyles.secondaryColor)}`,
borderTop: `${spinnerBorderWidth}px solid ${String(spinnerStyles.primaryColor)}`,
borderRadius: '50%',
width: spinnerStyles.width || '20px',
height: spinnerStyles.height || '20px',
display: 'block',
animationPlayState: 'paused',
});
}

container.setAttribute('data-type', 'spinner');
container.setAttribute('data-url', url);
const containerStyles = markdownStyle.loadingIndicatorContainer;
Object.assign(container.style, {
...markdownStyle.loadingIndicatorContainer,
position: 'absolute',
bottom: '0',
left: '0',
width: containerStyles?.width || 'auto',
height: containerStyles?.height || 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
container.contentEditable = 'false';
container.appendChild(spinner);

const keyframes = [{transform: 'rotate(0deg)'}, {transform: 'rotate(360deg)'}];

const options = {
duration: 1000,
iterations: Infinity,
};
const animation2 = spinner.animate(keyframes, options);
animation2.currentTime = currentTime;
return container;
}

const BLOCK_MARKDOWN_TYPES = ['inline-image'];

function isBlockMarkdownType(type: NodeType) {
Expand All @@ -152,7 +92,7 @@ function getFirstBlockMarkdownRange(ranges: MarkdownRange[]) {
}

function extendBlockStructure(
inputElement: HTMLMarkdownElement,
currentInput: MarkdownTextInputElement,
targetNode: TreeNode,
currentRange: MarkdownRange,
ranges: MarkdownRange[],
Expand All @@ -161,110 +101,12 @@ function extendBlockStructure(
) {
switch (currentRange.type) {
case 'inline-image':
return addInlineImagePreview(inputElement, targetNode, text, ranges, markdownStyle);
return addInlineImagePreview(currentInput, targetNode, text, ranges, markdownStyle);
default:
break;
}

return targetNode;
}

function replaceElementInTreeNode(targetNode: TreeNode, newElement: HTMLMarkdownElement) {
// Clear newElement from its children
[...newElement.children].forEach((child) => {
child.remove();
});
newElement.remove();

// Move all children from targetNode to newElement
[...targetNode.element.children].forEach((child) => {
newElement.appendChild(child);
});
targetNode.element.remove();

targetNode.parentNode?.element.appendChild(newElement);
return {...targetNode, element: newElement};
}

function getImageMeta(url: string, callback: (err: string | Event | null, img?: HTMLImageElement) => void) {
const img = new Image();
img.onload = () => callback(null, img);
img.onerror = (err) => callback(err);
img.src = url;
}

function addInlineImagePreview(inputElement: HTMLMarkdownElement, targetNode: TreeNode, text: string, ranges: MarkdownRange[], markdownStyle: PartialMarkdownStyle) {
const linkRange = ranges.find((r) => r.type === 'link');
let imageHref = '';
if (linkRange) {
imageHref = text.substring(linkRange.start, linkRange.start + linkRange.length);
}

// If the inline image markdown with the same href is already loaded, replace the targetNode with the already loaded preview
const alreadyLoadedPreview = inputElement.querySelector(`[data-image-href="${imageHref}"]`);
if (alreadyLoadedPreview) {
return replaceElementInTreeNode(targetNode, alreadyLoadedPreview as HTMLMarkdownElement);
}

const spinner = createLoadingIndicator(imageHref, markdownStyle);
if (spinner) {
targetNode.element.appendChild(spinner);
}

const maxWidth = parseStyleToNumber(`${markdownStyle.inlineImage?.maxWidth}`) || 0;
const maxHeight = parseStyleToNumber(`${markdownStyle.inlineImage?.maxHeight}`) || 0;
const imageMarginTop = parseStyleToNumber(`${markdownStyle.inlineImage?.marginTop}`) || 0;
const imageMarginBottom = parseStyleToNumber(`${markdownStyle.inlineImage?.marginBottom}`) || 0;

Object.assign(targetNode.element.style, {
display: 'block',
marginBottom: `${imageMarginBottom}px`,
paddingBottom: markdownStyle.loadingIndicatorContainer?.height || markdownStyle.loadingIndicator?.height || (!!markdownStyle.loadingIndicator && '30px') || undefined,
});

getImageMeta(imageHref, (_err, img) => {
if (!img) {
return;
}

const currentSpinner = inputElement.querySelector('[data-type="spinner"]');
if (currentSpinner !== spinner) {
return;
}

if (currentSpinner) {
currentSpinner.remove();
spinner.remove();
}

targetNode.element.setAttribute('data-image-href', imageHref);

const {naturalWidth, naturalHeight} = img;
let width: number | null = null;
let height: number | null = null;

let paddingValue = 0;
if (naturalWidth > naturalHeight) {
width = Math.min(maxWidth, naturalWidth);
paddingValue = (width / naturalWidth) * naturalHeight;
} else {
height = Math.min(maxHeight, naturalHeight);
paddingValue = height;
}

const widthSize = width ? `${width}px` : 'auto';
const heightSize = height ? `${height}px` : 'auto';

Object.assign(targetNode.element.style, {
backgroundImage: `url("${imageHref}")`,
backgroundPosition: `bottom left`,
backgroundSize: `${widthSize} ${heightSize}`,
backgroundRepeat: `no-repeat`,
paddingBottom: `${imageMarginTop + paddingValue}px`,
});
});

return targetNode;
}

export {addStyleToBlock, extendBlockStructure, isBlockMarkdownType, getFirstBlockMarkdownRange};
Loading

0 comments on commit ade3501

Please sign in to comment.