PageControls

This commit is contained in:
Johannes Zillmann 2021-03-09 08:17:50 +01:00
parent 8b7ae34bbc
commit 45355a9315
14 changed files with 164 additions and 128 deletions

View File

@ -12,14 +12,22 @@ export default class Debugger {
private context: TransformContext; private context: TransformContext;
private transformers: ItemTransformer[]; private transformers: ItemTransformer[];
private stageResultCache: StageResult[]; private stageResultCache: StageResult[];
pageCount: number;
fontMap: Map<string, object>; fontMap: Map<string, object>;
stageNames: string[]; stageNames: string[];
stageDescriptions: string[]; stageDescriptions: string[];
constructor(inputSchema: string[], inputItems: Item[], context: TransformContext, transformers: ItemTransformer[]) { constructor(
pageCount: number,
inputSchema: string[],
inputItems: Item[],
context: TransformContext,
transformers: ItemTransformer[],
) {
this.transformers = transformers; this.transformers = transformers;
this.context = context; this.context = context;
this.fontMap = context.fontMap; this.fontMap = context.fontMap;
this.pageCount = pageCount;
this.stageNames = ['Parse Result', ...transformers.map((t) => t.name)]; this.stageNames = ['Parse Result', ...transformers.map((t) => t.name)];
this.stageDescriptions = ['Initial items as parsed by PDFjs', ...transformers.map((t) => t.description)]; this.stageDescriptions = ['Initial items as parsed by PDFjs', ...transformers.map((t) => t.description)];
this.stageResultCache = [initialStage(inputSchema, inputItems)]; this.stageResultCache = [initialStage(inputSchema, inputItems)];

View File

@ -3,30 +3,13 @@ import type Metadata from './Metadata';
import type PageViewport from './parse/PageViewport'; import type PageViewport from './parse/PageViewport';
export default class ParseResult { export default class ParseResult {
fontMap: Map<string, object>;
pdfjsPages: any[];
pageViewports: PageViewport[];
metadata: Metadata;
schema: string[];
items: Item[];
constructor( constructor(
fontMap: Map<string, object>, public fontMap: Map<string, object>,
pdfjsPages: any[], public pageCount: number,
pageViewports: PageViewport[], public pdfjsPages: any[],
metadata: Metadata, public pageViewports: PageViewport[],
schema: string[], public metadata: Metadata,
items: Item[], public schema: string[],
) { public items: Item[],
this.fontMap = fontMap; ) {}
this.pdfjsPages = pdfjsPages;
this.pageViewports = pageViewports;
this.metadata = metadata;
this.schema = schema;
this.items = items;
}
pageCount(): number {
return this.pdfjsPages.length;
}
} }

View File

@ -23,6 +23,7 @@ export default class PdfParser {
.promise.then((pdfjsDocument: any) => { .promise.then((pdfjsDocument: any) => {
reporter.parsedDocumentHeader(pdfjsDocument.numPages); reporter.parsedDocumentHeader(pdfjsDocument.numPages);
return Promise.all([ return Promise.all([
pdfjsDocument,
pdfjsDocument.getMetadata().then((pdfjsMetadata: any) => { pdfjsDocument.getMetadata().then((pdfjsMetadata: any) => {
reporter.parsedMetadata(); reporter.parsedMetadata();
return new Metadata(pdfjsMetadata); return new Metadata(pdfjsMetadata);
@ -30,10 +31,15 @@ export default class PdfParser {
this.extractPagesSequentially(pdfjsDocument, reporter), this.extractPagesSequentially(pdfjsDocument, reporter),
]); ]);
}) })
.then(([metadata, pages]: [Metadata, ParsedPage[]]) => { .then(([pdfjsDocument, metadata, pages]: [any, Metadata, ParsedPage[]]) => {
return Promise.all([metadata, pages, this.gatherFontObjects(pages).finally(() => reporter.parsedFonts())]); return Promise.all([
pdfjsDocument,
metadata,
pages,
this.gatherFontObjects(pages).finally(() => reporter.parsedFonts()),
]);
}) })
.then(([metadata, pages, fontMap]: [Metadata, ParsedPage[], Map<string, object>]) => { .then(([pdfjsDocument, metadata, pages, fontMap]: [any, Metadata, ParsedPage[], Map<string, object>]) => {
const pdfjsPages = pages.map((page: any) => page.pdfjsPage); const pdfjsPages = pages.map((page: any) => page.pdfjsPage);
const items = pages.reduce((allItems: any[], page: any) => allItems.concat(page.items), []); const items = pages.reduce((allItems: any[], page: any) => allItems.concat(page.items), []);
const pageViewports = pdfjsPages.map((page: any) => { const pageViewports = pdfjsPages.map((page: any) => {
@ -43,7 +49,15 @@ export default class PdfParser {
this.pdfjs.Util.transform(viewPort.transform, itemTransform), this.pdfjs.Util.transform(viewPort.transform, itemTransform),
}; };
}); });
return new ParseResult(fontMap, pdfjsPages, pageViewports, metadata, this.schema, items); return new ParseResult(
fontMap,
pdfjsDocument.numPages,
pdfjsPages,
pageViewports,
metadata,
this.schema,
items,
);
}); });
} }

View File

@ -39,7 +39,7 @@ export default class PdfPipeline {
async debug(src: string | Uint8Array | object, progressListener: ProgressListenFunction): Promise<Debugger> { async debug(src: string | Uint8Array | object, progressListener: ProgressListenFunction): Promise<Debugger> {
const parseResult = await this.parse(src, progressListener); const parseResult = await this.parse(src, progressListener);
const context = { fontMap: parseResult.fontMap, pageViewports: parseResult.pageViewports }; const context = { fontMap: parseResult.fontMap, pageViewports: parseResult.pageViewports };
return new Debugger(parseResult.schema, parseResult.items, context, this.transformers); return new Debugger(parseResult.pageCount, parseResult.schema, parseResult.items, context, this.transformers);
} }
/** /**

View File

@ -22,7 +22,7 @@ export default class StageResult {
}, []); }, []);
} }
selectPages(relevantChangesOnly: boolean, groupItems: boolean, pinnedPage?: number): Page[] { selectPages(relevantChangesOnly: boolean, groupItems: boolean): Page[] {
let result: Page[]; let result: Page[];
// Ungroup pages // Ungroup pages
@ -32,11 +32,6 @@ export default class StageResult {
result = this.pages; result = this.pages;
} }
// Filter to pinned page
if (Number.isInteger(pinnedPage)) {
result = result.filter((page) => page.index === pinnedPage);
}
// Filter out item (groups) with no changes // Filter out item (groups) with no changes
if (relevantChangesOnly && !this.descriptor.debug?.showAll === true) { if (relevantChangesOnly && !this.descriptor.debug?.showAll === true) {
result = result.map( result = result.map(

View File

@ -36,7 +36,7 @@ describe('Transform Items', () => {
const trans1Items = parsedItems.map((item) => item.withData({ C: `c=${item.value('A')}+${item.value('B')}` })); const trans1Items = parsedItems.map((item) => item.withData({ C: `c=${item.value('A')}+${item.value('B')}` }));
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)]; const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers); const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']); expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
expect(debug.stageResults(0).schema).toEqual(parsedSchema.map((column) => ({ name: column }))); expect(debug.stageResults(0).schema).toEqual(parsedSchema.map((column) => ({ name: column })));
@ -62,7 +62,7 @@ describe('Transform Items', () => {
const trans1Items = parsedItems.map((item) => item.withData({ line: item.data['y'] })); const trans1Items = parsedItems.map((item) => item.withData({ line: item.data['y'] }));
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)]; const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers); const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']); expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'y' }]); expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'y' }]);
@ -100,7 +100,7 @@ test('Change inside of Line', async () => {
const trans1Items = swapElements([...parsedItems], 0, 1); const trans1Items = swapElements([...parsedItems], 0, 1);
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)]; const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers); const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']); expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'line' }]); expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'line' }]);
@ -134,7 +134,7 @@ describe('build schemas', () => {
function calculateSchema(inputSchema: string[], outputSchema: string[]): AnnotatedColumn[] { function calculateSchema(inputSchema: string[], outputSchema: string[]): AnnotatedColumn[] {
const transformers = [new TestTransformer('Trans1', {}, outputSchema, items)]; const transformers = [new TestTransformer('Trans1', {}, outputSchema, items)];
const debug = new Debugger(inputSchema, items, { fontMap: new Map(), pageViewports: [] }, transformers); const debug = new Debugger(1, inputSchema, items, { fontMap: new Map(), pageViewports: [] }, transformers);
return debug.stageResults(1).schema; return debug.stageResults(1).schema;
} }

View File

@ -21,7 +21,7 @@ test('basic example PDF parse', async () => {
const expectedPages = 7; const expectedPages = 7;
expect(result.metadata.title()).toEqual('ExamplePdf'); expect(result.metadata.title()).toEqual('ExamplePdf');
expect(result.metadata.author()).toEqual('Johannes Zillmann'); expect(result.metadata.author()).toEqual('Johannes Zillmann');
expect(result.pageCount()).toBe(expectedPages); expect(result.pageCount).toBe(expectedPages);
result.pdfjsPages.forEach((pdfPage, i) => { result.pdfjsPages.forEach((pdfPage, i) => {
expect(pdfPage._pageIndex).toBe(i); expect(pdfPage._pageIndex).toBe(i);
}); });

View File

@ -5,7 +5,6 @@ import AnnotatedColumn from 'src/debug/AnnotatedColumn';
import Page, { asPages } from 'src/debug/Page'; import Page, { asPages } from 'src/debug/Page';
import { items } from '../testItems'; import { items } from '../testItems';
import LineItemMerger from 'src/debug/LineItemMerger'; import LineItemMerger from 'src/debug/LineItemMerger';
import ItemGroup from 'src/debug/ItemGroup';
test('itemsUnpacked', async () => { test('itemsUnpacked', async () => {
const tracker = new ChangeTracker(); const tracker = new ChangeTracker();
@ -77,22 +76,6 @@ describe('select pages', () => {
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]); expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]); expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5]]); expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5]]);
const allGroupedWithPin = result.selectPages(false, true, 0);
expect(allGroupedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[0, 1], [2]]);
const relevantGroupedWithPin = result.selectPages(true, true, 0);
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[0, 1]]);
const relevantUnpackedWithPin = result.selectPages(true, false, 0);
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([]);
const allUnpackedWithPin = result.selectPages(false, false, 0);
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[0], [1], [2]]);
}); });
test('Changes on element level', async () => { test('Changes on element level', async () => {
@ -141,22 +124,6 @@ describe('select pages', () => {
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]); expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]); expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5], [6]]); expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5], [6]]);
const allGroupedWithPin = result.selectPages(false, true, 2);
expect(allGroupedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[4, 5], [6]]);
const relevantGroupedWithPin = result.selectPages(true, true, 2);
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[4, 5]]);
const relevantUnpackedWithPin = result.selectPages(true, false, 2);
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([[5]]);
const allUnpackedWithPin = result.selectPages(false, false, 2);
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[4], [5], [6]]);
}); });
test('showAll - grouped', async () => { test('showAll - grouped', async () => {

View File

@ -8,8 +8,8 @@
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons'; import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
import { debugStage } from '../config'; import { debugStage } from '../config';
import type StageResult from '@core/debug/StageResult';
import PageControl from './PageControl';
import Popup from '../components/Popup.svelte'; import Popup from '../components/Popup.svelte';
import PageSelectionPopup from './PageSelectionPopup.svelte'; import PageSelectionPopup from './PageSelectionPopup.svelte';
import Checkbox from '../components/Checkbox.svelte'; import Checkbox from '../components/Checkbox.svelte';
@ -18,26 +18,24 @@
export let stageNames: string[]; export let stageNames: string[];
export let stageDescriptions: string[]; export let stageDescriptions: string[];
export let pageControl: PageControl;
export let fontMap: Map<string, object>; export let fontMap: Map<string, object>;
export let stageResult: StageResult;
export let supportsGrouping: boolean; export let supportsGrouping: boolean;
export let supportsRelevanceFiltering: boolean; export let supportsRelevanceFiltering: boolean;
export let pinnedPage: number;
export let groupingEnabled = true; export let groupingEnabled = true;
export let onlyRelevantItems = true; export let onlyRelevantItems = true;
let { pinnedPageIndex, pagePinned } = pageControl;
$: canNext = $debugStage + 1 < stageNames.length; $: canNext = $debugStage + 1 < stageNames.length;
$: canPrev = $debugStage > 0; $: canPrev = $debugStage > 0;
$: pagesNumbers = new Set(stageResult.pages.map((page) => page.index));
$: maxPage = Math.max(...pagesNumbers);
$: pageIsPinned = !isNaN(pinnedPage);
</script> </script>
<div class="sticky top-0 pt-2 pb-1 z-20 bg-gray-50"> <div class="sticky top-0 pt-2 pb-1 z-20 bg-gray-50">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
{#if pageIsPinned} {#if $pagePinned}
<span on:click={() => (pinnedPage = undefined)} transition:slideH={{ duration: 180, easing: linear }}> <span on:click={() => pageControl.unpinPage()} transition:slideH={{ duration: 180, easing: linear }}>
<Icon class="text-xs hover:text-select hover:opacity-25 cursor-pointer opacity-75" icon={pin} /> <Icon class="text-xs hover:text-select hover:opacity-25 cursor-pointer opacity-75" icon={pin} />
</span> </span>
{/if} {/if}
@ -47,12 +45,7 @@
<BookOpen size="1x" class="hover:text-select cursor-pointer {opened && 'text-select'}" /> <BookOpen size="1x" class="hover:text-select cursor-pointer {opened && 'text-select'}" />
</span> </span>
<span slot="content"> <span slot="content">
<PageSelectionPopup <PageSelectionPopup {pageControl} />
{pagesNumbers}
{maxPage}
{pinnedPage}
on:pinPage={(e) => (pinnedPage = e.detail)}
on:unpinPage={() => (pinnedPage = undefined)} />
</span> </span>
</Popup> </Popup>
</span> </span>
@ -80,10 +73,10 @@
<div>|</div> <div>|</div>
<div>Transformation:</div> <div>Transformation:</div>
<span on:click={() => canPrev && debugStage.update((cur) => cur - 1)}> <span on:click={() => canPrev && debugStage.update((cur) => cur - 1)}>
<ArrowLeft size="1x" class={canPrev ? 'hover:text-select cursor-pointer' : 'opacity-50'} /> <ArrowLeft size="1x" class={canPrev ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span> </span>
<span on:click={() => canNext && debugStage.update((cur) => cur + 1)}> <span on:click={() => canNext && debugStage.update((cur) => cur + 1)}>
<ArrowRight size="1x" class={canNext ? 'hover:text-select cursor-pointer' : 'opacity-50'} /> <ArrowRight size="1x" class={canNext ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span> </span>
<span> <span>
<Popup> <Popup>

View File

@ -7,19 +7,20 @@
import { debugStage } from '../config'; import { debugStage } from '../config';
import ControlBar from './ControlBar.svelte'; import ControlBar from './ControlBar.svelte';
import ItemTable from './ItemTable.svelte'; import ItemTable from './ItemTable.svelte';
import PageControl from './PageControl';
export let debug: Debugger; export let debug: Debugger;
const pageControl = new PageControl(debug.pageCount);
const stageNames = debug.stageNames; const stageNames = debug.stageNames;
let pinnedPage: number; const { pinnedPageIndex } = pageControl;
let groupingEnabled = true; let groupingEnabled = true;
let onlyRelevantItems = true; let onlyRelevantItems = true;
$: stageResult = debug.stageResults($debugStage); $: stageResult = debug.stageResults($debugStage);
$: supportsGrouping = !!stageResult.descriptor?.debug?.itemMerger; $: supportsGrouping = !!stageResult.descriptor?.debug?.itemMerger;
$: supportsRelevanceFiltering = !stageResult.descriptor?.debug?.showAll; $: supportsRelevanceFiltering = !stageResult.descriptor?.debug?.showAll;
$: pageIsPinned = !isNaN(pinnedPage); $: visiblePages = pageControl.selectPages(stageResult, onlyRelevantItems, groupingEnabled, $pinnedPageIndex);
$: visiblePages = stageResult.selectPages(onlyRelevantItems, groupingEnabled, pinnedPage);
</script> </script>
<div class="mx-4"> <div class="mx-4">
@ -31,13 +32,12 @@
<ControlBar <ControlBar
{stageNames} {stageNames}
stageDescriptions={debug.stageDescriptions} stageDescriptions={debug.stageDescriptions}
{pageControl}
fontMap={debug.fontMap} fontMap={debug.fontMap}
{stageResult}
{supportsGrouping} {supportsGrouping}
{supportsRelevanceFiltering} {supportsRelevanceFiltering}
bind:groupingEnabled bind:groupingEnabled
bind:onlyRelevantItems bind:onlyRelevantItems />
bind:pinnedPage />
<!-- Stage Messages --> <!-- Stage Messages -->
<ul class="messages list-disc list-inside mb-2 p-2 bg-blue-50 rounded shadow text-sm"> <ul class="messages list-disc list-inside mb-2 p-2 bg-blue-50 rounded shadow text-sm">
@ -48,7 +48,7 @@
<!-- Items --> <!-- Items -->
{#if visiblePages.find((page) => page.itemGroups.length > 0)} {#if visiblePages.find((page) => page.itemGroups.length > 0)}
<ItemTable schema={stageResult.schema} pages={visiblePages} {pageIsPinned} changes={stageResult.changes} /> <ItemTable schema={stageResult.schema} pages={visiblePages} {pageControl} changes={stageResult.changes} />
{:else} {:else}
<!-- No items visible --> <!-- No items visible -->
<div class="flex space-x-1 items-center justify-center text-xl"> <div class="flex space-x-1 items-center justify-center text-xl">

View File

@ -1,5 +1,7 @@
<script> <script>
import { scale, fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { ArrowLeft, ArrowRight } from 'svelte-hero-icons';
import type ItemGroup from '@core/debug/ItemGroup'; import type ItemGroup from '@core/debug/ItemGroup';
import type ChangeIndex from '@core/debug/ChangeIndex'; import type ChangeIndex from '@core/debug/ChangeIndex';
@ -7,15 +9,16 @@
import ChangeSymbol from './ChangeSymbol.svelte'; import ChangeSymbol from './ChangeSymbol.svelte';
import { formatValue } from './formatValues'; import { formatValue } from './formatValues';
import PageControl from './PageControl';
export let pageControl: PageControl;
export let pageIdx: number; export let pageIdx: number;
export let itemIdx: number; export let itemIdx: number;
export let schema: AnnotatedColumn[]; export let schema: AnnotatedColumn[];
export let itemGroup: ItemGroup; export let itemGroup: ItemGroup;
export let changes: ChangeIndex; export let changes: ChangeIndex;
export let maxPage: number;
export let pageIsPinned: boolean;
let { pagePinned, canPrev, canNext } = pageControl;
let expandedItemGroup: { pageIndex: number; itemIndex: number }; let expandedItemGroup: { pageIndex: number; itemIndex: number };
$: expanded = $: expanded =
@ -36,7 +39,17 @@
<!-- Page number in first page item row --> <!-- Page number in first page item row -->
{#if itemIdx === 0} {#if itemIdx === 0}
<td id="page" class="page bg-gray-50 align-top"> <td id="page" class="page bg-gray-50 align-top">
<div>Page {pageIdx} {pageIsPinned ? '' : ' / ' + maxPage}</div> <div>Page {pageIdx + 1} {$pagePinned ? '' : ' / ' + pageControl.totalPages}</div>
{#if $pagePinned}
<div class="absolute flex ml-1 space-x-2">
<span on:click={() => pageControl.prev()}>
<ArrowLeft size="1x" class={$canPrev ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
<span on:click={() => pageControl.next()}>
<ArrowRight size="1x" class={$canNext ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
</div>
{/if}
</td> </td>
{:else} {:else}
<td id="page" /> <td id="page" />

View File

@ -7,21 +7,22 @@
import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation'; import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation';
import inView from '../actions/inView'; import inView from '../actions/inView';
import PageControl from './PageControl';
import ItemRow from './ItemRow.svelte'; import ItemRow from './ItemRow.svelte';
export let schema: AnnotatedColumn[]; export let schema: AnnotatedColumn[];
export let pages: Page[]; export let pages: Page[];
export let pageIsPinned: boolean; export let pageControl: PageControl;
export let changes: ChangeIndex; export let changes: ChangeIndex;
let { pagePinned } = pageControl;
let maxPage = pages[pages.length - 1].index; let maxPage = pages[pages.length - 1].index;
let maxItemsToRenderInOneLoad = 200; let maxItemsToRenderInOneLoad = 200;
let renderedMaxPage = 0; let renderedMaxPage = 0;
let expandedItemGroup: { pageIndex: number; itemIndex: number };
let renderedPages: Page[]; let renderedPages: Page[];
$: { $: {
if (pageIsPinned) { if ($pagePinned) {
renderedPages = pages; renderedPages = pages;
renderedMaxPage = 0; renderedMaxPage = 0;
} else { } else {
@ -44,13 +45,6 @@
} }
// console.log(`Render pages 0 to ${renderedMaxPage} with ${itemCount} items`); // console.log(`Render pages 0 to ${renderedMaxPage} with ${itemCount} items`);
} }
const isExpanded = (pageIndex: number, itemIndex: number) => {
return expandedItemGroup?.pageIndex === pageIndex && expandedItemGroup?.itemIndex === itemIndex;
};
const toggleRow = (pageIndex: number, itemIndex: number) => {
expandedItemGroup = isExpanded(pageIndex, itemIndex) ? undefined : { pageIndex, itemIndex };
};
</script> </script>
<!-- Item table --> <!-- Item table -->
@ -77,13 +71,13 @@
<!-- Page items --> <!-- Page items -->
{#each page.itemGroups as itemGroup, itemIdx} {#each page.itemGroups as itemGroup, itemIdx}
<ItemRow {pageIdx} {itemIdx} {schema} {itemGroup} {changes} {maxPage} {pageIsPinned} /> <ItemRow pageIdx={page.index} {itemIdx} {schema} {itemGroup} {changes} {pageControl} />
{/each} {/each}
{/each} {/each}
</tbody> </tbody>
</table> </table>
{#if !pageIsPinned} {#if !pagePinned}
{#if renderedMaxPage < pages.length} {#if renderedMaxPage < pages.length}
<span use:inView on:intersect={({ detail }) => detail && calculateNextPageToRenderTo()} /> <span use:inView on:intersect={({ detail }) => detail && calculateNextPageToRenderTo()} />
<div class="my-6 text-center text-2xl">...</div> <div class="my-6 text-center text-2xl">...</div>

View File

@ -0,0 +1,67 @@
import Page from '@core/debug/Page';
import StageResult from '@core/debug/StageResult';
import { Writable, writable, get, Readable, derived } from 'svelte/store';
export default class PageControl {
pinnedPageIndex: Writable<number | undefined>;
pagePinned: Readable<boolean>;
canPrev: Readable<boolean>;
canNext: Readable<boolean>;
pagesWithItems: Set<number> = new Set();
constructor(public totalPages: number) {
this.pinnedPageIndex = writable(undefined);
this.pagePinned = derived(this.pinnedPageIndex, (page) => !isNaN(page));
this.canPrev = derived([this.pinnedPageIndex, this.pagePinned], ([page, pinned]) => pinned && page > 0);
this.canNext = derived(
[this.pinnedPageIndex, this.pagePinned],
([page, pinned]) => pinned && page < totalPages - 1
);
this.next.bind(this);
this.prev.bind(this);
this.unpinPage.bind(this);
}
selectPages(stageResult: StageResult, relevantChangesOnly: boolean, groupItems: boolean, pinnedPage?: number) {
const allRelevantPages = stageResult.selectPages(relevantChangesOnly, groupItems);
this.pagesWithItems = new Set(
allRelevantPages.filter((page) => page.itemGroups.length > 0).map((page) => page.index)
);
if (Number.isInteger(pinnedPage)) {
return allRelevantPages.filter((page) => page.index === pinnedPage);
}
return allRelevantPages;
}
pageHasItems(pageIdx: number) {
return this.pagesWithItems.has(pageIdx);
}
next() {
if (get(this.canNext)) {
this.pinnedPageIndex.update((page) => page + 1);
}
}
prev() {
if (get(this.canPrev)) {
this.pinnedPageIndex.update((page) => page - 1);
}
}
pinPage(pageIdx: number) {
this.pinnedPageIndex.set(pageIdx);
}
unpinPage() {
this.pinnedPageIndex.set(undefined);
}
pinnedPage() {
return get(this.pinnedPageIndex);
}
pageIsPinned() {
return get(this.pagePinned);
}
}

View File

@ -1,37 +1,39 @@
<script> <script>
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { Collection } from 'svelte-hero-icons'; import { Collection } from 'svelte-hero-icons';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
export let pagesNumbers: Set<number>; import PageControl from './PageControl';
export let maxPage: number;
export let pinnedPage: number; export let pageControl: PageControl;
let { pinnedPageIndex, pagePinned } = pageControl;
const popupOpened: Writable<boolean> = getContext('popupOpened'); const popupOpened: Writable<boolean> = getContext('popupOpened');
const dispatch = createEventDispatcher();
function pinPage(index: number) { function pinPage(index: number) {
popupOpened.set(false); popupOpened.set(false);
dispatch('pinPage', index); pageControl.pinPage(index);
} }
function unpinPage() { function unpinPage() {
popupOpened.set(false); popupOpened.set(false);
dispatch('unpinPage'); pageControl.unpinPage();
} }
</script> </script>
<div class="absolute mt-2 p-2 flex bg-gray-200 shadow-lg rounded-sm overflow-auto max-h-96" transition:slide> <div class="absolute mt-2 p-2 flex bg-gray-200 shadow-lg rounded-sm overflow-auto max-h-96" transition:slide>
<span class="mt-1 pr-2" on:click={!!pinnedPage && unpinPage}> <span class="mt-1 pr-2" on:click={$pagePinned && unpinPage}>
<Collection size="1x" class={!!pinnedPage ? 'hover:text-select cursor-pointer' : 'opacity-50'} /> <Collection size="1x" class={$pagePinned ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
</span> </span>
<div class="grid gap-3" style="grid-template-columns: repeat({Math.min(20, maxPage + 1)}, minmax(0, 1fr));">
{#each new Array(maxPage + 1) as _, idx}
<div <div
on:click={() => pagesNumbers.has(idx) && pinPage(idx)} class="grid gap-3"
class="px-2 border border-gray-300 rounded-full text-center {pagesNumbers.has(idx) ? (pinnedPage === idx ? 'bg-select' : 'hover:text-select hover:border-select cursor-pointer') : 'opacity-50'}"> style="grid-template-columns: repeat({Math.min(20, pageControl.totalPages)}, minmax(0, 1fr));">
{idx} {#each new Array(pageControl.totalPages) as _, idx}
<div
on:click={() => pageControl.pageHasItems(idx) && pinPage(idx)}
class="px-2 border border-gray-300 rounded-full text-center {pageControl.pageHasItems(idx) ? ($pinnedPageIndex === idx ? 'bg-select' : 'hover:text-select hover:border-select cursor-pointer') : 'opacity-50'}">
{idx + 1}
</div> </div>
{/each} {/each}
</div> </div>