import defaultTo from 'lodash/defaultTo'; import { IFormattingTool } from '../types'; export interface IRichTextFormat { type: string; } export interface IRichTextValue { text: string; formats: IRichTextFormat[][]; start: number|null; end: number|null; } export const findToolForElement = ( tools: IFormattingTool[], element: Element, ): IFormattingTool|null => tools.find(tool => { if (tool.tagName && element.tagName !== tool.tagName) { return false; } if (tool.className && !element.classList.contains(tool.className)) { return false; } return true; }) || null; export const createFromString = ( value: string = '', formats: IRichTextFormat[] = [], ): IRichTextValue => ({ text: value, formats: (new Array(value.length)).fill([...formats]), start: null, end: null, }); const createFromDOMNodeRecursively = ( tools: IFormattingTool[], node: ChildNode, formats: IRichTextFormat[] = [], ): IRichTextValue => { if (node.nodeType === node.TEXT_NODE) { return createFromString(node.textContent || '', formats); } if (node.nodeType === node.ELEMENT_NODE) { return createFromDOMElement(tools, node as Element, formats); } return createFromString('', []); }; export const createFromDOMElement = ( tools: IFormattingTool[], element: Element, formats: IRichTextFormat[] = [], ): IRichTextValue => { const tool = findToolForElement(tools, element); const subFormats = tool ? [ ...formats.filter(f => f.type !== tool.name), { type: tool.name }, ] : [ ...formats ]; const nodes = Array.from(element.childNodes); return concat(...nodes.map(node => createFromDOMNodeRecursively(tools, node, subFormats))); }; export const createFromDOMString = ( tools: IFormattingTool[], value: string, formats: IRichTextFormat[] = [], ): IRichTextValue => { const div = document.createElement('div'); div.innerHTML = value; return createFromDOMElement(tools, div, formats); }; export const create = ( tools: IFormattingTool[], value: Element|string = '', ): IRichTextValue => { if (typeof value === 'string') { return createFromDOMString(tools, value); } return createFromDOMElement(tools, value); } export const toHTMLString = ( tools: IFormattingTool[], value: IRichTextValue, ) => { const tools = [...tools].sort((a, b) => { const tensionA = defaultTo(a.surfaceTension, Infinity); const tensionB = defaultTo(b.surfaceTension, Infinity); return tensionA - tensionB; }); const elementTree = []; const string = ''; for (let i = 0; i < value.text.length; i++) { const c = value.text[i]; const formats = value.formats[i]; const activeFormats = value.formats[i - 1] || []; const removedFormats = activeFormats .filter(a => !formats .find(f => f.type === a.type && JSON.stringify(f) === JSON.stringify(a))); const addedFormats = formats .filter(f => !activeFormats .find(a => a.type === f.type && JSON.stringify(a) === JSON.stringify(f))); console.log(c); for (let removedFormat of removedFormats) { const tool = tools.find(tool => tool.name === removedFormat.type); if (!tool) { continue; } tool. } } return string; } export const getActiveFormats = (richTextValue: IRichTextValue): IRichTextFormat[] => richTextValue.start !== null ? richTextValue.formats[richTextValue.start] : []; export const applyFormat = ( value: IRichTextValue, format: IRichTextFormat, startIndex?: number, endIndex?: number, ): IRichTextValue => { const start = defaultTo(defaultTo(startIndex, value.start), 0); const end = defaultTo(defaultTo(endIndex, value.end), value.text.length); return { ...value, formats: [ ...value.formats.slice(0, start), ...value.formats.slice(start, end).map(letterFormatList => [ ...letterFormatList.filter(letterFormat => letterFormat.type === format.type), format, ]), ...value.formats.slice(end), ], } }; export const removeFormat = ( value: IRichTextValue, format: IRichTextFormat, startIndex?: number, endIndex?: number, ): IRichTextValue => { const start = defaultTo(defaultTo(startIndex, value.start), 0); const end = defaultTo(defaultTo(endIndex, value.end), value.text.length); return { ...value, formats: [ ...value.formats.slice(0, start), ...value.formats.slice(start, end) .map(letterFormatList => letterFormatList.filter(letterFormat => letterFormat.type === format.type)), ...value.formats.slice(end), ], }; } export const toggleFormat = ( value: IRichTextValue, format: IRichTextFormat, ): IRichTextValue => { const activeFormats = getActiveFormats(value); if (activeFormats.find(f => f.type === format.type)) { return removeFormat(value, format); } return applyFormat(value, format); } export const concat = (...richTextValues:IRichTextValue[]): IRichTextValue => richTextValues .reduce((newValue, value) => ({ text: newValue.text + value.text, formats: [ ...newValue.formats, ...value.formats, ], start: newValue.start !== null ? newValue.start : value.start, end: value.end !== null ? value.end : newValue.end, }), { text: '', formats: [], start: null, end: null }); export const join = ( richTextValues:IRichTextValue[], separator?: string|IRichTextValue, ): IRichTextValue => richTextValues .reduce((total, value) => concat( total, ...(separator ? [typeof separator === 'string' ? createFromString(separator) : separator] : []), value, ), createFromString()); export const slice = ( value: IRichTextValue, startIndex?: number, endIndex?: number, ): IRichTextValue => { const start = defaultTo(defaultTo(startIndex, value.start), 0); const end = defaultTo(defaultTo(endIndex, value.end), value.text.length); return { text: value.text.slice(start, end), formats: value.formats.slice(start, end), start: value.start !== null ? (value.start >= start ? value.start - start : null) : null, end: value.end !== null ? (value.end <= end ? end - value.end : null) : null, }; }; export const insert = ( value: IRichTextValue, valueToInsert: IRichTextValue|string, startIndex?: number, endIndex?: number, ) => { const start = defaultTo(defaultTo(startIndex, value.start), value.text.length); const end = defaultTo(defaultTo(endIndex, value.end), value.text.length); return concat( slice(value, 0, start), typeof valueToInsert === 'string' ? createFromString(valueToInsert) : valueToInsert, slice(value, end, value.text.length), ); } export const split = ( value: IRichTextValue, valueToInsert: IRichTextValue|string, startIndex?: number, endIndex?: number, ) => { const start = defaultTo(defaultTo(startIndex, value.start), value.text.length); const end = defaultTo(defaultTo(endIndex, value.end), value.text.length); return concat( slice(value, 0, start), typeof valueToInsert === 'string' ? createFromString(valueToInsert) : valueToInsert, slice(value, end, value.text.length), ); }