New discussion button. Discussion highlighter.
This commit is contained in:
parent
167f3f50bc
commit
8767adc505
@ -13,6 +13,5 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
<script src="//cdn.monetizejs.com/api/js/latest/monetize.min.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -68,12 +68,4 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import 'common/app';
|
@import 'common/app';
|
||||||
|
|
||||||
.app__spash-screen {
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 600px;
|
|
||||||
height: 100%;
|
|
||||||
background: no-repeat center url('../assets/logo.svg');
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import EditorNewDiscussionButtonGutter from './gutters/EditorNewDiscussionButtonGutter';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
EditorNewDiscussionButtonGutter,
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('layout', [
|
...mapGetters('layout', [
|
||||||
'styles',
|
'styles',
|
||||||
@ -17,6 +22,43 @@ export default {
|
|||||||
'computedSettings',
|
'computedSettings',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
const editorElt = this.$el.querySelector('.editor__inner');
|
||||||
|
const onDiscussionEvt = cb => (evt) => {
|
||||||
|
let elt = evt.target;
|
||||||
|
while (elt && elt !== editorElt) {
|
||||||
|
if (elt.discussionId) {
|
||||||
|
cb(elt.discussionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elt = elt.parentNode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editorElt.addEventListener('mouseover', onDiscussionEvt(discussionId =>
|
||||||
|
editorElt.getElementsByClassName(`discussion-editor-highlighting-${discussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--hover')),
|
||||||
|
));
|
||||||
|
editorElt.addEventListener('mouseout', onDiscussionEvt(discussionId =>
|
||||||
|
editorElt.getElementsByClassName(`discussion-editor-highlighting-${discussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.remove('discussion-editor-highlighting--hover')),
|
||||||
|
));
|
||||||
|
editorElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
||||||
|
this.$store.commit('discussion/setCurrentDiscussionId', discussionId);
|
||||||
|
}));
|
||||||
|
this.$watch(
|
||||||
|
() => this.$store.state.discussion.currentDiscussionId,
|
||||||
|
(discussionId, oldDiscussionId) => {
|
||||||
|
if (oldDiscussionId) {
|
||||||
|
editorElt.querySelectorAll(`.discussion-editor-highlighting-${oldDiscussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.remove('discussion-editor-highlighting--selected'));
|
||||||
|
}
|
||||||
|
if (discussionId) {
|
||||||
|
editorElt.querySelectorAll(`.discussion-editor-highlighting-${discussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -47,11 +89,6 @@ export default {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discussion-highlight,
|
|
||||||
.find-replace-highlight {
|
|
||||||
background-color: transparentize(#ffe400, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide {
|
.hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop">
|
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop">
|
||||||
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}">
|
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}">
|
||||||
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
|
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
|
||||||
</div>
|
</div>
|
||||||
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
|
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{paddingLeft: leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
|
||||||
{{node.item.name}}
|
{{node.item.name}}
|
||||||
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
|
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
|
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
|
||||||
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||||
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{'padding-left': childLeftPadding}">
|
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{paddingLeft: childLeftPadding}">
|
||||||
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keyup.enter="submitNewChild()" @keyup.esc="submitNewChild(true)" v-model.trim="newChildName">
|
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keyup.enter="submitNewChild()" @keyup.esc="submitNewChild(true)" v-model.trim="newChildName">
|
||||||
</div>
|
</div>
|
||||||
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||||
|
@ -32,9 +32,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue';
|
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import editorEngineSvc from '../services/editorEngineSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
import cledit from '../libs/cledit';
|
import cledit from '../libs/cledit';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import EditorClassApplier from './common/EditorClassApplier';
|
import EditorClassApplier from './common/EditorClassApplier';
|
||||||
@ -63,8 +62,8 @@ class DynamicClassApplier {
|
|||||||
constructor(cssClass, offset, silent) {
|
constructor(cssClass, offset, silent) {
|
||||||
this.startMarker = new cledit.Marker(offset.start);
|
this.startMarker = new cledit.Marker(offset.start);
|
||||||
this.endMarker = new cledit.Marker(offset.end);
|
this.endMarker = new cledit.Marker(offset.end);
|
||||||
editorEngineSvc.clEditor.addMarker(this.startMarker);
|
editorSvc.clEditor.addMarker(this.startMarker);
|
||||||
editorEngineSvc.clEditor.addMarker(this.endMarker);
|
editorSvc.clEditor.addMarker(this.endMarker);
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
this.classApplier = new EditorClassApplier(
|
this.classApplier = new EditorClassApplier(
|
||||||
[`find-replace-${this.startMarker.id}`, cssClass],
|
[`find-replace-${this.startMarker.id}`, cssClass],
|
||||||
@ -76,8 +75,8 @@ class DynamicClassApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clean = () => {
|
clean = () => {
|
||||||
editorEngineSvc.clEditor.removeMarker(this.startMarker);
|
editorSvc.clEditor.removeMarker(this.startMarker);
|
||||||
editorEngineSvc.clEditor.removeMarker(this.endMarker);
|
editorSvc.clEditor.removeMarker(this.endMarker);
|
||||||
if (this.classApplier) {
|
if (this.classApplier) {
|
||||||
this.classApplier.stop();
|
this.classApplier.stop();
|
||||||
}
|
}
|
||||||
@ -117,7 +116,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.replaceRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'm' : 'mi');
|
this.replaceRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'm' : 'mi');
|
||||||
this.searchRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'gm' : 'gmi');
|
this.searchRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'gm' : 'gmi');
|
||||||
editorEngineSvc.clEditor.getContent().replace(this.searchRegex, (...params) => {
|
editorSvc.clEditor.getContent().replace(this.searchRegex, (...params) => {
|
||||||
const match = params[0];
|
const match = params[0];
|
||||||
const offset = params[params.length - 2];
|
const offset = params[params.length - 2];
|
||||||
offsetList.push({
|
offsetList.push({
|
||||||
@ -161,7 +160,7 @@ export default {
|
|||||||
find(mode = 'forward') {
|
find(mode = 'forward') {
|
||||||
const selectedClassApplier = this.selectedClassApplier;
|
const selectedClassApplier = this.selectedClassApplier;
|
||||||
this.unselectClassApplier();
|
this.unselectClassApplier();
|
||||||
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
const selectionMgr = editorSvc.clEditor.selectionMgr;
|
||||||
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||||
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||||
const keys = Object.keys(this.classAppliers);
|
const keys = Object.keys(this.classAppliers);
|
||||||
@ -208,21 +207,21 @@ export default {
|
|||||||
this.find();
|
this.find();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editorEngineSvc.clEditor.replaceAll(
|
editorSvc.clEditor.replaceAll(
|
||||||
this.replaceRegex, this.replaceText, this.selectedClassApplier.startMarker.offset);
|
this.replaceRegex, this.replaceText, this.selectedClassApplier.startMarker.offset);
|
||||||
Vue.nextTick(() => this.find());
|
this.$nextTick(() => this.find());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
replaceAll() {
|
replaceAll() {
|
||||||
if (this.searchRegex) {
|
if (this.searchRegex) {
|
||||||
editorEngineSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
|
editorSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
this.$store.commit('findReplace/setType');
|
this.$store.commit('findReplace/setType');
|
||||||
},
|
},
|
||||||
onEscape() {
|
onEscape() {
|
||||||
editorEngineSvc.clEditor.focus();
|
editorSvc.clEditor.focus();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -236,7 +235,7 @@ export default {
|
|||||||
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
|
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
|
||||||
this.$watch(() => this.findUseRegexp, this.debouncedHighlightOccurrences);
|
this.$watch(() => this.findUseRegexp, this.debouncedHighlightOccurrences);
|
||||||
// Refresh highlighting when content changes
|
// Refresh highlighting when content changes
|
||||||
editorEngineSvc.clEditor.on('contentChanged', this.debouncedHighlightOccurrences);
|
editorSvc.clEditor.on('contentChanged', this.debouncedHighlightOccurrences);
|
||||||
|
|
||||||
// Last open changes trigger focus on text input and find occurence in selection
|
// Last open changes trigger focus on text input and find occurence in selection
|
||||||
this.$watch(() => this.lastOpen, () => {
|
this.$watch(() => this.lastOpen, () => {
|
||||||
@ -266,7 +265,7 @@ export default {
|
|||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
// Unregister listeners
|
// Unregister listeners
|
||||||
editorEngineSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
|
editorSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
|
||||||
window.removeEventListener('keyup', this.onKeyup);
|
window.removeEventListener('keyup', this.onKeyup);
|
||||||
window.removeEventListener('focusin', this.onFocusIn);
|
window.removeEventListener('focusin', this.onFocusIn);
|
||||||
this.state = 'destroyed';
|
this.state = 'destroyed';
|
||||||
@ -350,10 +349,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.find-replace-highlighting {
|
.find-replace-highlighting {
|
||||||
background-color: #ff0;
|
background-color: $highlighting-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.find-replace-selection {
|
.find-replace-selection {
|
||||||
background-color: #ff9632;
|
background-color: $selection-highlighting-color;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -9,7 +9,10 @@
|
|||||||
<navigation-bar></navigation-bar>
|
<navigation-bar></navigation-bar>
|
||||||
</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 + 'px', 'font-size': 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__background"></div>
|
||||||
|
</div>
|
||||||
<editor></editor>
|
<editor></editor>
|
||||||
<div v-if="showFindReplace" class="layout__panel layout__panel--find-replace">
|
<div v-if="showFindReplace" class="layout__panel layout__panel--find-replace">
|
||||||
<find-replace></find-replace>
|
<find-replace></find-replace>
|
||||||
@ -18,7 +21,10 @@
|
|||||||
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
|
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
|
||||||
<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 + 'px', 'font-size': 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__background"></div>
|
||||||
|
</div>
|
||||||
<preview></preview>
|
<preview></preview>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,7 +50,6 @@ import Editor from './Editor';
|
|||||||
import Preview from './Preview';
|
import Preview from './Preview';
|
||||||
import FindReplace from './FindReplace';
|
import FindReplace from './FindReplace';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
import editorEngineSvc from '../services/editorEngineSvc';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -77,6 +82,7 @@ export default {
|
|||||||
window.addEventListener('resize', this.updateBodySize);
|
window.addEventListener('resize', this.updateBodySize);
|
||||||
window.addEventListener('keyup', this.saveSelection);
|
window.addEventListener('keyup', this.saveSelection);
|
||||||
window.addEventListener('mouseup', this.saveSelection);
|
window.addEventListener('mouseup', this.saveSelection);
|
||||||
|
window.addEventListener('focusin', this.saveSelection);
|
||||||
window.addEventListener('contextmenu', this.saveSelection);
|
window.addEventListener('contextmenu', this.saveSelection);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -87,12 +93,13 @@ export default {
|
|||||||
|
|
||||||
// Focus on the editor every time reader mode is disabled
|
// Focus on the editor every time reader mode is disabled
|
||||||
this.$watch(() => this.styles.showEditor,
|
this.$watch(() => this.styles.showEditor,
|
||||||
showEditor => showEditor && editorEngineSvc.clEditor.focus());
|
showEditor => showEditor && editorSvc.clEditor.focus());
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
window.removeEventListener('resize', this.updateStyle);
|
window.removeEventListener('resize', this.updateStyle);
|
||||||
window.removeEventListener('keyup', this.saveSelection);
|
window.removeEventListener('keyup', this.saveSelection);
|
||||||
window.removeEventListener('mouseup', this.saveSelection);
|
window.removeEventListener('mouseup', this.saveSelection);
|
||||||
|
window.removeEventListener('focusin', this.saveSelection);
|
||||||
window.removeEventListener('contextmenu', this.saveSelection);
|
window.removeEventListener('contextmenu', this.saveSelection);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -112,6 +119,7 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout__panel--navigation-bar {
|
.layout__panel--navigation-bar {
|
||||||
@ -145,4 +153,12 @@ 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>
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import editorEngineSvc from '../services/editorEngineSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
import ModalInner from './modals/common/ModalInner';
|
import ModalInner from './modals/common/ModalInner';
|
||||||
import FilePropertiesModal from './modals/FilePropertiesModal';
|
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||||
import SettingsModal from './modals/SettingsModal';
|
import SettingsModal from './modals/SettingsModal';
|
||||||
@ -120,7 +120,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
onEscape() {
|
onEscape() {
|
||||||
this.config.reject();
|
this.config.reject();
|
||||||
editorEngineSvc.clEditor.focus();
|
editorSvc.clEditor.focus();
|
||||||
},
|
},
|
||||||
onTab(evt) {
|
onTab(evt) {
|
||||||
const tabbables = getTabbables(this.$el);
|
const tabbables = getTabbables(this.$el);
|
||||||
|
@ -84,7 +84,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
import editorEngineSvc from '../services/editorEngineSvc';
|
|
||||||
import syncSvc from '../services/syncSvc';
|
import syncSvc from '../services/syncSvc';
|
||||||
import publishSvc from '../services/publishSvc';
|
import publishSvc from '../services/publishSvc';
|
||||||
import animationSvc from '../services/animationSvc';
|
import animationSvc from '../services/animationSvc';
|
||||||
@ -161,10 +160,10 @@ export default {
|
|||||||
'toggleSideBar',
|
'toggleSideBar',
|
||||||
]),
|
]),
|
||||||
undo() {
|
undo() {
|
||||||
return editorEngineSvc.clEditor.undoMgr.undo();
|
return editorSvc.clEditor.undoMgr.undo();
|
||||||
},
|
},
|
||||||
redo() {
|
redo() {
|
||||||
return editorEngineSvc.clEditor.undoMgr.redo();
|
return editorSvc.clEditor.undoMgr.redo();
|
||||||
},
|
},
|
||||||
requestSync() {
|
requestSync() {
|
||||||
if (this.isSyncPossible && !this.isSyncRequested) {
|
if (this.isSyncPossible && !this.isSyncRequested) {
|
||||||
@ -402,7 +401,6 @@ $t: 3000ms;
|
|||||||
.navigation-bar__spinner {
|
.navigation-bar__spinner {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
margin: 7px 0 0 8px;
|
margin: 7px 0 0 8px;
|
||||||
color: #b2b2b2;
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@ -416,7 +414,7 @@ $t: 3000ms;
|
|||||||
height: $d;
|
height: $d;
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: $b solid currentColor;
|
border: $b solid transparentize($navbar-color, 0.5);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
|
|
||||||
@ -426,20 +424,20 @@ $t: 3000ms;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
width: $b;
|
width: $b;
|
||||||
background-color: currentColor;
|
background-color: $navbar-color;
|
||||||
border-radius: $b * 0.5;
|
border-radius: $b * 0.5;
|
||||||
transform-origin: 50% 0;
|
transform-origin: 50% 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
height: $r * 0.35;
|
height: $r * 0.4;
|
||||||
left: $r - $b * 1.5;
|
left: $r - $b * 1.5;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
animation: spin $t linear infinite;
|
animation: spin $t linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
height: $r * 0.5;
|
height: $r * 0.6;
|
||||||
left: $r - $b * 1.5;
|
left: $r - $b * 1.5;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
animation: spin $t/4 linear infinite;
|
animation: spin $t/4 linear infinite;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<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>
|
</div>
|
||||||
<div v-if="!styles.showEditor" class="preview__button-bar">
|
<div v-if="!styles.showEditor" class="preview__button-bar">
|
||||||
<div class="preview__button" @click="toggleEditor(true)">
|
<div class="preview__button" @click="toggleEditor(true)">
|
||||||
@ -15,10 +16,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
|
import PreviewNewDiscussionButtonGutter from './gutters/PreviewNewDiscussionButtonGutter';
|
||||||
|
|
||||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
const appUri = `${window.location.protocol}//${window.location.host}`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
PreviewNewDiscussionButtonGutter,
|
||||||
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
previewTop: true,
|
previewTop: true,
|
||||||
}),
|
}),
|
||||||
@ -46,6 +51,43 @@ export default {
|
|||||||
this.previewTop = evt.target.scrollTop < 10;
|
this.previewTop = evt.target.scrollTop < 10;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
const previewElt = this.$el.querySelector('.preview__inner-2');
|
||||||
|
const onDiscussionEvt = cb => (evt) => {
|
||||||
|
let elt = evt.target;
|
||||||
|
while (elt && elt !== previewElt) {
|
||||||
|
if (elt.discussionId) {
|
||||||
|
cb(elt.discussionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elt = elt.parentNode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
previewElt.addEventListener('mouseover', onDiscussionEvt(discussionId =>
|
||||||
|
previewElt.getElementsByClassName(`discussion-preview-highlighting-${discussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--hover')),
|
||||||
|
));
|
||||||
|
previewElt.addEventListener('mouseout', onDiscussionEvt(discussionId =>
|
||||||
|
previewElt.getElementsByClassName(`discussion-preview-highlighting-${discussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.remove('discussion-preview-highlighting--hover')),
|
||||||
|
));
|
||||||
|
previewElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
||||||
|
this.$store.commit('discussion/setCurrentDiscussionId', discussionId);
|
||||||
|
}));
|
||||||
|
this.$watch(
|
||||||
|
() => this.$store.state.discussion.currentDiscussionId,
|
||||||
|
(discussionId, oldDiscussionId) => {
|
||||||
|
if (oldDiscussionId) {
|
||||||
|
previewElt.querySelectorAll(`.discussion-preview-highlighting-${oldDiscussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.remove('discussion-preview-highlighting--selected'));
|
||||||
|
}
|
||||||
|
if (discussionId) {
|
||||||
|
previewElt.querySelectorAll(`.discussion-preview-highlighting-${discussionId}`)
|
||||||
|
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
import editorEngineSvc from '../services/editorEngineSvc';
|
|
||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
|
|
||||||
class Stat {
|
class Stat {
|
||||||
@ -67,13 +66,13 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
computeText() {
|
computeText() {
|
||||||
this.textSelection = false;
|
this.textSelection = false;
|
||||||
let text = editorEngineSvc.clEditor.getContent();
|
let text = editorSvc.clEditor.getContent();
|
||||||
const beforeText = text.slice(0, editorEngineSvc.clEditor.selectionMgr.selectionEnd);
|
const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd);
|
||||||
const beforeLines = beforeText.split('\n');
|
const beforeLines = beforeText.split('\n');
|
||||||
this.line = beforeLines.length;
|
this.line = beforeLines.length;
|
||||||
this.column = beforeLines.pop().length;
|
this.column = beforeLines.pop().length;
|
||||||
|
|
||||||
const selectedText = editorEngineSvc.clEditor.selectionMgr.getSelectedText();
|
const selectedText = editorSvc.clEditor.selectionMgr.getSelectedText();
|
||||||
if (selectedText) {
|
if (selectedText) {
|
||||||
this.textSelection = true;
|
this.textSelection = true;
|
||||||
text = selectedText;
|
text = selectedText;
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
|
|
||||||
@ -71,7 +70,7 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
this.$nextTick(() => {
|
||||||
editorSvc.editorElt.parentNode.addEventListener('scroll', () => {
|
editorSvc.editorElt.parentNode.addEventListener('scroll', () => {
|
||||||
if (this.styles.showEditor) {
|
if (this.styles.showEditor) {
|
||||||
updateMaskY();
|
updateMaskY();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-image" :style="{'background-image': url}">
|
<div class="user-image" :style="{backgroundImage: url}">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import cledit from '../../libs/cledit';
|
import cledit from '../../libs/cledit';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import editorEngineSvc from '../../services/editorEngineSvc';
|
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
let savedSelection;
|
let savedSelection = null;
|
||||||
const nextTickCbs = [];
|
const nextTickCbs = [];
|
||||||
const nextTickExecCbs = cledit.Utils.debounce(() => {
|
const nextTickExecCbs = cledit.Utils.debounce(() => {
|
||||||
while (nextTickCbs.length) {
|
while (nextTickCbs.length) {
|
||||||
nextTickCbs.shift()();
|
nextTickCbs.shift()();
|
||||||
}
|
}
|
||||||
if (savedSelection) {
|
if (savedSelection) {
|
||||||
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
editorSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||||
savedSelection.start, savedSelection.end);
|
savedSelection.start, savedSelection.end);
|
||||||
}
|
}
|
||||||
savedSelection = null;
|
savedSelection = null;
|
||||||
@ -23,8 +22,8 @@ const nextTick = (cb) => {
|
|||||||
|
|
||||||
const nextTickRestoreSelection = () => {
|
const nextTickRestoreSelection = () => {
|
||||||
savedSelection = {
|
savedSelection = {
|
||||||
start: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
start: editorSvc.clEditor.selectionMgr.selectionStart,
|
||||||
end: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
end: editorSvc.clEditor.selectionMgr.selectionEnd,
|
||||||
};
|
};
|
||||||
nextTickExecCbs();
|
nextTickExecCbs();
|
||||||
};
|
};
|
||||||
@ -44,14 +43,14 @@ export default class EditorClassApplier {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
editorEngineSvc.clEditor.on('contentChanged', this.restoreClass);
|
editorSvc.clEditor.on('contentChanged', this.restoreClass);
|
||||||
nextTick(() => this.applyClass());
|
nextTick(() => this.applyClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
applyClass() {
|
applyClass() {
|
||||||
const offset = this.offsetGetter();
|
const offset = this.offsetGetter();
|
||||||
if (offset && offset.start !== offset.end) {
|
if (offset && offset.start !== offset.end) {
|
||||||
const range = editorEngineSvc.clEditor.selectionMgr.createRange(
|
const range = editorSvc.clEditor.selectionMgr.createRange(
|
||||||
Math.min(offset.start, offset.end),
|
Math.min(offset.start, offset.end),
|
||||||
Math.max(offset.start, offset.end),
|
Math.max(offset.start, offset.end),
|
||||||
);
|
);
|
||||||
@ -59,10 +58,10 @@ export default class EditorClassApplier {
|
|||||||
...this.properties,
|
...this.properties,
|
||||||
className: this.classGetter().join(' '),
|
className: this.classGetter().join(' '),
|
||||||
};
|
};
|
||||||
editorEngineSvc.clEditor.watcher.noWatch(() => {
|
editorSvc.clEditor.watcher.noWatch(() => {
|
||||||
utils.wrapRange(range, properties);
|
utils.wrapRange(range, properties);
|
||||||
});
|
});
|
||||||
if (editorEngineSvc.clEditor.selectionMgr.hasFocus()) {
|
if (editorSvc.clEditor.selectionMgr.hasFocus()) {
|
||||||
nextTickRestoreSelection();
|
nextTickRestoreSelection();
|
||||||
}
|
}
|
||||||
this.lastEltCount = this.eltCollection.length;
|
this.lastEltCount = this.eltCollection.length;
|
||||||
@ -70,16 +69,16 @@ export default class EditorClassApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeClass() {
|
removeClass() {
|
||||||
editorEngineSvc.clEditor.watcher.noWatch(() => {
|
editorSvc.clEditor.watcher.noWatch(() => {
|
||||||
utils.unwrapRange(this.eltCollection);
|
utils.unwrapRange(this.eltCollection);
|
||||||
});
|
});
|
||||||
if (editorEngineSvc.clEditor.selectionMgr.hasFocus()) {
|
if (editorSvc.clEditor.selectionMgr.hasFocus()) {
|
||||||
nextTickRestoreSelection();
|
nextTickRestoreSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
editorEngineSvc.clEditor.off('contentChanged', this.restoreClass);
|
editorSvc.clEditor.off('contentChanged', this.restoreClass);
|
||||||
nextTick(() => this.removeClass());
|
nextTick(() => this.removeClass());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
65
src/components/common/PreviewClassApplier.js
Normal file
65
src/components/common/PreviewClassApplier.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import cledit from '../../libs/cledit';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
|
const nextTickCbs = [];
|
||||||
|
const nextTickExecCbs = cledit.Utils.debounce(() => {
|
||||||
|
while (nextTickCbs.length) {
|
||||||
|
nextTickCbs.shift()();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextTick = (cb) => {
|
||||||
|
nextTickCbs.push(cb);
|
||||||
|
nextTickExecCbs();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class PreviewClassApplier {
|
||||||
|
constructor(classGetter, offsetGetter, properties) {
|
||||||
|
this.classGetter = typeof classGetter === 'function' ? classGetter : () => classGetter;
|
||||||
|
this.offsetGetter = typeof offsetGetter === 'function' ? offsetGetter : () => offsetGetter;
|
||||||
|
this.properties = properties || {};
|
||||||
|
this.eltCollection = editorSvc.previewElt.getElementsByClassName(this.classGetter()[0]);
|
||||||
|
this.lastEltCount = this.eltCollection.length;
|
||||||
|
|
||||||
|
this.restoreClass = () => {
|
||||||
|
if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
|
||||||
|
this.removeClass();
|
||||||
|
this.applyClass();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editorSvc.$on('previewHtml', this.restoreClass);
|
||||||
|
editorSvc.$on('sectionDescWithDiffsList', this.restoreClass);
|
||||||
|
nextTick(() => this.applyClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
applyClass() {
|
||||||
|
const offset = this.offsetGetter();
|
||||||
|
if (offset && offset.start !== offset.end) {
|
||||||
|
const start = cledit.Utils.findContainer(
|
||||||
|
editorSvc.previewElt, Math.min(offset.start, offset.end));
|
||||||
|
const end = cledit.Utils.findContainer(
|
||||||
|
editorSvc.previewElt, Math.max(offset.start, offset.end));
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(start.container, start.offsetInContainer);
|
||||||
|
range.setEnd(end.container, end.offsetInContainer);
|
||||||
|
const properties = {
|
||||||
|
...this.properties,
|
||||||
|
className: this.classGetter().join(' '),
|
||||||
|
};
|
||||||
|
utils.wrapRange(range, properties);
|
||||||
|
this.lastEltCount = this.eltCollection.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClass() {
|
||||||
|
utils.unwrapRange(this.eltCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
editorSvc.$off('previewHtml', this.restoreClass);
|
||||||
|
editorSvc.$off('sectionDescWithDiffsList', this.restoreClass);
|
||||||
|
nextTick(() => this.removeClass());
|
||||||
|
}
|
||||||
|
}
|
@ -206,6 +206,60 @@ textarea {
|
|||||||
background-size: contain;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-discussion-button {
|
||||||
|
color: rgba(0, 0, 0, 0.33);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
padding: 1px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-editor-highlighting,
|
||||||
|
.discussion-preview-highlighting {
|
||||||
|
background-color: mix(#fff, $selection-highlighting-color, 50%);
|
||||||
|
padding: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-editor-highlighting--hover,
|
||||||
|
.discussion-preview-highlighting--hover {
|
||||||
|
background-color: mix(#fff, $selection-highlighting-color, 25%);
|
||||||
|
|
||||||
|
* {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-editor-highlighting--selected,
|
||||||
|
.discussion-preview-highlighting--selected {
|
||||||
|
background-color: mix(#fff, $selection-highlighting-color, 10%);
|
||||||
|
|
||||||
|
* {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-preview-highlighting {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-preview-highlighting--selected {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden-rendering-container {
|
.hidden-rendering-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
color: $editor-color;
|
color: $editor-color;
|
||||||
font-family: $font-family-monospace;
|
font-family: $font-family-monospace;
|
||||||
font-size: $font-size-monospace;
|
font-size: $font-size-monospace;
|
||||||
word-break: break-all;
|
|
||||||
|
|
||||||
[class*='language-'] {
|
[class*='language-'] {
|
||||||
color: $editor-color-dark;
|
color: $editor-color-dark;
|
||||||
@ -30,6 +29,11 @@
|
|||||||
* {
|
* {
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&,
|
||||||
|
* {
|
||||||
|
line-height: $line-height-title;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
|
@ -4,7 +4,9 @@ $line-height-base: 1.67;
|
|||||||
$line-height-title: 1.33;
|
$line-height-title: 1.33;
|
||||||
$font-size-monospace: 0.85em;
|
$font-size-monospace: 0.85em;
|
||||||
$code-bg: rgba(0, 0, 0, 0.05);
|
$code-bg: rgba(0, 0, 0, 0.05);
|
||||||
$info-bg: transparentize(#f90, 0.85);
|
$highlighting-color: #ff0;
|
||||||
|
$selection-highlighting-color: #ff9632;
|
||||||
|
$info-bg: transparentize($selection-highlighting-color, 0.85);
|
||||||
$code-border-radius: 2px;
|
$code-border-radius: 2px;
|
||||||
$link-color: #0c93e4;
|
$link-color: #0c93e4;
|
||||||
$error-color: #f20;
|
$error-color: #f20;
|
||||||
|
56
src/components/gutters/EditorNewDiscussionButtonGutter.vue
Normal file
56
src/components/gutters/EditorNewDiscussionButtonGutter.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<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)">
|
||||||
|
<icon-message></icon-message>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
|
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.clEditor.selectionMgr.hasFocus()) {
|
||||||
|
this.selection = editorSvc.getTrimmedSelection();
|
||||||
|
if (this.selection) {
|
||||||
|
const text = editorSvc.clEditor.getContent();
|
||||||
|
offset = this.selection.end;
|
||||||
|
while (offset && text[offset - 1] === '\n') {
|
||||||
|
offset -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.coordinates = offset
|
||||||
|
? editorSvc.clEditor.selectionMgr.getCoordinates(offset)
|
||||||
|
: null;
|
||||||
|
}, 25);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
editorSvc.clEditor.selectionMgr.on('selectionChanged', () => this.checkSelection());
|
||||||
|
editorSvc.clEditor.selectionMgr.on('cursorCoordinatesChanged', () => this.checkSelection());
|
||||||
|
editorSvc.clEditor.on('focus', () => this.checkSelection());
|
||||||
|
editorSvc.clEditor.on('blur', () => this.checkSelection());
|
||||||
|
this.checkSelection();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
71
src/components/gutters/PreviewNewDiscussionButtonGutter.vue
Normal file
71
src/components/gutters/PreviewNewDiscussionButtonGutter.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<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>
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo">
|
<modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo">
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
<div class="google-photo__tumbnail" :style="{backgroundImage: thumbnailUrl}"></div>
|
||||||
<form-entry label="Title (optional)">
|
<form-entry label="Title (optional)">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="title" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="title" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
5
src/icons/Message.vue
Normal file
5
src/icons/Message.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M 21.9891,3.99805C 21.9891,2.89404 21.1031,1.99805 19.9991,1.99805L 3.99913,1.99805C 2.89512,1.99805 1.99913,2.89404 1.99913,3.99805L 1.99913,15.998C 1.99913,17.1021 2.89512,17.998 3.99913,17.998L 17.9991,17.998L 21.9991,21.998L 21.9891,3.99805 Z "/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -46,6 +46,7 @@ import Printer from './Printer';
|
|||||||
import Undo from './Undo';
|
import Undo from './Undo';
|
||||||
import Redo from './Redo';
|
import Redo from './Redo';
|
||||||
import ContentSave from './ContentSave';
|
import ContentSave from './ContentSave';
|
||||||
|
import Message from './Message';
|
||||||
|
|
||||||
Vue.component('iconProvider', Provider);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
@ -94,3 +95,4 @@ Vue.component('iconPrinter', Printer);
|
|||||||
Vue.component('iconUndo', Undo);
|
Vue.component('iconUndo', Undo);
|
||||||
Vue.component('iconRedo', Redo);
|
Vue.component('iconRedo', Redo);
|
||||||
Vue.component('iconContentSave', ContentSave);
|
Vue.component('iconContentSave', ContentSave);
|
||||||
|
Vue.component('iconMessage', Message);
|
||||||
|
@ -212,6 +212,7 @@ function cledit(contentElt, scrollElt, windowParam) {
|
|||||||
editor.$window.removeEventListener('keydown', windowKeydownListener)
|
editor.$window.removeEventListener('keydown', windowKeydownListener)
|
||||||
editor.$window.removeEventListener('mousedown', windowMouseListener)
|
editor.$window.removeEventListener('mousedown', windowMouseListener)
|
||||||
editor.$window.removeEventListener('mouseup', windowMouseListener)
|
editor.$window.removeEventListener('mouseup', windowMouseListener)
|
||||||
|
editor.$window.removeEventListener('resize', windowResizeListener)
|
||||||
editor.$trigger('destroy')
|
editor.$trigger('destroy')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -235,6 +236,15 @@ function cledit(contentElt, scrollElt, windowParam) {
|
|||||||
}
|
}
|
||||||
editor.$window.addEventListener('mousedown', windowMouseListener)
|
editor.$window.addEventListener('mousedown', windowMouseListener)
|
||||||
editor.$window.addEventListener('mouseup', windowMouseListener)
|
editor.$window.addEventListener('mouseup', windowMouseListener)
|
||||||
|
|
||||||
|
// Resize provokes cursor coordinate changes
|
||||||
|
function windowResizeListener() {
|
||||||
|
if (!tryDestroy()) {
|
||||||
|
selectionMgr.updateCursorCoordinates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.$window.addEventListener('resize', windowResizeListener)
|
||||||
|
|
||||||
// This can also provoke selection changes and does not fire mouseup event on Chrome/OSX
|
// This can also provoke selection changes and does not fire mouseup event on Chrome/OSX
|
||||||
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
|
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
|
||||||
|
|
||||||
|
@ -33,14 +33,8 @@ function makePatchableText(content, markerKeys, markerIdxMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (discussion.offset0 === discussion.offset1) {
|
addMarker('start');
|
||||||
// Remove discussion offsets if markers are at the same position
|
addMarker('end');
|
||||||
discussion.offset0 = undefined;
|
|
||||||
discussion.offset1 = undefined;
|
|
||||||
} else {
|
|
||||||
addMarker('offset0');
|
|
||||||
addMarker('offset1');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastOffset = 0;
|
let lastOffset = 0;
|
||||||
|
@ -1,167 +0,0 @@
|
|||||||
import DiffMatchPatch from 'diff-match-patch';
|
|
||||||
import cledit from '../libs/cledit';
|
|
||||||
import utils from './utils';
|
|
||||||
import diffUtils from './diffUtils';
|
|
||||||
import store from '../store';
|
|
||||||
|
|
||||||
let clEditor;
|
|
||||||
const newDiscussionMarker0 = new cledit.Marker(0);
|
|
||||||
const newDiscussionMarker1 = new cledit.Marker(0, true);
|
|
||||||
let markerKeys;
|
|
||||||
let markerIdxMap;
|
|
||||||
let previousPatchableText;
|
|
||||||
let currentPatchableText;
|
|
||||||
let discussionMarkers;
|
|
||||||
let isChangePatch;
|
|
||||||
let contentId;
|
|
||||||
|
|
||||||
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
|
||||||
function getMarker(offsetName) {
|
|
||||||
const markerOffset = discussion[offsetName];
|
|
||||||
const markerKey = discussionId + offsetName;
|
|
||||||
let marker = discussionMarkers[markerKey];
|
|
||||||
if (markerOffset !== undefined) {
|
|
||||||
if (!marker) {
|
|
||||||
marker = new cledit.Marker(markerOffset, offsetName === 'offset1');
|
|
||||||
marker.discussionId = discussionId;
|
|
||||||
marker.offsetName = offsetName;
|
|
||||||
clEditor.addMarker(marker);
|
|
||||||
discussionMarkers[markerKey] = marker;
|
|
||||||
}
|
|
||||||
onMarker(marker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getMarker('offset0');
|
|
||||||
getMarker('offset1');
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncDiscussionMarkers(content, writeOffsets) {
|
|
||||||
Object.keys(discussionMarkers).forEach((markerKey) => {
|
|
||||||
const marker = discussionMarkers[markerKey];
|
|
||||||
// Remove marker if discussion was removed
|
|
||||||
const discussion = content.discussions[marker.discussionId];
|
|
||||||
if (!discussion || discussion[marker.offsetName] === undefined) {
|
|
||||||
clEditor.removeMarker(marker);
|
|
||||||
delete discussionMarkers[markerKey];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(content.discussions).forEach((discussionId) => {
|
|
||||||
const discussion = content.discussions[discussionId];
|
|
||||||
getDiscussionMarkers(discussion, discussionId, writeOffsets
|
|
||||||
? (marker) => {
|
|
||||||
discussion[marker.offsetName] = marker.offset;
|
|
||||||
}
|
|
||||||
: (marker) => {
|
|
||||||
marker.offset = discussion[marker.offsetName];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDiscussionMarkers() {
|
|
||||||
Object.keys(discussionMarkers).forEach((markerKey) => {
|
|
||||||
const marker = discussionMarkers[markerKey];
|
|
||||||
clEditor.removeMarker(marker);
|
|
||||||
delete discussionMarkers[markerKey];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffMatchPatch = new DiffMatchPatch();
|
|
||||||
|
|
||||||
function makePatches() {
|
|
||||||
const diffs = diffMatchPatch.diff_main(previousPatchableText, currentPatchableText);
|
|
||||||
return diffMatchPatch.patch_make(previousPatchableText, diffs);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyPatches(patches) {
|
|
||||||
const newPatchableText = diffMatchPatch.patch_apply(patches, currentPatchableText)[0];
|
|
||||||
let result = newPatchableText;
|
|
||||||
if (markerKeys.length) {
|
|
||||||
// Strip text markers
|
|
||||||
result = result.replace(new RegExp(`[\ue000-${String.fromCharCode((0xe000 + markerKeys.length) - 1)}]`, 'g'), '');
|
|
||||||
}
|
|
||||||
// Expect a `contentChanged` event
|
|
||||||
if (result !== clEditor.getContent()) {
|
|
||||||
previousPatchableText = currentPatchableText;
|
|
||||||
currentPatchableText = newPatchableText;
|
|
||||||
isChangePatch = true;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function reversePatches(patches) {
|
|
||||||
const result = diffMatchPatch.patch_deepCopy(patches).reverse();
|
|
||||||
result.forEach((patch) => {
|
|
||||||
patch.diffs.forEach((diff) => {
|
|
||||||
diff[0] = -diff[0];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
clEditor: null,
|
|
||||||
createClEditor(editorElt) {
|
|
||||||
this.clEditor = cledit(editorElt, editorElt.parentNode);
|
|
||||||
clEditor = this.clEditor;
|
|
||||||
markerKeys = [];
|
|
||||||
markerIdxMap = Object.create(null);
|
|
||||||
discussionMarkers = {};
|
|
||||||
clEditor.on('contentChanged', (text) => {
|
|
||||||
const oldContent = store.getters['content/current'];
|
|
||||||
const newContent = {
|
|
||||||
...utils.deepCopy(oldContent),
|
|
||||||
text: utils.sanitizeText(text),
|
|
||||||
};
|
|
||||||
syncDiscussionMarkers(newContent, true);
|
|
||||||
if (!isChangePatch) {
|
|
||||||
previousPatchableText = currentPatchableText;
|
|
||||||
currentPatchableText = diffUtils.makePatchableText(newContent, markerKeys, markerIdxMap);
|
|
||||||
} else {
|
|
||||||
// Take a chance to restore discussion offsets on undo/redo
|
|
||||||
diffUtils.restoreDiscussionOffsets(newContent, markerKeys);
|
|
||||||
syncDiscussionMarkers(newContent, false);
|
|
||||||
}
|
|
||||||
store.dispatch('content/patchCurrent', newContent);
|
|
||||||
isChangePatch = false;
|
|
||||||
});
|
|
||||||
clEditor.addMarker(newDiscussionMarker0);
|
|
||||||
clEditor.addMarker(newDiscussionMarker1);
|
|
||||||
},
|
|
||||||
initClEditor(opts) {
|
|
||||||
const content = store.getters['content/current'];
|
|
||||||
if (content) {
|
|
||||||
const contentState = store.getters['contentState/current'];
|
|
||||||
const options = Object.assign({
|
|
||||||
selectionStart: contentState.selectionStart,
|
|
||||||
selectionEnd: contentState.selectionEnd,
|
|
||||||
patchHandler: {
|
|
||||||
makePatches,
|
|
||||||
applyPatches,
|
|
||||||
reversePatches,
|
|
||||||
},
|
|
||||||
}, opts);
|
|
||||||
|
|
||||||
if (contentId !== content.id) {
|
|
||||||
contentId = content.id;
|
|
||||||
currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
|
||||||
previousPatchableText = currentPatchableText;
|
|
||||||
syncDiscussionMarkers(content, false);
|
|
||||||
options.content = content.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
clEditor.init(options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
applyContent() {
|
|
||||||
if (clEditor) {
|
|
||||||
const content = store.getters['content/current'];
|
|
||||||
if (clEditor.setContent(content.text, true).range) {
|
|
||||||
// Marker will be recreated on contentChange
|
|
||||||
removeDiscussionMarkers();
|
|
||||||
} else {
|
|
||||||
syncDiscussionMarkers(content, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
@ -9,8 +9,8 @@ import markdownConversionSvc from './markdownConversionSvc';
|
|||||||
import markdownGrammarSvc from './markdownGrammarSvc';
|
import markdownGrammarSvc from './markdownGrammarSvc';
|
||||||
import sectionUtils from './sectionUtils';
|
import sectionUtils from './sectionUtils';
|
||||||
import extensionSvc from './extensionSvc';
|
import extensionSvc from './extensionSvc';
|
||||||
import animationSvc from './animationSvc';
|
import editorSvcDiscussions from './editorSvcDiscussions';
|
||||||
import editorEngineSvc from './editorEngineSvc';
|
import editorSvcUtils from './editorSvcUtils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
|
||||||
const debounce = cledit.Utils.debounce;
|
const debounce = cledit.Utils.debounce;
|
||||||
@ -30,14 +30,15 @@ const allowDebounce = (action, wait) => {
|
|||||||
const diffMatchPatch = new DiffMatchPatch();
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
let instantPreview = true;
|
let instantPreview = true;
|
||||||
let tokens;
|
let tokens;
|
||||||
const anchorHash = {};
|
|
||||||
|
|
||||||
const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event bus
|
// Use a vue instance as an event bus
|
||||||
|
const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, {
|
||||||
// Elements
|
// Elements
|
||||||
editorElt: null,
|
editorElt: null,
|
||||||
previewElt: null,
|
previewElt: null,
|
||||||
tocElt: null,
|
tocElt: null,
|
||||||
// Other objects
|
// Other objects
|
||||||
|
clEditor: null,
|
||||||
pagedownEditor: null,
|
pagedownEditor: null,
|
||||||
options: null,
|
options: null,
|
||||||
prismGrammars: null,
|
prismGrammars: null,
|
||||||
@ -54,99 +55,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
previewHtml: null,
|
previewHtml: null,
|
||||||
previewText: null,
|
previewText: null,
|
||||||
|
|
||||||
/**
|
|
||||||
* Get element and dimension that handles scrolling.
|
|
||||||
*/
|
|
||||||
getObjectToScroll() {
|
|
||||||
let elt = this.editorElt.parentNode;
|
|
||||||
let dimensionKey = 'editorDimension';
|
|
||||||
if (!store.getters['layout/styles'].showEditor) {
|
|
||||||
elt = this.previewElt.parentNode;
|
|
||||||
dimensionKey = 'previewDimension';
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
elt,
|
|
||||||
dimensionKey,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an object describing the position of the scroll bar in the file.
|
|
||||||
*/
|
|
||||||
getScrollPosition() {
|
|
||||||
const objToScroll = this.getObjectToScroll();
|
|
||||||
const scrollTop = objToScroll.elt.scrollTop;
|
|
||||||
let result;
|
|
||||||
if (this.sectionDescMeasuredList) {
|
|
||||||
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => {
|
|
||||||
if (scrollTop >= sectionDesc[objToScroll.dimensionKey].endOffset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const posInSection = (scrollTop - sectionDesc[objToScroll.dimensionKey].startOffset) /
|
|
||||||
(sectionDesc[objToScroll.dimensionKey].height || 1);
|
|
||||||
result = {
|
|
||||||
sectionIdx,
|
|
||||||
posInSection,
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the offset in the preview corresponding to the offset of the markdown in the editor
|
|
||||||
*/
|
|
||||||
getPreviewOffset(editorOffset) {
|
|
||||||
let previewOffset = 0;
|
|
||||||
let offset = editorOffset;
|
|
||||||
this.sectionDescList.some((sectionDesc) => {
|
|
||||||
if (!sectionDesc.textToPreviewDiffs) {
|
|
||||||
previewOffset = undefined;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (sectionDesc.section.text.length >= offset) {
|
|
||||||
previewOffset += diffMatchPatch.diff_xIndex(sectionDesc.textToPreviewDiffs, offset);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
offset -= sectionDesc.section.text.length;
|
|
||||||
previewOffset += sectionDesc.previewText.length;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
return previewOffset;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the offset of the markdown in the editor corresponding to the offset in the preview
|
|
||||||
*/
|
|
||||||
getEditorOffset(previewOffset) {
|
|
||||||
let offset = previewOffset;
|
|
||||||
let editorOffset = 0;
|
|
||||||
this.sectionDescList.some((sectionDesc) => {
|
|
||||||
if (!sectionDesc.textToPreviewDiffs) {
|
|
||||||
editorOffset = undefined;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (sectionDesc.previewText.length >= offset) {
|
|
||||||
const previewToTextDiffs = sectionDesc.textToPreviewDiffs
|
|
||||||
.map(diff => [-diff[0], diff[1]]);
|
|
||||||
editorOffset += diffMatchPatch.diff_xIndex(previewToTextDiffs, offset);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
offset -= sectionDesc.previewText.length;
|
|
||||||
editorOffset += sectionDesc.section.text.length;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
return editorOffset;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the pandoc AST generated from the file tokens and the converter options
|
|
||||||
*/
|
|
||||||
getPandocAst() {
|
|
||||||
return tokens && markdownItPandocRenderer(tokens, this.converter.options);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Prism grammar with the options
|
* Initialize the Prism grammar with the options
|
||||||
*/
|
*/
|
||||||
@ -183,7 +91,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
return 0.15;
|
return 0.15;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
editorEngineSvc.initClEditor(options);
|
this.initClEditorInternal(options);
|
||||||
this.restoreScrollPosition();
|
this.restoreScrollPosition();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -276,6 +184,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
});
|
});
|
||||||
this.sectionDescList = newSectionDescList;
|
this.sectionDescList = newSectionDescList;
|
||||||
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
||||||
|
this.$emit('previewHtml', this.previewHtml);
|
||||||
this.tocElt.classList[
|
this.tocElt.classList[
|
||||||
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
|
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
|
||||||
]('toc-tab--empty');
|
]('toc-tab--empty');
|
||||||
@ -306,7 +215,9 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
* Measure the height of each section in editor, preview and toc.
|
* Measure the height of each section in editor, preview and toc.
|
||||||
*/
|
*/
|
||||||
measureSectionDimensions: allowDebounce((restoreScrollPosition) => {
|
measureSectionDimensions: allowDebounce((restoreScrollPosition) => {
|
||||||
if (editorSvc.sectionDescList && this.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
if (editorSvc.sectionDescList &&
|
||||||
|
this.sectionDescList !== editorSvc.sectionDescMeasuredList
|
||||||
|
) {
|
||||||
sectionUtils.measureSectionDimensions(editorSvc);
|
sectionUtils.measureSectionDimensions(editorSvc);
|
||||||
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList;
|
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList;
|
||||||
if (restoreScrollPosition) {
|
if (restoreScrollPosition) {
|
||||||
@ -321,7 +232,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
*/
|
*/
|
||||||
makeTextToPreviewDiffs: allowDebounce(() => {
|
makeTextToPreviewDiffs: allowDebounce(() => {
|
||||||
if (editorSvc.sectionDescList &&
|
if (editorSvc.sectionDescList &&
|
||||||
editorSvc.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
editorSvc.sectionDescList !== editorSvc.sectionDescWithDiffsList
|
||||||
|
) {
|
||||||
editorSvc.sectionDescList
|
editorSvc.sectionDescList
|
||||||
.forEach((sectionDesc) => {
|
.forEach((sectionDesc) => {
|
||||||
if (!sectionDesc.textToPreviewDiffs) {
|
if (!sectionDesc.textToPreviewDiffs) {
|
||||||
@ -330,7 +242,9 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
sectionDesc.section.text, sectionDesc.previewText);
|
sectionDesc.section.text, sectionDesc.previewText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
editorSvc.previewTextWithDiffsList = editorSvc.previewText;
|
||||||
editorSvc.sectionDescWithDiffsList = editorSvc.sectionDescList;
|
editorSvc.sectionDescWithDiffsList = editorSvc.sectionDescList;
|
||||||
|
editorSvc.$emit('sectionDescWithDiffsList', editorSvc.sectionDescWithDiffsList);
|
||||||
}
|
}
|
||||||
}, 50),
|
}, 50),
|
||||||
|
|
||||||
@ -341,28 +255,12 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
const scrollPosition = editorSvc.getScrollPosition() ||
|
const scrollPosition = editorSvc.getScrollPosition() ||
|
||||||
store.getters['contentState/current'].scrollPosition;
|
store.getters['contentState/current'].scrollPosition;
|
||||||
store.dispatch('contentState/patchCurrent', {
|
store.dispatch('contentState/patchCurrent', {
|
||||||
selectionStart: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
selectionStart: editorSvc.clEditor.selectionMgr.selectionStart,
|
||||||
selectionEnd: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
selectionEnd: editorSvc.clEditor.selectionMgr.selectionEnd,
|
||||||
scrollPosition,
|
scrollPosition,
|
||||||
});
|
});
|
||||||
}, 100),
|
}, 100),
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the scroll position from the current file content state.
|
|
||||||
*/
|
|
||||||
restoreScrollPosition() {
|
|
||||||
const scrollPosition = store.getters['contentState/current'].scrollPosition;
|
|
||||||
if (scrollPosition && this.sectionDescMeasuredList) {
|
|
||||||
const objectToScroll = this.getObjectToScroll();
|
|
||||||
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
|
||||||
if (sectionDesc) {
|
|
||||||
const scrollTop = sectionDesc[objectToScroll.dimensionKey].startOffset +
|
|
||||||
(sectionDesc[objectToScroll.dimensionKey].height * scrollPosition.posInSection);
|
|
||||||
objectToScroll.elt.scrollTop = Math.floor(scrollTop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report selection from the preview to the editor.
|
* Report selection from the preview to the editor.
|
||||||
*/
|
*/
|
||||||
@ -392,8 +290,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
previewSelectionEndOffset = previewSelectionStartOffset + `${range}`.length;
|
previewSelectionEndOffset = previewSelectionStartOffset + `${range}`.length;
|
||||||
const editorStartOffset = editorSvc.getEditorOffset(previewSelectionStartOffset);
|
const editorStartOffset = editorSvc.getEditorOffset(previewSelectionStartOffset);
|
||||||
const editorEndOffset = editorSvc.getEditorOffset(previewSelectionEndOffset);
|
const editorEndOffset = editorSvc.getEditorOffset(previewSelectionEndOffset);
|
||||||
if (editorStartOffset !== undefined && editorEndOffset !== undefined) {
|
if (editorStartOffset != null && editorEndOffset != null) {
|
||||||
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
editorSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||||
editorStartOffset, editorEndOffset);
|
editorStartOffset, editorEndOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -403,35 +301,10 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
}, 50),
|
}, 50),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll the preview (or the editor if preview is hidden) to the specified anchor
|
* Returns the pandoc AST generated from the file tokens and the converter options
|
||||||
*/
|
*/
|
||||||
scrollToAnchor(anchor) {
|
getPandocAst() {
|
||||||
let scrollTop = 0;
|
return tokens && markdownItPandocRenderer(tokens, this.converter.options);
|
||||||
let scrollerElt = this.previewElt.parentNode;
|
|
||||||
const sectionDesc = anchorHash[anchor];
|
|
||||||
if (sectionDesc) {
|
|
||||||
if (store.getters['layout/styles'].showPreview) {
|
|
||||||
scrollTop = sectionDesc.previewDimension.startOffset;
|
|
||||||
} else {
|
|
||||||
scrollTop = sectionDesc.editorDimension.startOffset;
|
|
||||||
scrollerElt = this.editorElt.parentNode;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const elt = document.getElementById(anchor);
|
|
||||||
if (elt) {
|
|
||||||
scrollTop = elt.offsetTop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const maxScrollTop = scrollerElt.scrollHeight - scrollerElt.offsetHeight;
|
|
||||||
if (scrollTop < 0) {
|
|
||||||
scrollTop = 0;
|
|
||||||
} else if (scrollTop > maxScrollTop) {
|
|
||||||
scrollTop = maxScrollTop;
|
|
||||||
}
|
|
||||||
animationSvc.animate(scrollerElt)
|
|
||||||
.scrollTop(scrollTop)
|
|
||||||
.duration(360)
|
|
||||||
.start();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -442,26 +315,26 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
this.previewElt = previewElt;
|
this.previewElt = previewElt;
|
||||||
this.tocElt = tocElt;
|
this.tocElt = tocElt;
|
||||||
|
|
||||||
editorEngineSvc.createClEditor(editorElt);
|
this.createClEditor(editorElt);
|
||||||
editorEngineSvc.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
this.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||||
const parsingCtx = {
|
const parsingCtx = {
|
||||||
...this.parsingCtx,
|
...this.parsingCtx,
|
||||||
sectionList,
|
sectionList,
|
||||||
};
|
};
|
||||||
this.parsingCtx = parsingCtx;
|
this.parsingCtx = parsingCtx;
|
||||||
});
|
});
|
||||||
editorEngineSvc.clEditor.undoMgr.on('undoStateChange', () => {
|
this.clEditor.undoMgr.on('undoStateChange', () => {
|
||||||
const canUndo = editorEngineSvc.clEditor.undoMgr.canUndo();
|
const canUndo = this.clEditor.undoMgr.canUndo();
|
||||||
if (canUndo !== store.state.layout.canUndo) {
|
if (canUndo !== store.state.layout.canUndo) {
|
||||||
store.commit('layout/setCanUndo', canUndo);
|
store.commit('layout/setCanUndo', canUndo);
|
||||||
}
|
}
|
||||||
const canRedo = editorEngineSvc.clEditor.undoMgr.canRedo();
|
const canRedo = this.clEditor.undoMgr.canRedo();
|
||||||
if (canRedo !== store.state.layout.canRedo) {
|
if (canRedo !== store.state.layout.canRedo) {
|
||||||
store.commit('layout/setCanRedo', canRedo);
|
store.commit('layout/setCanRedo', canRedo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.pagedownEditor = pagedown({
|
this.pagedownEditor = pagedown({
|
||||||
input: Object.create(editorEngineSvc.clEditor),
|
input: Object.create(this.clEditor),
|
||||||
});
|
});
|
||||||
this.pagedownEditor.run();
|
this.pagedownEditor.run();
|
||||||
this.pagedownEditor.hooks.set('insertLinkDialog', (callback) => {
|
this.pagedownEditor.hooks.set('insertLinkDialog', (callback) => {
|
||||||
@ -513,7 +386,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
this.saveContentState();
|
this.saveContentState();
|
||||||
};
|
};
|
||||||
|
|
||||||
editorEngineSvc.clEditor.selectionMgr.on('selectionChanged',
|
this.clEditor.selectionMgr.on('selectionChanged',
|
||||||
(start, end, selectionRange) => onEditorChanged(undefined, selectionRange));
|
(start, end, selectionRange) => onEditorChanged(undefined, selectionRange));
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
@ -561,7 +434,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
|
|
||||||
let imgEltsToCache = [];
|
let imgEltsToCache = [];
|
||||||
if (store.getters['data/computedSettings'].editor.inlineImages) {
|
if (store.getters['data/computedSettings'].editor.inlineImages) {
|
||||||
editorEngineSvc.clEditor.highlighter.on('sectionHighlighted', (section) => {
|
this.clEditor.highlighter.on('sectionHighlighted', (section) => {
|
||||||
section.elt.getElementsByClassName('token img').cl_each((imgTokenElt) => {
|
section.elt.getElementsByClassName('token img').cl_each((imgTokenElt) => {
|
||||||
const srcElt = imgTokenElt.querySelector('.token.cl-src');
|
const srcElt = imgTokenElt.querySelector('.token.cl-src');
|
||||||
if (srcElt) {
|
if (srcElt) {
|
||||||
@ -587,7 +460,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
editorEngineSvc.clEditor.highlighter.on('highlighted', () => {
|
this.clEditor.highlighter.on('highlighted', () => {
|
||||||
imgEltsToCache.forEach((imgElt) => {
|
imgEltsToCache.forEach((imgElt) => {
|
||||||
const cachedImgElt = getFromImgCache(imgElt.src);
|
const cachedImgElt = getFromImgCache(imgElt.src);
|
||||||
if (cachedImgElt) {
|
if (cachedImgElt) {
|
||||||
@ -602,7 +475,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
triggerImgCacheGc();
|
triggerImgCacheGc();
|
||||||
});
|
});
|
||||||
|
|
||||||
editorEngineSvc.clEditor.on('contentChanged',
|
this.clEditor.on('contentChanged',
|
||||||
(content, diffs, sectionList) => onEditorChanged(sectionList));
|
(content, diffs, sectionList) => onEditorChanged(sectionList));
|
||||||
|
|
||||||
this.$emit('inited');
|
this.$emit('inited');
|
||||||
@ -636,18 +509,18 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
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(editorSvc.options)) {
|
if (JSON.stringify(options) !== JSON.stringify(this.options)) {
|
||||||
editorSvc.options = options;
|
this.options = options;
|
||||||
editorSvc.initPrism();
|
this.initPrism();
|
||||||
editorSvc.initConverter();
|
this.initConverter();
|
||||||
initClEditor = true;
|
initClEditor = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (initClEditor) {
|
if (initClEditor) {
|
||||||
editorSvc.initClEditor();
|
this.initClEditor();
|
||||||
}
|
}
|
||||||
// Apply possible text and discussion changes
|
// Apply possible text and discussion changes
|
||||||
editorEngineSvc.applyContent();
|
this.applyContent();
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
@ -655,12 +528,14 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
// Disable editor if hidden or if no content is loaded
|
// Disable editor if hidden or if no content is loaded
|
||||||
store.watch(
|
store.watch(
|
||||||
() => store.getters['content/current'].id && store.getters['layout/styles'].showEditor,
|
() => store.getters['content/current'].id && store.getters['layout/styles'].showEditor,
|
||||||
editable => editorEngineSvc.clEditor.toggleEditable(!!editable), {
|
editable => this.clEditor.toggleEditable(!!editable), {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
store.watch(() => store.getters['layout/styles'],
|
store.watch(() => store.getters['layout/styles'],
|
||||||
() => editorSvc.measureSectionDimensions(false, true));
|
() => this.measureSectionDimensions(false, true));
|
||||||
|
|
||||||
|
this.initHighlighters();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
257
src/services/editorSvcDiscussions.js
Normal file
257
src/services/editorSvcDiscussions.js
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
|
import cledit from '../libs/cledit';
|
||||||
|
import utils from './utils';
|
||||||
|
import diffUtils from './diffUtils';
|
||||||
|
import store from '../store';
|
||||||
|
import EditorClassApplier from '../components/common/EditorClassApplier';
|
||||||
|
import PreviewClassApplier from '../components/common/PreviewClassApplier';
|
||||||
|
|
||||||
|
let clEditor;
|
||||||
|
let discussionMarkers = {};
|
||||||
|
let markerKeys;
|
||||||
|
let markerIdxMap;
|
||||||
|
let previousPatchableText;
|
||||||
|
let currentPatchableText;
|
||||||
|
let isChangePatch;
|
||||||
|
let contentId;
|
||||||
|
let editorClassAppliers = {};
|
||||||
|
let previewClassAppliers = {};
|
||||||
|
|
||||||
|
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
||||||
|
function getMarker(offsetName) {
|
||||||
|
const markerKey = `${discussionId}:${offsetName}`;
|
||||||
|
let marker = discussionMarkers[markerKey];
|
||||||
|
if (!marker) {
|
||||||
|
marker = new cledit.Marker(discussion[offsetName], offsetName === 'end');
|
||||||
|
marker.discussionId = discussionId;
|
||||||
|
marker.offsetName = offsetName;
|
||||||
|
clEditor.addMarker(marker);
|
||||||
|
discussionMarkers[markerKey] = marker;
|
||||||
|
}
|
||||||
|
onMarker(marker);
|
||||||
|
}
|
||||||
|
getMarker('start');
|
||||||
|
getMarker('end');
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDiscussionMarkers(content, writeOffsets) {
|
||||||
|
const discussions = {
|
||||||
|
...content.discussions,
|
||||||
|
};
|
||||||
|
const newDiscussion = store.getters['discussion/newDiscussion'];
|
||||||
|
if (newDiscussion) {
|
||||||
|
discussions[store.state.discussion.newDiscussionId] = {
|
||||||
|
...newDiscussion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Object.keys(discussionMarkers).forEach((markerKey) => {
|
||||||
|
const marker = discussionMarkers[markerKey];
|
||||||
|
// Remove marker if discussion was removed
|
||||||
|
const discussion = discussions[marker.discussionId];
|
||||||
|
if (!discussion) {
|
||||||
|
clEditor.removeMarker(marker);
|
||||||
|
delete discussionMarkers[markerKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(discussions).forEach((discussionId) => {
|
||||||
|
const discussion = discussions[discussionId];
|
||||||
|
getDiscussionMarkers(discussion, discussionId, writeOffsets
|
||||||
|
? (marker) => {
|
||||||
|
discussion[marker.offsetName] = marker.offset;
|
||||||
|
}
|
||||||
|
: (marker) => {
|
||||||
|
marker.offset = discussion[marker.offsetName];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (writeOffsets && newDiscussion) {
|
||||||
|
store.commit('discussion/patchNewDiscussion',
|
||||||
|
discussions[store.state.discussion.newDiscussionId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDiscussionMarkers() {
|
||||||
|
Object.keys(discussionMarkers).forEach((markerKey) => {
|
||||||
|
clEditor.removeMarker(discussionMarkers[markerKey]);
|
||||||
|
});
|
||||||
|
discussionMarkers = {};
|
||||||
|
markerKeys = [];
|
||||||
|
markerIdxMap = Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
|
||||||
|
function makePatches() {
|
||||||
|
const diffs = diffMatchPatch.diff_main(previousPatchableText, currentPatchableText);
|
||||||
|
return diffMatchPatch.patch_make(previousPatchableText, diffs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatches(patches) {
|
||||||
|
const newPatchableText = diffMatchPatch.patch_apply(patches, currentPatchableText)[0];
|
||||||
|
let result = newPatchableText;
|
||||||
|
if (markerKeys.length) {
|
||||||
|
// Strip text markers
|
||||||
|
result = result.replace(new RegExp(`[\ue000-${String.fromCharCode((0xe000 + markerKeys.length) - 1)}]`, 'g'), '');
|
||||||
|
}
|
||||||
|
// Expect a `contentChanged` event
|
||||||
|
if (result !== clEditor.getContent()) {
|
||||||
|
previousPatchableText = currentPatchableText;
|
||||||
|
currentPatchableText = newPatchableText;
|
||||||
|
isChangePatch = true;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reversePatches(patches) {
|
||||||
|
const result = diffMatchPatch.patch_deepCopy(patches).reverse();
|
||||||
|
result.forEach((patch) => {
|
||||||
|
patch.diffs.forEach((diff) => {
|
||||||
|
diff[0] = -diff[0];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createClEditor(editorElt) {
|
||||||
|
this.clEditor = cledit(editorElt, editorElt.parentNode);
|
||||||
|
clEditor = this.clEditor;
|
||||||
|
removeDiscussionMarkers();
|
||||||
|
clEditor.on('contentChanged', (text) => {
|
||||||
|
const oldContent = store.getters['content/current'];
|
||||||
|
const newContent = {
|
||||||
|
...utils.deepCopy(oldContent),
|
||||||
|
text: utils.sanitizeText(text),
|
||||||
|
};
|
||||||
|
syncDiscussionMarkers(newContent, true);
|
||||||
|
if (!isChangePatch) {
|
||||||
|
previousPatchableText = currentPatchableText;
|
||||||
|
currentPatchableText = diffUtils.makePatchableText(newContent, markerKeys, markerIdxMap);
|
||||||
|
} else {
|
||||||
|
// Take a chance to restore discussion offsets on undo/redo
|
||||||
|
diffUtils.restoreDiscussionOffsets(newContent, markerKeys);
|
||||||
|
syncDiscussionMarkers(newContent, false);
|
||||||
|
}
|
||||||
|
store.dispatch('content/patchCurrent', newContent);
|
||||||
|
isChangePatch = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
initClEditorInternal(opts) {
|
||||||
|
const content = store.getters['content/current'];
|
||||||
|
if (content) {
|
||||||
|
const contentState = store.getters['contentState/current'];
|
||||||
|
const options = Object.assign({
|
||||||
|
selectionStart: contentState.selectionStart,
|
||||||
|
selectionEnd: contentState.selectionEnd,
|
||||||
|
patchHandler: {
|
||||||
|
makePatches,
|
||||||
|
applyPatches,
|
||||||
|
reversePatches,
|
||||||
|
},
|
||||||
|
}, opts);
|
||||||
|
|
||||||
|
if (contentId !== content.id) {
|
||||||
|
contentId = content.id;
|
||||||
|
currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||||
|
previousPatchableText = currentPatchableText;
|
||||||
|
syncDiscussionMarkers(content, false);
|
||||||
|
options.content = content.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
clEditor.init(options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyContent() {
|
||||||
|
if (clEditor) {
|
||||||
|
const content = store.getters['content/current'];
|
||||||
|
if (clEditor.setContent(content.text, true).range) {
|
||||||
|
// Marker will be recreated on contentChange
|
||||||
|
removeDiscussionMarkers();
|
||||||
|
} else {
|
||||||
|
syncDiscussionMarkers(content, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getTrimmedSelection() {
|
||||||
|
const selectionMgr = clEditor.selectionMgr;
|
||||||
|
let start = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||||
|
let end = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||||
|
const text = clEditor.getContent();
|
||||||
|
while ((text[start] || '').match(/\s/)) {
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
while ((text[end - 1] || '').match(/\s/)) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
return start < end && { start, end };
|
||||||
|
},
|
||||||
|
initHighlighters() {
|
||||||
|
store.watch(
|
||||||
|
() => store.getters['discussion/newDiscussion'],
|
||||||
|
() => syncDiscussionMarkers(store.getters['content/current'], false),
|
||||||
|
);
|
||||||
|
|
||||||
|
store.watch(
|
||||||
|
() => store.getters['discussion/currentFileDiscussions'],
|
||||||
|
(discussions) => {
|
||||||
|
// Editor class appliers
|
||||||
|
const oldEditorClassAppliers = editorClassAppliers;
|
||||||
|
editorClassAppliers = {};
|
||||||
|
Object.keys(discussions).forEach((discussionId) => {
|
||||||
|
const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
|
||||||
|
() => {
|
||||||
|
const classes = [`discussion-editor-highlighting-${discussionId}`, 'discussion-editor-highlighting'];
|
||||||
|
if (store.state.discussion.currentDiscussionId === discussionId) {
|
||||||
|
classes.push('discussion-editor-highlighting--selected');
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const startMarker = discussionMarkers[`${discussionId}:start`];
|
||||||
|
const endMarker = discussionMarkers[`${discussionId}:end`];
|
||||||
|
return {
|
||||||
|
start: startMarker.offset,
|
||||||
|
end: endMarker.offset,
|
||||||
|
};
|
||||||
|
}, { discussionId });
|
||||||
|
editorClassAppliers[discussionId] = classApplier;
|
||||||
|
});
|
||||||
|
Object.keys(oldEditorClassAppliers).forEach((discussionId) => {
|
||||||
|
if (!editorClassAppliers[discussionId]) {
|
||||||
|
oldEditorClassAppliers[discussionId].stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preview class appliers
|
||||||
|
const oldPreviewClassAppliers = previewClassAppliers;
|
||||||
|
previewClassAppliers = {};
|
||||||
|
Object.keys(discussions).forEach((discussionId) => {
|
||||||
|
const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
|
||||||
|
() => {
|
||||||
|
const classes = [`discussion-preview-highlighting-${discussionId}`, 'discussion-preview-highlighting'];
|
||||||
|
if (store.state.discussion.currentDiscussionId === discussionId) {
|
||||||
|
classes.push('discussion-preview-highlighting--selected');
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const startMarker = discussionMarkers[`${discussionId}:start`];
|
||||||
|
const endMarker = discussionMarkers[`${discussionId}:end`];
|
||||||
|
return {
|
||||||
|
start: this.getPreviewOffset(startMarker.offset),
|
||||||
|
end: this.getPreviewOffset(endMarker.offset),
|
||||||
|
};
|
||||||
|
}, { discussionId });
|
||||||
|
previewClassAppliers[discussionId] = classApplier;
|
||||||
|
});
|
||||||
|
Object.keys(oldPreviewClassAppliers).forEach((discussionId) => {
|
||||||
|
if (!previewClassAppliers[discussionId]) {
|
||||||
|
oldPreviewClassAppliers[discussionId].stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
129
src/services/editorSvcUtils.js
Normal file
129
src/services/editorSvcUtils.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
|
import animationSvc from './animationSvc';
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Get element and dimension that handles scrolling.
|
||||||
|
*/
|
||||||
|
getObjectToScroll() {
|
||||||
|
let elt = this.editorElt.parentNode;
|
||||||
|
let dimensionKey = 'editorDimension';
|
||||||
|
if (!store.getters['layout/styles'].showEditor) {
|
||||||
|
elt = this.previewElt.parentNode;
|
||||||
|
dimensionKey = 'previewDimension';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
elt,
|
||||||
|
dimensionKey,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object describing the position of the scroll bar in the file.
|
||||||
|
*/
|
||||||
|
getScrollPosition() {
|
||||||
|
const objToScroll = this.getObjectToScroll();
|
||||||
|
const scrollTop = objToScroll.elt.scrollTop;
|
||||||
|
let result;
|
||||||
|
if (this.sectionDescMeasuredList) {
|
||||||
|
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => {
|
||||||
|
if (scrollTop >= sectionDesc[objToScroll.dimensionKey].endOffset) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const posInSection = (scrollTop - sectionDesc[objToScroll.dimensionKey].startOffset) /
|
||||||
|
(sectionDesc[objToScroll.dimensionKey].height || 1);
|
||||||
|
result = {
|
||||||
|
sectionIdx,
|
||||||
|
posInSection,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the scroll position from the current file content state.
|
||||||
|
*/
|
||||||
|
restoreScrollPosition() {
|
||||||
|
const scrollPosition = store.getters['contentState/current'].scrollPosition;
|
||||||
|
if (scrollPosition && this.sectionDescMeasuredList) {
|
||||||
|
const objectToScroll = this.getObjectToScroll();
|
||||||
|
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
||||||
|
if (sectionDesc) {
|
||||||
|
const scrollTop = sectionDesc[objectToScroll.dimensionKey].startOffset +
|
||||||
|
(sectionDesc[objectToScroll.dimensionKey].height * scrollPosition.posInSection);
|
||||||
|
objectToScroll.elt.scrollTop = Math.floor(scrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the offset in the preview corresponding to the offset of the markdown in the editor
|
||||||
|
*/
|
||||||
|
getPreviewOffset(editorOffset) {
|
||||||
|
if (!this.sectionDescWithDiffsList) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let offset = editorOffset;
|
||||||
|
let previewOffset = 0;
|
||||||
|
this.sectionDescWithDiffsList.some((sectionDesc) => {
|
||||||
|
if (sectionDesc.section.text.length >= offset) {
|
||||||
|
previewOffset += diffMatchPatch.diff_xIndex(sectionDesc.textToPreviewDiffs, offset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
offset -= sectionDesc.section.text.length;
|
||||||
|
previewOffset += sectionDesc.previewText.length;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return previewOffset;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the offset of the markdown in the editor corresponding to the offset in the preview
|
||||||
|
*/
|
||||||
|
getEditorOffset(previewOffset) {
|
||||||
|
if (!this.sectionDescWithDiffsList) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let offset = previewOffset;
|
||||||
|
let editorOffset = 0;
|
||||||
|
this.sectionDescWithDiffsList.some((sectionDesc) => {
|
||||||
|
if (sectionDesc.previewText.length >= offset) {
|
||||||
|
const previewToTextDiffs = sectionDesc.textToPreviewDiffs
|
||||||
|
.map(diff => [-diff[0], diff[1]]);
|
||||||
|
editorOffset += diffMatchPatch.diff_xIndex(previewToTextDiffs, offset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
offset -= sectionDesc.previewText.length;
|
||||||
|
editorOffset += sectionDesc.section.text.length;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return editorOffset;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll the preview (or the editor if preview is hidden) to the specified anchor
|
||||||
|
*/
|
||||||
|
scrollToAnchor(anchor) {
|
||||||
|
let scrollTop = 0;
|
||||||
|
const scrollerElt = this.previewElt.parentNode;
|
||||||
|
const elt = document.getElementById(anchor);
|
||||||
|
if (elt) {
|
||||||
|
scrollTop = elt.offsetTop;
|
||||||
|
}
|
||||||
|
const maxScrollTop = scrollerElt.scrollHeight - scrollerElt.offsetHeight;
|
||||||
|
if (scrollTop < 0) {
|
||||||
|
scrollTop = 0;
|
||||||
|
} else if (scrollTop > maxScrollTop) {
|
||||||
|
scrollTop = maxScrollTop;
|
||||||
|
}
|
||||||
|
animationSvc.animate(scrollerElt)
|
||||||
|
.scrollTop(scrollTop)
|
||||||
|
.duration(360)
|
||||||
|
.start();
|
||||||
|
},
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
import cledit from '../../libs/cledit';
|
import cledit from '../../libs/cledit';
|
||||||
import editorSvc from '../editorSvc';
|
import editorSvc from '../editorSvc';
|
||||||
import editorEngineSvc from '../editorEngineSvc';
|
|
||||||
|
|
||||||
const Keystroke = cledit.Keystroke;
|
const Keystroke = cledit.Keystroke;
|
||||||
const indentRegexp = /^ {0,3}>[ ]*|^[ \t]*[*+-][ \t]|^([ \t]*)\d+\.[ \t]|^\s+/;
|
const indentRegexp = /^ {0,3}>[ ]*|^[ \t]*[*+-][ \t]|^([ \t]*)\d+\.[ \t]|^\s+/;
|
||||||
@ -116,7 +115,7 @@ function enterKeyHandler(evt, state) {
|
|||||||
clearNewline = true;
|
clearNewline = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
editorEngineSvc.clEditor.undoMgr.setCurrentMode('single');
|
editorSvc.clEditor.undoMgr.setCurrentMode('single');
|
||||||
|
|
||||||
state.before += `\n${indent}`;
|
state.before += `\n${indent}`;
|
||||||
state.selection = '';
|
state.selection = '';
|
||||||
@ -169,6 +168,6 @@ function tabKeyHandler(evt, state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editorSvc.$on('inited', () => {
|
editorSvc.$on('inited', () => {
|
||||||
editorEngineSvc.clEditor.addKeystroke(new Keystroke(enterKeyHandler, 50));
|
editorSvc.clEditor.addKeystroke(new Keystroke(enterKeyHandler, 50));
|
||||||
editorEngineSvc.clEditor.addKeystroke(new Keystroke(tabKeyHandler, 50));
|
editorSvc.clEditor.addKeystroke(new Keystroke(tabKeyHandler, 50));
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import editorEngineSvc from '../../services/editorEngineSvc';
|
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
|
|
||||||
// Skip shortcuts if modal is open or editor is hidden
|
// Skip shortcuts if modal is open or editor is hidden
|
||||||
@ -16,8 +15,8 @@ const pagedownHandler = name => () => {
|
|||||||
const findReplaceOpener = type => () => {
|
const findReplaceOpener = type => () => {
|
||||||
store.dispatch('findReplace/open', {
|
store.dispatch('findReplace/open', {
|
||||||
type,
|
type,
|
||||||
findText: editorEngineSvc.clEditor.selectionMgr.hasFocus() &&
|
findText: editorSvc.clEditor.selectionMgr.hasFocus() &&
|
||||||
editorEngineSvc.clEditor.selectionMgr.getSelectedText(),
|
editorSvc.clEditor.selectionMgr.getSelectedText(),
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@ -46,7 +45,7 @@ const methods = {
|
|||||||
const replacement = `${param2 || ''}`;
|
const replacement = `${param2 || ''}`;
|
||||||
if (text && replacement) {
|
if (text && replacement) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
const selectionMgr = editorSvc.clEditor.selectionMgr;
|
||||||
let offset = selectionMgr.selectionStart;
|
let offset = selectionMgr.selectionStart;
|
||||||
if (offset === selectionMgr.selectionEnd) {
|
if (offset === selectionMgr.selectionEnd) {
|
||||||
const range = selectionMgr.createRange(offset - text.length, offset);
|
const range = selectionMgr.createRange(offset - text.length, offset);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptyContent';
|
import empty from '../data/emptyContent';
|
||||||
import utils from '../../services/utils';
|
import utils from '../services/utils';
|
||||||
|
|
||||||
const module = moduleTemplate(empty);
|
const module = moduleTemplate(empty);
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptyContentState';
|
import empty from '../data/emptyContentState';
|
||||||
|
|
||||||
const module = moduleTemplate(empty, true);
|
const module = moduleTemplate(empty, true);
|
||||||
|
|
@ -1,13 +1,13 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import utils from '../../services/utils';
|
import utils from '../services/utils';
|
||||||
import defaultSettings from '../../data/defaultSettings.yml';
|
import defaultSettings from '../data/defaultSettings.yml';
|
||||||
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
import defaultLocalSettings from '../data/defaultLocalSettings';
|
||||||
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
import plainHtmlTemplate from '../data/plainHtmlTemplate.html';
|
||||||
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
import styledHtmlTemplate from '../data/styledHtmlTemplate.html';
|
||||||
import styledHtmlWithTocTemplate from '../../data/styledHtmlWithTocTemplate.html';
|
import styledHtmlWithTocTemplate from '../data/styledHtmlWithTocTemplate.html';
|
||||||
import jekyllSiteTemplate from '../../data/jekyllSiteTemplate.html';
|
import jekyllSiteTemplate from '../data/jekyllSiteTemplate.html';
|
||||||
|
|
||||||
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
||||||
|
|
51
src/store/discussion.js
Normal file
51
src/store/discussion.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import utils from '../services/utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
currentDiscussionId: null,
|
||||||
|
newDiscussion: null,
|
||||||
|
newDiscussionId: '',
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setCurrentDiscussionId: (state, value) => {
|
||||||
|
state.currentDiscussionId = value;
|
||||||
|
},
|
||||||
|
setNewDiscussion: (state, value) => {
|
||||||
|
state.newDiscussion = value;
|
||||||
|
state.newDiscussionId = utils.uid();
|
||||||
|
state.currentDiscussionId = state.newDiscussionId;
|
||||||
|
},
|
||||||
|
patchNewDiscussion: (state, value) => {
|
||||||
|
Object.assign(state.newDiscussion, value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
newDiscussion: state =>
|
||||||
|
state.currentDiscussionId === state.newDiscussionId && state.newDiscussion,
|
||||||
|
currentFileDiscussions: (state, getters, rootState, rootGetters) => {
|
||||||
|
const currentContent = rootGetters['content/current'];
|
||||||
|
const currentDiscussions = {
|
||||||
|
...currentContent.discussions,
|
||||||
|
};
|
||||||
|
const newDiscussion = getters.newDiscussion;
|
||||||
|
if (newDiscussion) {
|
||||||
|
currentDiscussions[state.newDiscussionId] = newDiscussion;
|
||||||
|
}
|
||||||
|
return currentDiscussions;
|
||||||
|
},
|
||||||
|
currentDiscussion: (state, getters) =>
|
||||||
|
getters.currentFileDiscussions[state.currentDiscussionId],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createNewDiscussion({ commit, rootGetters }, selection) {
|
||||||
|
if (selection) {
|
||||||
|
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
|
||||||
|
if (text.length > 250) {
|
||||||
|
text = `${text.slice(0, 249).trim()}…`;
|
||||||
|
}
|
||||||
|
commit('setNewDiscussion', { ...selection, text });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import emptyFile from '../../data/emptyFile';
|
import emptyFile from '../data/emptyFile';
|
||||||
import emptyFolder from '../../data/emptyFolder';
|
import emptyFolder from '../data/emptyFolder';
|
||||||
|
|
||||||
const setter = propertyName => (state, value) => {
|
const setter = propertyName => (state, value) => {
|
||||||
state[propertyName] = value;
|
state[propertyName] = value;
|
@ -1,5 +1,5 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptyFile';
|
import empty from '../data/emptyFile';
|
||||||
|
|
||||||
const module = moduleTemplate(empty);
|
const module = moduleTemplate(empty);
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptyFolder';
|
import empty from '../data/emptyFolder';
|
||||||
|
|
||||||
const module = moduleTemplate(empty);
|
const module = moduleTemplate(empty);
|
||||||
|
|
@ -2,27 +2,46 @@ import createLogger from 'vuex/dist/logger';
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
import contentState from './modules/contentState';
|
import contentState from './contentState';
|
||||||
import syncedContent from './modules/syncedContent';
|
import syncedContent from './syncedContent';
|
||||||
import content from './modules/content';
|
import content from './content';
|
||||||
import file from './modules/file';
|
import file from './file';
|
||||||
import findReplace from './modules/findReplace';
|
import findReplace from './findReplace';
|
||||||
import folder from './modules/folder';
|
import folder from './folder';
|
||||||
import publishLocation from './modules/publishLocation';
|
import publishLocation from './publishLocation';
|
||||||
import syncLocation from './modules/syncLocation';
|
import syncLocation from './syncLocation';
|
||||||
import data from './modules/data';
|
import data from './data';
|
||||||
import layout from './modules/layout';
|
import discussion from './discussion';
|
||||||
import explorer from './modules/explorer';
|
import layout from './layout';
|
||||||
import modal from './modules/modal';
|
import explorer from './explorer';
|
||||||
import notification from './modules/notification';
|
import modal from './modal';
|
||||||
import queue from './modules/queue';
|
import notification from './notification';
|
||||||
import userInfo from './modules/userInfo';
|
import queue from './queue';
|
||||||
|
import userInfo from './userInfo';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
const debug = NODE_ENV !== 'production';
|
const debug = NODE_ENV !== 'production';
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
contentState,
|
||||||
|
syncedContent,
|
||||||
|
content,
|
||||||
|
discussion,
|
||||||
|
file,
|
||||||
|
findReplace,
|
||||||
|
folder,
|
||||||
|
publishLocation,
|
||||||
|
syncLocation,
|
||||||
|
data,
|
||||||
|
layout,
|
||||||
|
explorer,
|
||||||
|
modal,
|
||||||
|
notification,
|
||||||
|
queue,
|
||||||
|
userInfo,
|
||||||
|
},
|
||||||
state: {
|
state: {
|
||||||
ready: false,
|
ready: false,
|
||||||
offline: false,
|
offline: false,
|
||||||
@ -98,23 +117,6 @@ const store = new Vuex.Store({
|
|||||||
.forEach(item => commit('publishLocation/deleteItem', item.id));
|
.forEach(item => commit('publishLocation/deleteItem', item.id));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modules: {
|
|
||||||
contentState,
|
|
||||||
syncedContent,
|
|
||||||
content,
|
|
||||||
file,
|
|
||||||
findReplace,
|
|
||||||
folder,
|
|
||||||
publishLocation,
|
|
||||||
syncLocation,
|
|
||||||
data,
|
|
||||||
layout,
|
|
||||||
explorer,
|
|
||||||
modal,
|
|
||||||
notification,
|
|
||||||
queue,
|
|
||||||
userInfo,
|
|
||||||
},
|
|
||||||
strict: debug,
|
strict: debug,
|
||||||
plugins: debug ? [createLogger()] : [],
|
plugins: debug ? [createLogger()] : [],
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const minPadding = 20;
|
const minPadding = 20;
|
||||||
const previewButtonWidth = 55;
|
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;
|
||||||
@ -20,7 +21,7 @@ const constants = {
|
|||||||
statusBarHeight: 20,
|
statusBarHeight: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
function computeStyles(state, localSettings, getters, styles = {
|
function computeStyles(state, getters, localSettings = getters['data/localSettings'], styles = {
|
||||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
||||||
showStatusBar: localSettings.showStatusBar,
|
showStatusBar: localSettings.showStatusBar,
|
||||||
showEditor: localSettings.showEditor,
|
showEditor: localSettings.showEditor,
|
||||||
@ -30,7 +31,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
showExplorer: localSettings.showExplorer,
|
showExplorer: localSettings.showExplorer,
|
||||||
layoutOverflow: false,
|
layoutOverflow: false,
|
||||||
}) {
|
}) {
|
||||||
styles.innerHeight = state.bodyHeight;
|
styles.innerHeight = state.layout.bodyHeight;
|
||||||
if (styles.showNavigationBar) {
|
if (styles.showNavigationBar) {
|
||||||
styles.innerHeight -= constants.navigationBarHeight;
|
styles.innerHeight -= constants.navigationBarHeight;
|
||||||
}
|
}
|
||||||
@ -38,7 +39,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
styles.innerHeight -= constants.statusBarHeight;
|
styles.innerHeight -= constants.statusBarHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
styles.innerWidth = state.bodyWidth;
|
styles.innerWidth = state.layout.bodyWidth;
|
||||||
if (styles.showSideBar) {
|
if (styles.showSideBar) {
|
||||||
styles.innerWidth -= constants.sideBarWidth;
|
styles.innerWidth -= constants.sideBarWidth;
|
||||||
}
|
}
|
||||||
@ -47,9 +48,12 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||||
|
const showGutter = getters['discussion/currentDiscussion'];
|
||||||
|
if (showGutter) {
|
||||||
|
doublePanelWidth -= gutterWidth;
|
||||||
|
}
|
||||||
if (doublePanelWidth < constants.editorMinWidth) {
|
if (doublePanelWidth < constants.editorMinWidth) {
|
||||||
doublePanelWidth = constants.editorMinWidth;
|
doublePanelWidth = constants.editorMinWidth;
|
||||||
styles.innerWidth = constants.editorMinWidth + constants.buttonBarWidth;
|
|
||||||
styles.layoutOverflow = true;
|
styles.layoutOverflow = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +61,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
styles.showSidePreview = false;
|
styles.showSidePreview = false;
|
||||||
styles.showPreview = false;
|
styles.showPreview = false;
|
||||||
styles.layoutOverflow = false;
|
styles.layoutOverflow = false;
|
||||||
return computeStyles(state, localSettings, getters, styles);
|
return computeStyles(state, getters, localSettings, styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
const computedSettings = getters['data/computedSettings'];
|
const computedSettings = getters['data/computedSettings'];
|
||||||
@ -83,10 +87,14 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
const panelWidth = Math.floor(doublePanelWidth / 2);
|
const panelWidth = Math.floor(doublePanelWidth / 2);
|
||||||
styles.previewWidth = styles.showSidePreview ?
|
styles.previewWidth = styles.showSidePreview ?
|
||||||
panelWidth :
|
panelWidth :
|
||||||
styles.innerWidth;
|
doublePanelWidth + constants.buttonBarWidth;
|
||||||
const previewLeftPadding = Math.max(
|
let previewRightPadding = Math.max(
|
||||||
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
|
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
|
||||||
let previewRightPadding = previewLeftPadding;
|
styles.previewGutterWidth = showGutter && !localSettings.showEditor
|
||||||
|
? gutterWidth
|
||||||
|
: 0;
|
||||||
|
const previewLeftPadding = previewRightPadding + styles.previewGutterWidth;
|
||||||
|
styles.previewGutterLeft = previewLeftPadding - minPadding;
|
||||||
if (!styles.showEditor && previewRightPadding < previewButtonWidth) {
|
if (!styles.showEditor && previewRightPadding < previewButtonWidth) {
|
||||||
previewRightPadding = previewButtonWidth;
|
previewRightPadding = previewButtonWidth;
|
||||||
}
|
}
|
||||||
@ -94,9 +102,14 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
styles.editorWidth = styles.showSidePreview ?
|
styles.editorWidth = styles.showSidePreview ?
|
||||||
panelWidth :
|
panelWidth :
|
||||||
doublePanelWidth;
|
doublePanelWidth;
|
||||||
const editorSidePadding = Math.max(
|
const editorRightPadding = Math.max(
|
||||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||||
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
styles.editorGutterWidth = showGutter && localSettings.showEditor
|
||||||
|
? gutterWidth
|
||||||
|
: 0;
|
||||||
|
const editorLeftPadding = editorRightPadding + styles.editorGutterWidth;
|
||||||
|
styles.editorGutterLeft = editorLeftPadding - minPadding;
|
||||||
|
styles.editorPadding = `${editorTopPadding}px ${editorRightPadding}px ${bottomPadding}px ${editorLeftPadding}px`;
|
||||||
|
|
||||||
styles.titleMaxWidth = styles.innerWidth -
|
styles.titleMaxWidth = styles.innerWidth -
|
||||||
navigationBarLeftButtonWidth -
|
navigationBarLeftButtonWidth -
|
||||||
@ -140,10 +153,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
constants: () => constants,
|
constants: () => constants,
|
||||||
styles: (state, getters, rootState, rootGetters) => {
|
styles: (state, getters, rootState, rootGetters) => computeStyles(rootState, rootGetters),
|
||||||
const localSettings = rootGetters['data/localSettings'];
|
|
||||||
return computeStyles(state, localSettings, rootGetters);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
updateBodySize({ commit, dispatch, rootGetters }) {
|
updateBodySize({ commit, dispatch, rootGetters }) {
|
@ -1,5 +1,5 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import utils from '../../services/utils';
|
import utils from '../services/utils';
|
||||||
|
|
||||||
export default (empty, simpleHash = false) => {
|
export default (empty, simpleHash = false) => {
|
||||||
// Use Date.now as a simple hash function, which is ok for not-synced types
|
// Use Date.now as a simple hash function, which is ok for not-synced types
|
@ -1,6 +1,6 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptyPublishLocation';
|
import empty from '../data/emptyPublishLocation';
|
||||||
import providerRegistry from '../../services/providers/providerRegistry';
|
import providerRegistry from '../services/providers/providerRegistry';
|
||||||
|
|
||||||
const module = moduleTemplate(empty);
|
const module = moduleTemplate(empty);
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptySyncLocation';
|
import empty from '../data/emptySyncLocation';
|
||||||
import providerRegistry from '../../services/providers/providerRegistry';
|
import providerRegistry from '../services/providers/providerRegistry';
|
||||||
|
|
||||||
const module = moduleTemplate(empty);
|
const module = moduleTemplate(empty);
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptySyncedContent';
|
import empty from '../data/emptySyncedContent';
|
||||||
|
|
||||||
const module = moduleTemplate(empty, true);
|
const module = moduleTemplate(empty, true);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user