UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

222 lines (218 loc) • 7.44 kB
import { SortOrder } from '../types'; export let ContentType = /*#__PURE__*/function (ContentType) { ContentType[ContentType["NUMBER"] = 0] = "NUMBER"; ContentType[ContentType["TEXT"] = 5] = "TEXT"; ContentType[ContentType["MENTION"] = 10] = "MENTION"; ContentType[ContentType["DATE"] = 15] = "DATE"; ContentType[ContentType["STATUS"] = 20] = "STATUS"; ContentType[ContentType["LINK"] = 25] = "LINK"; return ContentType; }({}); function getLinkMark(node) { const [linkMark] = node.marks.filter(mark => mark.type.name === 'link'); return linkMark || null; } function parseLocaleNumber(stringNumber, groupPattern, fractionPattern) { if (!stringNumber.trim().length) { return null; } const maybeANumber = Number.parseFloat(stringNumber.replace(groupPattern, '').replace(fractionPattern, '.')); if (Number.isNaN(maybeANumber)) { return null; } return maybeANumber; } export function createNormalizeTextParser() { // Source: https://stackoverflow.com/questions/12004808/does-javascript-take-local-decimal-separators-into-account const locale = window.navigator.language; const thousandSeparator = Intl.NumberFormat(locale).format(11111).replace(/\p{Number}/gu, ''); const decimalSeparator = Intl.NumberFormat(locale).format(1.1).replace(/\p{Number}/gu, ''); const numericPattern = new RegExp(`(\\d+(?:[${thousandSeparator}${decimalSeparator}]?\\d+)*)`, 'g'); const thousandSeparatorPattern = new RegExp('\\' + thousandSeparator, 'g'); const decimalSeparatorPattern = new RegExp('\\' + decimalSeparator); return text => { if (!text.trim().length) { return null; } // This will break the text apart at the locations of the formatted numbers const result = text.split(numericPattern); // We then put the text back together but with all the formatted numbers converted back to plain numerals, // for example a sentence like "What is 1,000.01% of 10,000.01" would be normalized and sorted as // if it's saying "What is 1000.01% of 10000.01". This way the Intl.Collator can use the numeric setting to sort // numeral values within string correctly. const tokens = result.reduce((acc, stringNumber) => { if (!(stringNumber !== null && stringNumber !== void 0 && stringNumber.length)) { return acc; } const maybeANumber = parseLocaleNumber(stringNumber, thousandSeparatorPattern, decimalSeparatorPattern); // NOTE: We know there can only be a single decimal separator. So we can assume that if the first found separator // is not at the same position as the last found one, then we can assume the locale used to format the number // is different to our locale. This will result in the value being treated as a string. if (maybeANumber !== null && stringNumber.indexOf(decimalSeparator) === stringNumber.lastIndexOf(decimalSeparator)) { acc.push(maybeANumber); } else { acc.push(stringNumber); } return acc; }, []); if (tokens.length === 1) { return tokens[0]; } return tokens.join(''); }; } export function extractMetaFromTextNode(textNode, normalizeTextParser) { // treat as a link if contain a link const linkMark = getLinkMark(textNode); if (linkMark) { const value = textNode.text || ''; return { type: ContentType.LINK, value }; } const text = textNode.text || ''; const normalizedText = normalizeTextParser(text); if (typeof normalizedText === 'number') { return { type: ContentType.NUMBER, value: normalizedText }; } return { type: ContentType.TEXT, value: normalizedText !== null && normalizedText !== void 0 ? normalizedText : text }; } function getMetaFromNode(node, options, normalizeTextParser) { if (!node) { return null; } const firstChild = node.firstChild; if (!firstChild) { return null; } switch (firstChild.type.name) { // Text case /* Get Meta value from the first child if the cell is of type * Heading (Any cell where the text is set to a heading type) * Paragraph (Normal text) */ case 'heading': case 'paragraph': { return getMetaFromNode(firstChild, options, normalizeTextParser); } case 'inlineCard': { const attrs = firstChild.attrs; const maybeTitle = options.getInlineCardTextFromStore(attrs); if (maybeTitle) { return { type: ContentType.LINK, value: maybeTitle }; } const url = attrs.url; return { type: ContentType.LINK, value: url ? url : '' }; } case 'text': { return extractMetaFromTextNode(firstChild, normalizeTextParser); } case 'status': { const text = firstChild.attrs.text; return { type: ContentType.STATUS, value: text }; } case 'date': { const timestamp = Number.parseInt(firstChild.attrs.timestamp, 20); return { type: ContentType.DATE, value: timestamp }; } case 'mention': { // TODO: Check what should be the fallback when mention does not have a text const text = firstChild.attrs.text || ''; return { type: ContentType.MENTION, value: text.toLowerCase() }; } default: return null; } } function compareValue(valueA, valueB) { if (valueA === valueB) { return 0; } if (typeof valueA === 'string' && typeof valueB === 'string') { return valueA.localeCompare(valueB, window.navigator.language, { caseFirst: 'upper', numeric: true }); } return valueA > valueB ? 1 : -1; } /** * Compare 2 prosemirror nodes and check if it's greater, equal or less than the other node * based on the sort order. * * @param {Node} nodeA * @param {Node} nodeB * @returns {(1 | 0 | -1)} * * For Ascending order: * 1 -> NodeA > NodeB * 0 -> NodeA === NodeB * -1 -> Node A < NodeB * For Descending order: * 1 -> NodeA < NodeB * 0 -> NodeA === NodeB * -1 -> Node A > NodeB * * If either node is empty: * The empty node is always treated as lower priority, * irrespective of the order. * * If no order is provided the method defaults to Ascending order, * like a regular JS sort method. */ export const createCompareNodes = (options, order = SortOrder.ASC) => { const normalizeTextParser = createNormalizeTextParser(); return (nodeA, nodeB) => { const metaNodeA = getMetaFromNode(nodeA, options, normalizeTextParser); const metaNodeB = getMetaFromNode(nodeB, options, normalizeTextParser); /* Donot switch the order (Asec or Desc) if either node is null. This will ensure that empty cells are always at the bottom during sorting. */ if (metaNodeA === null || metaNodeB === null) { return compareMetaFromNode(metaNodeA, metaNodeB); } return (order === SortOrder.DESC ? -1 : 1) * compareMetaFromNode(metaNodeA, metaNodeB); }; }; function compareMetaFromNode(metaNodeA, metaNodeB) { if (metaNodeA === metaNodeB) { return 0; } if (metaNodeA === null || metaNodeB === null) { return metaNodeB === null ? -1 : 1; } if (metaNodeA.type !== metaNodeB.type) { return metaNodeA.type > metaNodeB.type ? 1 : -1; } return compareValue(metaNodeA.value, metaNodeB.value); }