move code area to right on bigger screens
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.5",
|
"inversify": "^5.0.5",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"v-tooltip": "2.0.2",
|
"v-tooltip": "2.0.2",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
|
|||||||
27
src/App.vue
27
src/App.vue
@@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<TheHeader class="row" />
|
<TheHeader class="row" />
|
||||||
<TheSearchBar class="row" />
|
<TheSearchBar class="row" />
|
||||||
<TheScripts class="row"/>
|
<TheScriptArea class="row" />
|
||||||
<TheCodeArea class="row" theme="xcode" />
|
<TheCodeButtons class="row code-buttons" />
|
||||||
<TheCodeButtons class="row code-buttons" />
|
<TheFooter />
|
||||||
<TheFooter />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,17 +14,15 @@
|
|||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import TheHeader from '@/presentation/TheHeader.vue';
|
import TheHeader from '@/presentation/TheHeader.vue';
|
||||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
import TheCodeButtons from '@/presentation/Code/CodeButtons/TheCodeButtons.vue';
|
||||||
import TheCodeButtons from '@/presentation/CodeButtons/TheCodeButtons.vue';
|
import TheScriptArea from '@/presentation/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||||
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
TheHeader,
|
TheHeader,
|
||||||
TheCodeArea,
|
|
||||||
TheCodeButtons,
|
TheCodeButtons,
|
||||||
TheScripts,
|
TheScriptArea,
|
||||||
TheSearchBar,
|
TheSearchBar,
|
||||||
TheFooter,
|
TheFooter,
|
||||||
},
|
},
|
||||||
@@ -38,6 +35,7 @@ export default class App extends Vue {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
@import "@/presentation/styles/fonts.scss";
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
@import "@/presentation/styles/media.scss";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -49,12 +47,10 @@ body {
|
|||||||
color: $slate;
|
color: $slate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
max-width: 1500px;
|
max-width: 1600px;
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
margin: 0% 2% 0% 2%;
|
margin: 0% 2% 0% 2%;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
@@ -62,18 +58,15 @@ body {
|
|||||||
padding: 2%;
|
padding: 2%;
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-buttons {
|
.code-buttons {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@import "@/presentation/styles/tooltip.scss";
|
@import "@/presentation/styles/tooltip.scss";
|
||||||
@import "@/presentation/styles/tree.scss";
|
@import "@/presentation/styles/tree.scss";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|||||||
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
||||||
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf, faPlay } from '@fortawesome/free-solid-svg-icons';
|
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf, faPlay, faArrowsAltH } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
export class IconBootstrapper implements IVueBootstrapper {
|
export class IconBootstrapper implements IVueBootstrapper {
|
||||||
public bootstrap(vue: VueConstructor): void {
|
public bootstrap(vue: VueConstructor): void {
|
||||||
@@ -26,7 +26,9 @@ export class IconBootstrapper implements IVueBootstrapper {
|
|||||||
faPlay,
|
faPlay,
|
||||||
faSearch,
|
faSearch,
|
||||||
faBatteryFull, faBatteryHalf,
|
faBatteryFull, faBatteryHalf,
|
||||||
faInfoCircle);
|
faInfoCircle,
|
||||||
|
faArrowsAltH,
|
||||||
|
);
|
||||||
vue.component('font-awesome-icon', FontAwesomeIcon);
|
vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="editorId" class="code-area" ></div>
|
<Responsive v-on:sizeChanged="sizeChanged()">
|
||||||
|
<div
|
||||||
|
:id="editorId"
|
||||||
|
class="code-area"
|
||||||
|
></div>
|
||||||
|
</Responsive>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop } from 'vue-property-decorator';
|
import { Component, Prop } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import ace from 'ace-builds';
|
import ace from 'ace-builds';
|
||||||
import 'ace-builds/webpack-resolver';
|
import 'ace-builds/webpack-resolver';
|
||||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
@@ -12,8 +17,13 @@ import { IScript } from '@/domain/IScript';
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
||||||
|
import Responsive from '@/presentation/Responsive.vue';
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
components: {
|
||||||
|
Responsive,
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class TheCodeArea extends StatefulVue {
|
export default class TheCodeArea extends StatefulVue {
|
||||||
public readonly editorId = 'codeEditor';
|
public readonly editorId = 'codeEditor';
|
||||||
|
|
||||||
@@ -25,6 +35,11 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
public destroyed() {
|
public destroyed() {
|
||||||
this.destroyEditor();
|
this.destroyEditor();
|
||||||
}
|
}
|
||||||
|
public sizeChanged() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
this.destroyEditor();
|
this.destroyEditor();
|
||||||
@@ -44,7 +59,6 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.editor.setValue(event.code, 1);
|
this.editor.setValue(event.code, 1);
|
||||||
|
|
||||||
if (event.addedScripts && event.addedScripts.length) {
|
if (event.addedScripts && event.addedScripts.length) {
|
||||||
this.reactToChanges(event, event.addedScripts);
|
this.reactToChanges(event, event.addedScripts);
|
||||||
} else if (event.changedScripts && event.changedScripts.length) {
|
} else if (event.changedScripts && event.changedScripts.length) {
|
||||||
@@ -83,6 +97,7 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
private destroyEditor() {
|
private destroyEditor() {
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
|
this.editor = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,6 +110,7 @@ function initializeEditor(theme: string, editorId: string, language: ScriptingLa
|
|||||||
editor.setTheme(`ace/theme/${theme}`);
|
editor.setTheme(`ace/theme/${theme}`);
|
||||||
editor.setReadOnly(true);
|
editor.setReadOnly(true);
|
||||||
editor.setAutoScrollEditorIntoView(true);
|
editor.setAutoScrollEditorIntoView(true);
|
||||||
|
editor.setShowPrintMargin(false); // hides vertical line
|
||||||
editor.getSession().setUseWrapMode(true); // So code is readable on mobile
|
editor.getSession().setUseWrapMode(true); // So code is readable on mobile
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
@@ -129,17 +145,17 @@ function getDefaultCode(language: ScriptingLanguage): string {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
.code-area {
|
::v-deep .code-area {
|
||||||
width: 100%;
|
min-height: 200px;
|
||||||
max-height: 1000px;
|
width: 100%;
|
||||||
min-height: 200px;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
&__highlight {
|
&__highlight {
|
||||||
background-color:$accent;
|
background-color: $accent;
|
||||||
opacity: 0.2; // having procent fails in production (minified) build
|
opacity: 0.2; // having procent fails in production (minified) build
|
||||||
position:absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
75
src/presentation/Responsive.vue
Normal file
75
src/presentation/Responsive.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="containerElement" class="container">
|
||||||
|
<slot ref="containerElement"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Emit } from 'vue-property-decorator';
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
import { throttle } from './Throttle';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Responsive extends Vue {
|
||||||
|
private width: number;
|
||||||
|
private height: number;
|
||||||
|
private observer: ResizeObserver;
|
||||||
|
private get container(): HTMLElement { return this.$refs.containerElement as HTMLElement; }
|
||||||
|
|
||||||
|
public mounted() {
|
||||||
|
this.width = this.container.offsetWidth;
|
||||||
|
this.height = this.container.offsetHeight;
|
||||||
|
const resizeCallback = throttle(() => this.updateSize(), 200);
|
||||||
|
this.observer = new ResizeObserver(resizeCallback);
|
||||||
|
this.observer.observe(this.container);
|
||||||
|
this.fireChangeEvents();
|
||||||
|
}
|
||||||
|
public updateSize() {
|
||||||
|
let sizeChanged = false;
|
||||||
|
if (this.isWidthChanged()) {
|
||||||
|
this.updateWidth(this.container.offsetWidth);
|
||||||
|
sizeChanged = true;
|
||||||
|
}
|
||||||
|
if (this.isHeightChanged()) {
|
||||||
|
this.updateHeight(this.container.offsetHeight);
|
||||||
|
sizeChanged = true;
|
||||||
|
}
|
||||||
|
if (sizeChanged) {
|
||||||
|
this.$emit('sizeChanged');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Emit('widthChanged') public updateWidth(width: number) {
|
||||||
|
this.width = width;
|
||||||
|
}
|
||||||
|
@Emit('heightChanged') public updateHeight(height: number) {
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
public destroyed() {
|
||||||
|
if (this.observer) {
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fireChangeEvents() {
|
||||||
|
this.updateWidth(this.container.offsetWidth);
|
||||||
|
this.updateHeight(this.container.offsetHeight);
|
||||||
|
this.$emit('sizeChanged');
|
||||||
|
}
|
||||||
|
private isWidthChanged(): boolean {
|
||||||
|
return this.width !== this.container.offsetWidth;
|
||||||
|
}
|
||||||
|
private isHeightChanged(): boolean {
|
||||||
|
return this.height !== this.container.offsetHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block; // if inline then it has no height or weight
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<Responsive v-on:widthChanged="width = $event">
|
||||||
<div v-if="categoryIds != null && categoryIds.length > 0" class="cards">
|
<!-- <div id="responsivity-debug">
|
||||||
|
Width: {{ width || 'undefined' }}
|
||||||
|
Size: <span v-if="width <= 500">small</span><span v-if="width > 500 && width < 750">medium</span><span v-if="width >= 750">big</span>
|
||||||
|
</div> -->
|
||||||
|
<div v-if="categoryIds != null && categoryIds.length > 0" class="cards">
|
||||||
<CardListItem
|
<CardListItem
|
||||||
class="card"
|
class="card"
|
||||||
|
v-bind:class="{
|
||||||
|
'small-screen': width <= 500,
|
||||||
|
'medium-screen': width > 500 && width < 750,
|
||||||
|
'big-screen': width >= 750
|
||||||
|
}"
|
||||||
v-for="categoryId of categoryIds"
|
v-for="categoryId of categoryIds"
|
||||||
:data-category="categoryId"
|
:data-category="categoryId"
|
||||||
v-bind:key="categoryId"
|
v-bind:key="categoryId"
|
||||||
@@ -12,12 +21,13 @@
|
|||||||
</CardListItem>
|
</CardListItem>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="error">Something went bad 😢</div>
|
<div v-else class="error">Something went bad 😢</div>
|
||||||
</div>
|
</Responsive>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component } from 'vue-property-decorator';
|
|
||||||
import CardListItem from './CardListItem.vue';
|
import CardListItem from './CardListItem.vue';
|
||||||
|
import Responsive from '@/presentation/Responsive.vue';
|
||||||
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
@@ -26,9 +36,11 @@ import { ICategoryCollectionState } from '@/application/Context/State/ICategoryC
|
|||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
CardListItem,
|
CardListItem,
|
||||||
|
Responsive,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class CardList extends StatefulVue {
|
export default class CardList extends StatefulVue {
|
||||||
|
public width: number = 0;
|
||||||
public categoryIds: number[] = [];
|
public categoryIds: number[] = [];
|
||||||
public activeCategoryId?: number = null;
|
public activeCategoryId?: number = null;
|
||||||
|
|
||||||
@@ -75,6 +87,7 @@ export default class CardList extends StatefulVue {
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
font-family: $main-font;
|
font-family: $main-font;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -106,12 +106,7 @@ $expanded-margin-top: 30px;
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
width: calc((100% / 3) - #{$card-line-break-width});
|
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
// Media queries for stacking cards
|
|
||||||
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - #{$card-line-break-width}); }
|
|
||||||
@media screen and (max-width: $medium-screen-width) { width: 100%; }
|
|
||||||
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
|
||||||
|
|
||||||
&__inner {
|
&__inner {
|
||||||
padding: $card-padding $card-padding 0 $card-padding;
|
padding: $card-padding $card-padding 0 $card-padding;
|
||||||
@@ -241,31 +236,32 @@ $expanded-margin-top: 30px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@mixin adaptive-card($cards-in-row) {
|
||||||
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row
|
&.card {
|
||||||
.card:nth-of-type(3n+2) .card__expander {
|
width: calc((100% / #{$cards-in-row}) - #{$card-line-break-width});
|
||||||
margin-left: calc(-100% - #{$card-line-break-width});
|
@for $nth-card from 2 through $cards-in-row {
|
||||||
}
|
&:nth-of-type(#{$cards-in-row}n+#{$nth-card}) {
|
||||||
.card:nth-of-type(3n+3) .card__expander {
|
.card__expander {
|
||||||
margin-left: calc(-200% - (#{$card-line-break-width} * 2));
|
$card-left: -100% * ($nth-card - 1);
|
||||||
}
|
$additional-space: $card-line-break-width * ($nth-card - 1);
|
||||||
.card:nth-of-type(3n+4) {
|
margin-left: calc(#{$card-left} - #{$additional-space});
|
||||||
clear: left;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure new line after last row
|
||||||
|
$card-after-last: $cards-in-row + 1;
|
||||||
|
&:nth-of-type(#{$cards-in-row}n+#{$card-after-last}) {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.card__expander {
|
.card__expander {
|
||||||
width: calc(300% + (#{$card-line-break-width} * 2));
|
$all-cards-width: 100% * $cards-in-row;
|
||||||
|
$card-padding: $card-line-break-width * ($cards-in-row - 1);
|
||||||
|
width: calc(#{$all-cards-width} + #{$card-padding});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
|
.big-screen { @include adaptive-card(3); }
|
||||||
.card:nth-of-type(2n+2) .card__expander {
|
.medium-screen { @include adaptive-card(2); }
|
||||||
margin-left: calc(-100% - #{$card-line-break-width});
|
.small-screen { @include adaptive-card(1); }
|
||||||
}
|
|
||||||
.card:nth-of-type(2n+3) {
|
|
||||||
clear: left;
|
|
||||||
}
|
|
||||||
.card__expander {
|
|
||||||
width: calc(200% + #{$card-line-break-width});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="part select">Select:</div>
|
<div class="part">Select:</div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<SelectableOption
|
<SelectableOption
|
||||||
@@ -173,5 +173,4 @@ function areAllSelected(
|
|||||||
}
|
}
|
||||||
font-family: $normal-font;
|
font-family: $normal-font;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div v-for="os in this.allOses" :key="os.name">
|
<!-- <div>OS:</div> -->
|
||||||
<span
|
<div class="os-list">
|
||||||
class="name"
|
<div v-for="os in this.allOses" :key="os.name">
|
||||||
v-bind:class="{ 'current': currentOs === os.os }"
|
<span
|
||||||
v-on:click="changeOsAsync(os.os)">
|
class="os-name"
|
||||||
{{ os.name }}
|
v-bind:class="{ 'current': currentOs === os.os }"
|
||||||
</span>
|
v-on:click="changeOsAsync(os.os)">
|
||||||
|
{{ os.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -55,20 +58,24 @@ function renderOsName(os: OperatingSystem): string {
|
|||||||
font-family: $normal-font;
|
font-family: $normal-font;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
div + div::before {
|
.os-list {
|
||||||
content: "|";
|
display: flex;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.25rem;
|
||||||
}
|
div + div::before {
|
||||||
.name {
|
content: "|";
|
||||||
&:not(.current) {
|
margin-left: 0.5rem;
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.current {
|
.os-name {
|
||||||
color: $gray;
|
&:not(.current) {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.current {
|
||||||
|
color: $gray;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
77
src/presentation/Scripts/Menu/TheScriptsMenu.vue
Normal file
77
src/presentation/Scripts/Menu/TheScriptsMenu.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div id="container">
|
||||||
|
<TheSelector class="item" />
|
||||||
|
<TheOsChanger class="item" />
|
||||||
|
<TheGrouper
|
||||||
|
class="item"
|
||||||
|
v-on:groupingChanged="$emit('groupingChanged', $event)"
|
||||||
|
v-if="!this.isSearching" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component } from 'vue-property-decorator';
|
||||||
|
import TheOsChanger from './TheOsChanger.vue';
|
||||||
|
import TheSelector from './Selector/TheSelector.vue';
|
||||||
|
import TheGrouper from './Grouping/TheGrouper.vue';
|
||||||
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
TheSelector,
|
||||||
|
TheOsChanger,
|
||||||
|
TheGrouper,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class TheScriptsMenu extends StatefulVue {
|
||||||
|
public isSearching = false;
|
||||||
|
|
||||||
|
private listeners = new Array<IEventSubscription>();
|
||||||
|
|
||||||
|
public destroyed() {
|
||||||
|
this.unsubscribeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initialize(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
|
this.subscribe(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribe(state: ICategoryCollectionState) {
|
||||||
|
this.listeners.push(state.filter.filterRemoved.on(() => {
|
||||||
|
this.isSearching = false;
|
||||||
|
}));
|
||||||
|
state.filter.filtered.on(() => {
|
||||||
|
this.isSearching = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private unsubscribeAll() {
|
||||||
|
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||||
|
this.listeners.splice(0, this.listeners.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
#container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.item {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 5px 0 5px;
|
||||||
|
&:first-child {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
src/presentation/Scripts/Slider/Handle.vue
Normal file
71
src/presentation/Scripts/Slider/Handle.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="handle"
|
||||||
|
:style="{ cursor: cursorCssValue }"
|
||||||
|
@mousedown="startResize">
|
||||||
|
<div class="line"></div>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="image"
|
||||||
|
:icon="['fas', 'arrows-alt-h']"
|
||||||
|
/> <!-- exchange-alt arrows-alt-h-->
|
||||||
|
<div class="line"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Handle extends Vue {
|
||||||
|
public readonly cursorCssValue = 'ew-resize';
|
||||||
|
private initialX: number = undefined;
|
||||||
|
|
||||||
|
public startResize(event: MouseEvent): void {
|
||||||
|
this.initialX = event.clientX;
|
||||||
|
document.body.style.setProperty('cursor', this.cursorCssValue);
|
||||||
|
document.addEventListener('mousemove', this.resize);
|
||||||
|
window.addEventListener('mouseup', this.stopResize);
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
public resize(event: MouseEvent): void {
|
||||||
|
const displacementX = event.clientX - this.initialX;
|
||||||
|
this.$emit('resized', displacementX);
|
||||||
|
this.initialX = event.clientX;
|
||||||
|
}
|
||||||
|
public stopResize(): void {
|
||||||
|
document.body.style.removeProperty('cursor');
|
||||||
|
document.removeEventListener('mousemove', this.resize);
|
||||||
|
window.removeEventListener('mouseup', this.stopResize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
&:hover {
|
||||||
|
.line {
|
||||||
|
background: $gray;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
color: $gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
flex: 1;
|
||||||
|
background: $dark-gray;
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
color: $dark-gray;
|
||||||
|
}
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
53
src/presentation/Scripts/Slider/HorizontalResizeSlider.vue
Normal file
53
src/presentation/Scripts/Slider/HorizontalResizeSlider.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slider">
|
||||||
|
<div class="left" ref="leftElement">
|
||||||
|
<slot name="left"></slot>
|
||||||
|
</div>
|
||||||
|
<Handle class="handle" @resized="onResize($event)" />
|
||||||
|
<div class="right">
|
||||||
|
<slot name="right"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import Handle from './Handle.vue';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
Handle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class HorizontalResizeSlider extends Vue {
|
||||||
|
private get left(): HTMLElement { return this.$refs.leftElement as HTMLElement; }
|
||||||
|
|
||||||
|
public onResize(displacementX: number): void {
|
||||||
|
const leftWidth = this.left.offsetWidth + displacementX;
|
||||||
|
this.left.style.width = `${leftWidth}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/presentation/styles/media.scss";
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: $vertical-view-breakpoint) {
|
||||||
|
.slider {
|
||||||
|
flex-direction: column;
|
||||||
|
.left {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
.handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
src/presentation/Scripts/TheScriptArea.vue
Normal file
49
src/presentation/Scripts/TheScriptArea.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scripts">
|
||||||
|
<TheScriptsMenu v-on:groupingChanged="grouping = $event" />
|
||||||
|
<HorizontalResizeSlider class="row">
|
||||||
|
<template v-slot:left>
|
||||||
|
<TheScriptsList :grouping="grouping" />
|
||||||
|
</template>
|
||||||
|
<template v-slot:right>
|
||||||
|
<TheCodeArea theme="xcode" />
|
||||||
|
</template>
|
||||||
|
</HorizontalResizeSlider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import TheCodeArea from '@/presentation/Code/TheCodeArea.vue';
|
||||||
|
import TheScriptsList from '@/presentation/Scripts/TheScriptsList.vue';
|
||||||
|
import TheScriptsMenu from '@/presentation/Scripts/Menu/TheScriptsMenu.vue';
|
||||||
|
import HorizontalResizeSlider from '@/presentation/Scripts/Slider/HorizontalResizeSlider.vue';
|
||||||
|
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
TheCodeArea,
|
||||||
|
TheScriptsList,
|
||||||
|
TheScriptsMenu,
|
||||||
|
HorizontalResizeSlider,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class TheScriptArea extends Vue {
|
||||||
|
public grouping = Grouping.Cards;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.scripts {
|
||||||
|
> * + * {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::v-deep .left {
|
||||||
|
width: 55%; // initial width
|
||||||
|
min-width: 20%;
|
||||||
|
}
|
||||||
|
::v-deep .right {
|
||||||
|
min-width: 20%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,52 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="scripts">
|
||||||
<div class="heading">
|
<div v-if="!isSearching">
|
||||||
<TheSelector class="item"/>
|
<CardList v-if="grouping === Grouping.Cards"/>
|
||||||
<TheOsChanger class="item"/>
|
<div class="tree" v-if="grouping === Grouping.None">
|
||||||
<TheGrouper
|
<ScriptsTree />
|
||||||
class="item"
|
</div>
|
||||||
v-on:groupingChanged="onGroupingChanged($event)"
|
|
||||||
v-if="!this.isSearching" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="scripts">
|
<div v-else> <!-- Searching -->
|
||||||
<div v-if="!isSearching">
|
<div class="search">
|
||||||
<CardList v-if="currentGrouping === Grouping.Cards"/>
|
<div class="search__query">
|
||||||
<div class="tree" v-if="currentGrouping === Grouping.None">
|
<div>Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
|
||||||
<ScriptsTree />
|
<div class="search__query__close-button">
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'times']"
|
||||||
|
v-on:click="clearSearchQueryAsync()"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!searchHasMatches" class="search-no-matches">
|
||||||
|
<div>Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞</div>
|
||||||
|
<div>Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a> ✨</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else> <!-- Searching -->
|
<div v-if="searchHasMatches" class="tree tree--searching">
|
||||||
<div class="search">
|
<ScriptsTree />
|
||||||
<div class="search__query">
|
|
||||||
<div>Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
|
|
||||||
<div class="search__query__close-button">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'times']"
|
|
||||||
v-on:click="clearSearchQueryAsync()"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!searchHasMatches" class="search-no-matches">
|
|
||||||
<div>Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞</div>
|
|
||||||
<div>Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a> ✨</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="searchHasMatches" class="tree tree--searching">
|
|
||||||
<ScriptsTree />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
|
import TheGrouper from '@/presentation/Scripts/Menu/Grouping/TheGrouper.vue';
|
||||||
import TheOsChanger from '@/presentation/Scripts/TheOsChanger.vue';
|
|
||||||
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
|
|
||||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component, Prop } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { Grouping } from './Grouping/Grouping';
|
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||||
@@ -55,10 +43,8 @@ import { ApplicationFactory } from '@/application/ApplicationFactory';
|
|||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
TheGrouper,
|
TheGrouper,
|
||||||
TheSelector,
|
|
||||||
ScriptsTree,
|
ScriptsTree,
|
||||||
CardList,
|
CardList,
|
||||||
TheOsChanger,
|
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
threeDotsTrim(query: string) {
|
threeDotsTrim(query: string) {
|
||||||
@@ -70,10 +56,11 @@ filters: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class TheScripts extends StatefulVue {
|
export default class TheScriptsList extends StatefulVue {
|
||||||
|
@Prop() public grouping: Grouping;
|
||||||
|
|
||||||
public repositoryUrl = '';
|
public repositoryUrl = '';
|
||||||
public Grouping = Grouping; // Make it accessible from view
|
public Grouping = Grouping; // Make it accessible from the view
|
||||||
public currentGrouping = Grouping.Cards;
|
|
||||||
public searchQuery = '';
|
public searchQuery = '';
|
||||||
public isSearching = false;
|
public isSearching = false;
|
||||||
public searchHasMatches = false;
|
public searchHasMatches = false;
|
||||||
@@ -87,9 +74,6 @@ export default class TheScripts extends StatefulVue {
|
|||||||
const filter = context.state.filter;
|
const filter = context.state.filter;
|
||||||
filter.removeFilter();
|
filter.removeFilter();
|
||||||
}
|
}
|
||||||
public onGroupingChanged(group: Grouping) {
|
|
||||||
this.currentGrouping = group;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
this.events.unsubscribeAll();
|
this.events.unsubscribeAll();
|
||||||
@@ -114,11 +98,16 @@ export default class TheScripts extends StatefulVue {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
@import "@/presentation/styles/fonts.scss";
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
@import "@/presentation/styles/media.scss";
|
||||||
|
|
||||||
$inner-margin: 4px;
|
$inner-margin: 4px;
|
||||||
|
|
||||||
.scripts {
|
.scripts {
|
||||||
margin-top: $inner-margin;
|
margin-top: $inner-margin;
|
||||||
|
@media screen and (min-width: $vertical-view-breakpoint) { // so the current code is always visible
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
.tree {
|
.tree {
|
||||||
padding-left: 3%;
|
padding-left: 3%;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@@ -167,22 +156,4 @@ $inner-margin: 4px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
|
||||||
margin-top: $inner-margin;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
.item {
|
|
||||||
flex: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 5px 0 5px;
|
|
||||||
&:first-child {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -99,13 +99,13 @@ export default class TheFooter extends Vue {
|
|||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@media (max-width: $big-screen-width) {
|
@media screen and (max-width: $big-screen-width) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
&__section {
|
&__section {
|
||||||
display: flex;
|
display: flex;
|
||||||
@media (max-width: $big-screen-width) {
|
@media screen and (max-width: $big-screen-width) {
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
width:100%;
|
width:100%;
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
@@ -129,7 +129,7 @@ export default class TheFooter extends Vue {
|
|||||||
content: "|";
|
content: "|";
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
@media (max-width: $big-screen-width) {
|
@media screen and (max-width: $big-screen-width) {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
30
src/presentation/Throttle.ts
Normal file
30
src/presentation/Throttle.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export function throttle<T extends []>(
|
||||||
|
callback: (..._: T) => void, wait: number,
|
||||||
|
timer: ITimer = NodeTimer): (..._: T) => void {
|
||||||
|
let queuedToRun: ReturnType<typeof setTimeout>;
|
||||||
|
let previouslyRun: number;
|
||||||
|
return function invokeFn(...args: T) {
|
||||||
|
const now = timer.dateNow();
|
||||||
|
if (queuedToRun) {
|
||||||
|
queuedToRun = timer.clearTimeout(queuedToRun) as undefined;
|
||||||
|
}
|
||||||
|
if (!previouslyRun || (now - previouslyRun >= wait)) {
|
||||||
|
callback(...args);
|
||||||
|
previouslyRun = now;
|
||||||
|
} else {
|
||||||
|
queuedToRun = timer.setTimeout(invokeFn.bind(null, ...args), wait - (now - previouslyRun));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimer {
|
||||||
|
setTimeout: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
||||||
|
clearTimeout: (timeoutId: ReturnType<typeof setTimeout>) => void;
|
||||||
|
dateNow(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeTimer: ITimer = {
|
||||||
|
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
||||||
|
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
||||||
|
dateNow: () => Date.now(),
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
$big-screen-width: 992px;
|
$big-screen-width: 992px;
|
||||||
$medium-screen-width: 768px;
|
$medium-screen-width: 768px;
|
||||||
$small-screen-width: 380px;
|
$small-screen-width: 380px;
|
||||||
|
|
||||||
|
$vertical-view-breakpoint: 992px;
|
||||||
76
tests/unit/presentation/Throttle.spec.ts
Normal file
76
tests/unit/presentation/Throttle.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { throttle, ITimer } from '@/presentation/Throttle';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
|
|
||||||
|
describe('throttle', () => {
|
||||||
|
it('should call the callback immediately', () => {
|
||||||
|
// arrange
|
||||||
|
const timer = new TimerMock();
|
||||||
|
let totalRuns = 0;
|
||||||
|
const callback = () => totalRuns++;
|
||||||
|
const throttleFunc = throttle(callback, 500, timer);
|
||||||
|
// act
|
||||||
|
throttleFunc();
|
||||||
|
// assert
|
||||||
|
expect(totalRuns).to.equal(1);
|
||||||
|
});
|
||||||
|
it('should call the callback again after the timeout', () => {
|
||||||
|
// arrange
|
||||||
|
const timer = new TimerMock();
|
||||||
|
let totalRuns = 0;
|
||||||
|
const callback = () => totalRuns++;
|
||||||
|
const throttleFunc = throttle(callback, 500, timer);
|
||||||
|
// act
|
||||||
|
throttleFunc();
|
||||||
|
totalRuns--;
|
||||||
|
throttleFunc();
|
||||||
|
timer.tick(500);
|
||||||
|
// assert
|
||||||
|
expect(totalRuns).to.equal(1);
|
||||||
|
});
|
||||||
|
it('calls the callback at most once at given time', () => {
|
||||||
|
// arrange
|
||||||
|
const timer = new TimerMock();
|
||||||
|
let totalRuns = 0;
|
||||||
|
const callback = () => totalRuns++;
|
||||||
|
const waitInMs = 500;
|
||||||
|
const totalCalls = 10;
|
||||||
|
const throttleFunc = throttle(callback, waitInMs, timer);
|
||||||
|
// act
|
||||||
|
for (let i = 0; i < totalCalls; i++) {
|
||||||
|
timer.tick(waitInMs / totalCalls * i);
|
||||||
|
throttleFunc();
|
||||||
|
}
|
||||||
|
// assert
|
||||||
|
expect(totalRuns).to.equal(2); // initial and at the end
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TimerMock implements ITimer {
|
||||||
|
private timeChanged = new EventSource<number>();
|
||||||
|
private subscriptions = new Array<IEventSubscription>();
|
||||||
|
private currentTime = 0;
|
||||||
|
public setTimeout(callback: () => void, ms: number): NodeJS.Timeout {
|
||||||
|
const runTime = this.currentTime + ms;
|
||||||
|
const subscription = this.timeChanged.on((time) => {
|
||||||
|
if (time >= runTime) {
|
||||||
|
callback();
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.subscriptions.push(subscription);
|
||||||
|
return (this.subscriptions.length - 1) as any;
|
||||||
|
}
|
||||||
|
public clearTimeout(timeoutId: NodeJS.Timeout): void {
|
||||||
|
this.subscriptions[timeoutId as any].unsubscribe();
|
||||||
|
}
|
||||||
|
public dateNow(): number {
|
||||||
|
return this.currentTime;
|
||||||
|
}
|
||||||
|
public tick(ms: number): void {
|
||||||
|
this.currentTime = ms;
|
||||||
|
this.timeChanged.notify(this.currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user