mirror of
https://github.com/Microsoft/vscode
synced 2024-10-04 10:27:46 +00:00
fix type issues in h() (#155600)
- improve regex with named capture groups - drop $ in favor of inline id - add tests Co-authored-by: Henning Dieterichs <notify.henning.dieterichs@live.de> Co-authored-by: Henning Dieterichs <notify.henning.dieterichs@live.de>
This commit is contained in:
parent
4b95a2d3ed
commit
dc75592590
|
@ -1738,56 +1738,81 @@ type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Functi
|
|||
type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>;
|
||||
type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
||||
type ArrayToObj<T extends any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
|
||||
type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
|
||||
type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
|
||||
|
||||
type TagToElement<T> = T extends `.${string}`
|
||||
? HTMLDivElement
|
||||
: T extends `#${string}`
|
||||
? HTMLDivElement
|
||||
: T extends `${infer TStart}#${string}`
|
||||
? TStart extends keyof HTMLElementTagNameMap
|
||||
? HTMLElementTagNameMap[TStart]
|
||||
type TagToElement<T> = T extends `${infer TStart}#${string}`
|
||||
? TStart extends keyof HHTMLElementTagNameMap
|
||||
? HHTMLElementTagNameMap[TStart]
|
||||
: HTMLElement
|
||||
: T extends `${infer TStart}.${string}`
|
||||
? TStart extends keyof HTMLElementTagNameMap
|
||||
? HTMLElementTagNameMap[TStart]
|
||||
? TStart extends keyof HHTMLElementTagNameMap
|
||||
? HHTMLElementTagNameMap[TStart]
|
||||
: HTMLElement
|
||||
: T extends keyof HTMLElementTagNameMap
|
||||
? HTMLElementTagNameMap[T]
|
||||
: HTMLElement;
|
||||
|
||||
type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}`
|
||||
? { element: TagToElement<TTag>; id: TId }
|
||||
: { element: TagToElement<TTag>; id: 'root' };
|
||||
|
||||
type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId }
|
||||
? Record<(TId extends string ? TId : never) | 'root', TElement>
|
||||
: never;
|
||||
|
||||
type Child = HTMLElement | string | Record<string, HTMLElement>;
|
||||
type Children = []
|
||||
| [Child]
|
||||
| [Child, Child]
|
||||
| [Child, Child, Child]
|
||||
| [Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
|
||||
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child];
|
||||
|
||||
const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/;
|
||||
|
||||
/**
|
||||
* A helper function to create nested dom nodes.
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* private readonly htmlElements = h('div.code-view', [
|
||||
* h('div.title', { $: 'title' }),
|
||||
* const elements = h('div.code-view', [
|
||||
* h('div.title@title'),
|
||||
* h('div.container', [
|
||||
* h('div.gutter', { $: 'gutterDiv' }),
|
||||
* h('div', { $: 'editor' }),
|
||||
* h('div.gutter@gutterDiv'),
|
||||
* h('div@editor'),
|
||||
* ]),
|
||||
* ]);
|
||||
* private readonly editor = createEditor(this.htmlElements.editor);
|
||||
* const editor = createEditor(elements.editor);
|
||||
* ```
|
||||
*/
|
||||
export function h<TTag extends string>(
|
||||
tag: TTag
|
||||
): (Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
||||
export function h<TTag extends string, TId extends string>(
|
||||
tag: TTag,
|
||||
attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>
|
||||
): Record<TId | 'root', TagToElement<TTag>>;
|
||||
export function h<TTag extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
|
||||
tag: TTag,
|
||||
children: T
|
||||
): (ArrayToObj<T> & Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
||||
export function h<TTag extends string>(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>): Record<'root', TagToElement<TTag>>;
|
||||
export function h<TTag extends string, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
|
||||
tag: TTag,
|
||||
attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>,
|
||||
children: T
|
||||
): (ArrayToObj<T> & Record<TId, TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
||||
export function h<TTag extends string>
|
||||
(tag: TTag):
|
||||
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
||||
|
||||
export function h<TTag extends string, T extends Children>
|
||||
(tag: TTag, children: T):
|
||||
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
||||
|
||||
export function h<TTag extends string>
|
||||
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
|
||||
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
||||
|
||||
export function h<TTag extends string, T extends Children>
|
||||
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: T):
|
||||
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
||||
|
||||
export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
|
||||
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
|
||||
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
|
||||
|
@ -1800,25 +1825,29 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
|
|||
children = args[1];
|
||||
}
|
||||
|
||||
const match = SELECTOR_REGEX.exec(tag);
|
||||
const match = H_REGEX.exec(tag);
|
||||
|
||||
if (!match) {
|
||||
if (!match || !match.groups) {
|
||||
throw new Error('Bad use of h');
|
||||
}
|
||||
|
||||
const tagName = match[1] || 'div';
|
||||
const tagName = match.groups['tag'] || 'div';
|
||||
const el = document.createElement(tagName);
|
||||
|
||||
if (match[3]) {
|
||||
el.id = match[3];
|
||||
if (match.groups['id']) {
|
||||
el.id = match.groups['id'];
|
||||
}
|
||||
|
||||
if (match[4]) {
|
||||
el.className = match[4].replace(/\./g, ' ').trim();
|
||||
if (match.groups['class']) {
|
||||
el.className = match.groups['class'].replace(/\./g, ' ').trim();
|
||||
}
|
||||
|
||||
const result: Record<string, HTMLElement> = {};
|
||||
|
||||
if (match.groups['name']) {
|
||||
result[match.groups['name']] = el;
|
||||
}
|
||||
|
||||
if (children) {
|
||||
for (const c of children) {
|
||||
if (c instanceof HTMLElement) {
|
||||
|
@ -1833,10 +1862,6 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
|
|||
}
|
||||
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
if (key === '$') {
|
||||
result[value] = el;
|
||||
continue;
|
||||
}
|
||||
if (key === 'style') {
|
||||
for (const [cssKey, cssValue] of Object.entries(value)) {
|
||||
el.style.setProperty(
|
||||
|
|
|
@ -684,10 +684,10 @@ export enum TreeFindMode {
|
|||
|
||||
class FindWidget<T, TFilterData> extends Disposable {
|
||||
|
||||
private readonly elements = h('div.monaco-tree-type-filter', [
|
||||
h('div.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper', { $: 'grab' }),
|
||||
h('div.monaco-tree-type-filter-input', { $: 'findInput' }),
|
||||
h('div.monaco-tree-type-filter-actionbar', { $: 'actionbar' }),
|
||||
private readonly elements = h('.monaco-tree-type-filter', [
|
||||
h('.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper@grab'),
|
||||
h('.monaco-tree-type-filter-input@findInput'),
|
||||
h('.monaco-tree-type-filter-actionbar@actionbar'),
|
||||
]);
|
||||
|
||||
set mode(mode: TreeFindMode) {
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
const $ = dom.$;
|
||||
import { $, h, multibyteAwareBtoa } from 'vs/base/browser/dom';
|
||||
|
||||
suite('dom', () => {
|
||||
test('hasClass', () => {
|
||||
|
@ -73,9 +72,9 @@ suite('dom', () => {
|
|||
});
|
||||
|
||||
test('multibyteAwareBtoa', () => {
|
||||
assert.ok(dom.multibyteAwareBtoa('hello world').length > 0);
|
||||
assert.ok(dom.multibyteAwareBtoa('平仮名').length > 0);
|
||||
assert.ok(dom.multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013
|
||||
assert.ok(multibyteAwareBtoa('hello world').length > 0);
|
||||
assert.ok(multibyteAwareBtoa('平仮名').length > 0);
|
||||
assert.ok(multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013
|
||||
});
|
||||
|
||||
suite('$', () => {
|
||||
|
@ -129,4 +128,152 @@ suite('dom', () => {
|
|||
assert.strictEqual(firstChild.textContent, 'foobar');
|
||||
});
|
||||
});
|
||||
|
||||
suite('h', () => {
|
||||
test('should build simple nodes', () => {
|
||||
const div = h('div');
|
||||
assert(div.root instanceof HTMLElement);
|
||||
assert.strictEqual(div.root.tagName, 'DIV');
|
||||
|
||||
const span = h('span');
|
||||
assert(span.root instanceof HTMLElement);
|
||||
assert.strictEqual(span.root.tagName, 'SPAN');
|
||||
|
||||
const img = h('img');
|
||||
assert(img.root instanceof HTMLElement);
|
||||
assert.strictEqual(img.root.tagName, 'IMG');
|
||||
});
|
||||
|
||||
test('should handle ids and classes', () => {
|
||||
const divId = h('div#myid');
|
||||
assert.strictEqual(divId.root.tagName, 'DIV');
|
||||
assert.strictEqual(divId.root.id, 'myid');
|
||||
|
||||
const divClass = h('div.a');
|
||||
assert.strictEqual(divClass.root.tagName, 'DIV');
|
||||
assert.strictEqual(divClass.root.classList.length, 1);
|
||||
assert(divClass.root.classList.contains('a'));
|
||||
|
||||
const divClasses = h('div.a.b.c');
|
||||
assert.strictEqual(divClasses.root.tagName, 'DIV');
|
||||
assert.strictEqual(divClasses.root.classList.length, 3);
|
||||
assert(divClasses.root.classList.contains('a'));
|
||||
assert(divClasses.root.classList.contains('b'));
|
||||
assert(divClasses.root.classList.contains('c'));
|
||||
|
||||
const divAll = h('div#myid.a.b.c');
|
||||
assert.strictEqual(divAll.root.tagName, 'DIV');
|
||||
assert.strictEqual(divAll.root.id, 'myid');
|
||||
assert.strictEqual(divAll.root.classList.length, 3);
|
||||
assert(divAll.root.classList.contains('a'));
|
||||
assert(divAll.root.classList.contains('b'));
|
||||
assert(divAll.root.classList.contains('c'));
|
||||
|
||||
const spanId = h('span#myid');
|
||||
assert.strictEqual(spanId.root.tagName, 'SPAN');
|
||||
assert.strictEqual(spanId.root.id, 'myid');
|
||||
|
||||
const spanClass = h('span.a');
|
||||
assert.strictEqual(spanClass.root.tagName, 'SPAN');
|
||||
assert.strictEqual(spanClass.root.classList.length, 1);
|
||||
assert(spanClass.root.classList.contains('a'));
|
||||
|
||||
const spanClasses = h('span.a.b.c');
|
||||
assert.strictEqual(spanClasses.root.tagName, 'SPAN');
|
||||
assert.strictEqual(spanClasses.root.classList.length, 3);
|
||||
assert(spanClasses.root.classList.contains('a'));
|
||||
assert(spanClasses.root.classList.contains('b'));
|
||||
assert(spanClasses.root.classList.contains('c'));
|
||||
|
||||
const spanAll = h('span#myid.a.b.c');
|
||||
assert.strictEqual(spanAll.root.tagName, 'SPAN');
|
||||
assert.strictEqual(spanAll.root.id, 'myid');
|
||||
assert.strictEqual(spanAll.root.classList.length, 3);
|
||||
assert(spanAll.root.classList.contains('a'));
|
||||
assert(spanAll.root.classList.contains('b'));
|
||||
assert(spanAll.root.classList.contains('c'));
|
||||
});
|
||||
|
||||
test('should implicitly handle ids and classes', () => {
|
||||
const divId = h('#myid');
|
||||
assert.strictEqual(divId.root.tagName, 'DIV');
|
||||
assert.strictEqual(divId.root.id, 'myid');
|
||||
|
||||
const divClass = h('.a');
|
||||
assert.strictEqual(divClass.root.tagName, 'DIV');
|
||||
assert.strictEqual(divClass.root.classList.length, 1);
|
||||
assert(divClass.root.classList.contains('a'));
|
||||
|
||||
const divClasses = h('.a.b.c');
|
||||
assert.strictEqual(divClasses.root.tagName, 'DIV');
|
||||
assert.strictEqual(divClasses.root.classList.length, 3);
|
||||
assert(divClasses.root.classList.contains('a'));
|
||||
assert(divClasses.root.classList.contains('b'));
|
||||
assert(divClasses.root.classList.contains('c'));
|
||||
|
||||
const divAll = h('#myid.a.b.c');
|
||||
assert.strictEqual(divAll.root.tagName, 'DIV');
|
||||
assert.strictEqual(divAll.root.id, 'myid');
|
||||
assert.strictEqual(divAll.root.classList.length, 3);
|
||||
assert(divAll.root.classList.contains('a'));
|
||||
assert(divAll.root.classList.contains('b'));
|
||||
assert(divAll.root.classList.contains('c'));
|
||||
});
|
||||
|
||||
test('should handle @ identifiers', () => {
|
||||
const implicit = h('@el');
|
||||
assert.strictEqual(implicit.root, implicit.el);
|
||||
assert.strictEqual(implicit.el.tagName, 'DIV');
|
||||
|
||||
const explicit = h('div@el');
|
||||
assert.strictEqual(explicit.root, explicit.el);
|
||||
assert.strictEqual(explicit.el.tagName, 'DIV');
|
||||
|
||||
const implicitId = h('#myid@el');
|
||||
assert.strictEqual(implicitId.root, implicitId.el);
|
||||
assert.strictEqual(implicitId.el.tagName, 'DIV');
|
||||
assert.strictEqual(implicitId.root.id, 'myid');
|
||||
|
||||
const explicitId = h('div#myid@el');
|
||||
assert.strictEqual(explicitId.root, explicitId.el);
|
||||
assert.strictEqual(explicitId.el.tagName, 'DIV');
|
||||
assert.strictEqual(explicitId.root.id, 'myid');
|
||||
|
||||
const implicitClass = h('.a@el');
|
||||
assert.strictEqual(implicitClass.root, implicitClass.el);
|
||||
assert.strictEqual(implicitClass.el.tagName, 'DIV');
|
||||
assert.strictEqual(implicitClass.root.classList.length, 1);
|
||||
assert(implicitClass.root.classList.contains('a'));
|
||||
|
||||
const explicitClass = h('div.a@el');
|
||||
assert.strictEqual(explicitClass.root, explicitClass.el);
|
||||
assert.strictEqual(explicitClass.el.tagName, 'DIV');
|
||||
assert.strictEqual(explicitClass.root.classList.length, 1);
|
||||
assert(explicitClass.root.classList.contains('a'));
|
||||
});
|
||||
});
|
||||
|
||||
test('should recurse', () => {
|
||||
const result = h('div.code-view', [
|
||||
h('div.title@title'),
|
||||
h('div.container', [
|
||||
h('div.gutter@gutterDiv'),
|
||||
h('span@editor'),
|
||||
]),
|
||||
]);
|
||||
|
||||
assert.strictEqual(result.root.tagName, 'DIV');
|
||||
assert.strictEqual(result.root.className, 'code-view');
|
||||
assert.strictEqual(result.root.childElementCount, 2);
|
||||
assert.strictEqual(result.root.firstElementChild, result.title);
|
||||
assert.strictEqual(result.title.tagName, 'DIV');
|
||||
assert.strictEqual(result.title.className, 'title');
|
||||
assert.strictEqual(result.title.childElementCount, 0);
|
||||
assert.strictEqual(result.gutterDiv.tagName, 'DIV');
|
||||
assert.strictEqual(result.gutterDiv.className, 'gutter');
|
||||
assert.strictEqual(result.gutterDiv.childElementCount, 0);
|
||||
assert.strictEqual(result.editor.tagName, 'SPAN');
|
||||
assert.strictEqual(result.editor.className, '');
|
||||
assert.strictEqual(result.editor.childElementCount, 0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,14 +24,14 @@ export abstract class CodeEditorView extends Disposable {
|
|||
readonly model = this._viewModel.map(m => /** @description model */ m?.model);
|
||||
|
||||
protected readonly htmlElements = h('div.code-view', [
|
||||
h('div.title', { $: 'header' }, [
|
||||
h('span.title', { $: 'title' }),
|
||||
h('span.description', { $: 'description' }),
|
||||
h('span.detail', { $: 'detail' }),
|
||||
h('div.title@header', [
|
||||
h('span.title@title'),
|
||||
h('span.description@description'),
|
||||
h('span.detail@detail'),
|
||||
]),
|
||||
h('div.container', [
|
||||
h('div.gutter', { $: 'gutterDiv' }),
|
||||
h('div', { $: 'editor' }),
|
||||
h('div.gutter@gutterDiv'),
|
||||
h('div@editor'),
|
||||
]),
|
||||
]);
|
||||
|
||||
|
|
Loading…
Reference in a new issue