New discussion gutter
This commit is contained in:
parent
8767adc505
commit
fcff116d92
@ -14,6 +14,8 @@ import Layout from './Layout';
|
|||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
import SplashScreen from './SplashScreen';
|
import SplashScreen from './SplashScreen';
|
||||||
|
import timeSvc from '../services/timeSvc';
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
// Global directives
|
// Global directives
|
||||||
Vue.directive('focus', {
|
Vue.directive('focus', {
|
||||||
@ -48,6 +50,11 @@ Vue.directive('title', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Global filters
|
||||||
|
Vue.filter('formatTime', time =>
|
||||||
|
// Access the minute counter for reactive refresh
|
||||||
|
timeSvc.format(time, store.state.minuteCounter));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Layout,
|
Layout,
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
|
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
|
||||||
<editor-new-discussion-button-gutter></editor-new-discussion-button-gutter>
|
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
||||||
|
<comment-list v-if="styles.editorGutterWidth"></comment-list>
|
||||||
|
<editor-new-discussion-button></editor-new-discussion-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import EditorNewDiscussionButtonGutter from './gutters/EditorNewDiscussionButtonGutter';
|
import CommentList from './gutters/CommentList';
|
||||||
|
import EditorNewDiscussionButton from './gutters/EditorNewDiscussionButton';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorNewDiscussionButtonGutter,
|
CommentList,
|
||||||
|
EditorNewDiscussionButton,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('layout', [
|
...mapGetters('layout', [
|
||||||
@ -35,26 +39,28 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
editorElt.addEventListener('mouseover', onDiscussionEvt(discussionId =>
|
const classToggler = toggle => (discussionId) => {
|
||||||
editorElt.getElementsByClassName(`discussion-editor-highlighting-${discussionId}`)
|
editorElt.getElementsByClassName(`discussion-editor-highlighting--${discussionId}`)
|
||||||
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--hover')),
|
.cl_each(elt => elt.classList.toggle('discussion-editor-highlighting--hover', toggle));
|
||||||
));
|
document.getElementsByClassName(`comment--discussion-${discussionId}`)
|
||||||
editorElt.addEventListener('mouseout', onDiscussionEvt(discussionId =>
|
.cl_each(elt => elt.classList.toggle('comment--hover', toggle));
|
||||||
editorElt.getElementsByClassName(`discussion-editor-highlighting-${discussionId}`)
|
};
|
||||||
.cl_each(elt => elt.classList.remove('discussion-editor-highlighting--hover')),
|
|
||||||
));
|
editorElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
|
||||||
|
editorElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
|
||||||
editorElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
editorElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
||||||
this.$store.commit('discussion/setCurrentDiscussionId', discussionId);
|
this.$store.commit('discussion/setCurrentDiscussionId', discussionId);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.$watch(
|
this.$watch(
|
||||||
() => this.$store.state.discussion.currentDiscussionId,
|
() => this.$store.state.discussion.currentDiscussionId,
|
||||||
(discussionId, oldDiscussionId) => {
|
(discussionId, oldDiscussionId) => {
|
||||||
if (oldDiscussionId) {
|
if (oldDiscussionId) {
|
||||||
editorElt.querySelectorAll(`.discussion-editor-highlighting-${oldDiscussionId}`)
|
editorElt.querySelectorAll(`.discussion-editor-highlighting--${oldDiscussionId}`)
|
||||||
.cl_each(elt => elt.classList.remove('discussion-editor-highlighting--selected'));
|
.cl_each(elt => elt.classList.remove('discussion-editor-highlighting--selected'));
|
||||||
}
|
}
|
||||||
if (discussionId) {
|
if (discussionId) {
|
||||||
editorElt.querySelectorAll(`.discussion-editor-highlighting-${discussionId}`)
|
editorElt.querySelectorAll(`.discussion-editor-highlighting--${discussionId}`)
|
||||||
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
|
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -286,7 +286,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.find-replace__button {
|
.find-replace__button {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@ -298,6 +298,7 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
color: rgba(0, 0, 0, 0.25);
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
text-transform: none;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
@ -330,15 +331,15 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
color: rgba(0, 0, 0, 0.25);
|
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgba(0, 0, 0, 0.33);
|
color: rgba(0, 0, 0, 0.75);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,11 +10,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="layout__panel flex flex--row" :style="{height: styles.innerHeight + 'px'}">
|
<div class="layout__panel flex flex--row" :style="{height: styles.innerHeight + 'px'}">
|
||||||
<div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{width: (styles.editorWidth + styles.editorGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
|
<div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{width: (styles.editorWidth + styles.editorGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
|
||||||
<div class="gutter" v-if="styles.editorGutterWidth" :style="{left: styles.editorGutterLeft + 'px'}">
|
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
||||||
<div class="gutter__background"></div>
|
<div class="gutter__background" v-if="styles.editorGutterWidth" :style="{width: styles.editorGutterWidth + 'px'}"></div>
|
||||||
</div>
|
</div>
|
||||||
<editor></editor>
|
<editor></editor>
|
||||||
<div v-if="showFindReplace" class="layout__panel layout__panel--find-replace">
|
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
||||||
|
<sticky-comment v-if="styles.editorGutterWidth && stickyComment === 'top'"></sticky-comment>
|
||||||
|
<current-discussion v-if="styles.editorGutterWidth"></current-discussion>
|
||||||
|
</div>
|
||||||
|
<div class="layout__panel layout__panel--find-replace" v-if="showFindReplace">
|
||||||
<find-replace></find-replace>
|
<find-replace></find-replace>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -22,10 +26,14 @@
|
|||||||
<button-bar></button-bar>
|
<button-bar></button-bar>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{width: (styles.previewWidth + styles.previewGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
|
<div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{width: (styles.previewWidth + styles.previewGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
|
||||||
<div class="gutter" v-if="styles.previewGutterWidth" :style="{left: styles.previewGutterLeft + 'px'}">
|
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
||||||
<div class="gutter__background"></div>
|
<div class="gutter__background" v-if="styles.previewGutterWidth" :style="{width: styles.previewGutterWidth + 'px'}"></div>
|
||||||
</div>
|
</div>
|
||||||
<preview></preview>
|
<preview></preview>
|
||||||
|
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
||||||
|
<sticky-comment v-if="styles.previewGutterWidth && stickyComment === 'top'"></sticky-comment>
|
||||||
|
<current-discussion v-if="styles.previewGutterWidth"></current-discussion>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{height: constants.statusBarHeight + 'px'}">
|
<div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{height: constants.statusBarHeight + 'px'}">
|
||||||
@ -40,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
import NavigationBar from './NavigationBar';
|
import NavigationBar from './NavigationBar';
|
||||||
import ButtonBar from './ButtonBar';
|
import ButtonBar from './ButtonBar';
|
||||||
import StatusBar from './StatusBar';
|
import StatusBar from './StatusBar';
|
||||||
@ -48,6 +56,8 @@ import Explorer from './Explorer';
|
|||||||
import SideBar from './SideBar';
|
import SideBar from './SideBar';
|
||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
import Preview from './Preview';
|
import Preview from './Preview';
|
||||||
|
import StickyComment from './gutters/StickyComment';
|
||||||
|
import CurrentDiscussion from './gutters/CurrentDiscussion';
|
||||||
import FindReplace from './FindReplace';
|
import FindReplace from './FindReplace';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
|
|
||||||
@ -60,9 +70,14 @@ export default {
|
|||||||
SideBar,
|
SideBar,
|
||||||
Editor,
|
Editor,
|
||||||
Preview,
|
Preview,
|
||||||
|
StickyComment,
|
||||||
|
CurrentDiscussion,
|
||||||
FindReplace,
|
FindReplace,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState('discussion', [
|
||||||
|
'stickyComment',
|
||||||
|
]),
|
||||||
...mapGetters('layout', [
|
...mapGetters('layout', [
|
||||||
'constants',
|
'constants',
|
||||||
'styles',
|
'styles',
|
||||||
@ -130,13 +145,45 @@ export default {
|
|||||||
background-color: #007acc;
|
background-color: #007acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$editor-background: #fff;
|
||||||
|
|
||||||
.layout__panel--editor {
|
.layout__panel--editor {
|
||||||
background-color: #fff;
|
background-color: $editor-background;
|
||||||
|
|
||||||
|
.gutter__background,
|
||||||
|
.comment-list__current-discussion,
|
||||||
|
.sticky-comment,
|
||||||
|
.current-discussion {
|
||||||
|
background-color: mix(#000, $editor-background, 6.7%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-list__current-discussion,
|
||||||
|
.sticky-comment,
|
||||||
|
.current-discussion {
|
||||||
|
border-color: $editor-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$preview-background: #f3f3f3;
|
||||||
|
|
||||||
|
.layout__panel--preview,
|
||||||
|
.layout__panel--button-bar {
|
||||||
|
background-color: $preview-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout__panel--button-bar,
|
|
||||||
.layout__panel--preview {
|
.layout__panel--preview {
|
||||||
background-color: #f3f3f3;
|
.gutter__background,
|
||||||
|
.comment-list__current-discussion,
|
||||||
|
.sticky-comment,
|
||||||
|
.current-discussion {
|
||||||
|
background-color: mix(#000, $preview-background, 6.7%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-list__current-discussion,
|
||||||
|
.sticky-comment,
|
||||||
|
.current-discussion {
|
||||||
|
border-color: $preview-background;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout__panel--explorer,
|
.layout__panel--explorer,
|
||||||
@ -153,12 +200,4 @@ export default {
|
|||||||
height: auto;
|
height: auto;
|
||||||
border-top-right-radius: $border-radius-base;
|
border-top-right-radius: $border-radius-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutter__background {
|
|
||||||
position: absolute;
|
|
||||||
width: 9999px;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -184,7 +184,7 @@ export default {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
max-width: 500px;
|
max-width: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__inner-2 {
|
.modal__inner-2 {
|
||||||
@ -248,10 +248,6 @@ export default {
|
|||||||
.modal__button-bar {
|
.modal__button-bar {
|
||||||
margin-top: 1.75rem;
|
margin-top: 1.75rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-entry {
|
.form-entry {
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
<div class="preview__inner-1" @click="onClick" @scroll="onScroll">
|
<div class="preview__inner-1" @click="onClick" @scroll="onScroll">
|
||||||
<div class="preview__inner-2" :style="{padding: styles.previewPadding}">
|
<div class="preview__inner-2" :style="{padding: styles.previewPadding}">
|
||||||
</div>
|
</div>
|
||||||
<preview-new-discussion-button-gutter></preview-new-discussion-button-gutter>
|
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
||||||
|
<comment-list v-if="styles.previewGutterWidth"></comment-list>
|
||||||
|
<preview-new-discussion-button></preview-new-discussion-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!styles.showEditor" class="preview__button-bar">
|
</div>
|
||||||
<div class="preview__button" @click="toggleEditor(true)">
|
<div v-if="!styles.showEditor" class="preview__corner">
|
||||||
|
<button class="preview__button button" @click="toggleEditor(true)" v-title="'Edit file'">
|
||||||
<icon-pen></icon-pen>
|
<icon-pen></icon-pen>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -16,13 +19,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import PreviewNewDiscussionButtonGutter from './gutters/PreviewNewDiscussionButtonGutter';
|
import CommentList from './gutters/CommentList';
|
||||||
|
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
|
||||||
|
|
||||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
const appUri = `${window.location.protocol}//${window.location.host}`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PreviewNewDiscussionButtonGutter,
|
CommentList,
|
||||||
|
PreviewNewDiscussionButton,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
previewTop: true,
|
previewTop: true,
|
||||||
@ -64,26 +69,28 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
previewElt.addEventListener('mouseover', onDiscussionEvt(discussionId =>
|
const classToggler = toggle => (discussionId) => {
|
||||||
previewElt.getElementsByClassName(`discussion-preview-highlighting-${discussionId}`)
|
previewElt.getElementsByClassName(`discussion-preview-highlighting--${discussionId}`)
|
||||||
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--hover')),
|
.cl_each(elt => elt.classList.toggle('discussion-preview-highlighting--hover', toggle));
|
||||||
));
|
document.getElementsByClassName(`comment--discussion-${discussionId}`)
|
||||||
previewElt.addEventListener('mouseout', onDiscussionEvt(discussionId =>
|
.cl_each(elt => elt.classList.toggle('comment--hover', toggle));
|
||||||
previewElt.getElementsByClassName(`discussion-preview-highlighting-${discussionId}`)
|
};
|
||||||
.cl_each(elt => elt.classList.remove('discussion-preview-highlighting--hover')),
|
|
||||||
));
|
previewElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
|
||||||
|
previewElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
|
||||||
previewElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
previewElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
||||||
this.$store.commit('discussion/setCurrentDiscussionId', discussionId);
|
this.$store.commit('discussion/setCurrentDiscussionId', discussionId);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.$watch(
|
this.$watch(
|
||||||
() => this.$store.state.discussion.currentDiscussionId,
|
() => this.$store.state.discussion.currentDiscussionId,
|
||||||
(discussionId, oldDiscussionId) => {
|
(discussionId, oldDiscussionId) => {
|
||||||
if (oldDiscussionId) {
|
if (oldDiscussionId) {
|
||||||
previewElt.querySelectorAll(`.discussion-preview-highlighting-${oldDiscussionId}`)
|
previewElt.querySelectorAll(`.discussion-preview-highlighting--${oldDiscussionId}`)
|
||||||
.cl_each(elt => elt.classList.remove('discussion-preview-highlighting--selected'));
|
.cl_each(elt => elt.classList.remove('discussion-preview-highlighting--selected'));
|
||||||
}
|
}
|
||||||
if (discussionId) {
|
if (discussionId) {
|
||||||
previewElt.querySelectorAll(`.discussion-preview-highlighting-${discussionId}`)
|
previewElt.querySelectorAll(`.discussion-preview-highlighting--${discussionId}`)
|
||||||
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
|
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -113,23 +120,37 @@ export default {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview__button-bar {
|
$corner-size: 110px;
|
||||||
|
|
||||||
|
.preview__corner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 0;
|
||||||
right: 26px;
|
right: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
border-top: $corner-size solid rgba(0, 0, 0, 0.075);
|
||||||
|
border-left: $corner-size solid transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview__button {
|
.preview__button {
|
||||||
cursor: pointer;
|
position: absolute;
|
||||||
color: rgba(0, 0, 0, 0.25);
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: $border-radius-base;
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
color: rgba(0, 0, 0, 0.33);
|
||||||
color: rgba(0, 0, 0, 0.75);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,26 +4,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
import userSvc from '../services/userSvc';
|
||||||
|
|
||||||
const promised = {};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['userId'],
|
props: ['userId'],
|
||||||
computed: {
|
computed: {
|
||||||
url() {
|
url() {
|
||||||
const userInfo = this.$store.state.userInfo.itemMap[this.userId];
|
const userInfo = this.$store.state.userInfo.itemMap[this.userId];
|
||||||
return userInfo && `url('${userInfo.imageUrl}')`;
|
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (!promised[this.userId] && !this.$store.state.offline) {
|
userSvc.getInfo(this.userId);
|
||||||
promised[this.userId] = true;
|
|
||||||
googleHelper.getUser(this.userId)
|
|
||||||
.catch(() => {
|
|
||||||
promised[this.userId] = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
20
src/components/UserName.vue
Normal file
20
src/components/UserName.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<span class="user-name">{{name}}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import userSvc from '../services/userSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['userId'],
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
const userInfo = this.$store.state.userInfo.itemMap[this.userId];
|
||||||
|
return userInfo ? userInfo.name : 'Someone';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
userSvc.getInfo(this.userId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -23,7 +23,9 @@ export default class PreviewClassApplier {
|
|||||||
this.lastEltCount = this.eltCollection.length;
|
this.lastEltCount = this.eltCollection.length;
|
||||||
|
|
||||||
this.restoreClass = () => {
|
this.restoreClass = () => {
|
||||||
if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
|
if (!editorSvc.sectionDescWithDiffsList) {
|
||||||
|
this.removeClass();
|
||||||
|
} else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
|
||||||
this.removeClass();
|
this.removeClass();
|
||||||
this.applyClass();
|
this.applyClass();
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,9 @@ textarea {
|
|||||||
height: auto;
|
height: auto;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
text-transform: uppercase;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -212,6 +212,12 @@ textarea {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gutter__background {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.new-discussion-button {
|
.new-discussion-button {
|
||||||
color: rgba(0, 0, 0, 0.33);
|
color: rgba(0, 0, 0, 0.33);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -230,13 +236,13 @@ textarea {
|
|||||||
|
|
||||||
.discussion-editor-highlighting,
|
.discussion-editor-highlighting,
|
||||||
.discussion-preview-highlighting {
|
.discussion-preview-highlighting {
|
||||||
background-color: mix(#fff, $selection-highlighting-color, 50%);
|
background-color: mix(#fff, $selection-highlighting-color, 70%);
|
||||||
padding: 0.25em 0;
|
padding: 0.25em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discussion-editor-highlighting--hover,
|
.discussion-editor-highlighting--hover,
|
||||||
.discussion-preview-highlighting--hover {
|
.discussion-preview-highlighting--hover {
|
||||||
background-color: mix(#fff, $selection-highlighting-color, 25%);
|
background-color: mix(#fff, $selection-highlighting-color, 50%);
|
||||||
|
|
||||||
* {
|
* {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -245,7 +251,7 @@ textarea {
|
|||||||
|
|
||||||
.discussion-editor-highlighting--selected,
|
.discussion-editor-highlighting--selected,
|
||||||
.discussion-preview-highlighting--selected {
|
.discussion-preview-highlighting--selected {
|
||||||
background-color: mix(#fff, $selection-highlighting-color, 10%);
|
background-color: mix(#fff, $selection-highlighting-color, 20%);
|
||||||
|
|
||||||
* {
|
* {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -256,10 +262,6 @@ textarea {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discussion-preview-highlighting--selected {
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden-rendering-container {
|
.hidden-rendering-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
|
79
src/components/gutters/Comment.vue
Normal file
79
src/components/gutters/Comment.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment">
|
||||||
|
<div class="comment__header flex flex--row flex--space-between flex--align-center">
|
||||||
|
<div class="comment__user flex flex--row flex--align-center">
|
||||||
|
<div class="comment__user-image">
|
||||||
|
<user-image :user-id="comment.sub"></user-image>
|
||||||
|
</div>
|
||||||
|
<button class="comment__remove-button button" v-title="'Remove comment'" @click="removeComment">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
<user-name :user-id="comment.sub"></user-name>
|
||||||
|
</div>
|
||||||
|
<div class="comment__created">{{comment.created | formatTime}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="comment__text">
|
||||||
|
<div class="comment__text-inner" v-html="text"></div>
|
||||||
|
</div>
|
||||||
|
<div class="comment__buttons flex flex--row flex--end" v-if="showReply">
|
||||||
|
<button class="comment__button button" @click="setIsCommenting(true)">Reply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapMutations } from 'vuex';
|
||||||
|
import UserImage from '../UserImage';
|
||||||
|
import UserName from '../UserName';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
import htmlSanitizer from '../../libs/htmlSanitizer';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
UserImage,
|
||||||
|
UserName,
|
||||||
|
},
|
||||||
|
props: ['comment'],
|
||||||
|
computed: {
|
||||||
|
showReply() {
|
||||||
|
return this.comment === this.$store.getters['discussion/currentDiscussionLastComment'] &&
|
||||||
|
!this.$store.state.discussion.isCommenting;
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
return htmlSanitizer.sanitizeHtml(editorSvc.converter.render(this.comment.text));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations('discussion', [
|
||||||
|
'setIsCommenting',
|
||||||
|
]),
|
||||||
|
removeComment() {
|
||||||
|
this.$store.dispatch('modal/commentDeletion')
|
||||||
|
.then(
|
||||||
|
() => this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment }),
|
||||||
|
() => {}); // Cancel
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const isSticky = this.$el.parentNode.classList.contains('sticky-comment');
|
||||||
|
if (isSticky) {
|
||||||
|
const commentId = this.$store.getters['discussion/currentDiscussionLastCommentId'];
|
||||||
|
const scrollerElt = this.$el.querySelector('.comment__text-inner');
|
||||||
|
|
||||||
|
let scrollerMirrorElt;
|
||||||
|
const getScrollerMirrorElt = () => {
|
||||||
|
if (!scrollerMirrorElt) {
|
||||||
|
scrollerMirrorElt = document.querySelector(
|
||||||
|
`.comment-list .comment--${commentId} .comment__text-inner`);
|
||||||
|
}
|
||||||
|
return scrollerMirrorElt || { scrollTop: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollerElt.scrollTop = getScrollerMirrorElt().scrollTop;
|
||||||
|
scrollerElt.addEventListener('scroll', () => {
|
||||||
|
getScrollerMirrorElt().scrollTop = scrollerElt.scrollTop;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
360
src/components/gutters/CommentList.vue
Normal file
360
src/components/gutters/CommentList.vue
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment-list" :class="stickyComment && 'comment-list--' + stickyComment" :style="{width: constants.gutterWidth + 'px'}">
|
||||||
|
<comment v-for="(comment, discussionId) in currentFileDiscussionLastComments" :key="discussionId" v-if="comment.discussionId !== currentDiscussionId" :comment="comment" class="comment--last" :class="'comment--discussion-' + discussionId" :style="{top: tops[discussionId] + 'px'}" @click.native="setCurrentDiscussionId(discussionId)"></comment>
|
||||||
|
<div class="comment-list__current-discussion" :style="{top: tops.current + 'px'}">
|
||||||
|
<comment v-for="(comment, id) in currentDiscussionComments" :key="id" :comment="comment" :class="'comment--' + id"></comment>
|
||||||
|
<new-comment v-if="isCommenting"></new-comment>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||||
|
import Comment from './Comment';
|
||||||
|
import NewComment from './NewComment';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Comment,
|
||||||
|
NewComment,
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
tops: {},
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('layout', [
|
||||||
|
'constants',
|
||||||
|
'styles',
|
||||||
|
]),
|
||||||
|
...mapState('discussion', [
|
||||||
|
'currentDiscussionId',
|
||||||
|
'isCommenting',
|
||||||
|
'newCommentText',
|
||||||
|
'stickyComment',
|
||||||
|
]),
|
||||||
|
...mapGetters('discussion', [
|
||||||
|
'newDiscussion',
|
||||||
|
'currentDiscussion',
|
||||||
|
'currentFileDiscussions',
|
||||||
|
'currentFileDiscussionLastComments',
|
||||||
|
'currentDiscussionComments',
|
||||||
|
'currentDiscussionLastCommentId',
|
||||||
|
]),
|
||||||
|
updateTopsTrigger() {
|
||||||
|
return utils.serializeObject([
|
||||||
|
this.styles,
|
||||||
|
this.currentFileDiscussionLastComments,
|
||||||
|
this.currentDiscussionComments,
|
||||||
|
this.currentDiscussionId,
|
||||||
|
this.isCommenting,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
updateStickyTrigger() {
|
||||||
|
return utils.serializeObject([
|
||||||
|
this.updateTopsTrigger,
|
||||||
|
this.newCommentText,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations('discussion', [
|
||||||
|
'setCurrentDiscussionId',
|
||||||
|
]),
|
||||||
|
updateTops() {
|
||||||
|
const localSettings = this.$store.getters['data/localSettings'];
|
||||||
|
const minTop = -2;
|
||||||
|
let minCommentTop = minTop;
|
||||||
|
const getTop = (discussion, commentElt1, commentElt2, isCurrent) => {
|
||||||
|
const firstElt = commentElt1 || commentElt2;
|
||||||
|
const secondElt = commentElt1 && commentElt2;
|
||||||
|
const coordinates = localSettings.showEditor
|
||||||
|
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
||||||
|
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
||||||
|
let commentTop = minTop;
|
||||||
|
if (coordinates) {
|
||||||
|
commentTop = (coordinates.top + coordinates.height) - 80;
|
||||||
|
}
|
||||||
|
let top = commentTop;
|
||||||
|
if (isCurrent) {
|
||||||
|
top -= firstElt.offsetTop + 2; // 2 for top border
|
||||||
|
}
|
||||||
|
if (top < minTop) {
|
||||||
|
commentTop += minTop - top;
|
||||||
|
top = minTop;
|
||||||
|
}
|
||||||
|
if (commentTop < minCommentTop) {
|
||||||
|
top += minCommentTop - commentTop;
|
||||||
|
commentTop = minCommentTop;
|
||||||
|
}
|
||||||
|
minCommentTop = commentTop + firstElt.offsetHeight + 60;
|
||||||
|
if (secondElt) {
|
||||||
|
minCommentTop += secondElt.offsetHeight;
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the discussion top coordinates
|
||||||
|
const tops = {};
|
||||||
|
const discussions = this.currentFileDiscussions;
|
||||||
|
Object.keys(discussions)
|
||||||
|
.sort((id1, id2) => discussions[id1].end - discussions[id2].end)
|
||||||
|
.forEach((discussionId) => {
|
||||||
|
const discussion = this.currentFileDiscussions[discussionId];
|
||||||
|
if (discussion === this.currentDiscussion || discussion === this.newDiscussion) {
|
||||||
|
tops.current = getTop(
|
||||||
|
discussion,
|
||||||
|
this.currentDiscussionLastCommentId
|
||||||
|
&& this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`),
|
||||||
|
this.$el.querySelector('.comment--new'),
|
||||||
|
true);
|
||||||
|
} else {
|
||||||
|
tops[discussionId] = getTop(discussion,
|
||||||
|
this.$el.querySelector(`.comment--discussion-${discussionId}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.tops = tops;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$watch(
|
||||||
|
() => this.updateTopsTrigger,
|
||||||
|
() => this.updateTops(),
|
||||||
|
{ immediate: true });
|
||||||
|
|
||||||
|
const localSettings = this.$store.getters['data/localSettings'];
|
||||||
|
this.scrollerElt = localSettings.showEditor
|
||||||
|
? editorSvc.editorElt.parentNode
|
||||||
|
: editorSvc.previewElt.parentNode;
|
||||||
|
|
||||||
|
this.updateSticky = () => {
|
||||||
|
const commitIfDifferent = (value) => {
|
||||||
|
if (this.$store.state.discussion.stickyComment !== value) {
|
||||||
|
this.$store.commit('discussion/setStickyComment', value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let height = 0;
|
||||||
|
let offsetTop = this.tops.current;
|
||||||
|
const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`);
|
||||||
|
if (lastCommentElt) {
|
||||||
|
height += lastCommentElt.clientHeight;
|
||||||
|
offsetTop += lastCommentElt.offsetTop;
|
||||||
|
}
|
||||||
|
const newCommentElt = this.$el.querySelector('.comment--new');
|
||||||
|
if (newCommentElt) {
|
||||||
|
height += newCommentElt.clientHeight;
|
||||||
|
}
|
||||||
|
const currentDiscussionElt = document.querySelector('.current-discussion__inner');
|
||||||
|
const minOffsetTop = this.scrollerElt.scrollTop + 10;
|
||||||
|
const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height
|
||||||
|
- currentDiscussionElt.clientHeight - 10;
|
||||||
|
if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) {
|
||||||
|
commitIfDifferent('bottom');
|
||||||
|
} else if (offsetTop < minOffsetTop) {
|
||||||
|
commitIfDifferent('top');
|
||||||
|
} else {
|
||||||
|
commitIfDifferent(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.scrollerElt.addEventListener('scroll', this.updateSticky);
|
||||||
|
this.$watch(
|
||||||
|
() => this.updateStickyTrigger,
|
||||||
|
() => this.updateSticky(),
|
||||||
|
{ immediate: true });
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.scrollerElt.removeEventListener('scroll', this.updateSticky);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.comment-list {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment--last,
|
||||||
|
.comment-list__current-discussion {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment--last {
|
||||||
|
opacity: 0.33;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
* {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.comment--hover {
|
||||||
|
opacity: 0.67;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-list__current-discussion {
|
||||||
|
border-top: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
|
||||||
|
.comment-list--top & {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-list--bottom & {
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
padding: 5px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__header {
|
||||||
|
font-size: 0.75em;
|
||||||
|
padding-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__user-image {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
.comment:hover & {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.sticky-comment & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment--new:hover &,
|
||||||
|
.comment--last:hover & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__remove-button {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
padding: 1px;
|
||||||
|
color: rgba(0, 0, 0, 0.33);
|
||||||
|
margin-right: 5px;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment:hover & {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.sticky-comment & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment--last:hover & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__created {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__buttons {
|
||||||
|
padding: 10px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__button {
|
||||||
|
padding: 0 8px;
|
||||||
|
line-height: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__text {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
right: 0;
|
||||||
|
border-top: 8px solid #fff;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
dl {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-variant-ligatures: no-common-ligatures;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
caret-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__text-inner {
|
||||||
|
min-height: 37px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1px 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
|
.markdown-highlighting {
|
||||||
|
padding: 5px 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
165
src/components/gutters/CurrentDiscussion.vue
Normal file
165
src/components/gutters/CurrentDiscussion.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="current-discussion" :style="{width: constants.gutterWidth + 'px'}">
|
||||||
|
<sticky-comment v-if="stickyComment === 'bottom'"></sticky-comment>
|
||||||
|
<div class="current-discussion__inner">
|
||||||
|
<div class="flex flex--row flex--space-between">
|
||||||
|
<div class="current-discussion__buttons flex flex--row flex--end">
|
||||||
|
<button class="current-discussion__button button" v-if="showNext" @click="goToDiscussion(previousDiscussionId)" v-title="'Previous discussion'">
|
||||||
|
<icon-arrow-left></icon-arrow-left>
|
||||||
|
</button>
|
||||||
|
<button class="current-discussion__button current-discussion__button--rotate button" v-if="showNext" @click="goToDiscussion(nextDiscussionId)" v-title="'Next discussion'">
|
||||||
|
<icon-arrow-left></icon-arrow-left>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="current-discussion__buttons flex flex--row flex--end">
|
||||||
|
<button class="current-discussion__button current-discussion__button--remove button" v-if="showRemove" @click="removeDiscussion" v-title="'Remove discussion'">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
<button class="current-discussion__button button" @click="setCurrentDiscussionId()" v-title="'Close discussion'">
|
||||||
|
<icon-close></icon-close>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="current-discussion__text markdown-highlighting markdown-highlighting--inline">
|
||||||
|
<span @click="goToDiscussion()" v-html="text"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
import animationSvc from '../../services/animationSvc';
|
||||||
|
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||||
|
import StickyComment from './StickyComment';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StickyComment,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState('discussion', [
|
||||||
|
'stickyComment',
|
||||||
|
'currentDiscussionId',
|
||||||
|
]),
|
||||||
|
...mapGetters('discussion', [
|
||||||
|
'currentDiscussion',
|
||||||
|
'previousDiscussionId',
|
||||||
|
'nextDiscussionId',
|
||||||
|
'currentFileDiscussions',
|
||||||
|
]),
|
||||||
|
...mapGetters('layout', [
|
||||||
|
'constants',
|
||||||
|
]),
|
||||||
|
text() {
|
||||||
|
return markdownConversionSvc.highlight(this.currentDiscussion.text);
|
||||||
|
},
|
||||||
|
showNext() {
|
||||||
|
return this.nextDiscussionId && this.nextDiscussionId !== this.currentDiscussionId;
|
||||||
|
},
|
||||||
|
showRemove() {
|
||||||
|
return this.currentFileDiscussions[this.currentDiscussionId];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations('discussion', [
|
||||||
|
'setCurrentDiscussionId',
|
||||||
|
]),
|
||||||
|
goToDiscussion(discussionId = this.currentDiscussionId) {
|
||||||
|
this.setCurrentDiscussionId(discussionId);
|
||||||
|
const localSettings = this.$store.getters['data/localSettings'];
|
||||||
|
const discussion = this.currentFileDiscussions[discussionId];
|
||||||
|
const coordinates = localSettings.showEditor
|
||||||
|
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
||||||
|
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
||||||
|
if (!coordinates) {
|
||||||
|
this.$store.dispatch('notification/info', "Discussion can't be located in the file.");
|
||||||
|
} else {
|
||||||
|
const scrollerElt = localSettings.showEditor
|
||||||
|
? editorSvc.editorElt.parentNode
|
||||||
|
: editorSvc.previewElt.parentNode;
|
||||||
|
let scrollTop = coordinates.top - (scrollerElt.offsetHeight / 2);
|
||||||
|
const maxScrollTop = scrollerElt.scrollHeight - scrollerElt.offsetHeight;
|
||||||
|
if (scrollTop < 0) {
|
||||||
|
scrollTop = 0;
|
||||||
|
} else if (scrollTop > maxScrollTop) {
|
||||||
|
scrollTop = maxScrollTop;
|
||||||
|
}
|
||||||
|
animationSvc.animate(scrollerElt)
|
||||||
|
.scrollTop(scrollTop)
|
||||||
|
.duration(200)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeDiscussion() {
|
||||||
|
this.$store.dispatch('modal/discussionDeletion')
|
||||||
|
.then(
|
||||||
|
() => this.$store.dispatch('discussion/cleanCurrentFile', {
|
||||||
|
filterDiscussion: this.currentDiscussion,
|
||||||
|
}),
|
||||||
|
() => {}); // Cancel
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.current-discussion {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-top: 2px solid;
|
||||||
|
|
||||||
|
.sticky-comment {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-discussion__inner {
|
||||||
|
position: relative;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: $info-bg;
|
||||||
|
max-height: 130px; /* 3 lines max */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-discussion__buttons {
|
||||||
|
padding: 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-discussion__button {
|
||||||
|
width: 30px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 2px;
|
||||||
|
flex: none;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-discussion__button--remove {
|
||||||
|
/* Make the trash a bit smaller */
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-discussion__button--rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-discussion__text {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 0.2em 0;
|
||||||
|
background-color: mix(#fff, $selection-highlighting-color, 10%);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,24 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="gutter gutter--new-discussion-button" :style="{left: styles.editorGutterLeft + 'px'}">
|
|
||||||
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'Start a discussion'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'Start a discussion'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
||||||
<icon-message></icon-message>
|
<icon-message></icon-message>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
coordinates: null,
|
coordinates: null,
|
||||||
}),
|
}),
|
||||||
computed: {
|
|
||||||
...mapGetters('layout', [
|
|
||||||
'styles',
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('discussion', [
|
...mapActions('discussion', [
|
||||||
'createNewDiscussion',
|
'createNewDiscussion',
|
139
src/components/gutters/NewComment.vue
Normal file
139
src/components/gutters/NewComment.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment comment--new" @keyup.esc="setIsCommenting(false)">
|
||||||
|
<div class="comment__header flex flex--row flex--space-between flex--align-center">
|
||||||
|
<div class="comment__user flex flex--row flex--align-center">
|
||||||
|
<div class="comment__user-image">
|
||||||
|
<user-image :user-id="loginToken.sub"></user-image>
|
||||||
|
</div>
|
||||||
|
<span class="user-name">{{loginToken.name}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="comment__text">
|
||||||
|
<div class="comment__text-inner">
|
||||||
|
<pre class="markdown-highlighting"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="comment__buttons flex flex--row flex--end">
|
||||||
|
<button class="comment__button button" @click="setIsCommenting(false)">Cancel</button>
|
||||||
|
<button class="comment__button button" @click="addComment">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex';
|
||||||
|
import Prism from 'prismjs';
|
||||||
|
import UserImage from '../UserImage';
|
||||||
|
import cledit from '../../libs/cledit';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
UserImage,
|
||||||
|
},
|
||||||
|
computed: mapGetters('data', [
|
||||||
|
'loginToken',
|
||||||
|
]),
|
||||||
|
methods: {
|
||||||
|
...mapMutations('discussion', [
|
||||||
|
'setIsCommenting',
|
||||||
|
]),
|
||||||
|
addComment() {
|
||||||
|
const text = this.$store.state.discussion.newCommentText.trim();
|
||||||
|
if (text.length) {
|
||||||
|
if (text.length > 2000) {
|
||||||
|
this.$store.dispatch('notification/error', 'Comment is too long.');
|
||||||
|
} else {
|
||||||
|
// Create comment
|
||||||
|
const discussionId = this.$store.state.discussion.currentDiscussionId;
|
||||||
|
const comment = {
|
||||||
|
discussionId,
|
||||||
|
sub: this.loginToken.sub,
|
||||||
|
text,
|
||||||
|
created: Date.now(),
|
||||||
|
};
|
||||||
|
const patch = {
|
||||||
|
comments: {
|
||||||
|
...this.$store.getters['content/current'].comments,
|
||||||
|
[utils.uid()]: comment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Create discussion
|
||||||
|
if (discussionId === this.$store.state.discussion.newDiscussionId) {
|
||||||
|
patch.discussions = {
|
||||||
|
...this.$store.getters['content/current'].discussions,
|
||||||
|
[discussionId]: this.$store.getters['discussion/newDiscussion'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.$store.dispatch('content/patchCurrent', patch);
|
||||||
|
this.$store.commit('discussion/setNewCommentText');
|
||||||
|
this.$store.commit('discussion/setIsCommenting');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const preElt = this.$el.querySelector('pre.markdown-highlighting');
|
||||||
|
const scrollerElt = this.$el.querySelector('.comment__text-inner');
|
||||||
|
const clEditor = cledit(preElt, scrollerElt);
|
||||||
|
clEditor.init({
|
||||||
|
sectionHighlighter: section => Prism.highlight(
|
||||||
|
section.text, editorSvc.prismGrammars[section.data]),
|
||||||
|
sectionParser: text => markdownConversionSvc.parseSections(
|
||||||
|
editorSvc.converter, text).sections,
|
||||||
|
content: this.$store.state.discussion.newCommentText,
|
||||||
|
selectionStart: this.$store.state.discussion.newCommentSelection.start,
|
||||||
|
selectionEnd: this.$store.state.discussion.newCommentSelection.end,
|
||||||
|
getCursorFocusRatio: () => 0.2,
|
||||||
|
});
|
||||||
|
this.$nextTick(() => clEditor.focus());
|
||||||
|
|
||||||
|
// Save typed content and selection
|
||||||
|
clEditor.on('contentChanged', value =>
|
||||||
|
this.$store.commit('discussion/setNewCommentText', value));
|
||||||
|
clEditor.selectionMgr.on('selectionChanged', (start, end) =>
|
||||||
|
this.$store.commit('discussion/setNewCommentSelection', {
|
||||||
|
start, end,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const isSticky = this.$el.parentNode.classList.contains('sticky-comment');
|
||||||
|
if (isSticky) {
|
||||||
|
let scrollerMirrorElt;
|
||||||
|
const getScrollerMirrorElt = () => {
|
||||||
|
if (!scrollerMirrorElt) {
|
||||||
|
scrollerMirrorElt = document.querySelector(
|
||||||
|
'.comment-list .comment--new .comment__text-inner');
|
||||||
|
}
|
||||||
|
return scrollerMirrorElt || { scrollTop: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollerElt.scrollTop = getScrollerMirrorElt().scrollTop;
|
||||||
|
scrollerElt.addEventListener('scroll', () => {
|
||||||
|
getScrollerMirrorElt().scrollTop = scrollerElt.scrollTop;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Maintain the state with the sticky comment
|
||||||
|
this.$watch(
|
||||||
|
() => this.$store.state.discussion.stickyComment === null,
|
||||||
|
(isVisible) => {
|
||||||
|
clEditor.toggleEditable(isVisible);
|
||||||
|
if (isVisible) {
|
||||||
|
const text = this.$store.state.discussion.newCommentText;
|
||||||
|
clEditor.setContent(text);
|
||||||
|
const selection = this.$store.state.discussion.newCommentSelection;
|
||||||
|
clEditor.selectionMgr.setSelectionStartEnd(selection.start, selection.end);
|
||||||
|
clEditor.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
this.$watch(
|
||||||
|
() => this.$store.state.discussion.newCommentText,
|
||||||
|
newCommentText => clEditor.setContent(newCommentText),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
49
src/components/gutters/PreviewNewDiscussionButton.vue
Normal file
49
src/components/gutters/PreviewNewDiscussionButton.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'Start a discussion'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
||||||
|
<icon-message></icon-message>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions } from 'vuex';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
coordinates: null,
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
...mapActions('discussion', [
|
||||||
|
'createNewDiscussion',
|
||||||
|
]),
|
||||||
|
checkSelection() {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
let offset;
|
||||||
|
if (editorSvc.previewSelectionRange) {
|
||||||
|
this.selection = editorSvc.getTrimmedSelection();
|
||||||
|
if (this.selection) {
|
||||||
|
const text = editorSvc.previewTextWithDiffsList;
|
||||||
|
offset = editorSvc.getPreviewOffset(this.selection.end);
|
||||||
|
while (offset && text[offset - 1] === '\n') {
|
||||||
|
offset -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.coordinates = offset
|
||||||
|
? editorSvc.getPreviewOffsetCoordinates(offset)
|
||||||
|
: null;
|
||||||
|
}, 25);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
editorSvc.$on('previewSelectionRange', () => this.checkSelection());
|
||||||
|
this.$watch(
|
||||||
|
() => this.$store.getters['layout/styles'].previewWidth,
|
||||||
|
() => this.checkSelection());
|
||||||
|
this.checkSelection();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,71 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="gutter gutter--new-discussion-button" :style="{left: styles.previewGutterLeft + 'px'}">
|
|
||||||
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'Start a discussion'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
|
||||||
<icon-message></icon-message>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
|
||||||
import cledit from '../../libs/cledit';
|
|
||||||
import editorSvc from '../../services/editorSvc';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data: () => ({
|
|
||||||
coordinates: null,
|
|
||||||
}),
|
|
||||||
computed: {
|
|
||||||
...mapGetters('layout', [
|
|
||||||
'styles',
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions('discussion', [
|
|
||||||
'createNewDiscussion',
|
|
||||||
]),
|
|
||||||
checkSelection() {
|
|
||||||
clearTimeout(this.timeout);
|
|
||||||
this.timeout = setTimeout(() => {
|
|
||||||
let offset;
|
|
||||||
if (editorSvc.previewSelectionRange) {
|
|
||||||
this.selection = editorSvc.getTrimmedSelection();
|
|
||||||
if (this.selection) {
|
|
||||||
const text = editorSvc.previewTextWithDiffsList;
|
|
||||||
offset = editorSvc.getPreviewOffset(this.selection.end);
|
|
||||||
while (offset && text[offset - 1] === '\n') {
|
|
||||||
offset -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!offset) {
|
|
||||||
this.coordinates = null;
|
|
||||||
} else {
|
|
||||||
const start = cledit.Utils.findContainer(editorSvc.previewElt, offset - 1);
|
|
||||||
const end = cledit.Utils.findContainer(editorSvc.previewElt, offset);
|
|
||||||
const range = document.createRange();
|
|
||||||
range.setStart(start.container, start.offsetInContainer);
|
|
||||||
range.setEnd(end.container, end.offsetInContainer);
|
|
||||||
const rect = range.getBoundingClientRect();
|
|
||||||
const contentRect = editorSvc.previewElt.getBoundingClientRect();
|
|
||||||
this.coordinates = {
|
|
||||||
top: Math.round((rect.top - contentRect.top) + editorSvc.previewElt.scrollTop),
|
|
||||||
height: Math.round(rect.height),
|
|
||||||
left: Math.round((rect.right - contentRect.left) + editorSvc.previewElt.scrollLeft),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, 25);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
editorSvc.$on('previewSelectionRange', () => this.checkSelection());
|
|
||||||
this.$watch(
|
|
||||||
() => this.$store.getters['layout/styles'].previewWidth,
|
|
||||||
() => this.checkSelection());
|
|
||||||
this.checkSelection();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
45
src/components/gutters/StickyComment.vue
Normal file
45
src/components/gutters/StickyComment.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sticky-comment" :style="{width: constants.gutterWidth + 'px', top: top + 'px'}">
|
||||||
|
<comment v-if="currentDiscussionLastComment" :comment="currentDiscussionLastComment"></comment>
|
||||||
|
<new-comment v-if="isCommenting"></new-comment>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
import Comment from './Comment';
|
||||||
|
import NewComment from './NewComment';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Comment,
|
||||||
|
NewComment,
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
top: 0,
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('layout', [
|
||||||
|
'constants',
|
||||||
|
]),
|
||||||
|
...mapState('discussion', [
|
||||||
|
'isCommenting',
|
||||||
|
]),
|
||||||
|
...mapGetters('discussion', [
|
||||||
|
'currentDiscussionLastComment',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.sticky-comment {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
}
|
||||||
|
</style>
|
@ -16,6 +16,8 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
font-size: 17px;
|
||||||
|
text-transform: none;
|
||||||
|
|
||||||
div div {
|
div div {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -76,12 +76,14 @@ export default {
|
|||||||
font-size: 2.3em;
|
font-size: 2.3em;
|
||||||
margin: 0.75rem 0;
|
margin: 0.75rem 0;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
text-transform: none;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paypal-option__offer {
|
.paypal-option__offer {
|
||||||
|
@ -12,16 +12,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-entry__actions flex flex--row flex--end">
|
<div class="form-entry__actions flex flex--row flex--end">
|
||||||
<button class="form-entry__button button" @click="create">
|
<button class="form-entry__button button" @click="create" v-title="'New template'">
|
||||||
<icon-file-plus></icon-file-plus>
|
<icon-file-plus></icon-file-plus>
|
||||||
</button>
|
</button>
|
||||||
<button class="form-entry__button button" @click="copy">
|
<button class="form-entry__button button" @click="copy" v-title="'Copy template'">
|
||||||
<icon-file-multiple></icon-file-multiple>
|
<icon-file-multiple></icon-file-multiple>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="!isReadOnly" class="form-entry__button button" @click="isEditing = true">
|
<button v-if="!isReadOnly" class="form-entry__button button" @click="isEditing = true" v-title="'Rename template'">
|
||||||
<icon-pen></icon-pen>
|
<icon-pen></icon-pen>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="!isReadOnly" class="form-entry__button button" @click="remove">
|
<button v-if="!isReadOnly" class="form-entry__button button" @click="remove" v-title="'Remove template'">
|
||||||
<icon-delete></icon-delete>
|
<icon-delete></icon-delete>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,17 +24,22 @@ export default {
|
|||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
showSponsorButton() {
|
showSponsorButton() {
|
||||||
return !this.$store.getters.isSponsor && this.$store.getters['modal/config'].type !== 'sponsor';
|
const type = this.$store.getters['modal/config'].type;
|
||||||
|
return !this.$store.getters.isSponsor && type !== 'sponsor' && type !== 'signInForSponsorship';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sponsor() {
|
sponsor() {
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => !this.$store.getters['data/loginToken'] &&
|
.then(() => !this.$store.getters['data/loginToken'] &&
|
||||||
this.$store.dispatch('modal/signInRequired') // If user has to sign in
|
this.$store.dispatch('modal/signInForSponsorship') // If user has to sign in
|
||||||
.then(() => googleHelper.signin())
|
.then(() => googleHelper.signin())
|
||||||
.then(() => syncSvc.requestSync()))
|
.then(() => syncSvc.requestSync()))
|
||||||
.then(() => this.$store.dispatch('modal/open', 'sponsor'))
|
.then(() => {
|
||||||
|
if (!this.$store.getters.isSponsor) {
|
||||||
|
this.$store.dispatch('modal/open', 'sponsor');
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => { }); // Cancel
|
.catch(() => { }); // Cancel
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -65,7 +70,7 @@ export default {
|
|||||||
color: darken($error-color, 10%);
|
color: darken($error-color, 10%);
|
||||||
background-color: transparentize($error-color, 0.85);
|
background-color: transparentize($error-color, 0.85);
|
||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
padding: 1em;
|
padding: 1em 1.5em;
|
||||||
margin-bottom: 1.2em;
|
margin-bottom: 1.2em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -168,8 +168,10 @@ function Highlighter(editor) {
|
|||||||
}
|
}
|
||||||
this.addTrailingNode()
|
this.addTrailingNode()
|
||||||
self.$trigger('highlighted')
|
self.$trigger('highlighted')
|
||||||
|
if (editor.selectionMgr.hasFocus()) {
|
||||||
editor.selectionMgr.restoreSelection()
|
editor.selectionMgr.restoreSelection()
|
||||||
editor.selectionMgr.updateCursorCoordinates()
|
editor.selectionMgr.updateCursorCoordinates()
|
||||||
|
}
|
||||||
}.cl_bind(this))
|
}.cl_bind(this))
|
||||||
|
|
||||||
return sectionList
|
return sectionList
|
||||||
|
@ -11,6 +11,7 @@ import sectionUtils from './sectionUtils';
|
|||||||
import extensionSvc from './extensionSvc';
|
import extensionSvc from './extensionSvc';
|
||||||
import editorSvcDiscussions from './editorSvcDiscussions';
|
import editorSvcDiscussions from './editorSvcDiscussions';
|
||||||
import editorSvcUtils from './editorSvcUtils';
|
import editorSvcUtils from './editorSvcUtils';
|
||||||
|
import utils from './utils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
|
||||||
const debounce = cledit.Utils.debounce;
|
const debounce = cledit.Utils.debounce;
|
||||||
@ -77,6 +78,8 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
||||||
*/
|
*/
|
||||||
initClEditor() {
|
initClEditor() {
|
||||||
|
this.sectionDescMeasuredList = null;
|
||||||
|
this.sectionDescWithDiffsList = null;
|
||||||
const options = {
|
const options = {
|
||||||
sectionHighlighter: section => Prism.highlight(
|
sectionHighlighter: section => Prism.highlight(
|
||||||
section.text, this.prismGrammars[section.data]),
|
section.text, this.prismGrammars[section.data]),
|
||||||
@ -509,7 +512,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
if (content.properties !== lastProperties) {
|
if (content.properties !== lastProperties) {
|
||||||
lastProperties = content.properties;
|
lastProperties = content.properties;
|
||||||
const options = extensionSvc.getOptions(store.getters['content/currentProperties']);
|
const options = extensionSvc.getOptions(store.getters['content/currentProperties']);
|
||||||
if (JSON.stringify(options) !== JSON.stringify(this.options)) {
|
if (utils.serializeObject(options) !== utils.serializeObject(this.options)) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.initPrism();
|
this.initPrism();
|
||||||
this.initConverter();
|
this.initConverter();
|
||||||
@ -532,7 +535,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
store.watch(() => store.getters['layout/styles'],
|
store.watch(() => utils.serializeObject(store.getters['layout/styles']),
|
||||||
() => this.measureSectionDimensions(false, true));
|
() => this.measureSectionDimensions(false, true));
|
||||||
|
|
||||||
this.initHighlighters();
|
this.initHighlighters();
|
||||||
|
@ -201,7 +201,7 @@ export default {
|
|||||||
Object.keys(discussions).forEach((discussionId) => {
|
Object.keys(discussions).forEach((discussionId) => {
|
||||||
const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
|
const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
|
||||||
() => {
|
() => {
|
||||||
const classes = [`discussion-editor-highlighting-${discussionId}`, 'discussion-editor-highlighting'];
|
const classes = [`discussion-editor-highlighting--${discussionId}`, 'discussion-editor-highlighting'];
|
||||||
if (store.state.discussion.currentDiscussionId === discussionId) {
|
if (store.state.discussion.currentDiscussionId === discussionId) {
|
||||||
classes.push('discussion-editor-highlighting--selected');
|
classes.push('discussion-editor-highlighting--selected');
|
||||||
}
|
}
|
||||||
@ -210,7 +210,7 @@ export default {
|
|||||||
() => {
|
() => {
|
||||||
const startMarker = discussionMarkers[`${discussionId}:start`];
|
const startMarker = discussionMarkers[`${discussionId}:start`];
|
||||||
const endMarker = discussionMarkers[`${discussionId}:end`];
|
const endMarker = discussionMarkers[`${discussionId}:end`];
|
||||||
return {
|
return startMarker && endMarker && {
|
||||||
start: startMarker.offset,
|
start: startMarker.offset,
|
||||||
end: endMarker.offset,
|
end: endMarker.offset,
|
||||||
};
|
};
|
||||||
@ -229,7 +229,7 @@ export default {
|
|||||||
Object.keys(discussions).forEach((discussionId) => {
|
Object.keys(discussions).forEach((discussionId) => {
|
||||||
const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
|
const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
|
||||||
() => {
|
() => {
|
||||||
const classes = [`discussion-preview-highlighting-${discussionId}`, 'discussion-preview-highlighting'];
|
const classes = [`discussion-preview-highlighting--${discussionId}`, 'discussion-preview-highlighting'];
|
||||||
if (store.state.discussion.currentDiscussionId === discussionId) {
|
if (store.state.discussion.currentDiscussionId === discussionId) {
|
||||||
classes.push('discussion-preview-highlighting--selected');
|
classes.push('discussion-preview-highlighting--selected');
|
||||||
}
|
}
|
||||||
@ -238,7 +238,7 @@ export default {
|
|||||||
() => {
|
() => {
|
||||||
const startMarker = discussionMarkers[`${discussionId}:start`];
|
const startMarker = discussionMarkers[`${discussionId}:start`];
|
||||||
const endMarker = discussionMarkers[`${discussionId}:end`];
|
const endMarker = discussionMarkers[`${discussionId}:end`];
|
||||||
return {
|
return startMarker && endMarker && {
|
||||||
start: this.getPreviewOffset(startMarker.offset),
|
start: this.getPreviewOffset(startMarker.offset),
|
||||||
end: this.getPreviewOffset(endMarker.offset),
|
end: this.getPreviewOffset(endMarker.offset),
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import DiffMatchPatch from 'diff-match-patch';
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
|
import cledit from '../libs/cledit';
|
||||||
import animationSvc from './animationSvc';
|
import animationSvc from './animationSvc';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
|
||||||
@ -105,6 +106,24 @@ export default {
|
|||||||
return editorOffset;
|
return editorOffset;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the coordinates of an offset in the preview
|
||||||
|
*/
|
||||||
|
getPreviewOffsetCoordinates(offset) {
|
||||||
|
const start = cledit.Utils.findContainer(this.previewElt, offset && offset - 1);
|
||||||
|
const end = cledit.Utils.findContainer(this.previewElt, offset || offset + 1);
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(start.container, start.offsetInContainer);
|
||||||
|
range.setEnd(end.container, end.offsetInContainer);
|
||||||
|
const rect = range.getBoundingClientRect();
|
||||||
|
const contentRect = this.previewElt.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
top: Math.round((rect.top - contentRect.top) + this.previewElt.scrollTop),
|
||||||
|
height: Math.round(rect.height),
|
||||||
|
left: Math.round((rect.right - contentRect.left) + this.previewElt.scrollLeft),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll the preview (or the editor if preview is hidden) to the specified anchor
|
* Scroll the preview (or the editor if preview is hidden) to the specified anchor
|
||||||
*/
|
*/
|
||||||
|
@ -380,6 +380,7 @@ localDbSvc.sync()
|
|||||||
// Wait for the next watch tick
|
// Wait for the next watch tick
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
// Load contentState from DB
|
// Load contentState from DB
|
||||||
.then(() => localDbSvc.loadContentState(currentFile.id))
|
.then(() => localDbSvc.loadContentState(currentFile.id))
|
||||||
@ -388,8 +389,15 @@ localDbSvc.sync()
|
|||||||
// Load content from DB
|
// Load content from DB
|
||||||
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
||||||
.then(
|
.then(
|
||||||
// Success, set last opened file
|
() => {
|
||||||
() => store.dispatch('data/setLastOpenedId', currentFile.id),
|
// Set last opened file
|
||||||
|
store.dispatch('data/setLastOpenedId', currentFile.id);
|
||||||
|
// Cancel new discussion
|
||||||
|
store.commit('discussion/setCurrentDiscussionId');
|
||||||
|
// Open the gutter if file contains discussions
|
||||||
|
store.commit('discussion/setCurrentDiscussionId',
|
||||||
|
store.getters['discussion/nextDiscussionId']);
|
||||||
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
// Failure (content is not available), go back to previous file
|
// Failure (content is not available), go back to previous file
|
||||||
const lastOpenedFile = store.getters['file/lastOpened'];
|
const lastOpenedFile = store.getters['file/lastOpened'];
|
||||||
|
@ -88,6 +88,7 @@ export default {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
const data = utils.parseQueryParams(`${event.data}`.slice(1));
|
const data = utils.parseQueryParams(`${event.data}`.slice(1));
|
||||||
if (data.error || data.state !== state) {
|
if (data.error || data.state !== state) {
|
||||||
|
console.error(data); // eslint-disable-line no-console
|
||||||
reject('Could not get required authorization.');
|
reject('Could not get required authorization.');
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
|
@ -166,10 +166,10 @@ editorSvc.$on('previewText', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
store.watch(
|
store.watch(
|
||||||
() => store.getters['layout/styles'],
|
() => store.getters['layout/styles'].showEditor,
|
||||||
(styles) => {
|
(showEditor) => {
|
||||||
isScrollEditor = styles.showEditor;
|
isScrollEditor = showEditor;
|
||||||
isScrollPreview = !styles.showEditor;
|
isScrollPreview = !showEditor;
|
||||||
skipAnimation = true;
|
skipAnimation = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -116,6 +116,7 @@ export default {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
store.commit('userInfo/addItem', {
|
store.commit('userInfo/addItem', {
|
||||||
id: res.body.id,
|
id: res.body.id,
|
||||||
|
name: res.body.displayName,
|
||||||
imageUrl: res.body.image.url,
|
imageUrl: res.body.image.url,
|
||||||
});
|
});
|
||||||
return res.body;
|
return res.body;
|
||||||
|
@ -155,11 +155,19 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isWelcomeFile = () => {
|
const isWelcomeFile = () => {
|
||||||
|
if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) {
|
||||||
|
// If file has already been synced, keep on syncing
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const file = getFile();
|
const file = getFile();
|
||||||
const content = getContent();
|
const content = getContent();
|
||||||
|
if (!file || !content) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
||||||
const hash = content ? utils.hash(content.text) : 0;
|
const hash = utils.hash(content.text);
|
||||||
return file.name === 'Welcome file' && welcomeFileHashes[hash];
|
const hasDiscussions = Object.keys(content.discussions).length;
|
||||||
|
return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncOneContentLocation = () => {
|
const syncOneContentLocation = () => {
|
||||||
@ -180,8 +188,8 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
// Skip welcome file if not synchronized explicitly
|
// Skip welcome file if not synchronized explicitly
|
||||||
(syncLocations.length > 1 || !isWelcomeFile())
|
(syncLocations.length > 1 || !isWelcomeFile())
|
||||||
) {
|
) {
|
||||||
const token = provider.getToken(syncLocation);
|
const token = provider && provider.getToken(syncLocation);
|
||||||
result = provider && token && store.dispatch('queue/doWithLocation', {
|
result = token && store.dispatch('queue/doWithLocation', {
|
||||||
location: syncLocation,
|
location: syncLocation,
|
||||||
promise: provider.downloadContent(token, syncLocation)
|
promise: provider.downloadContent(token, syncLocation)
|
||||||
.then((serverContent = null) => {
|
.then((serverContent = null) => {
|
||||||
|
173
src/services/timeSvc.js
Normal file
173
src/services/timeSvc.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
// Credit: https://github.com/github/time-elements/
|
||||||
|
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
|
||||||
|
const pad = num => `0${num}`.slice(-2);
|
||||||
|
|
||||||
|
function strftime(time, formatString) {
|
||||||
|
const day = time.getDay();
|
||||||
|
const date = time.getDate();
|
||||||
|
const month = time.getMonth();
|
||||||
|
const year = time.getFullYear();
|
||||||
|
const hour = time.getHours();
|
||||||
|
const minute = time.getMinutes();
|
||||||
|
const second = time.getSeconds();
|
||||||
|
return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, (_arg) => {
|
||||||
|
let match;
|
||||||
|
const modifier = _arg[1];
|
||||||
|
switch (modifier) {
|
||||||
|
case '%':
|
||||||
|
default:
|
||||||
|
return '%';
|
||||||
|
case 'a':
|
||||||
|
return weekdays[day].slice(0, 3);
|
||||||
|
case 'A':
|
||||||
|
return weekdays[day];
|
||||||
|
case 'b':
|
||||||
|
return months[month].slice(0, 3);
|
||||||
|
case 'B':
|
||||||
|
return months[month];
|
||||||
|
case 'c':
|
||||||
|
return time.toString();
|
||||||
|
case 'd':
|
||||||
|
return pad(date);
|
||||||
|
case 'e':
|
||||||
|
return date;
|
||||||
|
case 'H':
|
||||||
|
return pad(hour);
|
||||||
|
case 'I':
|
||||||
|
return pad(strftime(time, '%l'));
|
||||||
|
case 'l':
|
||||||
|
return hour === 0 || hour === 12 ? 12 : (hour + 12) % 12;
|
||||||
|
case 'm':
|
||||||
|
return pad(month + 1);
|
||||||
|
case 'M':
|
||||||
|
return pad(minute);
|
||||||
|
case 'p':
|
||||||
|
return hour > 11 ? 'PM' : 'AM';
|
||||||
|
case 'P':
|
||||||
|
return hour > 11 ? 'pm' : 'am';
|
||||||
|
case 'S':
|
||||||
|
return pad(second);
|
||||||
|
case 'w':
|
||||||
|
return day;
|
||||||
|
case 'y':
|
||||||
|
return pad(year % 100);
|
||||||
|
case 'Y':
|
||||||
|
return year;
|
||||||
|
case 'Z':
|
||||||
|
match = time.toString().match(/\((\w+)\)$/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
case 'z':
|
||||||
|
match = time.toString().match(/\w([+-]\d\d\d\d) /);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let dayFirst = null;
|
||||||
|
let yearSeparator = null;
|
||||||
|
|
||||||
|
// Private: Determine if the day should be formatted before the month name in
|
||||||
|
// the user's current locale. For example, `9 Jun` for en-GB and `Jun 9`
|
||||||
|
// for en-US.
|
||||||
|
//
|
||||||
|
// Returns true if the day appears before the month.
|
||||||
|
function isDayFirst() {
|
||||||
|
if (dayFirst !== null) {
|
||||||
|
return dayFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('Intl' in window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { day: 'numeric', month: 'short' };
|
||||||
|
const formatter = new window.Intl.DateTimeFormat(undefined, options);
|
||||||
|
const output = formatter.format(new Date(0));
|
||||||
|
|
||||||
|
dayFirst = !!output.match(/^\d/);
|
||||||
|
return dayFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private: Determine if the year should be separated from the month and day
|
||||||
|
// with a comma. For example, `9 Jun 2014` in en-GB and `Jun 9, 2014` in en-US.
|
||||||
|
//
|
||||||
|
// Returns true if the date needs a separator.
|
||||||
|
function isYearSeparator() {
|
||||||
|
if (yearSeparator !== null) {
|
||||||
|
return yearSeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('Intl' in window)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { day: 'numeric', month: 'short', year: 'numeric' };
|
||||||
|
const formatter = new window.Intl.DateTimeFormat(undefined, options);
|
||||||
|
const output = formatter.format(new Date(0));
|
||||||
|
|
||||||
|
yearSeparator = !!output.match(/\d,/);
|
||||||
|
return yearSeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private: Determine if the date occurs in the same year as today's date.
|
||||||
|
//
|
||||||
|
// date - The Date to test.
|
||||||
|
//
|
||||||
|
// Returns true if it's this year.
|
||||||
|
function isThisYear(date) {
|
||||||
|
const now = new Date();
|
||||||
|
return now.getUTCFullYear() === date.getUTCFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RelativeTime {
|
||||||
|
constructor(date) {
|
||||||
|
this.date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
const ago = this.timeElapsed();
|
||||||
|
return ago || `on ${this.formatDate()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeElapsed() {
|
||||||
|
const ms = new Date().getTime() - this.date.getTime();
|
||||||
|
const sec = Math.round(ms / 1000);
|
||||||
|
const min = Math.round(sec / 60);
|
||||||
|
const hr = Math.round(min / 60);
|
||||||
|
const day = Math.round(hr / 24);
|
||||||
|
if (ms < 0) {
|
||||||
|
return 'just now';
|
||||||
|
} else if (sec < 45) {
|
||||||
|
return 'just now';
|
||||||
|
} else if (sec < 90) {
|
||||||
|
return 'a minute ago';
|
||||||
|
} else if (min < 45) {
|
||||||
|
return `${min} minutes ago`;
|
||||||
|
} else if (min < 90) {
|
||||||
|
return 'an hour ago';
|
||||||
|
} else if (hr < 24) {
|
||||||
|
return `${hr} hours ago`;
|
||||||
|
} else if (hr < 36) {
|
||||||
|
return 'a day ago';
|
||||||
|
} else if (day < 30) {
|
||||||
|
return `${day} days ago`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate() {
|
||||||
|
let format = isDayFirst() ? '%e %b' : '%b %e';
|
||||||
|
if (!isThisYear(this.date)) {
|
||||||
|
format += isYearSeparator() ? ', %Y' : ' %Y';
|
||||||
|
}
|
||||||
|
return strftime(this.date, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
format(time) {
|
||||||
|
return time && new RelativeTime(new Date(time)).toString();
|
||||||
|
},
|
||||||
|
};
|
28
src/services/userSvc.js
Normal file
28
src/services/userSvc.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
const promised = {};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getInfo(userId) {
|
||||||
|
if (!promised[userId]) {
|
||||||
|
// Try to find a token with this sub
|
||||||
|
const token = store.getters['data/googleTokens'][userId];
|
||||||
|
if (token) {
|
||||||
|
store.commit('userInfo/addItem', {
|
||||||
|
id: userId,
|
||||||
|
name: token.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user info from Google
|
||||||
|
if (!store.state.offline) {
|
||||||
|
promised[userId] = true;
|
||||||
|
googleHelper.getUser(userId)
|
||||||
|
.catch(() => {
|
||||||
|
promised[userId] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
@ -64,14 +64,16 @@ module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
|
|||||||
module.actions.toggleStatusBar = localSettingsToggler('showStatusBar');
|
module.actions.toggleStatusBar = localSettingsToggler('showStatusBar');
|
||||||
module.actions.toggleScrollSync = localSettingsToggler('scrollSync');
|
module.actions.toggleScrollSync = localSettingsToggler('scrollSync');
|
||||||
module.actions.toggleFocusMode = localSettingsToggler('focusMode');
|
module.actions.toggleFocusMode = localSettingsToggler('focusMode');
|
||||||
const notEnoughSpace = (rootGetters) => {
|
const notEnoughSpace = (getters) => {
|
||||||
const constants = rootGetters['layout/constants'];
|
const constants = getters['layout/constants'];
|
||||||
|
const showGutter = getters['discussion/currentDiscussion'];
|
||||||
return document.body.clientWidth < constants.editorMinWidth +
|
return document.body.clientWidth < constants.editorMinWidth +
|
||||||
constants.explorerWidth +
|
constants.explorerWidth +
|
||||||
constants.sideBarWidth +
|
constants.sideBarWidth +
|
||||||
constants.buttonBarWidth;
|
constants.buttonBarWidth +
|
||||||
|
(showGutter ? constants.gutterWidth : 0);
|
||||||
};
|
};
|
||||||
module.actions.toggleSideBar = ({ getters, dispatch, rootGetters }, value) => {
|
module.actions.toggleSideBar = ({ commit, getters, dispatch, rootGetters }, value) => {
|
||||||
// Reset side bar
|
// Reset side bar
|
||||||
dispatch('setSideBarPanel');
|
dispatch('setSideBarPanel');
|
||||||
// Close explorer if not enough space
|
// Close explorer if not enough space
|
||||||
@ -83,7 +85,7 @@ module.actions.toggleSideBar = ({ getters, dispatch, rootGetters }, value) => {
|
|||||||
}
|
}
|
||||||
dispatch('patchLocalSettings', patch);
|
dispatch('patchLocalSettings', patch);
|
||||||
};
|
};
|
||||||
module.actions.toggleExplorer = ({ getters, dispatch, rootGetters }, value) => {
|
module.actions.toggleExplorer = ({ commit, getters, dispatch, rootGetters }, value) => {
|
||||||
// Close side bar if not enough space
|
// Close side bar if not enough space
|
||||||
const patch = {
|
const patch = {
|
||||||
showExplorer: value === undefined ? !getters.localSettings.showExplorer : value,
|
showExplorer: value === undefined ? !getters.localSettings.showExplorer : value,
|
||||||
|
@ -1,51 +1,148 @@
|
|||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
|
|
||||||
|
const idShifter = offset => (state, getters) => {
|
||||||
|
const ids = Object.keys(getters.currentFileDiscussions);
|
||||||
|
const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length;
|
||||||
|
return ids[idx % ids.length];
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
currentDiscussionId: null,
|
currentDiscussionId: null,
|
||||||
newDiscussion: null,
|
newDiscussion: null,
|
||||||
newDiscussionId: '',
|
newDiscussionId: null,
|
||||||
|
newCommentText: '',
|
||||||
|
newCommentSelection: { start: 0, end: 0 },
|
||||||
|
isCommenting: false,
|
||||||
|
stickyComment: null,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setCurrentDiscussionId: (state, value) => {
|
setCurrentDiscussionId: (state, value) => {
|
||||||
|
if (state.currentDiscussionId !== value) {
|
||||||
state.currentDiscussionId = value;
|
state.currentDiscussionId = value;
|
||||||
|
state.isCommenting = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setNewDiscussion: (state, value) => {
|
setNewDiscussion: (state, value) => {
|
||||||
state.newDiscussion = value;
|
state.newDiscussion = value;
|
||||||
state.newDiscussionId = utils.uid();
|
state.newDiscussionId = utils.uid();
|
||||||
state.currentDiscussionId = state.newDiscussionId;
|
state.currentDiscussionId = state.newDiscussionId;
|
||||||
|
state.isCommenting = true;
|
||||||
},
|
},
|
||||||
patchNewDiscussion: (state, value) => {
|
patchNewDiscussion: (state, value) => {
|
||||||
Object.assign(state.newDiscussion, value);
|
Object.assign(state.newDiscussion, value);
|
||||||
},
|
},
|
||||||
|
setNewCommentText: (state, value) => {
|
||||||
|
state.newCommentText = value || '';
|
||||||
|
},
|
||||||
|
setNewCommentSelection: (state, value) => {
|
||||||
|
state.newCommentSelection = value;
|
||||||
|
},
|
||||||
|
setIsCommenting: (state, value) => {
|
||||||
|
state.isCommenting = value;
|
||||||
|
if (!value) {
|
||||||
|
state.newDiscussionId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setStickyComment: (state, value) => {
|
||||||
|
state.stickyComment = value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
newDiscussion: state =>
|
newDiscussion: state =>
|
||||||
state.currentDiscussionId === state.newDiscussionId && state.newDiscussion,
|
state.currentDiscussionId === state.newDiscussionId && state.newDiscussion,
|
||||||
|
currentFileDiscussionLastComments: (state, getters, rootState, rootGetters) => {
|
||||||
|
const discussions = rootGetters['content/current'].discussions;
|
||||||
|
const comments = rootGetters['content/current'].comments;
|
||||||
|
const discussionLastComments = {};
|
||||||
|
Object.keys(comments).forEach((commentId) => {
|
||||||
|
const comment = comments[commentId];
|
||||||
|
if (discussions[comment.discussionId]) {
|
||||||
|
const lastComment = discussionLastComments[comment.discussionId];
|
||||||
|
if (!lastComment || lastComment.created < comment.created) {
|
||||||
|
discussionLastComments[comment.discussionId] = comment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return discussionLastComments;
|
||||||
|
},
|
||||||
currentFileDiscussions: (state, getters, rootState, rootGetters) => {
|
currentFileDiscussions: (state, getters, rootState, rootGetters) => {
|
||||||
const currentContent = rootGetters['content/current'];
|
const currentFileDiscussions = {};
|
||||||
const currentDiscussions = {
|
|
||||||
...currentContent.discussions,
|
|
||||||
};
|
|
||||||
const newDiscussion = getters.newDiscussion;
|
const newDiscussion = getters.newDiscussion;
|
||||||
if (newDiscussion) {
|
if (newDiscussion) {
|
||||||
currentDiscussions[state.newDiscussionId] = newDiscussion;
|
currentFileDiscussions[state.newDiscussionId] = newDiscussion;
|
||||||
}
|
}
|
||||||
return currentDiscussions;
|
const discussions = rootGetters['content/current'].discussions;
|
||||||
|
const discussionLastComments = getters.currentFileDiscussionLastComments;
|
||||||
|
Object.keys(discussionLastComments)
|
||||||
|
.sort((id1, id2) =>
|
||||||
|
discussionLastComments[id2].created - discussionLastComments[id1].created)
|
||||||
|
.forEach((discussionId) => {
|
||||||
|
currentFileDiscussions[discussionId] = discussions[discussionId];
|
||||||
|
});
|
||||||
|
return currentFileDiscussions;
|
||||||
},
|
},
|
||||||
currentDiscussion: (state, getters) =>
|
currentDiscussion: (state, getters) =>
|
||||||
getters.currentFileDiscussions[state.currentDiscussionId],
|
getters.currentFileDiscussions[state.currentDiscussionId],
|
||||||
|
previousDiscussionId: idShifter(-1),
|
||||||
|
nextDiscussionId: idShifter(1),
|
||||||
|
currentDiscussionComments: (state, getters, rootState, rootGetters) => {
|
||||||
|
const comments = {};
|
||||||
|
if (getters.currentDiscussion) {
|
||||||
|
const contentComments = rootGetters['content/current'].comments;
|
||||||
|
Object.keys(contentComments)
|
||||||
|
.filter(commentId =>
|
||||||
|
contentComments[commentId].discussionId === state.currentDiscussionId)
|
||||||
|
.sort((id1, id2) =>
|
||||||
|
contentComments[id1].created - contentComments[id2].created)
|
||||||
|
.forEach((commentId) => {
|
||||||
|
comments[commentId] = contentComments[commentId];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return comments;
|
||||||
|
},
|
||||||
|
currentDiscussionLastCommentId: (state, getters) =>
|
||||||
|
Object.keys(getters.currentDiscussionComments).pop(),
|
||||||
|
currentDiscussionLastComment: (state, getters) =>
|
||||||
|
getters.currentDiscussionComments[getters.currentDiscussionLastCommentId],
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
createNewDiscussion({ commit, rootGetters }, selection) {
|
createNewDiscussion({ commit, rootGetters }, selection) {
|
||||||
if (selection) {
|
if (selection) {
|
||||||
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
|
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
|
||||||
if (text.length > 250) {
|
const maxLength = 80;
|
||||||
text = `${text.slice(0, 249).trim()}…`;
|
if (text.length > maxLength) {
|
||||||
|
text = `${text.slice(0, maxLength - 1).trim()}…`;
|
||||||
}
|
}
|
||||||
commit('setNewDiscussion', { ...selection, text });
|
commit('setNewDiscussion', { ...selection, text });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
cleanCurrentFile(
|
||||||
|
{ getters, rootGetters, commit, dispatch },
|
||||||
|
{ filterComment, filterDiscussion } = {},
|
||||||
|
) {
|
||||||
|
const discussions = rootGetters['content/current'].discussions;
|
||||||
|
const comments = rootGetters['content/current'].comments;
|
||||||
|
const patch = {
|
||||||
|
discussions: {},
|
||||||
|
comments: {},
|
||||||
|
};
|
||||||
|
Object.keys(comments).forEach((commentId) => {
|
||||||
|
const comment = comments[commentId];
|
||||||
|
const discussion = discussions[comment.discussionId];
|
||||||
|
if (discussion && comment !== filterComment && discussion !== filterDiscussion) {
|
||||||
|
patch.discussions[comment.discussionId] = discussion;
|
||||||
|
patch.comments[commentId] = comment;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextDiscussionId = getters.nextDiscussionId;
|
||||||
|
dispatch('content/patchCurrent', patch, { root: true });
|
||||||
|
if (!getters.currentDiscussion) {
|
||||||
|
// Keep the gutter open
|
||||||
|
commit('setCurrentDiscussionId', nextDiscussionId);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -46,6 +46,7 @@ const store = new Vuex.Store({
|
|||||||
ready: false,
|
ready: false,
|
||||||
offline: false,
|
offline: false,
|
||||||
lastOfflineCheck: 0,
|
lastOfflineCheck: 0,
|
||||||
|
minuteCounter: 0,
|
||||||
monetizeSponsor: false,
|
monetizeSponsor: false,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -69,6 +70,9 @@ const store = new Vuex.Store({
|
|||||||
updateLastOfflineCheck: (state) => {
|
updateLastOfflineCheck: (state) => {
|
||||||
state.lastOfflineCheck = Date.now();
|
state.lastOfflineCheck = Date.now();
|
||||||
},
|
},
|
||||||
|
updateMinuteCounter: (state) => {
|
||||||
|
state.minuteCounter += 1;
|
||||||
|
},
|
||||||
setMonetizeSponsor: (state, value) => {
|
setMonetizeSponsor: (state, value) => {
|
||||||
state.monetizeSponsor = value;
|
state.monetizeSponsor = value;
|
||||||
},
|
},
|
||||||
@ -121,4 +125,8 @@ const store = new Vuex.Store({
|
|||||||
plugins: debug ? [createLogger()] : [],
|
plugins: debug ? [createLogger()] : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
store.commit('updateMinuteCounter');
|
||||||
|
}, 60 * 1000);
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
const minPadding = 20;
|
const minPadding = 20;
|
||||||
const previewButtonWidth = 55;
|
|
||||||
const editorTopPadding = 10;
|
const editorTopPadding = 10;
|
||||||
const gutterWidth = 250;
|
|
||||||
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
|
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
|
||||||
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
||||||
const navigationBarRightButtonWidth = 38 + 8;
|
const navigationBarRightButtonWidth = 38 + 8;
|
||||||
@ -15,6 +13,7 @@ const minTitleMaxWidth = 200;
|
|||||||
const constants = {
|
const constants = {
|
||||||
editorMinWidth: 320,
|
editorMinWidth: 320,
|
||||||
explorerWidth: 250,
|
explorerWidth: 250,
|
||||||
|
gutterWidth: 250,
|
||||||
sideBarWidth: 280,
|
sideBarWidth: 280,
|
||||||
navigationBarHeight: 44,
|
navigationBarHeight: 44,
|
||||||
buttonBarWidth: 26,
|
buttonBarWidth: 26,
|
||||||
@ -48,9 +47,9 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
|
|||||||
}
|
}
|
||||||
|
|
||||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||||
const showGutter = getters['discussion/currentDiscussion'];
|
const showGutter = !!getters['discussion/currentDiscussion'];
|
||||||
if (showGutter) {
|
if (showGutter) {
|
||||||
doublePanelWidth -= gutterWidth;
|
doublePanelWidth -= constants.gutterWidth;
|
||||||
}
|
}
|
||||||
if (doublePanelWidth < constants.editorMinWidth) {
|
if (doublePanelWidth < constants.editorMinWidth) {
|
||||||
doublePanelWidth = constants.editorMinWidth;
|
doublePanelWidth = constants.editorMinWidth;
|
||||||
@ -87,17 +86,17 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
|
|||||||
const panelWidth = Math.floor(doublePanelWidth / 2);
|
const panelWidth = Math.floor(doublePanelWidth / 2);
|
||||||
styles.previewWidth = styles.showSidePreview ?
|
styles.previewWidth = styles.showSidePreview ?
|
||||||
panelWidth :
|
panelWidth :
|
||||||
doublePanelWidth + constants.buttonBarWidth;
|
doublePanelWidth;
|
||||||
let previewRightPadding = Math.max(
|
const previewRightPadding = Math.max(
|
||||||
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
|
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
|
||||||
|
if (!styles.showSidePreview) {
|
||||||
|
styles.previewWidth += constants.buttonBarWidth;
|
||||||
|
}
|
||||||
styles.previewGutterWidth = showGutter && !localSettings.showEditor
|
styles.previewGutterWidth = showGutter && !localSettings.showEditor
|
||||||
? gutterWidth
|
? constants.gutterWidth
|
||||||
: 0;
|
: 0;
|
||||||
const previewLeftPadding = previewRightPadding + styles.previewGutterWidth;
|
const previewLeftPadding = previewRightPadding + styles.previewGutterWidth;
|
||||||
styles.previewGutterLeft = previewLeftPadding - minPadding;
|
styles.previewGutterLeft = previewLeftPadding - minPadding;
|
||||||
if (!styles.showEditor && previewRightPadding < previewButtonWidth) {
|
|
||||||
previewRightPadding = previewButtonWidth;
|
|
||||||
}
|
|
||||||
styles.previewPadding = `${editorTopPadding}px ${previewRightPadding}px ${bottomPadding}px ${previewLeftPadding}px`;
|
styles.previewPadding = `${editorTopPadding}px ${previewRightPadding}px ${bottomPadding}px ${previewLeftPadding}px`;
|
||||||
styles.editorWidth = styles.showSidePreview ?
|
styles.editorWidth = styles.showSidePreview ?
|
||||||
panelWidth :
|
panelWidth :
|
||||||
@ -105,7 +104,7 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
|
|||||||
const editorRightPadding = Math.max(
|
const editorRightPadding = Math.max(
|
||||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||||
styles.editorGutterWidth = showGutter && localSettings.showEditor
|
styles.editorGutterWidth = showGutter && localSettings.showEditor
|
||||||
? gutterWidth
|
? constants.gutterWidth
|
||||||
: 0;
|
: 0;
|
||||||
const editorLeftPadding = editorRightPadding + styles.editorGutterWidth;
|
const editorLeftPadding = editorRightPadding + styles.editorGutterWidth;
|
||||||
styles.editorGutterLeft = editorLeftPadding - minPadding;
|
styles.editorGutterLeft = editorLeftPadding - minPadding;
|
||||||
|
@ -56,6 +56,16 @@ export default {
|
|||||||
resolveText: 'Yes, delete',
|
resolveText: 'Yes, delete',
|
||||||
rejectText: 'No',
|
rejectText: 'No',
|
||||||
}),
|
}),
|
||||||
|
discussionDeletion: ({ dispatch }) => dispatch('open', {
|
||||||
|
content: '<p>You are about to delete a discussion. Are you sure?</p>',
|
||||||
|
resolveText: 'Yes, delete',
|
||||||
|
rejectText: 'No',
|
||||||
|
}),
|
||||||
|
commentDeletion: ({ dispatch }) => dispatch('open', {
|
||||||
|
content: '<p>You are about to delete a comment. Are you sure?</p>',
|
||||||
|
resolveText: 'Yes, delete',
|
||||||
|
rejectText: 'No',
|
||||||
|
}),
|
||||||
trashDeletion: ({ dispatch }) => dispatch('open', {
|
trashDeletion: ({ dispatch }) => dispatch('open', {
|
||||||
content: '<p>Files in the trash are automatically deleted after 7 days of inactivity.</p>',
|
content: '<p>Files in the trash are automatically deleted after 7 days of inactivity.</p>',
|
||||||
resolveText: 'Ok',
|
resolveText: 'Ok',
|
||||||
@ -67,14 +77,15 @@ export default {
|
|||||||
}),
|
}),
|
||||||
providerRedirection: ({ dispatch }, { providerName, onResolve }) => dispatch('open', {
|
providerRedirection: ({ dispatch }, { providerName, onResolve }) => dispatch('open', {
|
||||||
content: `<p>You are about to navigate to the <b>${providerName}</b> authorization page.</p>`,
|
content: `<p>You are about to navigate to the <b>${providerName}</b> authorization page.</p>`,
|
||||||
resolveText: 'Ok, go on!',
|
resolveText: 'Ok, go on',
|
||||||
rejectText: 'Cancel',
|
rejectText: 'Cancel',
|
||||||
onResolve,
|
onResolve,
|
||||||
}),
|
}),
|
||||||
signInRequired: ({ dispatch }) => dispatch('open', {
|
signInForSponsorship: ({ dispatch }) => dispatch('open', {
|
||||||
content: `<p>We have to sign you in with <b>Google</b> in order to activate your sponsorship.</p>
|
type: 'signInForSponsorship',
|
||||||
<div class="modal__info"><b>Note:</b> This will backup and sync all your files and settings.</div>`,
|
content: `<p>You have to sign in with <b>Google</b> to enable your sponsorship.</p>
|
||||||
resolveText: 'Ok, sign in!',
|
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
||||||
|
resolveText: 'Ok, sign in',
|
||||||
rejectText: 'Cancel',
|
rejectText: 'Cancel',
|
||||||
}),
|
}),
|
||||||
sponsorOnly: ({ dispatch }) => dispatch('open', {
|
sponsorOnly: ({ dispatch }) => dispatch('open', {
|
||||||
|
Loading…
Reference in New Issue
Block a user