Skip to content

Commit 1a85b12

Browse files
author
Daniel Nagy
committed
first commit
0 parents  commit 1a85b12

19 files changed

+4294
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
3+
dist
4+
node_modules

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Boulevard
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Vampire
2+
3+
Slots without shadows.
4+
5+
* [License](#license)
6+
* [Installation](#installation)
7+
* [Example](#example)
8+
9+
## License
10+
11+
This software is provided free of charge and without restriction under the
12+
[MIT License](LICENSE.md).
13+
14+
## Installation
15+
16+
This module is installable through npm.
17+
18+
```
19+
npm install --save @boulevard/vampire
20+
```
21+
22+
## Example
23+
24+
This example uses LitElement; however, LitElement is not required to use this
25+
module.
26+
27+
```typescript
28+
import '@boulevard/vampire';
29+
import { render } from 'lit-html';
30+
import { customElement, html, LitElement } from 'lit-element';
31+
32+
const WithSlots = (BaseClass: typeof LitElement) => class extends BaseClass {
33+
static render = render;
34+
35+
constructor() {
36+
super();
37+
this.appendChild(this.renderRoot);
38+
}
39+
40+
createRenderRoot() {
41+
return document.createElement('v-root');
42+
}
43+
}
44+
45+
@customElement('x-example')
46+
export class ExampleElement extends WithSlots(LitElement) {
47+
render() {
48+
return html`
49+
<h5>Example</h5>
50+
<v-slot></v-slot>
51+
`;
52+
}
53+
}
54+
```
55+
56+
Given the following markup
57+
58+
```html
59+
<x-example>
60+
This content will be slotted.
61+
<x-example>
62+
```
63+
64+
The above component will produce the following output when rendered.
65+
66+
```html
67+
<x-example>
68+
<v-root>
69+
<h5>Example</h5>
70+
<v-slot>
71+
<v-slot-assigned-content>
72+
This content will be slotted.
73+
</v-slot-assigned-content>
74+
</v-slot>
75+
</v-root>
76+
<x-example>
77+
```

jest.config.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"globals": {
3+
"ts-jest": {
4+
"tsConfig": "test/tsconfig.test.json"
5+
}
6+
},
7+
"preset": "ts-jest/presets/js-with-ts",
8+
"rootDir": "test",
9+
"setupFilesAfterEnv": ["./polyfills.ts"],
10+
"testEnvironment": "jest-environment-jsdom-fourteen"
11+
}

package.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@boulevard/vampire",
3+
"version": "1.0.0-alpha.1",
4+
"description": "Shadowless slots.",
5+
"main": "dist/index.js",
6+
"module": "dist/index.js",
7+
"files": [
8+
"dist/*"
9+
],
10+
"publishConfig": {
11+
"access": "public"
12+
},
13+
"scripts": {
14+
"build": "tsc && cp src/vampire.css dist",
15+
"prebuild": "yarn run test",
16+
"prepublish": "yarn run build",
17+
"test": "jest --config jest.config.json",
18+
"test-debug": "node --inspect-brk $(npm bin)/jest --runInBand --config jest.config.json"
19+
},
20+
"author": "Daniel Nagy <[email protected]>",
21+
"repository": "github:Boulevard/vampire",
22+
"license": "MIT",
23+
"dependencies": {},
24+
"devDependencies": {
25+
"@types/jest": "^24.0.11",
26+
"@webcomponents/custom-elements": "^1.2.4",
27+
"jest": "^24.7.1",
28+
"jest-environment-jsdom-fourteen": "^0.1.0",
29+
"ts-jest": "^24.0.2",
30+
"typescript": "<3.3.0"
31+
}
32+
}

src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './vampire-root';
2+
export * from './vampire-slot';
3+
export * from './vampire-slot-assigned-content';
4+
export * from './vampire-slot-fallback-content';

src/utils.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function toggleClass(element: HTMLElement, className: string, state?: boolean): boolean {
2+
let addClass = typeof state === 'boolean'
3+
? state
4+
: !element.classList.contains(className);
5+
6+
if (addClass) {
7+
element.classList.add(className);
8+
} else {
9+
element.classList.remove(className);
10+
}
11+
12+
return addClass;
13+
}

src/vampire-root.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export class VampireRoot extends HTMLElement {
2+
static readonly tagName = 'v-root';
3+
}
4+
5+
declare global {
6+
interface HTMLElementTagNameMap {
7+
[VampireRoot.tagName]: VampireRoot;
8+
}
9+
}
10+
11+
customElements.define(VampireRoot.tagName, VampireRoot);

src/vampire-slot-assigned-content.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export class VampireSlotAssignedContent extends HTMLElement {
2+
static readonly tagName = 'v-slot-assigned-content';
3+
}
4+
5+
export namespace VampireSlotAssignedContent {
6+
export enum Classes {
7+
Hidden = 'v-slot__assigned-content--hidden'
8+
}
9+
}
10+
11+
declare global {
12+
interface HTMLElementTagNameMap {
13+
[VampireSlotAssignedContent.tagName]: VampireSlotAssignedContent;
14+
}
15+
}
16+
17+
customElements
18+
.define(VampireSlotAssignedContent.tagName, VampireSlotAssignedContent);

src/vampire-slot-fallback-content.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { VampireSlot } from './vampire-slot';
2+
import { toggleClass } from './utils';
3+
4+
export class VampireSlotFallbackContent extends HTMLElement {
5+
static readonly tagName = 'v-slot-fallback-content';
6+
7+
constructor() {
8+
super();
9+
this._onSlotChange = this._onSlotChange.bind(this);
10+
}
11+
12+
connectedCallback() {
13+
let hidden = false;
14+
15+
if (this.parentElement instanceof VampireSlot) {
16+
hidden = Boolean(this.parentElement.assignedNodes().length);
17+
18+
this.parentElement
19+
.addEventListener(VampireSlot.Events.SlotChange, this._onSlotChange);
20+
}
21+
22+
toggleClass(this, VampireSlotFallbackContent.Classes.Hidden, hidden);
23+
}
24+
25+
disconnectedCallback() {
26+
if (this.parentElement instanceof VampireSlot) {
27+
this.parentElement
28+
.removeEventListener(VampireSlot.Events.SlotChange, this._onSlotChange);
29+
}
30+
}
31+
32+
protected _onSlotChange(event: Event) {
33+
const slot = event.target as VampireSlot;
34+
const hidden = Boolean(slot.assignedNodes().length);
35+
36+
toggleClass(this, VampireSlotFallbackContent.Classes.Hidden, hidden);
37+
}
38+
}
39+
40+
export namespace VampireSlotFallbackContent {
41+
export enum Classes {
42+
Hidden = 'v-slot__fallback-content--hidden'
43+
}
44+
}
45+
46+
declare global {
47+
interface HTMLElementTagNameMap {
48+
[VampireSlotFallbackContent.tagName]: VampireSlotFallbackContent;
49+
}
50+
}
51+
52+
customElements
53+
.define(VampireSlotFallbackContent.tagName, VampireSlotFallbackContent);

src/vampire-slot.ts

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { VampireRoot } from './vampire-root';
2+
import { VampireSlotAssignedContent } from './vampire-slot-assigned-content';
3+
import { VampireSlotFallbackContent } from './vampire-slot-fallback-content';
4+
import { toggleClass } from './utils';
5+
6+
export class VampireSlot extends HTMLElement {
7+
static readonly tagName = 'v-slot';
8+
9+
name: string = this.getAttribute('name') || '';
10+
11+
protected _assignedContent: VampireSlotAssignedContent;
12+
protected _observer = new MutationObserver(() => this._updateAssignedContent());
13+
protected _vampireRoot: VampireRoot | null = null;
14+
15+
constructor() {
16+
super();
17+
18+
this._assignedContent = document.createElement(VampireSlotAssignedContent.tagName);
19+
this._assignedContent.classList.add(VampireSlotAssignedContent.Classes.Hidden);
20+
this.appendChild(this._assignedContent);
21+
22+
const observer = new MutationObserver(() => {
23+
const hidden = this._assignedContent.childNodes.length === 0;
24+
25+
toggleClass(this._assignedContent, VampireSlotAssignedContent.Classes.Hidden, hidden);
26+
this.dispatchEvent(new CustomEvent(VampireSlot.Events.SlotChange, {
27+
bubbles: true
28+
}));
29+
});
30+
31+
observer.observe(this._assignedContent, {childList: true});
32+
}
33+
34+
connectedCallback() {
35+
this._vampireRoot = this._getVampireRoot();
36+
37+
if (!this._vampireRoot) {
38+
return;
39+
}
40+
41+
this._updateAssignedContent();
42+
this._observer.observe(this._vampireRoot.parentElement!, {childList: true});
43+
}
44+
45+
disconnectedCallback() {
46+
this._observer.disconnect();
47+
48+
const element = this._vampireRoot && this._vampireRoot.parentElement;
49+
const fragment = document.createDocumentFragment();
50+
51+
Array.from(this._assignedContent.childNodes).forEach((child) => {
52+
fragment.appendChild(child);
53+
});
54+
55+
if (element) {
56+
element.appendChild(fragment);
57+
}
58+
59+
this._vampireRoot = null;
60+
}
61+
62+
assignedElements(options: {flatten?: boolean} = {}): Element[] {
63+
const assignedElements = Array.from(this._assignedContent.children);
64+
const fallbackContent = this.querySelector(VampireSlotFallbackContent.tagName);
65+
66+
return options.flatten && !assignedElements.length
67+
? fallbackContent ? Array.from(fallbackContent.children) : []
68+
: assignedElements;
69+
}
70+
71+
assignedNodes(options: {flatten?: boolean} = {}): Node[] {
72+
const assignedNodes = Array.from(this._assignedContent.childNodes);
73+
const fallbackContent = this.querySelector(VampireSlotFallbackContent.tagName);
74+
75+
return options.flatten && !assignedNodes.length
76+
? fallbackContent ? Array.from(fallbackContent.childNodes) : []
77+
: assignedNodes;
78+
}
79+
80+
protected _getSlotForNode(node: Node): string {
81+
return node instanceof HTMLElement ? node.getAttribute('v-slot') || '' : '';
82+
}
83+
84+
protected _getVampireRoot(): VampireRoot | null {
85+
let parent = this.parentElement;
86+
87+
while (parent !== null && !(parent instanceof VampireRoot)) {
88+
if (parent instanceof VampireSlot) {
89+
/**
90+
* There is nothing stoping someone from placing a <v-slot> in their
91+
* slotted content. If we encounter a <v-slot> within a <v-slot> just
92+
* ignore it.
93+
*/
94+
parent = null;
95+
break;
96+
}
97+
98+
parent = parent.parentElement;
99+
}
100+
101+
return parent;
102+
}
103+
104+
protected _updateAssignedContent() {
105+
if (!this._vampireRoot || !this._vampireRoot.parentElement) {
106+
return;
107+
}
108+
109+
const assignedContent = Array
110+
.from(this._vampireRoot.parentElement.childNodes)
111+
.filter((node) => !(node instanceof VampireRoot)
112+
&& this._getSlotForNode(node) === this.name);
113+
114+
if (assignedContent.length) {
115+
const fragment = document.createDocumentFragment();
116+
117+
assignedContent.forEach((node) => {
118+
fragment.appendChild(node);
119+
});
120+
121+
this._assignedContent.appendChild(fragment);
122+
}
123+
}
124+
}
125+
126+
export namespace VampireSlot {
127+
export enum Events {
128+
SlotChange = 'v::slotchange'
129+
}
130+
}
131+
132+
declare global {
133+
interface HTMLElementTagNameMap {
134+
[VampireSlot.tagName]: VampireSlot;
135+
}
136+
}
137+
138+
customElements.define(VampireSlot.tagName, VampireSlot);

0 commit comments

Comments
 (0)