UNPKG

@atlaskit/editor-plugin-quick-insert

Version:

Quick insert plugin for @atlaskit/editor-core

265 lines (262 loc) • 10.2 kB
import { ASK_ROVO_MENU_ITEM, MENU, RECOMMENDED_SECTION, SEARCH_RESULTS_SECTION } from '@atlaskit/editor-common/quick-insert/keys'; import { applyRegisteredItemRanks } from '@atlaskit/editor-common/registered-menu/item-rank'; import { isMenuFooterSectionKey } from '@atlaskit/editor-common/type-ahead-is-menu-footer-section-key'; import { isSectionOverflowItemKey } from '@atlaskit/editor-common/type-ahead-is-section-overflow-item-key'; import { getComponentIdentity, resolveSurface, willComponentRender } from '@atlaskit/editor-ui-control-model/surface-renderer'; import { normalizeQuickInsertMatchResult } from '../normalizeQuickInsertMatchResult'; import { QuickInsertSearchResultsSection } from './QuickInsertSearchResultsSection'; const SEARCH_TOP_MATCH_LIMIT = 5; const SEARCH_RESULTS_HEADING = { key: SEARCH_RESULTS_SECTION.key, type: SEARCH_RESULTS_SECTION.type, component: QuickInsertSearchResultsSection, parents: [{ key: MENU.key, type: MENU.type, rank: 0 }] }; const getParentRank = (component, parentKey) => { var _component$parents$fi, _component$parents, _component$parents$fi2; return (_component$parents$fi = (_component$parents = component.parents) === null || _component$parents === void 0 ? void 0 : (_component$parents$fi2 = _component$parents.find(parent => parent.key === parentKey)) === null || _component$parents$fi2 === void 0 ? void 0 : _component$parents$fi2.rank) !== null && _component$parents$fi !== void 0 ? _component$parents$fi : Number.MAX_SAFE_INTEGER; }; const compareMatchedItems = (a, b) => a.sectionRank - b.sectionRank || a.score - b.score || a.itemRank - b.itemRank || a.sourceIndex - b.sourceIndex || a.identity.localeCompare(b.identity); const compareSearchResults = (a, b) => a.score - b.score || a.sectionRank - b.sectionRank || a.itemRank - b.itemRank || a.sourceIndex - b.sourceIndex || a.identity.localeCompare(b.identity); const comparePrioritizedSearchResults = (a, b) => { var _b$priorityGroup, _a$priorityGroup; return ((_b$priorityGroup = b.priorityGroup) !== null && _b$priorityGroup !== void 0 ? _b$priorityGroup : 0) - ((_a$priorityGroup = a.priorityGroup) !== null && _a$priorityGroup !== void 0 ? _a$priorityGroup : 0) || compareSearchResults(a, b); }; const getSearchResults = matches => { const topMatches = Array.from(matches).sort(compareSearchResults).slice(0, SEARCH_TOP_MATCH_LIMIT); const topMatchIdentities = new Set(topMatches.map(({ identity }) => identity)); return [...topMatches, ...matches.filter(({ identity }) => !topMatchIdentities.has(identity))]; }; /** Recommended uses resolved items rather than registered children. */ const readMenuSections = ({ childrenMap, recommendedItems = [], sections, surfaceContext }) => { const renderedSections = []; for (const section of sections) { var _section$isHidden, _section$isHidden2, _childrenMap$get; if (section.type !== 'menu-section') { continue; } if (surfaceContext ? (_section$isHidden = section.isHidden) === null || _section$isHidden === void 0 ? void 0 : _section$isHidden.call(section, { surfaceContext }) : (_section$isHidden2 = section.isHidden) === null || _section$isHidden2 === void 0 ? void 0 : _section$isHidden2.call(section)) { continue; } if (section.key === RECOMMENDED_SECTION.key) { renderedSections.push({ section, items: recommendedItems }); continue; } const children = (_childrenMap$get = childrenMap.get(getComponentIdentity(section))) !== null && _childrenMap$get !== void 0 ? _childrenMap$get : []; const items = []; let viewMoreItem; for (const child of children) { if (child.type !== 'menu-item' || !willComponentRender(child, childrenMap, surfaceContext)) { continue; } if (isSectionOverflowItemKey(child.key)) { viewMoreItem ??= child; } else { items.push(child); } } // Empty registrations are visible leaves; containers need a visible child. if (children.length === 0 || items.length > 0 || viewMoreItem) { renderedSections.push({ section, items, viewMoreItem }); } } return renderedSections; }; const getOwnerSectionKey = (item, isRecommendedItem) => { var _item$parents$; return isRecommendedItem(item) ? RECOMMENDED_SECTION.key : (_item$parents$ = item.parents[0]) === null || _item$parents$ === void 0 ? void 0 : _item$parents$.key; }; const buildBrowseSections = ({ contentSections, isRecommendedItem, selectCategoryItems }) => { const sections = []; for (const { items, section, viewMoreItem } of contentSections) { const ownedItems = items.filter(item => getOwnerSectionKey(item, isRecommendedItem) === section.key); if (ownedItems.length === 0) { continue; } const selectedItems = viewMoreItem && selectCategoryItems ? selectCategoryItems(ownedItems) : ownedItems; sections.push([section, ...selectedItems, ...(viewMoreItem && selectedItems.length < ownedItems.length ? [viewMoreItem] : [])]); } return { fallbackItems: [], sections }; }; const buildSearchSections = ({ components, contentSections, formatMessage, isRecommendedItem, query, root }) => { const sourceIndexByComponent = new Map(components.map((component, index) => [component, index])); const matches = new Map(); let fallbackItem; for (const { items, section } of contentSections) { const sectionRank = getParentRank(section, root.key); for (const item of items) { var _item$match, _sourceIndexByCompone; if (item.key === ASK_ROVO_MENU_ITEM.key) { fallbackItem ??= item; } const match = (_item$match = item.match) === null || _item$match === void 0 ? void 0 : _item$match.call(item, { formatMessage, itemKey: item.key, query }); if (item.match && match === null || getOwnerSectionKey(item, isRecommendedItem) !== section.key) { continue; } const normalizedMatch = normalizeQuickInsertMatchResult(match); const candidate = { identity: getComponentIdentity(item), item, itemRank: getParentRank(item, section.key), ...normalizedMatch, sectionRank, sourceIndex: (_sourceIndexByCompone = sourceIndexByComponent.get(item)) !== null && _sourceIndexByCompone !== void 0 ? _sourceIndexByCompone : Number.MAX_SAFE_INTEGER }; const existing = matches.get(candidate.identity); if (!existing || compareMatchedItems(candidate, existing) < 0) { matches.set(candidate.identity, candidate); } } } const uniqueMatches = Array.from(matches.values()).sort(compareMatchedItems); const prioritizedMatches = uniqueMatches.filter(({ priorityGroup }) => priorityGroup !== undefined); const unprioritizedMatches = uniqueMatches.filter(({ priorityGroup }) => priorityGroup === undefined); const orderedSearchResults = [...prioritizedMatches.sort(comparePrioritizedSearchResults).map(({ item }) => item), ...getSearchResults(unprioritizedMatches).map(({ item }) => item)]; if (orderedSearchResults.length > 0) { return { fallbackItems: [], sections: [[SEARCH_RESULTS_HEADING, ...orderedSearchResults]] }; } return { fallbackItems: fallbackItem ? [fallbackItem] : [], sections: [] }; }; /** * Resolves the Recommended items for a browse. Receives the unranked components so a resolver * can look recommendations up by their registered keys. */ /** * Builds the registered quick-insert menu model. An empty query browses every * category; a non-empty query returns ranked matches. The footer is shared by both. */ export const getQuickInsertMenuModel = ({ components, formatMessage, getItemRank, getRecommendedItems, query, rootComponent, selectCategoryItems, surfaceContext }) => { var _root$isHidden, _root$isHidden2, _contentSections$find; const rankedComponents = applyRegisteredItemRanks(components, getItemRank); const { childrenMap, root, topLevelChildren = [] } = resolveSurface(rankedComponents, rootComponent); if (!root || (surfaceContext ? (_root$isHidden = root.isHidden) === null || _root$isHidden === void 0 ? void 0 : _root$isHidden.call(root, { surfaceContext }) : (_root$isHidden2 = root.isHidden) === null || _root$isHidden2 === void 0 ? void 0 : _root$isHidden2.call(root))) { return { fallbackItems: [], footer: undefined, root: undefined, sections: [] }; } const isBrowse = query === ''; const renderedSections = readMenuSections({ sections: topLevelChildren, childrenMap, surfaceContext, recommendedItems: isBrowse ? getRecommendedItems === null || getRecommendedItems === void 0 ? void 0 : getRecommendedItems({ components, surfaceContext }) : undefined }); if (topLevelChildren.length > 0 && renderedSections.length === 0) { return { fallbackItems: [], footer: undefined, root: undefined, sections: [] }; } const footerSection = renderedSections.find(({ section }) => isMenuFooterSectionKey(section.key)); const contentSections = renderedSections.filter(({ section }) => !isMenuFooterSectionKey(section.key) && (isBrowse || section.key !== RECOMMENDED_SECTION.key)); const recommendedIdentities = new Set((_contentSections$find = contentSections.find(({ section }) => section.key === RECOMMENDED_SECTION.key)) === null || _contentSections$find === void 0 ? void 0 : _contentSections$find.items.map(getComponentIdentity)); const isRecommendedItem = item => recommendedIdentities.has(getComponentIdentity(item)); const content = isBrowse ? buildBrowseSections({ contentSections, isRecommendedItem, selectCategoryItems }) : buildSearchSections({ components: rankedComponents, contentSections, formatMessage, isRecommendedItem, query, root }); const footer = footerSection === null || footerSection === void 0 ? void 0 : footerSection.items[0]; return { ...content, footer, root }; };