Initial commit
This commit is contained in:
24
src/presentation/Bootstrapping/ApplicationBootstrapper.ts
Normal file
24
src/presentation/Bootstrapping/ApplicationBootstrapper.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { TreeBootstrapper } from './Modules/TreeBootstrapper';
|
||||
import { IconBootstrapper } from './Modules/IconBootstrapper';
|
||||
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
||||
import { VueBootstrapper } from './Modules/VueBootstrapper';
|
||||
import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
|
||||
|
||||
export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
vue.config.productionTip = false;
|
||||
const bootstrappers = this.getAllBootstrappers();
|
||||
for (const bootstrapper of bootstrappers) {
|
||||
bootstrapper.bootstrap(vue);
|
||||
}
|
||||
}
|
||||
|
||||
private getAllBootstrappers(): IVueBootstrapper[] {
|
||||
return [
|
||||
new IconBootstrapper(),
|
||||
new TreeBootstrapper(),
|
||||
new VueBootstrapper(),
|
||||
new TooltipBootstrapper(),
|
||||
];
|
||||
}
|
||||
}
|
||||
7
src/presentation/Bootstrapping/IVueBootstrapper.ts
Normal file
7
src/presentation/Bootstrapping/IVueBootstrapper.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { VueConstructor } from 'vue';
|
||||
|
||||
export interface IVueBootstrapper {
|
||||
bootstrap(vue: VueConstructor): void;
|
||||
}
|
||||
|
||||
export { VueConstructor };
|
||||
18
src/presentation/Bootstrapping/Modules/IconBootstrapper.ts
Normal file
18
src/presentation/Bootstrapping/Modules/IconBootstrapper.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IVueBootstrapper, VueConstructor } from './../IVueBootstrapper';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
/** BRAND ICONS (PREFIX: fab) */
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
/** REGULAR ICONS (PREFIX: far) */
|
||||
import { faFolderOpen, faFolder } from '@fortawesome/free-regular-svg-icons';
|
||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
||||
export class IconBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
library.add(faGithub, faFolderOpen, faFolder,
|
||||
faTimes, faFileDownload, faCopy, faSearch, faInfoCircle);
|
||||
vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
export class TooltipBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
vue.use(VTooltip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import LiquorTree from 'liquor-tree';
|
||||
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper';
|
||||
|
||||
export class TreeBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
vue.use(LiquorTree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper';
|
||||
|
||||
export class VueBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
vue.config.productionTip = false;
|
||||
}
|
||||
}
|
||||
74
src/presentation/IconButton.vue
Normal file
74
src/presentation/IconButton.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<button class="button" @click="onClicked">
|
||||
<font-awesome-icon
|
||||
class="button__icon"
|
||||
:icon="[iconPrefix, iconName]" size="2x" />
|
||||
<div class="button__text">{{text}}</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue, IApplicationState } from './StatefulVue';
|
||||
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from './../infrastructure/Clipboard';
|
||||
|
||||
@Component
|
||||
export default class IconButton extends StatefulVue {
|
||||
@Prop() public text!: number;
|
||||
@Prop() public iconPrefix!: string;
|
||||
@Prop() public iconName!: string;
|
||||
|
||||
@Emit('click')
|
||||
public onClicked() {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
background-color: $accent;
|
||||
border: none;
|
||||
color: $white;
|
||||
padding:20px;
|
||||
transition-duration: 0.4s;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3px 9px $dark-slate;
|
||||
border-radius: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
// border: 0.1em solid $slate;
|
||||
// border-radius: 80px;
|
||||
// padding: 0.5em;
|
||||
width: 10%;
|
||||
min-width: 90px;
|
||||
&:hover {
|
||||
background: $white;
|
||||
box-shadow: 0px 2px 10px 5px $accent;
|
||||
color: $black;
|
||||
}
|
||||
&:hover>&__text {
|
||||
display: block;
|
||||
}
|
||||
&:hover>&__icon {
|
||||
display: none;
|
||||
}
|
||||
&__text {
|
||||
display: none;
|
||||
font-family: 'Yesteryear', cursive;
|
||||
font-size: 1.5em;
|
||||
color: $gray;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
62
src/presentation/Scripts/Cards/CardList.vue
Normal file
62
src/presentation/Scripts/Cards/CardList.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="categoryIds != null && categoryIds.length > 0" class="cards">
|
||||
<CardListItem
|
||||
class="card"
|
||||
v-for="categoryId of categoryIds"
|
||||
v-bind:key="categoryId"
|
||||
:categoryId="categoryId"
|
||||
:activeCategoryId="activeCategoryId"
|
||||
v-on:selected="onSelected(categoryId, $event)">
|
||||
</CardListItem>
|
||||
</div>
|
||||
<div v-else class="error">Something went bad 😢</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import CardListItem from './CardListItem.vue';
|
||||
import { StatefulVue, IApplicationState } from '@/presentation/StatefulVue';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
CardListItem,
|
||||
},
|
||||
})
|
||||
export default class CardList extends StatefulVue {
|
||||
public categoryIds: number[] = [];
|
||||
public activeCategoryId?: number = null;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.setCategories(state.categories);
|
||||
}
|
||||
|
||||
public onSelected(categoryId: number, isExpanded: boolean) {
|
||||
this.activeCategoryId = isExpanded ? categoryId : undefined;
|
||||
}
|
||||
|
||||
private setCategories(categories: ReadonlyArray<ICategory>): void {
|
||||
this.categoryIds = categories.map((category) => category.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
.card {
|
||||
|
||||
}
|
||||
}
|
||||
.error {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 3.5em;
|
||||
font: $default-font;
|
||||
}
|
||||
</style>
|
||||
248
src/presentation/Scripts/Cards/CardListItem.vue
Normal file
248
src/presentation/Scripts/Cards/CardListItem.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="card"
|
||||
v-on:click="onSelected(!isExpanded)"
|
||||
v-bind:class="{
|
||||
'is-collapsed': !isExpanded,
|
||||
'is-inactive': activeCategoryId && activeCategoryId != categoryId,
|
||||
'is-expanded': isExpanded}">
|
||||
<div class="card__inner">
|
||||
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
|
||||
<span v-else>Oh no 😢</span>
|
||||
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="expand-button" />
|
||||
</div>
|
||||
<div class="card__expander" v-on:click.stop>
|
||||
<font-awesome-icon :icon="['fas', 'times']" class="close-button" v-on:click="onSelected(false)"/>
|
||||
<CardListItemScripts :categoryId="categoryId"></CardListItemScripts>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator';
|
||||
import CardListItemScripts from './CardListItemScripts.vue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
CardListItemScripts,
|
||||
},
|
||||
})
|
||||
export default class CardListItem extends StatefulVue {
|
||||
@Prop() public categoryId!: number;
|
||||
@Prop() public activeCategoryId!: number;
|
||||
public cardTitle?: string = '';
|
||||
public isExpanded: boolean = false;
|
||||
|
||||
@Emit('selected')
|
||||
public onSelected(isExpanded: boolean) {
|
||||
this.isExpanded = isExpanded;
|
||||
}
|
||||
|
||||
@Watch('activeCategoryId')
|
||||
public async onActiveCategoryChanged(value: |number) {
|
||||
this.isExpanded = value === this.categoryId;
|
||||
}
|
||||
|
||||
public async mounted() {
|
||||
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined;
|
||||
}
|
||||
|
||||
@Watch('categoryId')
|
||||
public async onCategoryIdChanged(value: |number) {
|
||||
this.cardTitle = value ? await this.getCardTitleAsync(value) : undefined;
|
||||
}
|
||||
|
||||
|
||||
private async getCardTitleAsync(categoryId: number): Promise<string | undefined> {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
const category = state.getCategory(this.categoryId);
|
||||
return category ? category.name : undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
.card {
|
||||
margin: 15px;
|
||||
width: calc((100% / 3) - 30px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
//media queries for stacking cards
|
||||
@media screen and (max-width: 991px) {
|
||||
width: calc((100% / 2) - 30px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.card__inner {
|
||||
background-color: $accent;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
width: 100%;
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
background-color: $gray;
|
||||
color: $light-gray;
|
||||
font-size: 1.5em;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:after {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
width: 100%;
|
||||
margin-top: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
//Expander
|
||||
&__expander {
|
||||
transition: all 0.2s ease-in-out;
|
||||
background-color: $slate;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
text-transform: uppercase;
|
||||
color: $light-gray;
|
||||
font-size: 1.5em;
|
||||
|
||||
.close-button {
|
||||
font-size: 0.75em;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
|
||||
.card__inner {
|
||||
&:after {
|
||||
content: "";
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.card__expander {
|
||||
max-height: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-expanded {
|
||||
|
||||
.card__inner {
|
||||
background-color: $accent;
|
||||
|
||||
&:after{
|
||||
content: "";
|
||||
opacity: 1;
|
||||
display: block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
left: calc(50% - 15px);
|
||||
border-left: 15px solid transparent;
|
||||
border-right: 15px solid transparent;
|
||||
border-bottom: 15px solid #333a45;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card__expander {
|
||||
min-height: 200px;
|
||||
// max-height: 1000px;
|
||||
// overflow-y: auto;
|
||||
|
||||
margin-top: 30px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.card__inner {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-inactive {
|
||||
.card__inner {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.card__inner {
|
||||
background-color: $gray;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Expander Widths
|
||||
|
||||
//when 3 cards in a row
|
||||
@media screen and (min-width: 992px) {
|
||||
|
||||
.card:nth-of-type(3n+2) .card__expander {
|
||||
margin-left: calc(-100% - 30px);
|
||||
}
|
||||
.card:nth-of-type(3n+3) .card__expander {
|
||||
margin-left: calc(-200% - 60px);
|
||||
}
|
||||
.card:nth-of-type(3n+4) {
|
||||
clear: left;
|
||||
}
|
||||
.card__expander {
|
||||
width: calc(300% + 60px);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//when 2 cards in a row
|
||||
@media screen and (min-width: 768px) and (max-width: 991px) {
|
||||
|
||||
.card:nth-of-type(2n+2) .card__expander {
|
||||
margin-left: calc(-100% - 30px);
|
||||
}
|
||||
.card:nth-of-type(2n+3) {
|
||||
clear: left;
|
||||
}
|
||||
.card__expander {
|
||||
width: calc(200% + 30px);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
101
src/presentation/Scripts/Cards/CardListItemScripts.vue
Normal file
101
src/presentation/Scripts/Cards/CardListItemScripts.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<span>
|
||||
<span v-if="nodes != null && nodes.length > 0">
|
||||
<SelectableTree
|
||||
:nodes="nodes"
|
||||
:selectedNodeIds="selectedNodeIds"
|
||||
:filterPredicate="filterPredicate"
|
||||
:filterText="filterText"
|
||||
v-on:nodeSelected="checkNodeAsync($event)">
|
||||
</SelectableTree>
|
||||
</span>
|
||||
<span v-else>Nooo 😢</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
|
||||
import { IFilterMatches } from '@/application/State/Filter/IFilterMatches';
|
||||
import { ScriptNodeParser } from './ScriptNodeParser';
|
||||
import SelectableTree, { FilterPredicate } from './../SelectableTree/SelectableTree.vue';
|
||||
import { INode } from './../SelectableTree/INode';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
SelectableTree,
|
||||
},
|
||||
})
|
||||
export default class CardListItemScripts extends StatefulVue {
|
||||
@Prop() public categoryId!: number;
|
||||
|
||||
public nodes?: INode[] = null;
|
||||
public selectedNodeIds?: string[] = null;
|
||||
public filterText?: string = null;
|
||||
|
||||
private matches?: IFilterMatches;
|
||||
|
||||
public async mounted() {
|
||||
// React to state changes
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.reactToChanges(state);
|
||||
// Update initial state
|
||||
await this.updateNodesAsync(this.categoryId);
|
||||
}
|
||||
|
||||
public async checkNodeAsync(node: INode) {
|
||||
if (node.children != null && node.children.length > 0) {
|
||||
return; // only interested in script nodes
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
if (node.selected) {
|
||||
state.selection.addSelectedScript(node.id);
|
||||
} else {
|
||||
state.selection.removeSelectedScript(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('categoryId')
|
||||
public async updateNodesAsync(categoryId: |number) {
|
||||
this.nodes = categoryId ?
|
||||
await ScriptNodeParser.parseNodes(categoryId, await this.getCurrentStateAsync())
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public filterPredicate(node: INode): boolean {
|
||||
return this.matches.scriptMatches.some((script: IScript) => script.id === node.id);
|
||||
}
|
||||
|
||||
private reactToChanges(state: IApplicationState) {
|
||||
// Update selection data
|
||||
const updateNodeSelection = (node: INode, selectedScripts: ReadonlyArray<IScript>): INode => {
|
||||
return {
|
||||
id: node.id,
|
||||
text: node.text,
|
||||
selected: selectedScripts.some((script) => script.id === node.id),
|
||||
children: node.children ? node.children.map((child) => updateNodeSelection(child, selectedScripts)) : [],
|
||||
documentationUrls: node.documentationUrls,
|
||||
};
|
||||
};
|
||||
state.selection.changed.on(
|
||||
(selectedScripts: ReadonlyArray<IScript>) =>
|
||||
this.nodes = this.nodes.map((node: INode) => updateNodeSelection(node, selectedScripts)),
|
||||
);
|
||||
// Update search / filter data
|
||||
state.filter.filterRemoved.on(() =>
|
||||
this.filterText = '');
|
||||
state.filter.filtered.on((matches: IFilterMatches) => {
|
||||
this.filterText = matches.query;
|
||||
this.matches = matches;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
43
src/presentation/Scripts/Cards/ScriptNodeParser.ts
Normal file
43
src/presentation/Scripts/Cards/ScriptNodeParser.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ICategory } from './../../../domain/ICategory';
|
||||
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
|
||||
import { INode } from './../SelectableTree/INode';
|
||||
|
||||
export class ScriptNodeParser {
|
||||
public static parseNodes(categoryId: number, state: IApplicationState): INode[] | undefined {
|
||||
const category = state.getCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id ${categoryId} does not exist`);
|
||||
}
|
||||
const tree = this.parseNodesRecursively(category, state.selection);
|
||||
return tree;
|
||||
}
|
||||
|
||||
private static parseNodesRecursively(parentCategory: ICategory, selection: IUserSelection): INode[] {
|
||||
const nodes = new Array<INode>();
|
||||
if (parentCategory.subCategories && parentCategory.subCategories.length > 0) {
|
||||
for (const subCategory of parentCategory.subCategories) {
|
||||
const subCategoryNodes = this.parseNodesRecursively(subCategory, selection);
|
||||
nodes.push(
|
||||
{
|
||||
id: `cat${subCategory.id}`,
|
||||
text: subCategory.name,
|
||||
selected: false,
|
||||
children: subCategoryNodes,
|
||||
documentationUrls: subCategory.documentationUrls,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (parentCategory.scripts && parentCategory.scripts.length > 0) {
|
||||
for (const script of parentCategory.scripts) {
|
||||
nodes.push( {
|
||||
id: script.id,
|
||||
text: script.name,
|
||||
selected: selection.isSelected(script),
|
||||
children: undefined,
|
||||
documentationUrls: script.documentationUrls,
|
||||
});
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
19
src/presentation/Scripts/CategoryTree.vue
Normal file
19
src/presentation/Scripts/CategoryTree.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<CardList v-if="isGrouped">
|
||||
</CardList>
|
||||
<SelectableTree></SelectableTree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
|
||||
/** Shows content of single category or many categories */
|
||||
@Component
|
||||
export default class CategoryTree extends StatefulVue {
|
||||
@Prop() public data!: Category | Category[];
|
||||
}
|
||||
</script>
|
||||
7
src/presentation/Scripts/SelectableTree/INode.ts
Normal file
7
src/presentation/Scripts/SelectableTree/INode.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface INode {
|
||||
readonly id: string;
|
||||
readonly text: string;
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
readonly children?: ReadonlyArray<INode>;
|
||||
readonly selected: boolean;
|
||||
}
|
||||
46
src/presentation/Scripts/SelectableTree/Node.vue
Normal file
46
src/presentation/Scripts/SelectableTree/Node.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div id="node">
|
||||
<div>{{ this.data.text }}</div>
|
||||
<div
|
||||
v-for="url of this.data.documentationUrls"
|
||||
v-bind:key="url">
|
||||
<a :href="url"
|
||||
:alt="url"
|
||||
target="_blank" class="docs"
|
||||
v-tooltip.top-center="url"
|
||||
v-on:click.stop>
|
||||
<font-awesome-icon :icon="['fas', 'info-circle']" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { INode } from './INode';
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component
|
||||
export default class Node extends Vue {
|
||||
@Prop() public data: INode;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
#node {
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
.docs {
|
||||
color: $gray;
|
||||
cursor: pointer;
|
||||
margin-left:5px;
|
||||
&:hover {
|
||||
color: $slate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
181
src/presentation/Scripts/SelectableTree/SelectableTree.vue
Normal file
181
src/presentation/Scripts/SelectableTree/SelectableTree.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<span>
|
||||
<span v-if="initialNodes != null && initialNodes.length > 0">
|
||||
<tree :options="liquorTreeOptions"
|
||||
:data="this.initialNodes"
|
||||
v-on:node:checked="nodeSelected($event)"
|
||||
v-on:node:unchecked="nodeSelected($event)"
|
||||
ref="treeElement"
|
||||
>
|
||||
<span class="tree-text" slot-scope="{ node }">
|
||||
<Node :data="convertExistingToNode(node)"/>
|
||||
</span>
|
||||
</tree>
|
||||
</span>
|
||||
<span v-else>Nooo 😢</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit, Watch } from 'vue-property-decorator';
|
||||
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree } from 'liquor-tree';
|
||||
import Node from './Node.vue';
|
||||
import { INode } from './INode';
|
||||
export type FilterPredicate = (node: INode) => boolean;
|
||||
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component({
|
||||
components: {
|
||||
LiquorTree,
|
||||
Node,
|
||||
},
|
||||
})
|
||||
export default class SelectableTree extends Vue {
|
||||
@Prop() public filterPredicate?: FilterPredicate;
|
||||
@Prop() public filterText?: string;
|
||||
@Prop() public nodes?: INode[];
|
||||
|
||||
public initialNodes?: ILiquorTreeNewNode[] = null;
|
||||
public liquorTreeOptions = this.getLiquorTreeOptions();
|
||||
|
||||
public mounted() {
|
||||
// console.log('Mounted', 'initial nodes', this.nodes);
|
||||
// console.log('Mounted', 'initial model', this.getLiquorTreeApi().model);
|
||||
|
||||
if (this.nodes) {
|
||||
this.initialNodes = this.nodes.map((node) => this.toLiquorTreeNode(node));
|
||||
} else {
|
||||
throw new Error('Initial nodes are null or empty');
|
||||
}
|
||||
|
||||
if (this.filterText) {
|
||||
this.updateFilterText(this.filterText);
|
||||
}
|
||||
}
|
||||
|
||||
public nodeSelected(node: ILiquorTreeExistingNode) {
|
||||
this.$emit('nodeSelected', this.convertExistingToNode(node));
|
||||
return;
|
||||
}
|
||||
|
||||
@Watch('filterText')
|
||||
public updateFilterText(filterText: |string) {
|
||||
const api = this.getLiquorTreeApi();
|
||||
if (!filterText) {
|
||||
api.clearFilter();
|
||||
} else {
|
||||
api.filter('filtered'); // text does not matter, it'll trigger the predicate
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('nodes', {deep: true})
|
||||
public setSelectedStatus(nodes: |ReadonlyArray<INode>) {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
throw new Error('Updated nodes are null or empty');
|
||||
}
|
||||
// Update old node properties, re-setting it changes expanded status etc.
|
||||
// It'll not be needed when this is merged: https://github.com/amsik/liquor-tree/pull/141
|
||||
const updateCheckedState = (
|
||||
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
|
||||
updatedNodes: ReadonlyArray<INode>): ILiquorTreeNewNode[] => {
|
||||
const newNodes = new Array<ILiquorTreeNewNode>();
|
||||
for (const oldNode of oldNodes) {
|
||||
for (const updatedNode of updatedNodes) {
|
||||
if (oldNode.id === updatedNode.id) {
|
||||
const newState = oldNode.states;
|
||||
newState.checked = updatedNode.selected;
|
||||
newNodes.push({
|
||||
id: oldNode.id,
|
||||
text: updatedNode.text,
|
||||
children: oldNode.children == null ? [] :
|
||||
updateCheckedState(
|
||||
oldNode.children,
|
||||
updatedNode.children),
|
||||
state: newState,
|
||||
data: {
|
||||
documentationUrls: oldNode.data.documentationUrls,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return newNodes;
|
||||
};
|
||||
const newModel = updateCheckedState(
|
||||
this.getLiquorTreeApi().model, nodes);
|
||||
this.getLiquorTreeApi().setModel(newModel);
|
||||
}
|
||||
|
||||
private convertItem(liquorTreeNode: ILiquorTreeNewNode): INode {
|
||||
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
|
||||
return {
|
||||
id: liquorTreeNode.id,
|
||||
text: liquorTreeNode.text,
|
||||
selected: liquorTreeNode.state && liquorTreeNode.state.checked,
|
||||
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
|
||||
? [] : liquorTreeNode.children.map((childNode) => this.convertItem(childNode)),
|
||||
documentationUrls: liquorTreeNode.data.documentationUrls,
|
||||
};
|
||||
}
|
||||
|
||||
private convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode): INode {
|
||||
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
|
||||
return {
|
||||
id: liquorTreeNode.id,
|
||||
text: liquorTreeNode.data.text,
|
||||
selected: liquorTreeNode.states && liquorTreeNode.states.checked,
|
||||
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
|
||||
? [] : liquorTreeNode.children.map((childNode) => this.convertExistingToNode(childNode)),
|
||||
documentationUrls: liquorTreeNode.data.documentationUrls,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private toLiquorTreeNode(node: INode): ILiquorTreeNewNode {
|
||||
if (!node) { throw new Error('node is undefined'); }
|
||||
return {
|
||||
id: node.id,
|
||||
text: node.text,
|
||||
state: {
|
||||
checked: node.selected,
|
||||
},
|
||||
children: (!node.children || node.children.length === 0) ? [] :
|
||||
node.children.map((childNode) => this.toLiquorTreeNode(childNode)),
|
||||
data: {
|
||||
documentationUrls: node.documentationUrls,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getLiquorTreeOptions(): any {
|
||||
return {
|
||||
checkbox: true,
|
||||
checkOnSelect: true,
|
||||
deletion: (node) => !node.children || node.children.length === 0,
|
||||
filter: {
|
||||
matcher: (query: string, node: ILiquorTreeExistingNode) => {
|
||||
if (!this.filterPredicate) {
|
||||
throw new Error('Cannot filter as predicate is null');
|
||||
}
|
||||
return this.filterPredicate(this.convertExistingToNode(node));
|
||||
},
|
||||
emptyText: '🕵️Hmm.. Can not see one 🧐',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getLiquorTreeApi(): ILiquorTree {
|
||||
if (!this.$refs.treeElement) {
|
||||
throw new Error('Referenced tree element cannot be found. Probably it\'s not rendered?');
|
||||
}
|
||||
return (this.$refs.treeElement as any).tree;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
</style>
|
||||
32
src/presentation/Scripts/Selector/SelectableOption.vue
Normal file
32
src/presentation/Scripts/Selector/SelectableOption.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<span
|
||||
v-bind:class="{ 'disabled': enabled, 'enabled': !enabled}"
|
||||
@click="onClicked()">{{label}}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
|
||||
@Component
|
||||
export default class SelectableOption extends StatefulVue {
|
||||
@Prop() public enabled: boolean;
|
||||
@Prop() public label: string;
|
||||
@Emit('click') public onClicked() { return; }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
.enabled {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
font-weight:bold;
|
||||
text-decoration:underline;
|
||||
}
|
||||
}
|
||||
.disabled {
|
||||
color:$gray;
|
||||
}
|
||||
</style>
|
||||
106
src/presentation/Scripts/Selector/TheSelector.vue
Normal file
106
src/presentation/Scripts/Selector/TheSelector.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="part">Select:</div>
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
label="Recommended"
|
||||
:enabled="isRecommendedSelected"
|
||||
@click="selectRecommendedAsync()" />
|
||||
</div>
|
||||
<div class="part"> | </div>
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
label="All"
|
||||
:enabled="isAllSelected"
|
||||
@click="selectAllAsync()" />
|
||||
</div>
|
||||
<div class="part"> | </div>
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
label="None"
|
||||
:enabled="isNoneSelected"
|
||||
@click="selectNoneAsync()">
|
||||
</SelectableOption>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import SelectableOption from './SelectableOption.vue';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { IScript } from '@/domain/Script';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
SelectableOption,
|
||||
},
|
||||
})
|
||||
export default class TheSelector extends StatefulVue {
|
||||
public isAllSelected = false;
|
||||
public isNoneSelected = false;
|
||||
public isRecommendedSelected = false;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.updateSelections(state);
|
||||
state.selection.changed.on(() => {
|
||||
this.updateSelections(state);
|
||||
});
|
||||
}
|
||||
|
||||
public async selectAllAsync(): Promise<void> {
|
||||
if (this.isAllSelected) {
|
||||
return;
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.selectAll();
|
||||
}
|
||||
|
||||
public async selectRecommendedAsync(): Promise<void> {
|
||||
if (this.isRecommendedSelected) {
|
||||
return;
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.selectOnly(state.defaultScripts);
|
||||
}
|
||||
|
||||
public async selectNoneAsync(): Promise<void> {
|
||||
if (this.isNoneSelected) {
|
||||
return;
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.deselectAll();
|
||||
}
|
||||
|
||||
private updateSelections(state: IApplicationState) {
|
||||
this.isNoneSelected = state.selection.totalSelected === 0;
|
||||
this.isAllSelected = state.selection.totalSelected === state.appTotalScripts;
|
||||
this.isRecommendedSelected = this.areSame(state.defaultScripts, state.selection.selectedScripts);
|
||||
}
|
||||
|
||||
private areSame(scripts: ReadonlyArray<IScript>, other: ReadonlyArray<IScript>): boolean {
|
||||
return (scripts.length === other.length) &&
|
||||
scripts.every((script) => other.some((s) => s.id === script.id));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items:flex-start;
|
||||
|
||||
.part {
|
||||
display: flex;
|
||||
margin-right:5px;
|
||||
}
|
||||
font:16px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
}
|
||||
|
||||
</style>
|
||||
48
src/presentation/Scripts/TheGrouper.vue
Normal file
48
src/presentation/Scripts/TheGrouper.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
Group by: <span
|
||||
v-bind:class="{ 'disabled': isGrouped, 'enabled': !isGrouped}"
|
||||
@click="changeGrouping()" >Cards</Span> |
|
||||
<span class="action"
|
||||
v-bind:class="{ 'disabled': !isGrouped, 'enabled': isGrouped}"
|
||||
@click="changeGrouping()">None</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
|
||||
@Component
|
||||
export default class TheGrouper extends StatefulVue {
|
||||
public isGrouped = true;
|
||||
|
||||
public changeGrouping() {
|
||||
this.isGrouped = !this.isGrouped;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
.container {
|
||||
// text-align:left;
|
||||
font:16px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
|
||||
}
|
||||
.enabled {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
font-weight:bold;
|
||||
text-decoration:underline;
|
||||
}
|
||||
}
|
||||
.disabled {
|
||||
color:$gray;
|
||||
}
|
||||
|
||||
</style>
|
||||
11
src/presentation/StatefulVue.ts
Normal file
11
src/presentation/StatefulVue.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ApplicationState, IApplicationState } from '../application/State/ApplicationState';
|
||||
import { Vue } from 'vue-property-decorator';
|
||||
export { IApplicationState };
|
||||
|
||||
export abstract class StatefulVue extends Vue {
|
||||
public isLoading = true;
|
||||
|
||||
protected getCurrentStateAsync(): Promise<IApplicationState> {
|
||||
return ApplicationState.GetAsync();
|
||||
}
|
||||
}
|
||||
53
src/presentation/TheCodeArea.vue
Normal file
53
src/presentation/TheCodeArea.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div :id="editorId" class="code-area" ></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue, IApplicationState } from './StatefulVue';
|
||||
import ace from 'ace-builds';
|
||||
import 'ace-builds/webpack-resolver';
|
||||
|
||||
|
||||
@Component
|
||||
export default class TheCodeArea extends StatefulVue {
|
||||
public readonly editorId = 'codeEditor';
|
||||
private editor!: ace.Ace.Editor;
|
||||
|
||||
@Prop() private theme!: string;
|
||||
|
||||
public async mounted() {
|
||||
this.editor = this.initializeEditor();
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.updateCode(state.code.current);
|
||||
state.code.changed.on((code) => this.updateCode(code));
|
||||
}
|
||||
|
||||
private updateCode(code: string) {
|
||||
this.editor.setValue(code || 'Something is bad 😢', 1);
|
||||
}
|
||||
|
||||
private initializeEditor(): ace.Ace.Editor {
|
||||
const lang = 'batchfile';
|
||||
const theme = this.theme || 'github';
|
||||
const editor = ace.edit(this.editorId);
|
||||
editor.getSession().setMode(`ace/mode/${lang}`);
|
||||
editor.setTheme(`ace/theme/${theme}`);
|
||||
editor.setReadOnly(true);
|
||||
editor.setAutoScrollEditorIntoView(true);
|
||||
// this.editor.getSession().setUseWrapMode(true);
|
||||
// this.editor.setOption("indentedSoftWrap", false);
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.code-area {
|
||||
/* ----- Fill its parent div ------ */
|
||||
width: 100%;
|
||||
/* height */
|
||||
max-height: 1000px;
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
64
src/presentation/TheCodeButtons.vue
Normal file
64
src/presentation/TheCodeButtons.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="container" v-if="hasCode">
|
||||
<IconButton
|
||||
text="Copy"
|
||||
v-on:click="copyCodeAsync"
|
||||
icon-prefix="fas" icon-name="copy">
|
||||
</IconButton>
|
||||
<IconButton
|
||||
text="Download"
|
||||
v-on:click="saveCodeAsync"
|
||||
icon-prefix="fas" icon-name="file-download">
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue, IApplicationState } from './StatefulVue';
|
||||
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from './../infrastructure/Clipboard';
|
||||
import IconButton from './IconButton.vue';
|
||||
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
})
|
||||
export default class TheCodeButtons extends StatefulVue {
|
||||
public hasCode = false;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.hasCode = state.code.current && state.code.current.length > 0;
|
||||
state.code.changed.on((code) => {
|
||||
this.hasCode = code && code.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
public async copyCodeAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
Clipboard.copyText(state.code.current);
|
||||
}
|
||||
|
||||
public async saveCodeAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
SaveFileDialog.saveText(state.code.current, 'privacy-script.bat');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
.container > * + * {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
</style>
|
||||
66
src/presentation/TheHeader.vue
Normal file
66
src/presentation/TheHeader.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div id="container">
|
||||
<h1 class="child title" >{{ title }}</h1>
|
||||
<h2 class="child subtitle">{{ subtitle }}</h2>
|
||||
<a :href="githubUrl" target="_blank" class="child github" >
|
||||
<font-awesome-icon :icon="['fab', 'github']" size="3x" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
|
||||
@Component
|
||||
export default class TheHeader extends StatefulVue {
|
||||
private title: string = '';
|
||||
private subtitle: string = '';
|
||||
@Prop() private githubUrl!: string;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.title = state.appName;
|
||||
this.subtitle = `Privacy generator tool for Windows v${state.appVersion}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
#container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.child {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
color: $black;
|
||||
text-transform: uppercase;
|
||||
font-size: 2.5em;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
color: $gray;
|
||||
font-family: 'Yesteryear', cursive;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.github {
|
||||
color:inherit;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
78
src/presentation/TheSearchBar.vue
Normal file
78
src/presentation/TheSearchBar.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="search">
|
||||
<input type="text" class="searchTerm" placeholder="Search for configurations"
|
||||
@input="updateFilterAsync($event.target.value)" >
|
||||
<div class="iconWrapper">
|
||||
<font-awesome-icon :icon="['fas', 'search']" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
|
||||
@Component
|
||||
export default class TheSearchBar extends StatefulVue {
|
||||
|
||||
|
||||
public async updateFilterAsync(filter: |string) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
if (!filter) {
|
||||
state.filter.removeFilter();
|
||||
} else {
|
||||
state.filter.setFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
.container {
|
||||
padding-top: 30px;
|
||||
padding-right: 30%;
|
||||
padding-left: 30%;
|
||||
font: $default-font;
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.searchTerm {
|
||||
width: 100%;
|
||||
border: 1.5px solid $gray;
|
||||
border-right: none;
|
||||
height: 36px;
|
||||
border-radius: 3px 0 0 3px;
|
||||
padding-left:10px;
|
||||
padding-right:10px;
|
||||
outline: none;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.searchTerm:focus{
|
||||
color: $slate;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
width: 40px;
|
||||
height: 36px;
|
||||
border: 1px solid $gray;
|
||||
background: $gray;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
border-radius: 0 5px 5px 0;
|
||||
font-size: 20px;
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
8
src/presentation/styles/colors.scss
Normal file
8
src/presentation/styles/colors.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
$white: #fff;
|
||||
$light-gray: #eceef1;
|
||||
$gray: darken(#eceef1, 30%);
|
||||
$dark-gray: #616f86;
|
||||
$slate: darken(#eceef1, 70%);
|
||||
$dark-slate: #2f3133;
|
||||
$accent: #1abc9c;
|
||||
$black: #000
|
||||
26
src/presentation/styles/fonts.scss
Normal file
26
src/presentation/styles/fonts.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Slabo 27px';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Slabo 27px'), local('Slabo27px-Regular'), url('~@/presentation/styles/fonts/Slabo27px-v6.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Slabo 27px';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Slabo 27px'), local('Slabo27px-Regular'), url('~@/presentation/styles/fonts/Slabo27px-v6.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Yesteryear';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Yesteryear'), local('Yesteryear-Regular'), url('~@/presentation/styles/fonts/yesteryear-v8.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
$default-font: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
BIN
src/presentation/styles/fonts/Slabo27px-v6.woff2
Normal file
BIN
src/presentation/styles/fonts/Slabo27px-v6.woff2
Normal file
Binary file not shown.
BIN
src/presentation/styles/fonts/yesteryear-v8.woff2
Normal file
BIN
src/presentation/styles/fonts/yesteryear-v8.woff2
Normal file
Binary file not shown.
43
src/presentation/styles/tooltip.scss
Normal file
43
src/presentation/styles/tooltip.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
// based on https://github.com/Akryum/v-tooltip/blob/83615e394c96ca491a4df04b892ae87e833beb97/demo-src/src/App.vue#L179-L303
|
||||
.tooltip {
|
||||
display: block !important;
|
||||
z-index: 10000;
|
||||
.tooltip-inner {
|
||||
background: $black;
|
||||
color: $white;
|
||||
border-radius: 16px;
|
||||
padding: 5px 10px 4px;
|
||||
}
|
||||
.tooltip-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: $black;
|
||||
z-index: 1;
|
||||
}
|
||||
&[x-placement^="top"] {
|
||||
margin-bottom: 5px;
|
||||
.tooltip-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
&[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity .15s, visibility .15s;
|
||||
}
|
||||
&[aria-hidden='false'] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
}
|
||||
35
src/presentation/styles/tree.scss
Normal file
35
src/presentation/styles/tree.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
// Overrides base styling for LiquorTree
|
||||
|
||||
.tree-node > .tree-content > .tree-anchor > span {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.tree-arrow.has-child {
|
||||
&.rtl:after, &:after {
|
||||
border-color: $white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node.selected > .tree-content {
|
||||
> .tree-anchor > span {
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-content:hover {
|
||||
background: $dark-gray !important;
|
||||
}
|
||||
|
||||
.tree-checkbox {
|
||||
&.checked {
|
||||
background: $accent !important;
|
||||
}
|
||||
&.indeterminate {
|
||||
border-color: $gray !important;
|
||||
}
|
||||
background: $dark-slate !important;
|
||||
}
|
||||
Reference in New Issue
Block a user