Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"neowiki-subject-editor-save-scope": "Note shown beside the subject editor's save button, naming what the save writes. Parameters:\n* $1 - how many subjects have unsaved changes\n* $2 - how many wiki pages hold them\nShown only when the user cannot see the whole scope already, so $1 is at least two or the one changed subject is not the one on screen.",
"neowiki-subject-tree-label": "Accessible name of the structure tree in the subject editor, listing the subjects reachable from the one being edited.",
"neowiki-subject-editor-resize-navigator": "Accessible name of the divider between the structure tree and the edit form in the subject editor. Dragging it, or moving it with the arrow keys, changes how wide the structure panel is. The panel itself is named by {{msg-mw|neowiki-subject-tree-label}}.",
"neowiki-subject-tree-not-linked": "Caption of the structure tree group holding subjects that are open or have unsaved changes but are not reachable through the relations of the subject being edited.",
"neowiki-subject-tree-not-linked": "Names the structure tree entries for subjects that are open or have unsaved changes but are not reachable through the relations of the subject being edited.",
"neowiki-subject-picker-placeholder": "Placeholder text shown in the subject picker input field, which matches typed text against subject names and accepts a complete subject ID.",
"neowiki-schema-picker-placeholder": "Placeholder text shown in the schema picker input field.",
"neowiki-subject-picker-no-results": "Message shown in the subject picker dropdown when no subjects match the search query.",
Expand Down
35 changes: 8 additions & 27 deletions resources/ext.neowiki/src/components/SubjectEditor/SubjectTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,6 @@
:label="$i18n( 'neowiki-subject-tree-label' ).text()"
@select="selectItem"
>
<!-- Unlinked: the row is itself the click target. The dialog's own header still shows
the older "Schema: X" text until that header is converted. -->
<template #secondary="{ item }">
<SchemaNameDisplay
v-if="item.secondaryLabel"
:schema-name="item.secondaryLabel"
link="none"
/>
</template>

<!-- Rendered here rather than inside NeoTree: the dot carries an i18n message of its
own, and it belongs in the row a treeitem takes its accessible name from. -->
<template #trailing="{ item }">
Expand All @@ -27,7 +17,6 @@
import { computed, shallowReactive, watch } from 'vue';
import NeoTree from '@/components/common/NeoTree/NeoTree.vue';
import UnsavedDot from '@/components/common/UnsavedDot.vue';
import SchemaNameDisplay from '@/components/common/SchemaNameDisplay.vue';
import type { NeoTreeItem } from '@/components/common/NeoTree/NeoTreeModel.ts';
import { nodeFor, walkSubjectTree } from './SubjectTreeWalk.ts';
import type { SubjectTreeWalkResult, WalkNode } from './SubjectTreeWalk.ts';
Expand Down Expand Up @@ -126,8 +115,9 @@ const strayNodes = computed( (): WalkNode[] => {
.map( ( id ) => nodeFor( `stray:${ id }`, id, props.editedSubjects.get( id ) ) );
} );

