@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
222 lines (218 loc) • 7.44 kB
JavaScript
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);
}