Channel list rendering with Vue
Co-Authored-By: Tim Miller-Williams <timmw@users.noreply.github.com>
This commit is contained in:
parent
18bca3bce1
commit
7e332b817d
@ -85,8 +85,15 @@ rules:
|
||||
space-in-parens: [error, never]
|
||||
space-infix-ops: error
|
||||
spaced-comment: [error, always]
|
||||
strict: error
|
||||
strict: off
|
||||
template-curly-spacing: error
|
||||
yoda: error
|
||||
vue/html-indent: [error, tab]
|
||||
vue/require-default-prop: off
|
||||
|
||||
extends: eslint:recommended
|
||||
plugins:
|
||||
- vue
|
||||
|
||||
extends:
|
||||
- eslint:recommended
|
||||
- plugin:vue/recommended
|
||||
|
130
client/components/App.vue
Normal file
130
client/components/App.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div
|
||||
id="viewport"
|
||||
role="tablist">
|
||||
<aside id="sidebar">
|
||||
<div class="scrollable-area">
|
||||
<div class="logo-container">
|
||||
<img
|
||||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`"
|
||||
class="logo"
|
||||
alt="The Lounge">
|
||||
<img
|
||||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`"
|
||||
class="logo-inverted"
|
||||
alt="The Lounge">
|
||||
</div>
|
||||
<Network
|
||||
:networks="networks"
|
||||
:active-channel="activeChannel"/>
|
||||
</div>
|
||||
<footer id="footer">
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Sign in"><button
|
||||
class="icon sign-in"
|
||||
data-target="#sign-in"
|
||||
aria-label="Sign in"
|
||||
role="tab"
|
||||
aria-controls="sign-in"
|
||||
aria-selected="false"/></span>
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Connect to network"><button
|
||||
class="icon connect"
|
||||
data-target="#connect"
|
||||
aria-label="Connect to network"
|
||||
role="tab"
|
||||
aria-controls="connect"
|
||||
aria-selected="false"/></span>
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Settings"><button
|
||||
class="icon settings"
|
||||
data-target="#settings"
|
||||
aria-label="Settings"
|
||||
role="tab"
|
||||
aria-controls="settings"
|
||||
aria-selected="false"/></span>
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Help"><button
|
||||
class="icon help"
|
||||
data-target="#help"
|
||||
aria-label="Help"
|
||||
role="tab"
|
||||
aria-controls="help"
|
||||
aria-selected="false"/></span>
|
||||
</footer>
|
||||
</aside>
|
||||
<div id="sidebar-overlay"/>
|
||||
<article id="windows">
|
||||
<div
|
||||
id="chat-container"
|
||||
class="window">
|
||||
<div id="chat"/>
|
||||
<div id="connection-error"/>
|
||||
<form
|
||||
id="form"
|
||||
method="post"
|
||||
action="">
|
||||
<span id="nick"/>
|
||||
<textarea
|
||||
id="input"
|
||||
class="mousetrap"/>
|
||||
<span
|
||||
id="submit-tooltip"
|
||||
class="tooltipped tooltipped-w tooltipped-no-touch"
|
||||
aria-label="Send message">
|
||||
<button
|
||||
id="submit"
|
||||
type="submit"
|
||||
aria-label="Send message"/>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
id="sign-in"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Sign-in"/>
|
||||
<div
|
||||
id="connect"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Connect"/>
|
||||
<div
|
||||
id="settings"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Settings"/>
|
||||
<div
|
||||
id="help"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Help"/>
|
||||
<div
|
||||
id="changelog"
|
||||
class="window"
|
||||
aria-label="Changelog"/>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Network from "./Network.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Network,
|
||||
},
|
||||
props: {
|
||||
activeChannel: Object,
|
||||
networks: Array,
|
||||
},
|
||||
methods: {
|
||||
isPublic: () => document.body.classList.contains("public"),
|
||||
},
|
||||
};
|
||||
</script>
|
90
client/components/Channel.vue
Normal file
90
client/components/Channel.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div
|
||||
:key="channel.id"
|
||||
:class="[ channel.type, { active: activeChannel && channel.id === activeChannel.channel.id } ]"
|
||||
:aria-label="channel.name"
|
||||
:title="channel.name"
|
||||
:data-id="channel.id"
|
||||
:data-target="'#chan-' + channel.id"
|
||||
:aria-controls="'#chan-' + channel.id"
|
||||
:aria-selected="activeChannel && channel.id === activeChannel.channel.id"
|
||||
class="chan"
|
||||
role="tab"
|
||||
>
|
||||
<template v-if="channel.type === 'lobby'">
|
||||
<button
|
||||
:aria-controls="'network-' + network.uuid"
|
||||
class="collapse-network"
|
||||
aria-label="Collapse"
|
||||
aria-expanded="true">
|
||||
<span class="collapse-network-icon"/>
|
||||
</button>
|
||||
<div class="lobby-wrap">
|
||||
<span
|
||||
:title="channel.name"
|
||||
class="name">{{ channel.name }}</span>
|
||||
<span
|
||||
class="not-secure-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Insecure connection">
|
||||
<span class="not-secure-icon"/>
|
||||
</span>
|
||||
<span
|
||||
class="not-connected-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Disconnected">
|
||||
<span class="not-connected-icon"/>
|
||||
</span>
|
||||
<span
|
||||
v-if="channel.unread"
|
||||
:class="{ highlight: channel.highlight }"
|
||||
class="badge">{{ channel.unread | roundBadgeNumber }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="add-channel-tooltip tooltipped tooltipped-w tooltipped-no-touch"
|
||||
aria-label="Join a channel…"
|
||||
data-alt-label="Cancel">
|
||||
<button
|
||||
:aria-controls="'join-channel-' + channel.id"
|
||||
class="add-channel"
|
||||
aria-label="Join a channel…"/>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
:title="channel.name"
|
||||
class="name">{{ channel.name }}</span>
|
||||
<span
|
||||
v-if="channel.unread"
|
||||
:class="{ highlight: channel.highlight }"
|
||||
class="badge">{{ channel.unread | roundBadgeNumber }}</span>
|
||||
<template v-if="channel.type === 'channel'">
|
||||
<span
|
||||
class="close-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Leave">
|
||||
<button
|
||||
class="close"
|
||||
aria-label="Leave"/>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
class="close-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Close">
|
||||
<button
|
||||
class="close"
|
||||
aria-label="Close"/>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Channel",
|
||||
props: {
|
||||
activeChannel: Object,
|
||||
network: Object,
|
||||
channel: Object,
|
||||
},
|
||||
};
|
||||
</script>
|
42
client/components/JoinChannel.vue
Normal file
42
client/components/JoinChannel.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<form
|
||||
:id="'join-channel-' + channel.id"
|
||||
class="join-form"
|
||||
method="post"
|
||||
action=""
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="channel"
|
||||
placeholder="Channel"
|
||||
pattern="[^\s]+"
|
||||
maxlength="200"
|
||||
title="The channel name may not contain spaces"
|
||||
required
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="input"
|
||||
name="key"
|
||||
placeholder="Password (optional)"
|
||||
pattern="[^\s]+"
|
||||
maxlength="200"
|
||||
title="The channel password may not contain spaces"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-small">Join</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "JoinChannel",
|
||||
props: {
|
||||
channel: Object,
|
||||
},
|
||||
};
|
||||
</script>
|
115
client/components/Network.vue
Normal file
115
client/components/Network.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="networks.length === 0"
|
||||
class="empty">
|
||||
You are not connected to any networks yet.
|
||||
</div>
|
||||
<Draggable
|
||||
v-else
|
||||
:list="networks"
|
||||
:options="{ handle: '.lobby', draggable: '.network', ghostClass: 'network-placeholder' }"
|
||||
class="networks"
|
||||
@change="onNetworkSort"
|
||||
@start="onDragStart"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<div
|
||||
v-for="network in networks"
|
||||
:key="network.uuid"
|
||||
:class="{ 'not-connected': !network.status.connected, 'not-secure': !network.status.secure }"
|
||||
:id="'network-' + network.uuid"
|
||||
:data-uuid="network.uuid"
|
||||
:data-nick="network.nick"
|
||||
class="network"
|
||||
role="region"
|
||||
>
|
||||
<Channel
|
||||
:channel="network.channels[0]"
|
||||
:network="network"
|
||||
:active-channel="activeChannel"
|
||||
/>
|
||||
<JoinChannel :channel="network.channels[0]"/>
|
||||
|
||||
<Draggable
|
||||
:options="{ draggable: '.chan', ghostClass: 'chan-placeholder' }"
|
||||
:list="network.channels"
|
||||
class="channels"
|
||||
@change="onChannelSort"
|
||||
@start="onDragStart"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<Channel
|
||||
v-for="(channel, index) in network.channels"
|
||||
v-if="index > 0"
|
||||
:key="channel.id"
|
||||
:channel="channel"
|
||||
:network="network"
|
||||
:active-channel="activeChannel"
|
||||
/>
|
||||
</Draggable>
|
||||
</div>
|
||||
</Draggable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from "vuedraggable";
|
||||
import JoinChannel from "./JoinChannel.vue";
|
||||
import Channel from "./Channel.vue";
|
||||
|
||||
// TODO: ignoreSortSync should be removed
|
||||
import {findChannel} from "../js/vue";
|
||||
import socket from "../js/socket";
|
||||
// import options from "../js/options";
|
||||
|
||||
export default {
|
||||
name: "Network",
|
||||
components: {
|
||||
JoinChannel,
|
||||
Channel,
|
||||
Draggable,
|
||||
},
|
||||
props: {
|
||||
activeChannel: Object,
|
||||
networks: Array,
|
||||
},
|
||||
methods: {
|
||||
onDragStart(e) {
|
||||
e.target.classList.add("ui-sortable-helper");
|
||||
},
|
||||
onDragEnd(e) {
|
||||
e.target.classList.remove("ui-sortable-helper");
|
||||
},
|
||||
onNetworkSort(e) {
|
||||
if (!e.moved) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "networks",
|
||||
order: this.networks.map((n) => n.uuid),
|
||||
});
|
||||
|
||||
// options.settings.ignoreSortSync = true;
|
||||
},
|
||||
onChannelSort(e) {
|
||||
if (!e.moved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = findChannel(e.moved.element.id);
|
||||
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "channels",
|
||||
target: channel.network.uuid,
|
||||
order: channel.network.channels.map((c) => c.id),
|
||||
});
|
||||
|
||||
// options.settings.ignoreSortSync = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -603,10 +603,6 @@ background on hover (unless active) */
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
#sidebar .networks:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar .network,
|
||||
#sidebar .network-placeholder {
|
||||
position: relative;
|
||||
@ -631,7 +627,7 @@ background on hover (unless active) */
|
||||
#sidebar .chan-placeholder {
|
||||
border: 1px dashed #99a2b4;
|
||||
border-radius: 6px;
|
||||
margin: -1px 10px;
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
#sidebar .network-placeholder {
|
||||
@ -779,12 +775,6 @@ background on hover (unless active) */
|
||||
transform: rotate(45deg) translateZ(0);
|
||||
}
|
||||
|
||||
#sidebar .network .lobby:nth-last-child(2) .collapse-network {
|
||||
/* Hide collapse button if there are no channels/queries */
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#sidebar .network .collapse-network {
|
||||
width: 40px;
|
||||
opacity: 0.4;
|
||||
@ -896,6 +886,7 @@ background on hover (unless active) */
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#loading,
|
||||
#windows .window {
|
||||
background: var(--window-bg-color);
|
||||
display: none;
|
||||
@ -1605,7 +1596,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
content: "Search Results";
|
||||
}
|
||||
|
||||
#loading.active {
|
||||
#loading {
|
||||
font-size: 14px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
@ -1646,7 +1637,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
#windows .logo-inverted {
|
||||
#loading .logo-inverted {
|
||||
display: none; /* In dark themes, inverted logo must be used instead */
|
||||
}
|
||||
|
||||
@ -2410,10 +2401,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#sidebar .empty::before {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#viewport .lt,
|
||||
#viewport .channel .rt {
|
||||
display: flex;
|
||||
|
@ -104,6 +104,7 @@
|
||||
<div id="changelog" class="window" aria-label="Changelog"></div>
|
||||
</article>
|
||||
</div>
|
||||
<div id="viewport"></div>
|
||||
|
||||
<div id="context-menu-container"></div>
|
||||
<div id="image-viewer"></div>
|
||||
|
@ -8,7 +8,6 @@ const emojiMap = require("./libs/simplemap.json");
|
||||
const options = require("./options");
|
||||
const constants = require("./constants");
|
||||
|
||||
const input = $("#input");
|
||||
let textcomplete;
|
||||
let enabled = false;
|
||||
|
||||
@ -16,6 +15,7 @@ module.exports = {
|
||||
enable: enableAutocomplete,
|
||||
disable() {
|
||||
if (enabled) {
|
||||
const input = $("#input");
|
||||
input.off("input.tabcomplete");
|
||||
Mousetrap(input.get(0)).unbind("tab", "keydown");
|
||||
textcomplete.destroy();
|
||||
@ -74,7 +74,7 @@ const nicksStrategy = {
|
||||
}
|
||||
|
||||
// If there is whitespace in the input already, append space to nick
|
||||
if (position > 0 && /\s/.test(input.val())) {
|
||||
if (position > 0 && /\s/.test($("#input").val())) {
|
||||
return original + " ";
|
||||
}
|
||||
|
||||
@ -179,6 +179,7 @@ function enableAutocomplete() {
|
||||
let tabCount = 0;
|
||||
let lastMatch = "";
|
||||
let currentMatches = [];
|
||||
const input = $("#input");
|
||||
|
||||
input.on("input.tabcomplete", () => {
|
||||
tabCount = 0;
|
||||
|
@ -1,7 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
// vendor libraries
|
||||
require("jquery-ui/ui/widgets/sortable");
|
||||
const $ = require("jquery");
|
||||
const moment = require("moment");
|
||||
|
||||
@ -19,12 +18,12 @@ require("./keybinds");
|
||||
require("./clipboard");
|
||||
const contextMenuFactory = require("./contextMenuFactory");
|
||||
|
||||
const {vueApp, findChannel} = require("./vue");
|
||||
|
||||
$(function() {
|
||||
const sidebar = $("#sidebar, #footer");
|
||||
const chat = $("#chat");
|
||||
|
||||
$(document.body).data("app-name", document.title);
|
||||
|
||||
const viewport = $("#viewport");
|
||||
|
||||
function storeSidebarVisibility(name, state) {
|
||||
@ -176,16 +175,14 @@ $(function() {
|
||||
self.data("id")
|
||||
);
|
||||
|
||||
sidebar.find(".active")
|
||||
.removeClass("active")
|
||||
.attr("aria-selected", false);
|
||||
const channel = findChannel(self.data("id"));
|
||||
|
||||
self.addClass("active")
|
||||
.attr("aria-selected", true)
|
||||
.find(".badge")
|
||||
.attr("data-highlight", 0)
|
||||
.removeClass("highlight")
|
||||
.empty();
|
||||
vueApp.activeChannel = channel;
|
||||
|
||||
if (channel) {
|
||||
channel.channel.highlight = 0;
|
||||
channel.channel.unread = 0;
|
||||
}
|
||||
|
||||
if (sidebar.find(".highlight").length === 0) {
|
||||
utils.toggleNotificationMarkers(false);
|
||||
|
@ -5,7 +5,6 @@ const templates = require("../views");
|
||||
const options = require("./options");
|
||||
const renderPreview = require("./renderPreview");
|
||||
const utils = require("./utils");
|
||||
const sorting = require("./sorting");
|
||||
const constants = require("./constants");
|
||||
const condensed = require("./condensed");
|
||||
const JoinChannel = require("./join-channel");
|
||||
@ -215,13 +214,6 @@ function renderChannelUsers(data) {
|
||||
function renderNetworks(data, singleNetwork) {
|
||||
const collapsed = new Set(JSON.parse(storage.get("thelounge.networks.collapsed")));
|
||||
|
||||
sidebar.find(".empty").hide();
|
||||
sidebar.find(".networks").append(
|
||||
templates.network({
|
||||
networks: data.networks,
|
||||
}).trim()
|
||||
);
|
||||
|
||||
// Add keyboard handlers to the "Join a channel…" form inputs/button
|
||||
JoinChannel.handleKeybinds(data.networks);
|
||||
|
||||
@ -287,7 +279,6 @@ function renderNetworks(data, singleNetwork) {
|
||||
}
|
||||
|
||||
utils.confirmExit();
|
||||
sorting();
|
||||
|
||||
if (sidebar.find(".highlight").length) {
|
||||
utils.toggleNotificationMarkers(true);
|
||||
|
@ -1,9 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const viewport = document.getElementById("viewport");
|
||||
const menu = document.getElementById("sidebar");
|
||||
const sidebarOverlay = document.getElementById("sidebar-overlay");
|
||||
|
||||
let touchStartPos = null;
|
||||
let touchCurPos = null;
|
||||
let touchStartTime = 0;
|
||||
@ -14,12 +10,16 @@ let menuIsAbsolute = false;
|
||||
|
||||
class SlideoutMenu {
|
||||
static enable() {
|
||||
this.viewport = document.getElementById("viewport");
|
||||
this.menu = document.getElementById("sidebar");
|
||||
this.sidebarOverlay = document.getElementById("sidebar-overlay");
|
||||
|
||||
document.body.addEventListener("touchstart", onTouchStart, {passive: true});
|
||||
}
|
||||
|
||||
static toggle(state) {
|
||||
menuIsOpen = state;
|
||||
viewport.classList.toggle("menu-open", state);
|
||||
this.viewport.classList.toggle("menu-open", state);
|
||||
}
|
||||
|
||||
static isOpen() {
|
||||
@ -35,7 +35,7 @@ function onTouchStart(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = window.getComputedStyle(menu);
|
||||
const styles = window.getComputedStyle(this.menu);
|
||||
|
||||
menuWidth = parseFloat(styles.width);
|
||||
menuIsAbsolute = styles.position === "absolute";
|
||||
@ -65,7 +65,7 @@ function onTouchMove(e) {
|
||||
const devicePixelRatio = window.devicePixelRatio || 2;
|
||||
|
||||
if (Math.abs(distX) > devicePixelRatio) {
|
||||
viewport.classList.toggle("menu-dragging", true);
|
||||
this.viewport.classList.toggle("menu-dragging", true);
|
||||
menuIsMoving = true;
|
||||
}
|
||||
}
|
||||
@ -85,8 +85,8 @@ function onTouchMove(e) {
|
||||
distX = 0;
|
||||
}
|
||||
|
||||
menu.style.transform = "translate3d(" + distX + "px, 0, 0)";
|
||||
sidebarOverlay.style.opacity = distX / menuWidth;
|
||||
this.menu.style.transform = "translate3d(" + distX + "px, 0, 0)";
|
||||
this.sidebarOverlay.style.opacity = distX / menuWidth;
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
@ -99,9 +99,9 @@ function onTouchEnd() {
|
||||
|
||||
document.body.removeEventListener("touchmove", onTouchMove);
|
||||
document.body.removeEventListener("touchend", onTouchEnd);
|
||||
viewport.classList.toggle("menu-dragging", false);
|
||||
menu.style.transform = null;
|
||||
sidebarOverlay.style.opacity = null;
|
||||
this.viewport.classList.toggle("menu-dragging", false);
|
||||
this.menu.style.transform = null;
|
||||
this.sidebarOverlay.style.opacity = null;
|
||||
|
||||
touchStartPos = null;
|
||||
touchCurPos = null;
|
||||
|
@ -21,6 +21,7 @@ socket.on("auth", function(data) {
|
||||
if (data.serverHash > -1) {
|
||||
utils.serverHash = data.serverHash;
|
||||
|
||||
$("#loading").remove();
|
||||
login.html(templates.windows.sign_in());
|
||||
|
||||
utils.togglePasswordField("#sign-in .reveal-password");
|
||||
|
@ -9,6 +9,7 @@ const slideoutMenu = require("../slideout");
|
||||
const sidebar = $("#sidebar");
|
||||
const storage = require("../localStorage");
|
||||
const utils = require("../utils");
|
||||
const {Vue, vueApp} = require("../vue");
|
||||
|
||||
socket.on("init", function(data) {
|
||||
$("#loading-page-message, #connection-error").text("Rendering…");
|
||||
@ -18,13 +19,12 @@ socket.on("init", function(data) {
|
||||
|
||||
if (lastMessageId > -1) {
|
||||
previousActive = sidebar.find(".active").data("id");
|
||||
sidebar.find(".networks").empty();
|
||||
}
|
||||
|
||||
if (data.networks.length === 0) {
|
||||
sidebar.find(".empty").show();
|
||||
} else {
|
||||
render.renderNetworks(data);
|
||||
vueApp.networks = data.networks;
|
||||
|
||||
if (data.networks.length > 0) {
|
||||
Vue.nextTick(() => render.renderNetworks(data));
|
||||
}
|
||||
|
||||
$("#connection-error").removeClass("shown");
|
||||
@ -66,7 +66,7 @@ socket.on("init", function(data) {
|
||||
}
|
||||
}
|
||||
|
||||
openCorrectChannel(previousActive, data.active);
|
||||
Vue.nextTick(() => openCorrectChannel(previousActive, data.active));
|
||||
});
|
||||
|
||||
function openCorrectChannel(clientActive, serverActive) {
|
||||
|
@ -6,32 +6,31 @@ const render = require("../render");
|
||||
const chat = $("#chat");
|
||||
const templates = require("../../views");
|
||||
const sidebar = $("#sidebar");
|
||||
const {Vue, vueApp} = require("../vue");
|
||||
|
||||
socket.on("join", function(data) {
|
||||
const id = data.network;
|
||||
const network = sidebar.find(`.network[data-uuid="${id}"]`);
|
||||
const channels = network.children();
|
||||
const position = $(channels[data.index || channels.length - 1]); // Put channel in correct position, or the end if we don't have one
|
||||
const sidebarEntry = templates.chan({
|
||||
channels: [data.chan],
|
||||
});
|
||||
$(sidebarEntry).insertAfter(position);
|
||||
vueApp.networks.find((n) => n.uuid === data.network)
|
||||
.channels.splice(data.index || -1, 0, data.chan);
|
||||
|
||||
chat.append(
|
||||
templates.chat({
|
||||
channels: [data.chan],
|
||||
})
|
||||
);
|
||||
render.renderChannel(data.chan);
|
||||
|
||||
Vue.nextTick(() => render.renderChannel(data.chan));
|
||||
|
||||
// Queries do not automatically focus, unless the user did a whois
|
||||
if (data.chan.type === "query" && !data.shouldOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
sidebar.find(".chan")
|
||||
.sort(function(a, b) {
|
||||
return $(a).data("id") - $(b).data("id");
|
||||
})
|
||||
.last()
|
||||
.trigger("click");
|
||||
Vue.nextTick(() => {
|
||||
sidebar.find(".chan")
|
||||
.sort(function(a, b) {
|
||||
return $(a).data("id") - $(b).data("id");
|
||||
})
|
||||
.last()
|
||||
.trigger("click");
|
||||
});
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ const cleanIrcMessage = require("../libs/handlebars/ircmessageparser/cleanIrcMes
|
||||
const webpush = require("../webpush");
|
||||
const chat = $("#chat");
|
||||
const sidebar = $("#sidebar");
|
||||
const {vueApp, findChannel} = require("../vue");
|
||||
|
||||
let pop;
|
||||
|
||||
@ -30,26 +31,31 @@ socket.on("msg", function(data) {
|
||||
function processReceivedMessage(data) {
|
||||
let targetId = data.chan;
|
||||
let target = "#chan-" + targetId;
|
||||
let channel = chat.find(target);
|
||||
let sidebarTarget = sidebar.find("[data-target='" + target + "']");
|
||||
let channelContainer = chat.find(target);
|
||||
let channel = findChannel(data.chan);
|
||||
|
||||
// Clear unread/highlight counter if self-message
|
||||
if (data.msg.self) {
|
||||
channel.channel.highlight = 0;
|
||||
channel.channel.unread = 0;
|
||||
|
||||
utils.updateTitle();
|
||||
}
|
||||
|
||||
// Display received notices and errors in currently active channel.
|
||||
// Reloading the page will put them back into the lobby window.
|
||||
if (data.msg.showInActive) {
|
||||
const activeOnNetwork = sidebarTarget.parent().find(".active");
|
||||
// We only want to put errors/notices in active channel if they arrive on the same network
|
||||
if (data.msg.showInActive && vueApp.activeChannel && vueApp.activeChannel.network === channel.network) {
|
||||
channel = vueApp.activeChannel;
|
||||
|
||||
// We only want to put errors/notices in active channel if they arrive on the same network
|
||||
if (activeOnNetwork.length > 0) {
|
||||
targetId = data.chan = activeOnNetwork.data("id");
|
||||
targetId = data.chan = vueApp.activeChannel.channel.id;
|
||||
|
||||
target = "#chan-" + targetId;
|
||||
channel = chat.find(target);
|
||||
sidebarTarget = sidebar.find("[data-target='" + target + "']");
|
||||
}
|
||||
target = "#chan-" + targetId;
|
||||
channelContainer = chat.find(target);
|
||||
}
|
||||
|
||||
const scrollContainer = channel.find(".chat");
|
||||
const container = channel.find(".messages");
|
||||
const scrollContainer = channelContainer.find(".chat");
|
||||
const container = channelContainer.find(".messages");
|
||||
const activeChannelId = chat.find(".chan.active").data("id");
|
||||
|
||||
if (data.msg.type === "channel_list" || data.msg.type === "ban_list" || data.msg.type === "ignore_list") {
|
||||
@ -60,7 +66,7 @@ function processReceivedMessage(data) {
|
||||
render.appendMessage(
|
||||
container,
|
||||
targetId,
|
||||
channel.data("type"),
|
||||
channelContainer.data("type"),
|
||||
data.msg
|
||||
);
|
||||
|
||||
@ -68,7 +74,7 @@ function processReceivedMessage(data) {
|
||||
scrollContainer.trigger("keepToBottom");
|
||||
}
|
||||
|
||||
notifyMessage(targetId, channel, data);
|
||||
notifyMessage(targetId, channelContainer, data);
|
||||
|
||||
let shouldMoveMarker = data.msg.self;
|
||||
|
||||
@ -95,16 +101,6 @@ function processReceivedMessage(data) {
|
||||
.appendTo(container);
|
||||
}
|
||||
|
||||
// Clear unread/highlight counter if self-message
|
||||
if (data.msg.self) {
|
||||
sidebarTarget.find(".badge")
|
||||
.attr("data-highlight", 0)
|
||||
.removeClass("highlight")
|
||||
.empty();
|
||||
|
||||
utils.updateTitle();
|
||||
}
|
||||
|
||||
let messageLimit = 0;
|
||||
|
||||
if (activeChannelId !== targetId) {
|
||||
@ -116,11 +112,11 @@ function processReceivedMessage(data) {
|
||||
}
|
||||
|
||||
if (messageLimit > 0) {
|
||||
render.trimMessageInChannel(channel, messageLimit);
|
||||
render.trimMessageInChannel(channelContainer, messageLimit);
|
||||
}
|
||||
|
||||
if ((data.msg.type === "message" || data.msg.type === "action") && channel.hasClass("channel")) {
|
||||
const nicks = channel.find(".userlist").data("nicks");
|
||||
if ((data.msg.type === "message" || data.msg.type === "action") && channelContainer.hasClass("channel")) {
|
||||
const nicks = channelContainer.find(".userlist").data("nicks");
|
||||
|
||||
if (nicks) {
|
||||
const find = nicks.indexOf(data.msg.from.nick);
|
||||
|
@ -6,13 +6,18 @@ const render = require("../render");
|
||||
const templates = require("../../views");
|
||||
const sidebar = $("#sidebar");
|
||||
const utils = require("../utils");
|
||||
const {Vue, vueApp} = require("../vue");
|
||||
|
||||
socket.on("network", function(data) {
|
||||
render.renderNetworks(data, true);
|
||||
vueApp.networks.push(data.networks[0]);
|
||||
|
||||
sidebar.find(".chan")
|
||||
.last()
|
||||
.trigger("click");
|
||||
Vue.nextTick(() => {
|
||||
render.renderNetworks(data, true);
|
||||
|
||||
sidebar.find(".chan")
|
||||
.last()
|
||||
.trigger("click");
|
||||
});
|
||||
|
||||
$("#connect")
|
||||
.find(".btn")
|
||||
@ -20,14 +25,13 @@ socket.on("network", function(data) {
|
||||
});
|
||||
|
||||
socket.on("network_changed", function(data) {
|
||||
sidebar.find(`.network[data-uuid="${data.network}"]`).data("options", data.serverOptions);
|
||||
vueApp.networks.find((n) => n.uuid === data.network).serverOptions = data.serverOptions;
|
||||
});
|
||||
|
||||
socket.on("network:status", function(data) {
|
||||
sidebar
|
||||
.find(`.network[data-uuid="${data.network}"]`)
|
||||
.toggleClass("not-connected", !data.connected)
|
||||
.toggleClass("not-secure", !data.secure);
|
||||
const network = vueApp.networks.find((n) => n.uuid === data.network);
|
||||
network.status.connected = data.connected;
|
||||
network.status.secure = data.secure;
|
||||
});
|
||||
|
||||
socket.on("network:info", function(data) {
|
||||
|
@ -3,6 +3,7 @@
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const utils = require("../utils");
|
||||
const {vueApp, findChannel} = require("../vue");
|
||||
|
||||
// Sync unread badge and marker when other clients open a channel
|
||||
socket.on("open", function(id) {
|
||||
@ -10,24 +11,25 @@ socket.on("open", function(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = $("#chat #chan-" + id);
|
||||
|
||||
// Don't do anything if the channel is active on this client
|
||||
if (channel.length === 0 || channel.hasClass("active")) {
|
||||
if (vueApp.activeChannel && vueApp.activeChannel.channel.id === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the unread badge
|
||||
$("#sidebar").find(".chan[data-id='" + id + "'] .badge")
|
||||
.attr("data-highlight", 0)
|
||||
.removeClass("highlight")
|
||||
.empty();
|
||||
const channel = findChannel(id);
|
||||
|
||||
if (channel) {
|
||||
channel.channel.highlight = 0;
|
||||
channel.channel.unread = 0;
|
||||
}
|
||||
|
||||
utils.updateTitle();
|
||||
|
||||
// Move unread marker to the bottom
|
||||
channel
|
||||
const channelContainer = $("#chat #chan-" + id);
|
||||
channelContainer
|
||||
.find(".unread-marker")
|
||||
.data("unread-id", 0)
|
||||
.appendTo(channel.find(".messages"));
|
||||
.appendTo(channelContainer.find(".messages"));
|
||||
});
|
||||
|
@ -2,19 +2,19 @@
|
||||
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const sidebar = $("#sidebar");
|
||||
const {vueApp} = require("../vue");
|
||||
|
||||
socket.on("part", function(data) {
|
||||
const chanMenuItem = sidebar.find(".chan[data-id='" + data.chan + "']");
|
||||
|
||||
// When parting from the active channel/query, jump to the network's lobby
|
||||
if (chanMenuItem.hasClass("active")) {
|
||||
chanMenuItem
|
||||
.parent(".network")
|
||||
if (vueApp.activeChannel && vueApp.activeChannel.channel.id === data.chan) {
|
||||
$("#sidebar .chan[data-id='" + data.chan + "']")
|
||||
.closest(".network")
|
||||
.find(".lobby")
|
||||
.trigger("click");
|
||||
}
|
||||
|
||||
chanMenuItem.remove();
|
||||
$("#chan-" + data.chan).remove();
|
||||
|
||||
const network = vueApp.networks.find((n) => n.uuid === data.network);
|
||||
network.channels.splice(network.channels.findIndex((c) => c.id === data.chan), 1);
|
||||
});
|
||||
|
@ -4,8 +4,11 @@ const $ = require("jquery");
|
||||
const chat = $("#chat");
|
||||
const socket = require("../socket");
|
||||
const sidebar = $("#sidebar");
|
||||
const {Vue, vueApp} = require("../vue");
|
||||
|
||||
socket.on("quit", function(data) {
|
||||
vueApp.networks.splice(vueApp.networks.findIndex((n) => n.uuid === data.network), 1);
|
||||
|
||||
const id = data.network;
|
||||
const network = sidebar.find(`.network[data-uuid="${id}"]`);
|
||||
|
||||
@ -14,18 +17,16 @@ socket.on("quit", function(data) {
|
||||
chat.find($(this).attr("data-target")).remove();
|
||||
});
|
||||
|
||||
network.remove();
|
||||
Vue.nextTick(() => {
|
||||
const chan = sidebar.find(".chan");
|
||||
|
||||
const chan = sidebar.find(".chan");
|
||||
|
||||
if (chan.length === 0) {
|
||||
sidebar.find(".empty").show();
|
||||
|
||||
// Open the connect window
|
||||
$("#footer .connect").trigger("click", {
|
||||
pushState: false,
|
||||
});
|
||||
} else {
|
||||
chan.eq(0).trigger("click");
|
||||
}
|
||||
if (chan.length === 0) {
|
||||
// Open the connect window
|
||||
$("#footer .connect").trigger("click", {
|
||||
pushState: false,
|
||||
});
|
||||
} else {
|
||||
chan.eq(0).trigger("click");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,64 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const sidebar = $("#sidebar, #footer");
|
||||
const socket = require("./socket");
|
||||
const options = require("./options");
|
||||
|
||||
module.exports = function() {
|
||||
sidebar.find(".networks").sortable({
|
||||
axis: "y",
|
||||
containment: "parent",
|
||||
cursor: "move",
|
||||
distance: 12,
|
||||
items: ".network",
|
||||
handle: ".lobby",
|
||||
placeholder: "network-placeholder",
|
||||
forcePlaceholderSize: true,
|
||||
tolerance: "pointer", // Use the pointer to figure out where the network is in the list
|
||||
|
||||
update() {
|
||||
const order = [];
|
||||
|
||||
sidebar.find(".network").each(function() {
|
||||
const id = $(this).data("uuid");
|
||||
order.push(id);
|
||||
});
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "networks",
|
||||
order: order,
|
||||
});
|
||||
|
||||
options.settings.ignoreSortSync = true;
|
||||
},
|
||||
});
|
||||
sidebar.find(".network").sortable({
|
||||
axis: "y",
|
||||
containment: "parent",
|
||||
cursor: "move",
|
||||
distance: 12,
|
||||
items: ".chan:not(.lobby)",
|
||||
placeholder: "chan-placeholder",
|
||||
forcePlaceholderSize: true,
|
||||
tolerance: "pointer", // Use the pointer to figure out where the channel is in the list
|
||||
|
||||
update(e, ui) {
|
||||
const order = [];
|
||||
const network = ui.item.parent();
|
||||
|
||||
network.find(".chan").each(function() {
|
||||
const id = $(this).data("id");
|
||||
order.push(id);
|
||||
});
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "channels",
|
||||
target: network.data("uuid"),
|
||||
order: order,
|
||||
});
|
||||
|
||||
options.settings.ignoreSortSync = true;
|
||||
},
|
||||
});
|
||||
};
|
@ -3,6 +3,7 @@
|
||||
const $ = require("jquery");
|
||||
const escape = require("css.escape");
|
||||
const viewport = $("#viewport");
|
||||
const {vueApp} = require("./vue");
|
||||
|
||||
var serverHash = -1; // eslint-disable-line no-var
|
||||
var lastMessageId = -1; // eslint-disable-line no-var
|
||||
@ -101,18 +102,20 @@ function toggleNotificationMarkers(newState) {
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
let title = $(document.body).data("app-name");
|
||||
const chanTitle = $("#sidebar").find(".chan.active").attr("aria-label");
|
||||
let title = vueApp.appName;
|
||||
|
||||
if (chanTitle && chanTitle.length > 0) {
|
||||
title = `${chanTitle} — ${title}`;
|
||||
if (vueApp.activeChannel) {
|
||||
title = `${vueApp.activeChannel.channel.name} — ${vueApp.activeChannel.network.name} — ${title}`;
|
||||
}
|
||||
|
||||
// add highlight count to title
|
||||
let alertEventCount = 0;
|
||||
$(".badge.highlight").each(function() {
|
||||
alertEventCount += parseInt($(this).attr("data-highlight"));
|
||||
});
|
||||
|
||||
for (const network of vueApp.networks) {
|
||||
for (const channel of network.channels) {
|
||||
alertEventCount += channel.highlight;
|
||||
}
|
||||
}
|
||||
|
||||
if (alertEventCount > 0) {
|
||||
title = `(${alertEventCount}) ${title}`;
|
||||
|
39
client/js/vue.js
Normal file
39
client/js/vue.js
Normal file
@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
const Vue = require("vue").default;
|
||||
const App = require("../components/App.vue").default;
|
||||
const roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber");
|
||||
|
||||
Vue.filter("roundBadgeNumber", roundBadgeNumber);
|
||||
|
||||
const vueApp = new Vue({
|
||||
el: "#viewport",
|
||||
data: {
|
||||
appName: document.title,
|
||||
activeChannel: null,
|
||||
networks: [],
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App, {
|
||||
props: this,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function findChannel(id) {
|
||||
for (const network of vueApp.networks) {
|
||||
for (const channel of network.channels) {
|
||||
if (channel.id === id) {
|
||||
return {network, channel};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Vue,
|
||||
vueApp,
|
||||
findChannel,
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
{{#each channels}}
|
||||
<div
|
||||
class="chan {{type}} chan-{{slugify name}}"
|
||||
data-id="{{id}}"
|
||||
data-target="#chan-{{id}}"
|
||||
role="tab"
|
||||
aria-label="{{name}}"
|
||||
aria-controls="chan-{{id}}"
|
||||
aria-selected="false"
|
||||
>
|
||||
{{#equal type "lobby"}}
|
||||
<button class="collapse-network" aria-label="Collapse" aria-controls="network-{{../uuid}}" aria-expanded="true">
|
||||
<span class="collapse-network-icon"></span>
|
||||
</button>
|
||||
<div class="lobby-wrap">
|
||||
<span class="name" title="{{name}}">{{name}}</span>
|
||||
<span class="not-secure-tooltip tooltipped tooltipped-w" aria-label="Insecure connection">
|
||||
<span class="not-secure-icon"></span>
|
||||
</span>
|
||||
<span class="not-connected-tooltip tooltipped tooltipped-w" aria-label="Disconnected">
|
||||
<span class="not-connected-icon"></span>
|
||||
</span>
|
||||
<span class="badge{{#if highlight}} highlight{{/if}}" data-highlight="{{highlight}}">{{#if unread}}{{roundBadgeNumber unread}}{{/if}}</span>
|
||||
</div>
|
||||
<span class="add-channel-tooltip tooltipped tooltipped-w tooltipped-no-touch" aria-label="Join a channel…" data-alt-label="Cancel">
|
||||
<button class="add-channel" aria-label="Join a channel…" aria-controls="join-channel-{{id}}"></button>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="name" title="{{name}}">{{name}}</span>
|
||||
<span class="badge{{#if highlight}} highlight{{/if}}" data-highlight="{{highlight}}">{{#if unread}}{{roundBadgeNumber unread}}{{/if}}</span>
|
||||
{{#equal type "channel"}}
|
||||
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Leave">
|
||||
<button class="close" aria-label="Leave"></button>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Close">
|
||||
<button class="close" aria-label="Close"></button>
|
||||
</span>
|
||||
{{/equal}}
|
||||
{{/equal}}
|
||||
</div>
|
||||
{{#equal type "lobby"}}
|
||||
{{> join_channel}}
|
||||
{{/equal}}
|
||||
{{/each}}
|
@ -1,5 +0,0 @@
|
||||
<form id="join-channel-{{id}}" class="join-form" method="post" action="" autocomplete="off">
|
||||
<input type="text" class="input" name="channel" placeholder="Channel" pattern="[^\s]+" maxlength="200" title="The channel name may not contain spaces" required>
|
||||
<input type="password" class="input" name="key" placeholder="Password (optional)" pattern="[^\s]+" maxlength="200" title="The channel password may not contain spaces" autocomplete="new-password">
|
||||
<button type="submit" class="btn btn-small">Join</button>
|
||||
</form>
|
@ -1,12 +0,0 @@
|
||||
{{#each networks}}
|
||||
<section
|
||||
class="network name-{{slugify name}} {{#if serverOptions.NETWORK}}network-{{slugify serverOptions.NETWORK}}{{/if}} {{#unless status.connected}}not-connected{{/unless}} {{#unless status.secure}}not-secure{{/unless}}"
|
||||
id="network-{{uuid}}"
|
||||
data-uuid="{{uuid}}"
|
||||
data-nick="{{nick}}"
|
||||
data-options="{{tojson serverOptions}}"
|
||||
role="region"
|
||||
>
|
||||
{{> chan}}
|
||||
</section>
|
||||
{{/each}}
|
@ -16,7 +16,7 @@
|
||||
"coverage": "run-s test:{client,server} && nyc --nycrc-path=test/.nycrc-report report",
|
||||
"dev": "run-p watch start",
|
||||
"lint:css": "stylelint --color \"client/**/*.css\"",
|
||||
"lint:js": "eslint . --report-unused-disable-directives --color",
|
||||
"lint:js": "eslint . --ext .js,.vue --report-unused-disable-directives --color",
|
||||
"start": "node index start",
|
||||
"test": "run-p --aggregate-output --continue-on-error lint:* test:{client,server}",
|
||||
"test:browser": "webpack-dev-server --config=webpack.config-browser.js",
|
||||
@ -80,6 +80,7 @@
|
||||
"css.escape": "1.5.1",
|
||||
"emoji-regex": "7.0.3",
|
||||
"eslint": "5.13.0",
|
||||
"eslint-plugin-vue": "4.5.0",
|
||||
"fuzzy": "0.1.3",
|
||||
"graphql-request": "1.8.2",
|
||||
"handlebars": "4.1.0",
|
||||
@ -105,6 +106,10 @@
|
||||
"stylelint-config-standard": "18.2.0",
|
||||
"textcomplete": "0.17.1",
|
||||
"undate": "0.3.0",
|
||||
"vue": "2.5.16",
|
||||
"vue-loader": "15.2.4",
|
||||
"vue-template-compiler": "2.5.16",
|
||||
"vuedraggable": "2.16.0",
|
||||
"webpack": "4.29.3",
|
||||
"webpack-cli": "3.2.3",
|
||||
"webpack-dev-server": "3.1.14"
|
||||
|
@ -35,6 +35,7 @@ exports.input = function(network, chan, cmd, args) {
|
||||
network.channels = _.without(network.channels, target);
|
||||
target.destroy();
|
||||
this.emit("part", {
|
||||
network: network.uuid,
|
||||
chan: target.id,
|
||||
});
|
||||
this.save();
|
||||
|
@ -29,6 +29,7 @@ module.exports = function(irc, network) {
|
||||
chan.destroy();
|
||||
client.save();
|
||||
client.emit("part", {
|
||||
network: network.uuid,
|
||||
chan: chan.id,
|
||||
});
|
||||
} else {
|
||||
|
@ -4,6 +4,7 @@ const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const VueLoaderPlugin = require("vue-loader/lib/plugin");
|
||||
|
||||
const config = {
|
||||
mode: process.env.NODE_ENV === "production" ? "production" : "development",
|
||||
@ -19,6 +20,12 @@ const config = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: {
|
||||
loader: "vue-loader",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: [
|
||||
@ -98,6 +105,7 @@ const config = {
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin(),
|
||||
new VueLoaderPlugin(),
|
||||
new CopyPlugin([
|
||||
{
|
||||
from: "./node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff*",
|
||||
|
147
yarn.lock
147
yarn.lock
@ -594,6 +594,20 @@
|
||||
"@types/unist" "*"
|
||||
"@types/vfile-message" "*"
|
||||
|
||||
"@vue/component-compiler-utils@^1.2.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.3.1.tgz#686f0b913d59590ae327b2a1cb4b6d9b931bbe0e"
|
||||
dependencies:
|
||||
consolidate "^0.15.1"
|
||||
hash-sum "^1.0.2"
|
||||
lru-cache "^4.1.2"
|
||||
merge-source-map "^1.1.0"
|
||||
postcss "^6.0.20"
|
||||
postcss-selector-parser "^3.1.1"
|
||||
prettier "^1.13.0"
|
||||
source-map "^0.5.6"
|
||||
vue-template-es2015-compiler "^1.6.0"
|
||||
|
||||
"@webassemblyjs/ast@1.7.11":
|
||||
version "1.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
|
||||
@ -746,9 +760,23 @@ acorn-dynamic-import@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
|
||||
|
||||
acorn-jsx@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
|
||||
dependencies:
|
||||
acorn "^3.0.4"
|
||||
|
||||
acorn-jsx@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.0.tgz#958584ddb60990c02c97c1bd9d521fce433bb101"
|
||||
|
||||
acorn@^3.0.4:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
|
||||
|
||||
acorn@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
|
||||
|
||||
acorn@^6.0.2, acorn@^6.0.5:
|
||||
version "6.0.5"
|
||||
@ -1198,7 +1226,11 @@ blob@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
|
||||
|
||||
bluebird@^3.5.1, bluebird@^3.5.3:
|
||||
bluebird@^3.1.1, bluebird@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
|
||||
bluebird@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
|
||||
|
||||
@ -1845,6 +1877,12 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
|
||||
consolidate@^0.15.1:
|
||||
version "0.15.1"
|
||||
resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
|
||||
dependencies:
|
||||
bluebird "^3.1.1"
|
||||
|
||||
constants-browserify@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||
@ -2125,6 +2163,10 @@ date-now@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
||||
|
||||
de-indent@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||
|
||||
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
@ -2544,6 +2586,19 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
|
||||
eslint-plugin-vue@4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.5.0.tgz#09d6597f4849e31a3846c2c395fccf17685b69c3"
|
||||
dependencies:
|
||||
vue-eslint-parser "^2.0.3"
|
||||
|
||||
eslint-scope@^3.7.1:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
|
||||
dependencies:
|
||||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-scope@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172"
|
||||
@ -2600,6 +2655,13 @@ eslint@5.13.0:
|
||||
table "^5.0.2"
|
||||
text-table "^0.2.0"
|
||||
|
||||
espree@^3.5.2:
|
||||
version "3.5.4"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
|
||||
dependencies:
|
||||
acorn "^5.5.0"
|
||||
acorn-jsx "^3.0.0"
|
||||
|
||||
espree@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.0.tgz#fc7f984b62b36a0f543b13fb9cd7b9f4a7f5b65c"
|
||||
@ -2616,7 +2678,7 @@ esprima@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
|
||||
esquery@^1.0.1:
|
||||
esquery@^1.0.0, esquery@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
|
||||
dependencies:
|
||||
@ -3385,6 +3447,10 @@ hash-base@^3.0.0:
|
||||
inherits "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
hash-sum@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
|
||||
|
||||
hash.js@^1.0.0, hash.js@^1.0.3:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
|
||||
@ -3398,7 +3464,7 @@ hasha@^3.0.0:
|
||||
dependencies:
|
||||
is-stream "^1.0.1"
|
||||
|
||||
he@1.1.1:
|
||||
he@1.1.1, he@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
||||
|
||||
@ -4434,9 +4500,9 @@ lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||
|
||||
lru-cache@^4.0.1, lru-cache@^4.1.1:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
|
||||
lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
|
||||
dependencies:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
@ -5736,7 +5802,7 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
|
||||
indexes-of "^1.0.1"
|
||||
uniq "^1.0.1"
|
||||
|
||||
postcss-selector-parser@^3.1.0:
|
||||
postcss-selector-parser@^3.1.0, postcss-selector-parser@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
|
||||
dependencies:
|
||||
@ -5786,7 +5852,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
|
||||
source-map "^0.5.6"
|
||||
supports-color "^3.2.3"
|
||||
|
||||
postcss@^6.0.1, postcss@^6.0.23:
|
||||
postcss@^6.0.1, postcss@^6.0.20, postcss@^6.0.23:
|
||||
version "6.0.23"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
|
||||
dependencies:
|
||||
@ -5818,6 +5884,10 @@ prepend-http@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
|
||||
prettier@^1.13.0:
|
||||
version "1.13.7"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281"
|
||||
|
||||
primer-support@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/primer-support/-/primer-support-5.0.0.tgz#d19c7cea59e8783400b9391943c8a2bb2ebddc5e"
|
||||
@ -6649,6 +6719,10 @@ sort-keys@^1.0.0:
|
||||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
sortablejs@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.7.0.tgz#80a2b2370abd568e1cec8c271131ef30a904fa28"
|
||||
|
||||
source-list-map@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||
@ -7567,6 +7641,59 @@ vm-browserify@0.0.4:
|
||||
dependencies:
|
||||
indexof "0.0.1"
|
||||
|
||||
vue-eslint-parser@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
eslint-scope "^3.7.1"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
espree "^3.5.2"
|
||||
esquery "^1.0.0"
|
||||
lodash "^4.17.4"
|
||||
|
||||
vue-hot-reload-api@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"
|
||||
|
||||
vue-loader@15.2.4:
|
||||
version "15.2.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.2.4.tgz#a7b923123d3cf87230a8ff54a1c16d31a6c5dbb4"
|
||||
dependencies:
|
||||
"@vue/component-compiler-utils" "^1.2.1"
|
||||
hash-sum "^1.0.2"
|
||||
loader-utils "^1.1.0"
|
||||
vue-hot-reload-api "^2.3.0"
|
||||
vue-style-loader "^4.1.0"
|
||||
|
||||
vue-style-loader@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.0.tgz#7588bd778e2c9f8d87bfc3c5a4a039638da7a863"
|
||||
dependencies:
|
||||
hash-sum "^1.0.2"
|
||||
loader-utils "^1.0.2"
|
||||
|
||||
vue-template-compiler@2.5.16:
|
||||
version "2.5.16"
|
||||
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
|
||||
dependencies:
|
||||
de-indent "^1.0.2"
|
||||
he "^1.1.0"
|
||||
|
||||
vue-template-es2015-compiler@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
|
||||
|
||||
vue@2.5.16:
|
||||
version "2.5.16"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"
|
||||
|
||||
vuedraggable@2.16.0:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.16.0.tgz#52127081a2adb3de5fabd214d404ff3eee63575a"
|
||||
dependencies:
|
||||
sortablejs "^1.7.0"
|
||||
|
||||
watchpack@^1.5.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
|
||||
|
Loading…
Reference in New Issue
Block a user