@jackuait/blok 0.7.1 → 0.7.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-D-7DpjTs.mjs → blok-Ua3rzLVN.mjs} +1210 -1174
- package/dist/chunks/{constants-DXYRzX7f.mjs → constants-CNjvg-95.mjs} +314 -176
- package/dist/chunks/{tools-Chd7Auwx.mjs → tools-DB7A1iro.mjs} +31 -12
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/constants/data-attributes.ts +2 -0
- package/src/components/inline-tools/inline-tool-marker.ts +11 -0
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +1 -9
- package/src/components/modules/caret.ts +13 -1
- package/src/components/modules/paste/google-docs-preprocessor.ts +96 -38
- package/src/components/modules/toolbar/blockSettings.ts +1 -1
- package/src/components/modules/toolbar/index.ts +37 -2
- package/src/components/modules/toolbar/inline/index.ts +24 -2
- package/src/components/selection/cursor.ts +7 -0
- package/src/components/ui/toolbox.ts +14 -0
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +1 -1
- package/src/components/utils/popover/components/search-input/search-input.const.ts +1 -0
- package/src/components/utils/popover/components/search-input/search-input.ts +32 -1
- package/src/components/utils/popover/popover-desktop.ts +298 -36
- package/src/components/utils/popover/popover-inline.ts +8 -0
- package/src/styles/main.css +39 -11
- package/src/tools/paragraph/index.ts +3 -5
- package/src/tools/table/index.ts +70 -0
- package/src/tools/table/table-cell-blocks.ts +15 -3
- package/src/tools/table/table-cell-clipboard.ts +32 -5
|
@@ -3,7 +3,8 @@ import { Flipper } from '../../flipper';
|
|
|
3
3
|
import { keyCodes } from '../../utils';
|
|
4
4
|
|
|
5
5
|
import type { PopoverItem, PopoverItemRenderParamsMap } from './components/popover-item';
|
|
6
|
-
import { PopoverItemSeparator, css as popoverItemCls
|
|
6
|
+
import { PopoverItemSeparator, css as popoverItemCls, PopoverItemDefault, PopoverItemType } from './components/popover-item';
|
|
7
|
+
import type { PopoverItemParams } from '@/types/utils/popover/popover-item';
|
|
7
8
|
import { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html';
|
|
8
9
|
import type { SearchableItem } from './components/search-input';
|
|
9
10
|
import { SearchInput, SearchInputEvent, scoreSearchMatch } from './components/search-input';
|
|
@@ -67,6 +68,14 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
67
68
|
*/
|
|
68
69
|
private leftAlignElement: HTMLElement | undefined;
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Updates the element whose left edge is used for horizontal positioning.
|
|
73
|
+
* @param element - new element to align against, or undefined to fall back to trigger
|
|
74
|
+
*/
|
|
75
|
+
public setLeftAlignElement(element: HTMLElement | undefined): void {
|
|
76
|
+
this.leftAlignElement = element;
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
/**
|
|
71
80
|
* Popover size cache
|
|
72
81
|
*/
|
|
@@ -78,6 +87,20 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
78
87
|
*/
|
|
79
88
|
private originalItemOrder: Element[] | undefined;
|
|
80
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Cache of promoted items built from nested children.
|
|
92
|
+
* Built once on first non-empty search, destroyed on clear/hide/destroy.
|
|
93
|
+
*/
|
|
94
|
+
private promotedItemCache: {
|
|
95
|
+
items: PopoverItemDefault[];
|
|
96
|
+
parentChains: Map<PopoverItemDefault, string[]>;
|
|
97
|
+
} | null = null;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Temporary group separator elements injected during search.
|
|
101
|
+
*/
|
|
102
|
+
private promotedSeparators: HTMLElement[] = [];
|
|
103
|
+
|
|
81
104
|
/**
|
|
82
105
|
* Construct the instance
|
|
83
106
|
* @param params - popover params
|
|
@@ -313,6 +336,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
313
336
|
* Closes popover
|
|
314
337
|
*/
|
|
315
338
|
public hide = (): void => {
|
|
339
|
+
this.cleanupPromotedItems();
|
|
316
340
|
super.hide();
|
|
317
341
|
|
|
318
342
|
this.destroyNestedPopoverIfExists();
|
|
@@ -405,7 +429,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
405
429
|
* Destroys nested popover unless the mouse moved into it.
|
|
406
430
|
* @param event - mouseleave event
|
|
407
431
|
*/
|
|
408
|
-
|
|
432
|
+
protected handleMouseLeave(event: Event): void {
|
|
409
433
|
const mouseEvent = event as MouseEvent;
|
|
410
434
|
const relatedTarget = mouseEvent.relatedTarget;
|
|
411
435
|
|
|
@@ -422,6 +446,31 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
422
446
|
this.previouslyHoveredItem = null;
|
|
423
447
|
}
|
|
424
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Retrieves popover item that is the target of the specified event.
|
|
451
|
+
* Overridden to include promoted items from recursive search.
|
|
452
|
+
* @param event - event to retrieve popover item from
|
|
453
|
+
*/
|
|
454
|
+
protected override getTargetItem(event: Event): PopoverItemDefault | PopoverItemHtml | undefined {
|
|
455
|
+
const allItems = this.promotedItemCache !== null
|
|
456
|
+
? [...this.items, ...this.promotedItemCache.items]
|
|
457
|
+
: this.items;
|
|
458
|
+
|
|
459
|
+
return allItems
|
|
460
|
+
.filter((item): item is PopoverItemDefault | PopoverItemHtml =>
|
|
461
|
+
item instanceof PopoverItemDefault || item instanceof PopoverItemHtml
|
|
462
|
+
)
|
|
463
|
+
.find(item => {
|
|
464
|
+
const itemEl = item.getElement();
|
|
465
|
+
|
|
466
|
+
if (itemEl === null) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return event.composedPath().includes(itemEl);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
425
474
|
/**
|
|
426
475
|
* Sets CSS variable with position of item near which nested popover should be displayed.
|
|
427
476
|
* Is used for correct positioning of the nested popover
|
|
@@ -724,6 +773,125 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
724
773
|
focusedItem?.onFocus();
|
|
725
774
|
};
|
|
726
775
|
|
|
776
|
+
/**
|
|
777
|
+
* Builds cache of PopoverItemDefault instances from nested children.
|
|
778
|
+
* Recursively walks the item tree to arbitrary depth.
|
|
779
|
+
* Each cached item is mapped to its parent chain for group labeling.
|
|
780
|
+
*/
|
|
781
|
+
private buildPromotedItemCache(): { items: PopoverItemDefault[]; parentChains: Map<PopoverItemDefault, string[]> } {
|
|
782
|
+
const cache = {
|
|
783
|
+
items: [] as PopoverItemDefault[],
|
|
784
|
+
parentChains: new Map<PopoverItemDefault, string[]>(),
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
this.collectPromotedChildren(this.items, [], cache);
|
|
788
|
+
|
|
789
|
+
return cache;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Recursively collects default child items from items that have children.
|
|
794
|
+
* @param items - items to inspect for children
|
|
795
|
+
* @param parentChain - ancestor label chain accumulated so far
|
|
796
|
+
* @param cache - mutable cache to populate
|
|
797
|
+
*/
|
|
798
|
+
private collectPromotedChildren(
|
|
799
|
+
items: PopoverItem[],
|
|
800
|
+
parentChain: string[],
|
|
801
|
+
cache: { items: PopoverItemDefault[]; parentChains: Map<PopoverItemDefault, string[]> }
|
|
802
|
+
): void {
|
|
803
|
+
for (const item of items) {
|
|
804
|
+
if (!(item instanceof PopoverItemDefault) || !item.hasChildren) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const label = item.title ?? item.name ?? '';
|
|
809
|
+
const newChain = [...parentChain, label];
|
|
810
|
+
|
|
811
|
+
this.collectDefaultChildren(item.children, newChain, cache);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Constructs PopoverItemDefault instances from raw params and adds them to the cache.
|
|
817
|
+
* @param childParams - raw child item params from a parent item
|
|
818
|
+
* @param parentChain - ancestor label chain for this group
|
|
819
|
+
* @param cache - mutable cache to populate
|
|
820
|
+
*/
|
|
821
|
+
private collectDefaultChildren(
|
|
822
|
+
childParams: PopoverItemParams[],
|
|
823
|
+
parentChain: string[],
|
|
824
|
+
cache: { items: PopoverItemDefault[]; parentChains: Map<PopoverItemDefault, string[]> }
|
|
825
|
+
): void {
|
|
826
|
+
for (const childParam of childParams) {
|
|
827
|
+
if (childParam.type !== undefined && childParam.type !== PopoverItemType.Default) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const childInstance = new PopoverItemDefault(childParam);
|
|
832
|
+
|
|
833
|
+
if (childInstance.name !== undefined && this.isNamePermanentlyHidden(childInstance.name)) {
|
|
834
|
+
childInstance.destroy();
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
cache.items.push(childInstance);
|
|
839
|
+
cache.parentChains.set(childInstance, parentChain);
|
|
840
|
+
|
|
841
|
+
if (childInstance.hasChildren) {
|
|
842
|
+
this.collectPromotedChildren([childInstance], parentChain, cache);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Removes promoted items and group separators from DOM and destroys cached instances.
|
|
849
|
+
* Idempotent — safe to call when cache is already null.
|
|
850
|
+
*/
|
|
851
|
+
private cleanupPromotedItems(): void {
|
|
852
|
+
for (const separator of this.promotedSeparators) {
|
|
853
|
+
separator.remove();
|
|
854
|
+
}
|
|
855
|
+
this.promotedSeparators = [];
|
|
856
|
+
|
|
857
|
+
if (this.promotedItemCache !== null) {
|
|
858
|
+
for (const item of this.promotedItemCache.items) {
|
|
859
|
+
item.getElement()?.remove();
|
|
860
|
+
item.destroy();
|
|
861
|
+
}
|
|
862
|
+
this.promotedItemCache = null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Creates a group separator element for promoted search results.
|
|
868
|
+
* @param label - the parent chain label (e.g., "Convert to" or "Parent › Child")
|
|
869
|
+
*/
|
|
870
|
+
private createGroupSeparator(label: string): HTMLElement {
|
|
871
|
+
const el = document.createElement('div');
|
|
872
|
+
|
|
873
|
+
el.setAttribute(DATA_ATTR.promotedGroupLabel, '');
|
|
874
|
+
el.setAttribute('role', 'separator');
|
|
875
|
+
el.className = 'px-3 pt-2.5 pb-1 text-[11px] font-medium uppercase tracking-wide text-gray-text/50 cursor-default';
|
|
876
|
+
el.textContent = label;
|
|
877
|
+
|
|
878
|
+
return el;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Appends DOM elements for a group of promoted items to the items container.
|
|
883
|
+
* @param groupItems - promoted items with their scores
|
|
884
|
+
*/
|
|
885
|
+
private appendPromotedGroupElements(groupItems: Array<{ item: PopoverItemDefault; score: number }>): void {
|
|
886
|
+
for (const { item } of groupItems) {
|
|
887
|
+
const el = item.getElement();
|
|
888
|
+
|
|
889
|
+
if (el !== null) {
|
|
890
|
+
this.nodes.items?.appendChild(el);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
727
895
|
/**
|
|
728
896
|
* Adds search to the popover
|
|
729
897
|
*/
|
|
@@ -733,7 +901,42 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
733
901
|
placeholder: this.messages.search,
|
|
734
902
|
});
|
|
735
903
|
|
|
736
|
-
this.search.on(SearchInputEvent.Search,
|
|
904
|
+
this.search.on(SearchInputEvent.Search, (searchData: { query: string; items: SearchableItem[] }) => {
|
|
905
|
+
const isEmptyQuery = searchData.query === '';
|
|
906
|
+
|
|
907
|
+
if (isEmptyQuery) {
|
|
908
|
+
this.cleanupPromotedItems();
|
|
909
|
+
this.onSearch({
|
|
910
|
+
query: searchData.query,
|
|
911
|
+
topLevelItems: searchData.items as unknown as PopoverItemDefault[],
|
|
912
|
+
promotedItems: [],
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Build cache on first non-empty search
|
|
919
|
+
if (this.promotedItemCache === null) {
|
|
920
|
+
this.promotedItemCache = this.buildPromotedItemCache();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Score promoted items against the query
|
|
924
|
+
const { parentChains } = this.promotedItemCache;
|
|
925
|
+
const promotedScored = this.promotedItemCache.items
|
|
926
|
+
.map(item => ({
|
|
927
|
+
item,
|
|
928
|
+
score: scoreSearchMatch(item, searchData.query),
|
|
929
|
+
chain: parentChains.get(item) ?? [],
|
|
930
|
+
}))
|
|
931
|
+
.filter(({ score }) => score > 0)
|
|
932
|
+
.sort((a, b) => b.score - a.score);
|
|
933
|
+
|
|
934
|
+
this.onSearch({
|
|
935
|
+
query: searchData.query,
|
|
936
|
+
topLevelItems: searchData.items as unknown as PopoverItemDefault[],
|
|
937
|
+
promotedItems: promotedScored,
|
|
938
|
+
});
|
|
939
|
+
});
|
|
737
940
|
|
|
738
941
|
const searchElement = this.search.getElement();
|
|
739
942
|
|
|
@@ -749,45 +952,60 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
749
952
|
*/
|
|
750
953
|
public override filterItems(query: string): void {
|
|
751
954
|
if (query === '') {
|
|
955
|
+
this.cleanupPromotedItems();
|
|
752
956
|
this.onSearch({
|
|
753
957
|
query,
|
|
754
|
-
|
|
958
|
+
topLevelItems: this.itemsDefault,
|
|
959
|
+
promotedItems: [],
|
|
755
960
|
});
|
|
756
961
|
|
|
757
962
|
return;
|
|
758
963
|
}
|
|
759
964
|
|
|
760
|
-
|
|
965
|
+
// Build cache on first non-empty search
|
|
966
|
+
if (this.promotedItemCache === null) {
|
|
967
|
+
this.promotedItemCache = this.buildPromotedItemCache();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Score top-level items
|
|
971
|
+
const topLevelScored = this.itemsDefault
|
|
761
972
|
.map(item => ({ item, score: scoreSearchMatch(item, query) }))
|
|
762
973
|
.filter(({ score }) => score > 0)
|
|
763
974
|
.sort((a, b) => b.score - a.score);
|
|
764
975
|
|
|
765
|
-
|
|
976
|
+
// Score promoted items from cache
|
|
977
|
+
const { parentChains: chains } = this.promotedItemCache;
|
|
978
|
+
const promotedScored = this.promotedItemCache.items
|
|
979
|
+
.map(item => ({
|
|
980
|
+
item,
|
|
981
|
+
score: scoreSearchMatch(item, query),
|
|
982
|
+
chain: chains.get(item) ?? [],
|
|
983
|
+
}))
|
|
984
|
+
.filter(({ score }) => score > 0)
|
|
985
|
+
.sort((a, b) => b.score - a.score);
|
|
766
986
|
|
|
767
987
|
this.onSearch({
|
|
768
988
|
query,
|
|
769
|
-
|
|
989
|
+
topLevelItems: topLevelScored.map(({ item }) => item),
|
|
990
|
+
promotedItems: promotedScored,
|
|
770
991
|
});
|
|
771
992
|
}
|
|
772
993
|
|
|
773
994
|
/**
|
|
774
|
-
* Handles
|
|
775
|
-
*
|
|
776
|
-
* @param data.query - search query text
|
|
777
|
-
* @param data.items - search results
|
|
995
|
+
* Handles search results from both filterItems and SearchInput.
|
|
996
|
+
* Renders top-level matches and promoted children with group separators.
|
|
778
997
|
*/
|
|
779
|
-
private onSearch = (data: {
|
|
998
|
+
private onSearch = (data: {
|
|
999
|
+
query: string;
|
|
1000
|
+
topLevelItems: PopoverItemDefault[] | SearchableItem[];
|
|
1001
|
+
promotedItems: Array<{ item: PopoverItemDefault; score: number; chain: string[] }>;
|
|
1002
|
+
}): void => {
|
|
780
1003
|
const isEmptyQuery = data.query === '';
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
// Cast data.items to PopoverItemDefault[] since we know that's what filterItems passes
|
|
784
|
-
const matchingItems = data.items as unknown as PopoverItemDefault[];
|
|
1004
|
+
const matchingTopLevel = data.topLevelItems as unknown as PopoverItemDefault[];
|
|
1005
|
+
const isNothingFound = matchingTopLevel.length === 0 && data.promotedItems.length === 0;
|
|
785
1006
|
|
|
786
1007
|
/**
|
|
787
1008
|
* When nothing is found, disable transitions so items hide instantly.
|
|
788
|
-
* The "Nothing found" message fade-in provides the visual transition;
|
|
789
|
-
* animating the last items' collapse simultaneously causes a jarring
|
|
790
|
-
* height bounce in the popover container.
|
|
791
1009
|
*/
|
|
792
1010
|
if (isNothingFound) {
|
|
793
1011
|
this.items.forEach(item => {
|
|
@@ -800,48 +1018,92 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
800
1018
|
const isDefaultItem = item instanceof PopoverItemDefault;
|
|
801
1019
|
const isSeparatorOrHtml = item instanceof PopoverItemSeparator || item instanceof PopoverItemHtml;
|
|
802
1020
|
const isHidden = isDefaultItem
|
|
803
|
-
? !
|
|
1021
|
+
? !matchingTopLevel.includes(item) || (item.name !== undefined && this.isNamePermanentlyHidden(item.name))
|
|
804
1022
|
: isSeparatorOrHtml && (isNothingFound || !isEmptyQuery);
|
|
805
1023
|
|
|
806
1024
|
item.toggleHidden(isHidden);
|
|
807
1025
|
});
|
|
808
1026
|
|
|
809
1027
|
if (isNothingFound) {
|
|
810
|
-
// Force reflow so the instant hide takes effect, then restore transitions
|
|
811
1028
|
this.nodes.popoverContainer.offsetHeight;
|
|
812
1029
|
this.items.forEach(item => {
|
|
813
1030
|
item.getElement()?.style.removeProperty('transition-duration');
|
|
814
1031
|
});
|
|
815
1032
|
}
|
|
816
1033
|
|
|
817
|
-
// Reorder DOM elements to reflect ranking
|
|
818
|
-
if (!isEmptyQuery &&
|
|
819
|
-
this.reorderItemsByRank(
|
|
1034
|
+
// Reorder top-level DOM elements to reflect ranking
|
|
1035
|
+
if (!isEmptyQuery && matchingTopLevel.length > 0) {
|
|
1036
|
+
this.reorderItemsByRank(matchingTopLevel);
|
|
820
1037
|
} else if (isEmptyQuery && this.originalItemOrder !== undefined) {
|
|
821
1038
|
this.restoreOriginalItemOrder();
|
|
822
1039
|
}
|
|
823
1040
|
|
|
1041
|
+
// Detach previous promoted elements from DOM (don't destroy cache)
|
|
1042
|
+
for (const separator of this.promotedSeparators) {
|
|
1043
|
+
separator.remove();
|
|
1044
|
+
}
|
|
1045
|
+
this.promotedSeparators = [];
|
|
1046
|
+
|
|
1047
|
+
if (this.promotedItemCache !== null) {
|
|
1048
|
+
for (const item of this.promotedItemCache.items) {
|
|
1049
|
+
item.getElement()?.remove();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Render promoted items grouped by parent chain
|
|
1054
|
+
if (data.promotedItems.length > 0) {
|
|
1055
|
+
const groups = new Map<string, Array<{ item: PopoverItemDefault; score: number }>>();
|
|
1056
|
+
|
|
1057
|
+
for (const entry of data.promotedItems) {
|
|
1058
|
+
const label = entry.chain.join(' \u203A ');
|
|
1059
|
+
const existing = groups.get(label);
|
|
1060
|
+
|
|
1061
|
+
if (existing !== undefined) {
|
|
1062
|
+
existing.push({ item: entry.item, score: entry.score });
|
|
1063
|
+
} else {
|
|
1064
|
+
groups.set(label, [{ item: entry.item, score: entry.score }]);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Sort groups by best score in each group
|
|
1069
|
+
const sortedGroups = [...groups.entries()].sort((a, b) => {
|
|
1070
|
+
const bestA = Math.max(...a[1].map(e => e.score));
|
|
1071
|
+
const bestB = Math.max(...b[1].map(e => e.score));
|
|
1072
|
+
|
|
1073
|
+
return bestB - bestA;
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
for (const [label, groupItems] of sortedGroups) {
|
|
1077
|
+
const separator = this.createGroupSeparator(label);
|
|
1078
|
+
|
|
1079
|
+
this.promotedSeparators.push(separator);
|
|
1080
|
+
this.nodes.items?.appendChild(separator);
|
|
1081
|
+
|
|
1082
|
+
this.appendPromotedGroupElements(groupItems);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
824
1086
|
this.toggleNothingFoundMessage(isNothingFound);
|
|
825
1087
|
|
|
826
|
-
|
|
827
|
-
const
|
|
1088
|
+
// Build flippable elements list: top-level matches + promoted items
|
|
1089
|
+
const topLevelFlippable = isEmptyQuery
|
|
1090
|
+
? this.flippableElements
|
|
1091
|
+
: matchingTopLevel.map(item => item.getElement());
|
|
1092
|
+
|
|
1093
|
+
const promotedFlippable = data.promotedItems.map(({ item }) => item.getElement());
|
|
1094
|
+
|
|
1095
|
+
const flippableElements = [
|
|
1096
|
+
...topLevelFlippable,
|
|
1097
|
+
...promotedFlippable,
|
|
1098
|
+
].filter((el): el is HTMLElement => el !== null);
|
|
828
1099
|
|
|
829
1100
|
if (!this.flipper?.isActivated) {
|
|
830
1101
|
return;
|
|
831
1102
|
}
|
|
832
1103
|
|
|
833
|
-
/** Update flipper items with only visible */
|
|
834
1104
|
this.flipper.deactivate();
|
|
835
|
-
this.flipper.activate(flippableElements
|
|
1105
|
+
this.flipper.activate(flippableElements);
|
|
836
1106
|
|
|
837
|
-
/**
|
|
838
|
-
* Focus first item after filtering.
|
|
839
|
-
* Always skip the first Tab press so it just "enters" the menu rather than
|
|
840
|
-
* advancing to second item. This applies regardless of whether the query is
|
|
841
|
-
* empty (initial "/" open) or non-empty (user is typing to filter), because
|
|
842
|
-
* the user's keyboard focus is still in the search input - pressing Tab
|
|
843
|
-
* should enter the list at item 0, not advance from 0 to 1.
|
|
844
|
-
*/
|
|
845
1107
|
if (flippableElements.length > 0) {
|
|
846
1108
|
this.flipper.focusItem(0, { skipNextTab: true });
|
|
847
1109
|
}
|
|
@@ -240,6 +240,14 @@ export class PopoverInline extends PopoverDesktop {
|
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Disable mouse-leave event handling.
|
|
245
|
+
* Inline toolbar uses click-to-toggle for nested popovers, not hover.
|
|
246
|
+
*/
|
|
247
|
+
protected override handleMouseLeave(): void {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
243
251
|
/**
|
|
244
252
|
* Sets CSS variable with position of item near which nested popover should be displayed.
|
|
245
253
|
* Is used to position nested popover right below clicked item
|
package/src/styles/main.css
CHANGED
|
@@ -37,9 +37,11 @@
|
|
|
37
37
|
`all: initial !important` — main editor wrapper (outermost boundary).
|
|
38
38
|
Resets every property to its CSS initial value with !important priority,
|
|
39
39
|
blocking even `!important` host-page styles from cascading in.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
However, `!important` declarations always beat normal declarations
|
|
41
|
+
regardless of specificity or source order, so Tailwind utility classes
|
|
42
|
+
applied to the wrapper element itself (e.g. `relative`, `z-1`) are
|
|
43
|
+
overridden. Properties that the wrapper needs must be explicitly
|
|
44
|
+
re-applied with `!important` below (same pattern as font-family/color).
|
|
43
45
|
|
|
44
46
|
[data-blok-popover]:not([data-blok-popover-inline])
|
|
45
47
|
Inherited-properties-only reset — toolbox/settings popovers, appended
|
|
@@ -72,6 +74,27 @@
|
|
|
72
74
|
*/
|
|
73
75
|
[data-blok-interface=blok] {
|
|
74
76
|
all: initial !important;
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
Re-apply layout properties that `all: initial` resets.
|
|
80
|
+
The wrapper must be a positioned containing block so that
|
|
81
|
+
absolutely-positioned children (inline toolbar, main toolbar)
|
|
82
|
+
resolve against it rather than the document root. Without this,
|
|
83
|
+
the inline toolbar renders off-screen when the page is scrolled.
|
|
84
|
+
*/
|
|
85
|
+
position: relative !important;
|
|
86
|
+
box-sizing: border-box !important;
|
|
87
|
+
display: block !important;
|
|
88
|
+
z-index: 1 !important;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
RTL direction is set conditionally via JS (ui.ts) using the
|
|
93
|
+
data-blok-rtl attribute. `all: initial` resets direction to `ltr`,
|
|
94
|
+
so we must re-apply it with !important when the attribute is present.
|
|
95
|
+
*/
|
|
96
|
+
[data-blok-interface=blok][data-blok-rtl=true] {
|
|
97
|
+
direction: rtl !important;
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
/* Font family — user-configurable via config.style.fontFamily */
|
|
@@ -327,8 +350,9 @@
|
|
|
327
350
|
resize: vertical;
|
|
328
351
|
}
|
|
329
352
|
|
|
330
|
-
/* Remove inner padding in Chrome and Safari on macOS. */
|
|
331
|
-
:where([data-blok-interface], [data-blok-popover]) ::-webkit-search-decoration
|
|
353
|
+
/* Remove inner padding and native cancel button in Chrome and Safari on macOS. */
|
|
354
|
+
:where([data-blok-interface], [data-blok-popover]) ::-webkit-search-decoration,
|
|
355
|
+
:where([data-blok-interface], [data-blok-popover]) ::-webkit-search-cancel-button {
|
|
332
356
|
-webkit-appearance: none;
|
|
333
357
|
}
|
|
334
358
|
|
|
@@ -394,10 +418,13 @@
|
|
|
394
418
|
outline: none;
|
|
395
419
|
}
|
|
396
420
|
|
|
397
|
-
/* Never show a focus outline on
|
|
398
|
-
is sufficient affordance regardless of how focus was triggered.
|
|
399
|
-
|
|
400
|
-
|
|
421
|
+
/* Never show a focus outline on text-entry elements — the text cursor
|
|
422
|
+
is sufficient affordance regardless of how focus was triggered.
|
|
423
|
+
Browsers always match :focus-visible on these elements even on mouse
|
|
424
|
+
click (per CSS Selectors L4), so the :focus-visible restore rule below
|
|
425
|
+
would otherwise override element-level outline-hidden utilities. */
|
|
426
|
+
[data-blok-interface] :is([contenteditable], input, textarea):focus-visible,
|
|
427
|
+
[data-blok-popover] :is([contenteditable], input, textarea):focus-visible {
|
|
401
428
|
outline: none;
|
|
402
429
|
}
|
|
403
430
|
|
|
@@ -952,8 +979,9 @@
|
|
|
952
979
|
* When the user types "/" to open the toolbox, the contenteditable
|
|
953
980
|
* transforms to look like a search input with a placeholder.
|
|
954
981
|
*/
|
|
955
|
-
[data-blok-slash-search]
|
|
956
|
-
|
|
982
|
+
[data-blok-slash-search],
|
|
983
|
+
[data-blok-slash-search]:focus-visible {
|
|
984
|
+
@apply bg-search-input-bg rounded-[4px] transition-colors duration-150 max-w-[240px];
|
|
957
985
|
}
|
|
958
986
|
|
|
959
987
|
[data-blok-slash-search]::after {
|
|
@@ -346,11 +346,9 @@ export class Paragraph implements BlockTool {
|
|
|
346
346
|
|
|
347
347
|
this._data = data;
|
|
348
348
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
});
|
|
349
|
+
if (this._element) {
|
|
350
|
+
this._element.innerHTML = this._data.text || '';
|
|
351
|
+
}
|
|
354
352
|
}
|
|
355
353
|
|
|
356
354
|
/**
|
package/src/tools/table/index.ts
CHANGED
|
@@ -1395,6 +1395,20 @@ export class Table implements BlockTool {
|
|
|
1395
1395
|
return;
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
|
+
/**
|
|
1399
|
+
* Single-cell (1×1) payloads should insert content inline at the caret
|
|
1400
|
+
* position rather than replacing the entire target cell. This matches user
|
|
1401
|
+
* expectations: copying one cell and pasting into another cell (or the same
|
|
1402
|
+
* cell) appends/inserts the text instead of overwriting.
|
|
1403
|
+
*/
|
|
1404
|
+
if (payload.rows === 1 && payload.cols === 1) {
|
|
1405
|
+
e.preventDefault();
|
|
1406
|
+
e.stopPropagation();
|
|
1407
|
+
this.insertSingleCellPayloadInline(payload.cells[0][0]);
|
|
1408
|
+
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1398
1412
|
e.preventDefault();
|
|
1399
1413
|
e.stopPropagation();
|
|
1400
1414
|
|
|
@@ -1406,6 +1420,62 @@ export class Table implements BlockTool {
|
|
|
1406
1420
|
this.pastePayloadIntoCells(gridEl, payload, targetRowIndex, targetColIndex);
|
|
1407
1421
|
}
|
|
1408
1422
|
|
|
1423
|
+
/**
|
|
1424
|
+
* Insert the content of a single clipboard cell at the current caret position.
|
|
1425
|
+
* Extracts text from each block and joins with line breaks.
|
|
1426
|
+
*/
|
|
1427
|
+
private insertSingleCellPayloadInline(cell: { blocks: ClipboardBlockData[] }): void {
|
|
1428
|
+
const html = cell.blocks
|
|
1429
|
+
.map((block) => {
|
|
1430
|
+
if (typeof block.data.text === 'string') {
|
|
1431
|
+
return block.data.text;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
return '';
|
|
1435
|
+
})
|
|
1436
|
+
.filter(Boolean)
|
|
1437
|
+
.join('<br>');
|
|
1438
|
+
|
|
1439
|
+
if (!html) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const selection = window.getSelection();
|
|
1444
|
+
|
|
1445
|
+
if (!selection || selection.rangeCount === 0) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const range = selection.getRangeAt(0);
|
|
1450
|
+
|
|
1451
|
+
range.deleteContents();
|
|
1452
|
+
|
|
1453
|
+
const fragment = document.createDocumentFragment();
|
|
1454
|
+
const wrapper = document.createElement('div');
|
|
1455
|
+
|
|
1456
|
+
wrapper.innerHTML = html;
|
|
1457
|
+
|
|
1458
|
+
Array.from(wrapper.childNodes).forEach((child) => fragment.appendChild(child));
|
|
1459
|
+
|
|
1460
|
+
if (fragment.childNodes.length === 0) {
|
|
1461
|
+
fragment.appendChild(new Text());
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const lastChild = fragment.lastChild as ChildNode;
|
|
1465
|
+
|
|
1466
|
+
range.insertNode(fragment);
|
|
1467
|
+
|
|
1468
|
+
const newRange = document.createRange();
|
|
1469
|
+
const nodeToSetCaret = lastChild.nodeType === Node.TEXT_NODE ? lastChild : lastChild.firstChild;
|
|
1470
|
+
|
|
1471
|
+
if (nodeToSetCaret !== null && nodeToSetCaret.textContent !== null) {
|
|
1472
|
+
newRange.setStart(nodeToSetCaret, nodeToSetCaret.textContent.length);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
selection.removeAllRanges();
|
|
1476
|
+
selection.addRange(newRange);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1409
1479
|
private pastePayloadIntoCells(
|
|
1410
1480
|
gridEl: HTMLElement,
|
|
1411
1481
|
payload: TableCellsClipboard,
|
|
@@ -144,8 +144,8 @@ export class TableCellBlocks {
|
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// ArrowDown at last row -> exit table
|
|
148
|
-
if (event.key === 'ArrowDown' && position.row === this.getRowCount() - 1) {
|
|
147
|
+
// ArrowDown at last row -> exit table (skip if already handled by block-level navigation)
|
|
148
|
+
if (event.key === 'ArrowDown' && !event.defaultPrevented && position.row === this.getRowCount() - 1) {
|
|
149
149
|
event.preventDefault();
|
|
150
150
|
this.exitTableForward();
|
|
151
151
|
}
|
|
@@ -492,7 +492,19 @@ export class TableCellBlocks {
|
|
|
492
492
|
return;
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
-
|
|
495
|
+
// Insert at the correct DOM position based on the flat array order,
|
|
496
|
+
// so that pressing Enter on a non-last paragraph inserts the new block
|
|
497
|
+
// right after the current one instead of always at the end of the cell.
|
|
498
|
+
const blocksCount = this.api.blocks.getBlocksCount();
|
|
499
|
+
const nextSiblingHolder = Array.from(
|
|
500
|
+
{ length: blocksCount - index - 1 },
|
|
501
|
+
(_, offset) => this.api.blocks.getBlockByIndex(index + 1 + offset)
|
|
502
|
+
).find(
|
|
503
|
+
candidate => candidate?.holder.parentElement === container
|
|
504
|
+
)?.holder ?? null;
|
|
505
|
+
|
|
506
|
+
// insertBefore(el, null) is equivalent to appendChild
|
|
507
|
+
container.insertBefore(block.holder, nextSiblingHolder);
|
|
496
508
|
this.api.blocks.setBlockParent(blockId, this.tableBlockId);
|
|
497
509
|
this.stripPlaceholders(container);
|
|
498
510
|
}
|