-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathBookmarkBar.ts
139 lines (119 loc) · 4.75 KB
/
BookmarkBar.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { append, create, h } from 'stage1';
import { compile } from 'stage1/macro' with { type: 'macro' };
import { BookmarkNode, type BookmarkTreeNode, Folder } from './BookmarkNode';
declare global {
interface HTMLElement {
/**
* BookmarkBar synthetic `mouseenter` event handler. Note the property is
* named `__mouseover` but it actually works like `mouseenter`.
*/
__mouseover(event: MouseEvent): void;
/**
* BookmarkBar synthetic `mouseleave` event handler. Note the property is
* named `__mouseout` but it actually works like `mouseleave`.
*/
__mouseout(event: MouseEvent): void;
}
}
type BookmarkBarComponent = HTMLDivElement;
export const BookmarkBar = (): BookmarkBarComponent => {
const root = create('div');
root.id = 'b';
void chrome.bookmarks.getChildren('1').then((bookmarks) => {
const len = bookmarks.length;
// Since we can't determine an element's width before it's included in the
// DOM, we have to insert BookmarkNodes individually until no more can fit.
// Any leftover items are then placed in an overflow folder.
const resize = () => {
performance.mark('BookmarkBar');
// Remove all child nodes
root.textContent = '';
// Max width is root minus overflow folder width (68 == 24px svg + 2 * 9px
// svg padding + 2 * 13px bookmark item padding)
const maxWidth = root.clientWidth - 68;
const otherBookmarksFolder = append(
Folder({ id: '2', title: 'Other Bookmarks' }),
root,
);
// NOTE: The elements we're measuring don't have a border or margin so
// we can use clientWidth instead of offsetWidth for better performance.
let width = otherBookmarksFolder.clientWidth;
let index = 0;
let node: ReturnType<typeof BookmarkNode>;
// Add one bookmark at a time until we overflow the max width
for (; index < len; index++) {
node = append(BookmarkNode(bookmarks[index]), root);
width += node.clientWidth;
if (width >= maxWidth) {
// Remove the node which overflowed
node.remove();
break;
}
}
if (index < len) {
const overflowBookmarksFolder = append(
Folder({} as BookmarkTreeNode, false, bookmarks.slice(index)),
root,
);
overflowBookmarksFolder.className += ' end';
append(
h<SVGElement>(
// https://github.com/feathericons/feather/blob/master/icons/corner-right-down.svg
compile(`
<svg id=io>
<polyline points="10 15 15 20 20 15"/>
<path d="M4 4h7a4 4 0 0 1 4 4v12"/>
</svg>
`).html,
),
overflowBookmarksFolder,
);
}
// The "Other Bookmarks" folder was added first so overflow calculation
// is correct but now move it to its proper position at the end
append(otherBookmarksFolder, root).className += ' end';
performance.measure('BookmarkBar', 'BookmarkBar');
};
// HACK: Workaround for race condition. This script is loaded asynchronously,
// which yields the best performance, but it means this code may execute
// before the CSS has loaded. Styles are needed to calculate the bookmark
// item widths, so wait until the CSS is ready.
const waitForStylesThenResize = () => {
// biome-ignore lint/style/useExplicitLengthCheck: byte savings
if (document.styleSheets.length) {
resize();
} else {
setTimeout(waitForStylesThenResize);
}
};
waitForStylesThenResize();
window.onresize = resize;
});
// Synthetic `mouseenter` and `mouseleave` event handler
// XXX: Similar to stage1 synthetic event logic but does not stop propagating
// once an event handler is called + extra relatedTarget checks
// https://github.com/maxmilton/stage1/blob/08cb3c08cb3e5513c181f768ae92c488cfe2a17a/src/events.ts#L3
// eslint-disable-next-line no-multi-assign
root.onmouseover = root.onmouseout = (event) => {
const eventKey = ('__' + event.type) as '__mouseover' | '__mouseout';
// null when mouse moves from/to outside the viewport
const related = event.relatedTarget as Node | null;
let node = event.target as
| (Node & {
__mouseover?(event2: MouseEvent): void;
__mouseout?(event2: MouseEvent): void;
})
| null;
while (node) {
if (node[eventKey] && (!related || !node.contains(related))) {
node[eventKey](event);
}
node = node.parentNode;
}
};
return root;
};
// // Improve performance of lookups on DOM nodes
// // @ts-expect-error -- add new properties to HTMLElement
// // eslint-disable-next-line no-multi-assign
// Element.prototype.__mouseover = Element.prototype.__mouseout = undefined;