const treeShape = computed( (): WalkNode => {
const root = walk.value.root;
// Captioned rather than given a relation they do not have.
const treeItem = computed( (): NeoTreeItem<string> => {
const root = toTreeItem( walk.value.root );

if ( strayNodes.value.length === 0 ) {
return root;
Expand All @@ -138,33 +128,24 @@ const treeShape = computed( (): WalkNode => {
return {
...root,
children: [
...root.children,
...strayNodes.value.map( ( node ) => ( { ...node, propertyName: caption } ) )
...root.children ?? [],
...strayNodes.value.map( ( node ) => toTreeItem( node, caption ) )
]
};
} );

const treeItem = computed( (): NeoTreeItem<string> => toTreeItem( treeShape.value ) );

function toTreeItem( node: WalkNode ): NeoTreeItem<string> {
function toTreeItem( node: WalkNode, groupLabel?: string ): NeoTreeItem<string> {
return {
key: node.key,
label: node.label,
// Withheld where the name already names the Schema; the walk decides that per node.
secondaryLabel: node.schemaLabel ?? undefined,
groupLabel,
active: node.subjectId === props.activeId,
attrs: { 'data-mw-neowiki-subject-id': node.subjectId },
children: childItemsOf( node ),
children: node.children.map( ( child ) => toTreeItem( child, child.propertyName ) ),
data: node.subjectId
};
}

// Each child carries its relation property's name; NeoTree gathers the contiguous run of
// children sharing one into a single captioned group.
function childItemsOf( node: WalkNode ): NeoTreeItem<string>[] {
return node.children.map( ( child ) => ( { ...toTreeItem( child ), groupLabel: child.propertyName } ) );
}

function isUnsaved( subjectId: string ): boolean {
return props.unsavedIds.includes( subjectId );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import { relationTargetsOf } from './SubjectTreeModel.ts';
import type { Subject } from '@/domain/Subject.ts';
import { subjectDisplayName } from '@/presentation/subjectDisplayName.ts';
import { schemaNameToShow } from '@/presentation/schemaNameToShow.ts';
import type { Schema, SchemaName } from '@/domain/Schema.ts';

// Levels of relation targets to walk from the root: person -> birth event -> time span is 2.
Expand All @@ -18,8 +17,6 @@ export interface WalkNode {
key: string;
subjectId: string;
label: string;
// The Schema label set beside the name, or null where the name already names the Schema.
schemaLabel: string | null;
// The relation property this node hangs under; the root hangs under none. The children
// of one property are contiguous, in the Schema's order.
propertyName?: string;
Expand All @@ -34,7 +31,6 @@ export function nodeFor( key: string, subjectId: string, subject: Subject | unde
key,
subjectId,
label: subject === undefined ? subjectId : subjectDisplayName( subject ),
schemaLabel: subject === undefined ? null : schemaNameToShow( subject ),
children: [],
};
}
Expand Down
67 changes: 45 additions & 22 deletions resources/ext.neowiki/src/components/common/NeoTree/NeoTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@
:select="selectItem"
:keydown="onKeydown"
>
<template #secondary="slotProps">
<slot
name="secondary"
v-bind="slotProps"
/>
</template>

<template #trailing="slotProps">
<slot
name="trailing"
Expand Down Expand Up @@ -55,15 +48,12 @@ const emit = defineEmits<{
type NodeSlot = ( slotProps: { item: NeoTreeItem<T> } ) => unknown;

defineSlots<{
// Replaces a node's plain `secondaryLabel` text, inside the same element, so a consumer
// can treat it without the tree knowing what it means.
secondary?: NodeSlot;
trailing?: NodeSlot;
}>();

// Printed order, which is both the order Up/Down move through and the order the element ids
// are numbered in. A top-level item's `groupLabel` has nowhere to print: a caption may not sit
// inside the tree container itself.
// are numbered in. A top-level item's `groupLabel` is dropped: the tree renders its items
// without grouping them, so nothing decides whether that caption takes a line or a row.
const flatItems = computed( (): NeoTreeItem<T>[] => {
const items: NeoTreeItem<T>[] = [];

Expand Down Expand Up @@ -174,12 +164,19 @@ function onKeydown( event: KeyboardEvent, item: NeoTreeItem<T> ): void {
border-inline-start: @border-subtle;
}

/* The line-height is set rather than inherited: left to the skin it is whatever that
skin says. */
&__edge,
&__node-caption {
font-size: @font-size-x-small;
line-height: @line-height-xx-small;
color: @color-subtle;
}

/* The inline padding matches a row's, so a caption starts where the node labels start. */
&__edge {
display: block;
padding: @spacing-30 @spacing-35 @spacing-12;
font-size: @font-size-x-small;
color: @color-subtle;
}

&__node {
Expand All @@ -189,14 +186,19 @@ function onKeydown( event: KeyboardEvent, item: NeoTreeItem<T> ): void {
}
}

&__node-name {
&__node-name,
&__node-line {
display: flex;
align-items: center;
gap: @spacing-25;
}

&__node-name {
box-sizing: @box-sizing-base;
width: @size-full;
min-height: @size-200;
padding: 0 @spacing-35;
/* The block padding is absorbed by min-height until a row's content takes two lines. */
padding: @spacing-12 @spacing-35;
background-color: @background-color-transparent;
border-radius: @border-radius-base;
/* Only the row selects; the rest of the <li> is the subtree, which is not clickable. */
Expand Down Expand Up @@ -228,6 +230,7 @@ function onKeydown( event: KeyboardEvent, item: NeoTreeItem<T> ): void {
color: @color-emphasized;
}

/* Rows keep a common height, so a name gives way at its end. */
&__node-label {
min-width: 0;
font-weight: @font-weight-bold;
Expand All @@ -236,13 +239,33 @@ function onKeydown( event: KeyboardEvent, item: NeoTreeItem<T> ): void {
white-space: nowrap;
}

/* Set apart by colour rather than size: the row already sits a step below body text.
`min-width: 0` lets it shrink below its content, so a long label gives way to the
node's own name instead of squeezing it to nothing. */
&__node-secondary {
flex: 0 1 auto;
/* Never abbreviated, as on its own line it never was: property names are authored on-wiki. */
&__node-caption {
min-width: 0;
color: @color-subtle;
}

/* Transparent until a row folds, where it becomes the one item a wrap may move: otherwise
the trailing slot is left on a line of its own once the name has taken one. */
&__node-line {
display: contents;
}

/* The basis is the name's own `max-content` width, so the line breaks exactly when the
caption and the whole name cannot share it. A proportional basis gets both ends wrong. */
&__node-name--folded {
flex-wrap: wrap;
/* The boxes differ in height, so centring them leaves the text off by half of it. */
align-items: baseline;
align-content: center;
gap: 0 @spacing-50;
}

&__node-name--folded > &__node-line {
display: flex;
align-items: center;
gap: @spacing-25;
min-width: 0;
flex-basis: max-content;
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ export interface NeoTreeItem<T> {
// by two paths is two items, and a shared key collapses them into one focus target.
key: string;
label: string;
secondaryLabel?: string;
active?: boolean;
// Caption printed once above this item and the contiguous siblings sharing it. One caption
// repeated after an interruption prints twice, as two groups.
// Caption for this item and the contiguous siblings sharing it: printed above them where
// there are several, and on the item's own row where it is alone, joining that row's
// accessible name. Repeated after an interruption it prints twice, as two groups. Dropped
// on a top-level item, which the tree renders without grouping.
groupLabel?: string;
attrs?: Record<string, string>;
children?: NeoTreeItem<T>[];
Expand Down
62 changes: 38 additions & 24 deletions resources/ext.neowiki/src/components/common/NeoTree/NeoTreeNode.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!-- A group's caption names the group through `aria-labelledby`, and is `aria-hidden`
because only `treeitem` and `group` may be children of a tree. -->
<!-- A group's caption names the group through `aria-labelledby`, and is `aria-hidden` because
only `treeitem` and `group` may be children of a tree. A caption heading a single row is
folded onto that row instead, where it names the treeitem directly. -->
<template>
<li
:id="elementId"
Expand All @@ -18,20 +19,22 @@
<span
:id="`${ elementId }-name`"
class="ext-neowiki-tree__node-name"
:class="{ 'ext-neowiki-tree__node-name--folded': folded }"
@click.stop="select( item )"
>
<span class="ext-neowiki-tree__node-label">{{ item.label }}</span>
<span
v-if="item.secondaryLabel"
class="ext-neowiki-tree__node-secondary"
><slot
name="secondary"
:item="item"
>{{ item.secondaryLabel }}</slot></span>
<slot
name="trailing"
:item="item"
/>
v-if="folded"
class="ext-neowiki-tree__node-caption"
>{{ item.groupLabel }}</span>

<!-- One flex item, so a wrap cannot leave the trailing slot on a line of its own. -->
<span class="ext-neowiki-tree__node-line">
<span class="ext-neowiki-tree__node-label">{{ item.label }}</span>
<slot
name="trailing"
:item="item"
/>
</span>
</span>

<div
Expand All @@ -41,7 +44,7 @@
role="none"
>
<span
v-if="group.label !== undefined"
v-if="group.captionOnItsOwnLine"
:id="`${ groupId( index ) }-label`"
class="ext-neowiki-tree__edge"
aria-hidden="true"
Expand All @@ -51,24 +54,18 @@
:id="groupId( index )"
class="ext-neowiki-tree__group"
role="group"
:aria-labelledby="group.label === undefined ? undefined : `${ groupId( index ) }-label`"
:aria-labelledby="group.captionOnItsOwnLine ? `${ groupId( index ) }-label` : undefined"
>
<NeoTreeNode
v-for="child in group.items"
:key="child.key"
:item="child"
:folded="group.captionOnTheRow"
:element-ids="elementIds"
:roving-key="rovingKey"
:select="select"
:keydown="keydown"
>
<template #secondary="slotProps">
<slot
name="secondary"
v-bind="slotProps"
/>
</template>

<template #trailing="slotProps">
<slot
name="trailing"
Expand All @@ -92,6 +89,9 @@ type NodeSlot = ( slotProps: { item: NeoTreeItem<T> } ) => unknown;

const props = defineProps<{
item: NeoTreeItem<T>;
// Whether this node prints its own `groupLabel` on its row. Only the parent knows, because
// it depends on how many siblings share that caption.
folded?: boolean;
// Minted by the tree over the whole flattened list: a node cannot see its own position in it.
elementIds: ReadonlyMap<string, string>;
rovingKey: string | null;
Expand All @@ -100,7 +100,6 @@ const props = defineProps<{
}>();

defineSlots<{
secondary?: NodeSlot;
trailing?: NodeSlot;
}>();

Expand All @@ -109,6 +108,11 @@ const elementId = computed( (): string => props.elementIds.get( props.item.key )
interface RenderGroup {
label: string | undefined;
items: NeoTreeItem<T>[];
// Over several rows a caption is doing a caption's job and keeps its line; over one it is a
// line of chrome introducing a line of content, so it moves onto that row. Decided here,
// where the run's size is known.
captionOnItsOwnLine: boolean;
captionOnTheRow: boolean;
}

// Contiguous children sharing a caption form one group; an unchanged caption continues it,
Expand All @@ -124,7 +128,17 @@ const groups = computed( (): RenderGroup[] => {
continue;
}

rendered.push( { label: child.groupLabel, items: [ child ] } );
rendered.push( {
label: child.groupLabel,
items: [ child ],
captionOnItsOwnLine: false,
captionOnTheRow: false
} );
}

for ( const group of rendered ) {
group.captionOnItsOwnLine = Boolean( group.label ) && group.items.length > 1;
group.captionOnTheRow = Boolean( group.label ) && group.items.length === 1;
}

return rendered;
Expand Down
Loading