New discussion button. Discussion highlighter.
This commit is contained in:
parent
167f3f50bc
commit
8767adc505
@ -13,6 +13,5 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="//cdn.monetizejs.com/api/js/latest/monetize.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -68,12 +68,4 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@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>
|
||||
|
@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import EditorNewDiscussionButtonGutter from './gutters/EditorNewDiscussionButtonGutter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorNewDiscussionButtonGutter,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
@ -17,6 +22,43 @@ export default {
|
||||
'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>
|
||||
|
||||
@ -47,11 +89,6 @@ export default {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.discussion-highlight,
|
||||
.find-replace-highlight {
|
||||
background-color: transparentize(#ffe400, 0.5);
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
<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__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">
|
||||
</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}}
|
||||
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
|
||||
</div>
|
||||
<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>
|
||||
<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">
|
||||
</div>
|
||||
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||
|
@ -32,9 +32,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import cledit from '../libs/cledit';
|
||||
import store from '../store';
|
||||
import EditorClassApplier from './common/EditorClassApplier';
|
||||
@ -63,8 +62,8 @@ class DynamicClassApplier {
|
||||
constructor(cssClass, offset, silent) {
|
||||
this.startMarker = new cledit.Marker(offset.start);
|
||||
this.endMarker = new cledit.Marker(offset.end);
|
||||
editorEngineSvc.clEditor.addMarker(this.startMarker);
|
||||
editorEngineSvc.clEditor.addMarker(this.endMarker);
|
||||
editorSvc.clEditor.addMarker(this.startMarker);
|
||||
editorSvc.clEditor.addMarker(this.endMarker);
|
||||
if (!silent) {
|
||||
this.classApplier = new EditorClassApplier(
|
||||
[`find-replace-${this.startMarker.id}`, cssClass],
|
||||
@ -76,8 +75,8 @@ class DynamicClassApplier {
|
||||
}
|
||||
|
||||
clean = () => {
|
||||
editorEngineSvc.clEditor.removeMarker(this.startMarker);
|
||||
editorEngineSvc.clEditor.removeMarker(this.endMarker);
|
||||
editorSvc.clEditor.removeMarker(this.startMarker);
|
||||
editorSvc.clEditor.removeMarker(this.endMarker);
|
||||
if (this.classApplier) {
|
||||
this.classApplier.stop();
|
||||
}
|
||||
@ -117,7 +116,7 @@ export default {
|
||||
}
|
||||
this.replaceRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'm' : 'mi');
|
||||
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 offset = params[params.length - 2];
|
||||
offsetList.push({
|
||||
@ -161,7 +160,7 @@ export default {
|
||||
find(mode = 'forward') {
|
||||
const selectedClassApplier = this.selectedClassApplier;
|
||||
this.unselectClassApplier();
|
||||
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
||||
const selectionMgr = editorSvc.clEditor.selectionMgr;
|
||||
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||
const keys = Object.keys(this.classAppliers);
|
||||
@ -208,21 +207,21 @@ export default {
|
||||
this.find();
|
||||
return;
|
||||
}
|
||||
editorEngineSvc.clEditor.replaceAll(
|
||||
editorSvc.clEditor.replaceAll(
|
||||
this.replaceRegex, this.replaceText, this.selectedClassApplier.startMarker.offset);
|
||||
Vue.nextTick(() => this.find());
|
||||
this.$nextTick(() => this.find());
|
||||
}
|
||||
},
|
||||
replaceAll() {
|
||||
if (this.searchRegex) {
|
||||
editorEngineSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
|
||||
editorSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$store.commit('findReplace/setType');
|
||||
},
|
||||
onEscape() {
|
||||
editorEngineSvc.clEditor.focus();
|
||||
editorSvc.clEditor.focus();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -236,7 +235,7 @@ export default {
|
||||
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
|
||||
this.$watch(() => this.findUseRegexp, this.debouncedHighlightOccurrences);
|
||||
// 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
|
||||
this.$watch(() => this.lastOpen, () => {
|
||||
@ -266,7 +265,7 @@ export default {
|
||||
},
|
||||
destroyed() {
|
||||
// Unregister listeners
|
||||
editorEngineSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
|
||||
editorSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
|
||||
window.removeEventListener('keyup', this.onKeyup);
|
||||
window.removeEventListener('focusin', this.onFocusIn);
|
||||
this.state = 'destroyed';
|
||||
@ -350,10 +349,10 @@ export default {
|
||||
}
|
||||
|
||||
.find-replace-highlighting {
|
||||
background-color: #ff0;
|
||||
background-color: $highlighting-color;
|
||||
}
|
||||
|
||||
.find-replace-selection {
|
||||
background-color: #ff9632;
|
||||
background-color: $selection-highlighting-color;
|
||||
}
|
||||
</style>
|
||||
|
@ -9,7 +9,10 @@
|
||||
<navigation-bar></navigation-bar>
|
||||
</div>
|
||||
<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>
|
||||
<div v-if="showFindReplace" class="layout__panel layout__panel--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' }">
|
||||
<button-bar></button-bar>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,7 +50,6 @@ import Editor from './Editor';
|
||||
import Preview from './Preview';
|
||||
import FindReplace from './FindReplace';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -77,6 +82,7 @@ export default {
|
||||
window.addEventListener('resize', this.updateBodySize);
|
||||
window.addEventListener('keyup', this.saveSelection);
|
||||
window.addEventListener('mouseup', this.saveSelection);
|
||||
window.addEventListener('focusin', this.saveSelection);
|
||||
window.addEventListener('contextmenu', this.saveSelection);
|
||||
},
|
||||
mounted() {
|
||||
@ -87,12 +93,13 @@ export default {
|
||||
|
||||
// Focus on the editor every time reader mode is disabled
|
||||
this.$watch(() => this.styles.showEditor,
|
||||
showEditor => showEditor && editorEngineSvc.clEditor.focus());
|
||||
showEditor => showEditor && editorSvc.clEditor.focus());
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.updateStyle);
|
||||
window.removeEventListener('keyup', this.saveSelection);
|
||||
window.removeEventListener('mouseup', this.saveSelection);
|
||||
window.removeEventListener('focusin', this.saveSelection);
|
||||
window.removeEventListener('contextmenu', this.saveSelection);
|
||||
},
|
||||
};
|
||||
@ -112,6 +119,7 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout__panel--navigation-bar {
|
||||
@ -145,4 +153,12 @@ export default {
|
||||
height: auto;
|
||||
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>
|
||||
|
@ -42,7 +42,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import ModalInner from './modals/common/ModalInner';
|
||||
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||
import SettingsModal from './modals/SettingsModal';
|
||||
@ -120,7 +120,7 @@ export default {
|
||||
methods: {
|
||||
onEscape() {
|
||||
this.config.reject();
|
||||
editorEngineSvc.clEditor.focus();
|
||||
editorSvc.clEditor.focus();
|
||||
},
|
||||
onTab(evt) {
|
||||
const tabbables = getTabbables(this.$el);
|
||||
|
@ -84,7 +84,6 @@
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import publishSvc from '../services/publishSvc';
|
||||
import animationSvc from '../services/animationSvc';
|
||||
@ -161,10 +160,10 @@ export default {
|
||||
'toggleSideBar',
|
||||
]),
|
||||
undo() {
|
||||
return editorEngineSvc.clEditor.undoMgr.undo();
|
||||
return editorSvc.clEditor.undoMgr.undo();
|
||||
},
|
||||
redo() {
|
||||
return editorEngineSvc.clEditor.undoMgr.redo();
|
||||
return editorSvc.clEditor.undoMgr.redo();
|
||||
},
|
||||
requestSync() {
|
||||
if (this.isSyncPossible && !this.isSyncRequested) {
|
||||
@ -402,7 +401,6 @@ $t: 3000ms;
|
||||
.navigation-bar__spinner {
|
||||
width: 24px;
|
||||
margin: 7px 0 0 8px;
|
||||
color: #b2b2b2;
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
@ -416,7 +414,7 @@ $t: 3000ms;
|
||||
height: $d;
|
||||
display: block;
|
||||
position: relative;
|
||||
border: $b solid currentColor;
|
||||
border: $b solid transparentize($navbar-color, 0.5);
|
||||
border-radius: 50%;
|
||||
margin: 2px;
|
||||
|
||||
@ -426,20 +424,20 @@ $t: 3000ms;
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: $b;
|
||||
background-color: currentColor;
|
||||
background-color: $navbar-color;
|
||||
border-radius: $b * 0.5;
|
||||
transform-origin: 50% 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
height: $r * 0.35;
|
||||
height: $r * 0.4;
|
||||
left: $r - $b * 1.5;
|
||||
top: 50%;
|
||||
animation: spin $t linear infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: $r * 0.5;
|
||||
height: $r * 0.6;
|
||||
left: $r - $b * 1.5;
|
||||
top: 50%;
|
||||
animation: spin $t/4 linear infinite;
|
||||
|
@ -3,6 +3,7 @@
|
||||
<div class="preview__inner-1" @click="onClick" @scroll="onScroll">
|
||||
<div class="preview__inner-2" :style="{padding: styles.previewPadding}">
|
||||
</div>
|
||||
<preview-new-discussion-button-gutter></preview-new-discussion-button-gutter>
|
||||
</div>
|
||||
<div v-if="!styles.showEditor" class="preview__button-bar">
|
||||
<div class="preview__button" @click="toggleEditor(true)">
|
||||
@ -15,10 +16,14 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import PreviewNewDiscussionButtonGutter from './gutters/PreviewNewDiscussionButtonGutter';
|
||||
|
||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PreviewNewDiscussionButtonGutter,
|
||||
},
|
||||
data: () => ({
|
||||
previewTop: true,
|
||||
}),
|
||||
@ -46,6 +51,43 @@ export default {
|
||||
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>
|
||||
|
||||
|
@ -25,7 +25,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import utils from '../services/utils';
|
||||
|
||||
class Stat {
|
||||
@ -67,13 +66,13 @@ export default {
|
||||
methods: {
|
||||
computeText() {
|
||||
this.textSelection = false;
|
||||
let text = editorEngineSvc.clEditor.getContent();
|
||||
const beforeText = text.slice(0, editorEngineSvc.clEditor.selectionMgr.selectionEnd);
|
||||
let text = editorSvc.clEditor.getContent();
|
||||
const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd);
|
||||
const beforeLines = beforeText.split('\n');
|
||||
this.line = beforeLines.length;
|
||||
this.column = beforeLines.pop().length;
|
||||
|
||||
const selectedText = editorEngineSvc.clEditor.selectionMgr.getSelectedText();
|
||||
const selectedText = editorSvc.clEditor.selectionMgr.getSelectedText();
|
||||
if (selectedText) {
|
||||
this.textSelection = true;
|
||||
text = selectedText;
|
||||
|
@ -6,7 +6,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
|
||||
@ -71,7 +70,7 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
Vue.nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
editorSvc.editorElt.parentNode.addEventListener('scroll', () => {
|
||||
if (this.styles.showEditor) {
|
||||
updateMaskY();
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="user-image" :style="{'background-image': url}">
|
||||
<div class="user-image" :style="{backgroundImage: url}">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,16 +1,15 @@
|
||||
import cledit from '../../libs/cledit';
|
||||
import editorSvc from '../../services/editorSvc';
|
||||
import editorEngineSvc from '../../services/editorEngineSvc';
|
||||
import utils from '../../services/utils';
|
||||
|
||||
let savedSelection;
|
||||
let savedSelection = null;
|
||||
const nextTickCbs = [];
|
||||
const nextTickExecCbs = cledit.Utils.debounce(() => {
|
||||
while (nextTickCbs.length) {
|
||||
nextTickCbs.shift()();
|
||||
}
|
||||
if (savedSelection) {
|
||||
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||
editorSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||
savedSelection.start, savedSelection.end);
|
||||
}
|
||||
savedSelection = null;
|
||||
@ -23,8 +22,8 @@ const nextTick = (cb) => {
|
||||
|
||||
const nextTickRestoreSelection = () => {
|
||||
savedSelection = {
|
||||
start: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
||||
end: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
||||
start: editorSvc.clEditor.selectionMgr.selectionStart,
|
||||
end: editorSvc.clEditor.selectionMgr.selectionEnd,
|
||||
};
|
||||
nextTickExecCbs();
|
||||
};
|
||||
@ -44,14 +43,14 @@ export default class EditorClassApplier {
|
||||
}
|
||||
};
|
||||
|
||||
editorEngineSvc.clEditor.on('contentChanged', this.restoreClass);
|
||||
editorSvc.clEditor.on('contentChanged', this.restoreClass);
|
||||
nextTick(() => this.applyClass());
|
||||
}
|
||||
|
||||
applyClass() {
|
||||
const offset = this.offsetGetter();
|
||||
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.max(offset.start, offset.end),
|
||||
);
|
||||
@ -59,10 +58,10 @@ export default class EditorClassApplier {
|
||||
...this.properties,
|
||||
className: this.classGetter().join(' '),
|
||||
};
|
||||
editorEngineSvc.clEditor.watcher.noWatch(() => {
|
||||
editorSvc.clEditor.watcher.noWatch(() => {
|
||||
utils.wrapRange(range, properties);
|
||||
});
|
||||
if (editorEngineSvc.clEditor.selectionMgr.hasFocus()) {
|
||||
if (editorSvc.clEditor.selectionMgr.hasFocus()) {
|
||||
nextTickRestoreSelection();
|
||||
}
|
||||
this.lastEltCount = this.eltCollection.length;
|
||||
@ -70,16 +69,16 @@ export default class EditorClassApplier {
|
||||
}
|
||||
|
||||
removeClass() {
|
||||
editorEngineSvc.clEditor.watcher.noWatch(() => {
|
||||
editorSvc.clEditor.watcher.noWatch(() => {
|
||||
utils.unwrapRange(this.eltCollection);
|
||||
});
|
||||
if (editorEngineSvc.clEditor.selectionMgr.hasFocus()) {
|
||||
if (editorSvc.clEditor.selectionMgr.hasFocus()) {
|
||||
nextTickRestoreSelection();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
editorEngineSvc.clEditor.off('contentChanged', this.restoreClass);
|
||||
editorSvc.clEditor.off('contentChanged', this.restoreClass);
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
|
@ -21,7 +21,6 @@
|
||||
color: $editor-color;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
word-break: break-all;
|
||||
|
||||
[class*='language-'] {
|
||||
color: $editor-color-dark;
|
||||
@ -30,6 +29,11 @@
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
&,
|
||||
* {
|
||||
line-height: $line-height-title;
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
|
@ -4,7 +4,9 @@ $line-height-base: 1.67;
|
||||
$line-height-title: 1.33;
|
||||
$font-size-monospace: 0.85em;
|
||||
$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;
|
||||
$link-color: #0c93e4;
|
||||
$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>
|
||||
<modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo">
|
||||
<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)">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="title" @keyup.enter="resolve()">
|
||||
</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 Redo from './Redo';
|
||||
import ContentSave from './ContentSave';
|
||||
import Message from './Message';
|
||||
|
||||
Vue.component('iconProvider', Provider);
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
@ -94,3 +95,4 @@ Vue.component('iconPrinter', Printer);
|
||||
Vue.component('iconUndo', Undo);
|
||||
Vue.component('iconRedo', Redo);
|
||||
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('mousedown', windowMouseListener)
|
||||
editor.$window.removeEventListener('mouseup', windowMouseListener)
|
||||
editor.$window.removeEventListener('resize', windowResizeListener)
|
||||
editor.$trigger('destroy')
|
||||
return true
|
||||
}
|
||||
@ -235,6 +236,15 @@ function cledit(contentElt, scrollElt, windowParam) {
|
||||
}
|
||||
editor.$window.addEventListener('mousedown', 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
|
||||
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
|
||||
|
||||
|
@ -33,14 +33,8 @@ function makePatchableText(content, markerKeys, markerIdxMap) {
|
||||
}
|
||||
}
|
||||
|
||||
if (discussion.offset0 === discussion.offset1) {
|
||||
// Remove discussion offsets if markers are at the same position
|
||||
discussion.offset0 = undefined;
|
||||
discussion.offset1 = undefined;
|
||||
} else {
|
||||
addMarker('offset0');
|
||||
addMarker('offset1');
|
||||
}
|
||||
addMarker('start');
|
||||
addMarker('end');
|
||||
});
|
||||
|
||||
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 sectionUtils from './sectionUtils';
|
||||
import extensionSvc from './extensionSvc';
|
||||
import animationSvc from './animationSvc';
|
||||
import editorEngineSvc from './editorEngineSvc';
|
||||
import editorSvcDiscussions from './editorSvcDiscussions';
|
||||
import editorSvcUtils from './editorSvcUtils';
|
||||
import store from '../store';
|
||||
|
||||
const debounce = cledit.Utils.debounce;
|
||||
@ -30,14 +30,15 @@ const allowDebounce = (action, wait) => {
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
let instantPreview = true;
|
||||
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
|
||||
editorElt: null,
|
||||
previewElt: null,
|
||||
tocElt: null,
|
||||
// Other objects
|
||||
clEditor: null,
|
||||
pagedownEditor: null,
|
||||
options: null,
|
||||
prismGrammars: null,
|
||||
@ -54,99 +55,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
previewHtml: 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
|
||||
*/
|
||||
@ -183,7 +91,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
return 0.15;
|
||||
},
|
||||
};
|
||||
editorEngineSvc.initClEditor(options);
|
||||
this.initClEditorInternal(options);
|
||||
this.restoreScrollPosition();
|
||||
},
|
||||
|
||||
@ -276,6 +184,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
});
|
||||
this.sectionDescList = newSectionDescList;
|
||||
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
||||
this.$emit('previewHtml', this.previewHtml);
|
||||
this.tocElt.classList[
|
||||
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
|
||||
]('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.
|
||||
*/
|
||||
measureSectionDimensions: allowDebounce((restoreScrollPosition) => {
|
||||
if (editorSvc.sectionDescList && this.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
||||
if (editorSvc.sectionDescList &&
|
||||
this.sectionDescList !== editorSvc.sectionDescMeasuredList
|
||||
) {
|
||||
sectionUtils.measureSectionDimensions(editorSvc);
|
||||
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList;
|
||||
if (restoreScrollPosition) {
|
||||
@ -321,7 +232,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
*/
|
||||
makeTextToPreviewDiffs: allowDebounce(() => {
|
||||
if (editorSvc.sectionDescList &&
|
||||
editorSvc.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
||||
editorSvc.sectionDescList !== editorSvc.sectionDescWithDiffsList
|
||||
) {
|
||||
editorSvc.sectionDescList
|
||||
.forEach((sectionDesc) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
editorSvc.previewTextWithDiffsList = editorSvc.previewText;
|
||||
editorSvc.sectionDescWithDiffsList = editorSvc.sectionDescList;
|
||||
editorSvc.$emit('sectionDescWithDiffsList', editorSvc.sectionDescWithDiffsList);
|
||||
}
|
||||
}, 50),
|
||||
|
||||
@ -341,28 +255,12 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
const scrollPosition = editorSvc.getScrollPosition() ||
|
||||
store.getters['contentState/current'].scrollPosition;
|
||||
store.dispatch('contentState/patchCurrent', {
|
||||
selectionStart: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
||||
selectionEnd: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
||||
selectionStart: editorSvc.clEditor.selectionMgr.selectionStart,
|
||||
selectionEnd: editorSvc.clEditor.selectionMgr.selectionEnd,
|
||||
scrollPosition,
|
||||
});
|
||||
}, 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.
|
||||
*/
|
||||
@ -392,8 +290,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
previewSelectionEndOffset = previewSelectionStartOffset + `${range}`.length;
|
||||
const editorStartOffset = editorSvc.getEditorOffset(previewSelectionStartOffset);
|
||||
const editorEndOffset = editorSvc.getEditorOffset(previewSelectionEndOffset);
|
||||
if (editorStartOffset !== undefined && editorEndOffset !== undefined) {
|
||||
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||
if (editorStartOffset != null && editorEndOffset != null) {
|
||||
editorSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||
editorStartOffset, editorEndOffset);
|
||||
}
|
||||
}
|
||||
@ -403,35 +301,10 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
}, 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) {
|
||||
let scrollTop = 0;
|
||||
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();
|
||||
getPandocAst() {
|
||||
return tokens && markdownItPandocRenderer(tokens, this.converter.options);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -442,26 +315,26 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
this.previewElt = previewElt;
|
||||
this.tocElt = tocElt;
|
||||
|
||||
editorEngineSvc.createClEditor(editorElt);
|
||||
editorEngineSvc.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||
this.createClEditor(editorElt);
|
||||
this.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||
const parsingCtx = {
|
||||
...this.parsingCtx,
|
||||
sectionList,
|
||||
};
|
||||
this.parsingCtx = parsingCtx;
|
||||
});
|
||||
editorEngineSvc.clEditor.undoMgr.on('undoStateChange', () => {
|
||||
const canUndo = editorEngineSvc.clEditor.undoMgr.canUndo();
|
||||
this.clEditor.undoMgr.on('undoStateChange', () => {
|
||||
const canUndo = this.clEditor.undoMgr.canUndo();
|
||||
if (canUndo !== store.state.layout.canUndo) {
|
||||
store.commit('layout/setCanUndo', canUndo);
|
||||
}
|
||||
const canRedo = editorEngineSvc.clEditor.undoMgr.canRedo();
|
||||
const canRedo = this.clEditor.undoMgr.canRedo();
|
||||
if (canRedo !== store.state.layout.canRedo) {
|
||||
store.commit('layout/setCanRedo', canRedo);
|
||||
}
|
||||
});
|
||||
this.pagedownEditor = pagedown({
|
||||
input: Object.create(editorEngineSvc.clEditor),
|
||||
input: Object.create(this.clEditor),
|
||||
});
|
||||
this.pagedownEditor.run();
|
||||
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();
|
||||
};
|
||||
|
||||
editorEngineSvc.clEditor.selectionMgr.on('selectionChanged',
|
||||
this.clEditor.selectionMgr.on('selectionChanged',
|
||||
(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 = [];
|
||||
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) => {
|
||||
const srcElt = imgTokenElt.querySelector('.token.cl-src');
|
||||
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) => {
|
||||
const cachedImgElt = getFromImgCache(imgElt.src);
|
||||
if (cachedImgElt) {
|
||||
@ -602,7 +475,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
triggerImgCacheGc();
|
||||
});
|
||||
|
||||
editorEngineSvc.clEditor.on('contentChanged',
|
||||
this.clEditor.on('contentChanged',
|
||||
(content, diffs, sectionList) => onEditorChanged(sectionList));
|
||||
|
||||
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) {
|
||||
lastProperties = content.properties;
|
||||
const options = extensionSvc.getOptions(store.getters['content/currentProperties']);
|
||||
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
||||
editorSvc.options = options;
|
||||
editorSvc.initPrism();
|
||||
editorSvc.initConverter();
|
||||
if (JSON.stringify(options) !== JSON.stringify(this.options)) {
|
||||
this.options = options;
|
||||
this.initPrism();
|
||||
this.initConverter();
|
||||
initClEditor = true;
|
||||
}
|
||||
}
|
||||
if (initClEditor) {
|
||||
editorSvc.initClEditor();
|
||||
this.initClEditor();
|
||||
}
|
||||
// Apply possible text and discussion changes
|
||||
editorEngineSvc.applyContent();
|
||||
this.applyContent();
|
||||
}, {
|
||||
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
|
||||
store.watch(
|
||||
() => store.getters['content/current'].id && store.getters['layout/styles'].showEditor,
|
||||
editable => editorEngineSvc.clEditor.toggleEditable(!!editable), {
|
||||
editable => this.clEditor.toggleEditable(!!editable), {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
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 editorSvc from '../editorSvc';
|
||||
import editorEngineSvc from '../editorEngineSvc';
|
||||
|
||||
const Keystroke = cledit.Keystroke;
|
||||
const indentRegexp = /^ {0,3}>[ ]*|^[ \t]*[*+-][ \t]|^([ \t]*)\d+\.[ \t]|^\s+/;
|
||||
@ -116,7 +115,7 @@ function enterKeyHandler(evt, state) {
|
||||
clearNewline = true;
|
||||
}
|
||||
|
||||
editorEngineSvc.clEditor.undoMgr.setCurrentMode('single');
|
||||
editorSvc.clEditor.undoMgr.setCurrentMode('single');
|
||||
|
||||
state.before += `\n${indent}`;
|
||||
state.selection = '';
|
||||
@ -169,6 +168,6 @@ function tabKeyHandler(evt, state) {
|
||||
}
|
||||
|
||||
editorSvc.$on('inited', () => {
|
||||
editorEngineSvc.clEditor.addKeystroke(new Keystroke(enterKeyHandler, 50));
|
||||
editorEngineSvc.clEditor.addKeystroke(new Keystroke(tabKeyHandler, 50));
|
||||
editorSvc.clEditor.addKeystroke(new Keystroke(enterKeyHandler, 50));
|
||||
editorSvc.clEditor.addKeystroke(new Keystroke(tabKeyHandler, 50));
|
||||
});
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Mousetrap from 'mousetrap';
|
||||
import store from '../../store';
|
||||
import editorSvc from '../../services/editorSvc';
|
||||
import editorEngineSvc from '../../services/editorEngineSvc';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
|
||||
// Skip shortcuts if modal is open or editor is hidden
|
||||
@ -16,8 +15,8 @@ const pagedownHandler = name => () => {
|
||||
const findReplaceOpener = type => () => {
|
||||
store.dispatch('findReplace/open', {
|
||||
type,
|
||||
findText: editorEngineSvc.clEditor.selectionMgr.hasFocus() &&
|
||||
editorEngineSvc.clEditor.selectionMgr.getSelectedText(),
|
||||
findText: editorSvc.clEditor.selectionMgr.hasFocus() &&
|
||||
editorSvc.clEditor.selectionMgr.getSelectedText(),
|
||||
});
|
||||
return true;
|
||||
};
|
||||
@ -46,7 +45,7 @@ const methods = {
|
||||
const replacement = `${param2 || ''}`;
|
||||
if (text && replacement) {
|
||||
setTimeout(() => {
|
||||
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
||||
const selectionMgr = editorSvc.clEditor.selectionMgr;
|
||||
let offset = selectionMgr.selectionStart;
|
||||
if (offset === selectionMgr.selectionEnd) {
|
||||
const range = selectionMgr.createRange(offset - text.length, offset);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptyContent';
|
||||
import utils from '../../services/utils';
|
||||
import empty from '../data/emptyContent';
|
||||
import utils from '../services/utils';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptyContentState';
|
||||
import empty from '../data/emptyContentState';
|
||||
|
||||
const module = moduleTemplate(empty, true);
|
||||
|
@ -1,13 +1,13 @@
|
||||
import Vue from 'vue';
|
||||
import yaml from 'js-yaml';
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import utils from '../../services/utils';
|
||||
import defaultSettings from '../../data/defaultSettings.yml';
|
||||
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
||||
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
||||
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
||||
import styledHtmlWithTocTemplate from '../../data/styledHtmlWithTocTemplate.html';
|
||||
import jekyllSiteTemplate from '../../data/jekyllSiteTemplate.html';
|
||||
import utils from '../services/utils';
|
||||
import defaultSettings from '../data/defaultSettings.yml';
|
||||
import defaultLocalSettings from '../data/defaultLocalSettings';
|
||||
import plainHtmlTemplate from '../data/plainHtmlTemplate.html';
|
||||
import styledHtmlTemplate from '../data/styledHtmlTemplate.html';
|
||||
import styledHtmlWithTocTemplate from '../data/styledHtmlWithTocTemplate.html';
|
||||
import jekyllSiteTemplate from '../data/jekyllSiteTemplate.html';
|
||||
|
||||
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 emptyFile from '../../data/emptyFile';
|
||||
import emptyFolder from '../../data/emptyFolder';
|
||||
import emptyFile from '../data/emptyFile';
|
||||
import emptyFolder from '../data/emptyFolder';
|
||||
|
||||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptyFile';
|
||||
import empty from '../data/emptyFile';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptyFolder';
|
||||
import empty from '../data/emptyFolder';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
@ -2,27 +2,46 @@ import createLogger from 'vuex/dist/logger';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import utils from '../services/utils';
|
||||
import contentState from './modules/contentState';
|
||||
import syncedContent from './modules/syncedContent';
|
||||
import content from './modules/content';
|
||||
import file from './modules/file';
|
||||
import findReplace from './modules/findReplace';
|
||||
import folder from './modules/folder';
|
||||
import publishLocation from './modules/publishLocation';
|
||||
import syncLocation from './modules/syncLocation';
|
||||
import data from './modules/data';
|
||||
import layout from './modules/layout';
|
||||
import explorer from './modules/explorer';
|
||||
import modal from './modules/modal';
|
||||
import notification from './modules/notification';
|
||||
import queue from './modules/queue';
|
||||
import userInfo from './modules/userInfo';
|
||||
import contentState from './contentState';
|
||||
import syncedContent from './syncedContent';
|
||||
import content from './content';
|
||||
import file from './file';
|
||||
import findReplace from './findReplace';
|
||||
import folder from './folder';
|
||||
import publishLocation from './publishLocation';
|
||||
import syncLocation from './syncLocation';
|
||||
import data from './data';
|
||||
import discussion from './discussion';
|
||||
import layout from './layout';
|
||||
import explorer from './explorer';
|
||||
import modal from './modal';
|
||||
import notification from './notification';
|
||||
import queue from './queue';
|
||||
import userInfo from './userInfo';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const debug = NODE_ENV !== 'production';
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
contentState,
|
||||
syncedContent,
|
||||
content,
|
||||
discussion,
|
||||
file,
|
||||
findReplace,
|
||||
folder,
|
||||
publishLocation,
|
||||
syncLocation,
|
||||
data,
|
||||
layout,
|
||||
explorer,
|
||||
modal,
|
||||
notification,
|
||||
queue,
|
||||
userInfo,
|
||||
},
|
||||
state: {
|
||||
ready: false,
|
||||
offline: false,
|
||||
@ -98,23 +117,6 @@ const store = new Vuex.Store({
|
||||
.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,
|
||||
plugins: debug ? [createLogger()] : [],
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
const minPadding = 20;
|
||||
const previewButtonWidth = 55;
|
||||
const editorTopPadding = 10;
|
||||
const gutterWidth = 250;
|
||||
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
|
||||
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
||||
const navigationBarRightButtonWidth = 38 + 8;
|
||||
@ -20,7 +21,7 @@ const constants = {
|
||||
statusBarHeight: 20,
|
||||
};
|
||||
|
||||
function computeStyles(state, localSettings, getters, styles = {
|
||||
function computeStyles(state, getters, localSettings = getters['data/localSettings'], styles = {
|
||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
||||
showStatusBar: localSettings.showStatusBar,
|
||||
showEditor: localSettings.showEditor,
|
||||
@ -30,7 +31,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
showExplorer: localSettings.showExplorer,
|
||||
layoutOverflow: false,
|
||||
}) {
|
||||
styles.innerHeight = state.bodyHeight;
|
||||
styles.innerHeight = state.layout.bodyHeight;
|
||||
if (styles.showNavigationBar) {
|
||||
styles.innerHeight -= constants.navigationBarHeight;
|
||||
}
|
||||
@ -38,7 +39,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
styles.innerHeight -= constants.statusBarHeight;
|
||||
}
|
||||
|
||||
styles.innerWidth = state.bodyWidth;
|
||||
styles.innerWidth = state.layout.bodyWidth;
|
||||
if (styles.showSideBar) {
|
||||
styles.innerWidth -= constants.sideBarWidth;
|
||||
}
|
||||
@ -47,9 +48,12 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
}
|
||||
|
||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||
const showGutter = getters['discussion/currentDiscussion'];
|
||||
if (showGutter) {
|
||||
doublePanelWidth -= gutterWidth;
|
||||
}
|
||||
if (doublePanelWidth < constants.editorMinWidth) {
|
||||
doublePanelWidth = constants.editorMinWidth;
|
||||
styles.innerWidth = constants.editorMinWidth + constants.buttonBarWidth;
|
||||
styles.layoutOverflow = true;
|
||||
}
|
||||
|
||||
@ -57,7 +61,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
styles.showSidePreview = false;
|
||||
styles.showPreview = false;
|
||||
styles.layoutOverflow = false;
|
||||
return computeStyles(state, localSettings, getters, styles);
|
||||
return computeStyles(state, getters, localSettings, styles);
|
||||
}
|
||||
|
||||
const computedSettings = getters['data/computedSettings'];
|
||||
@ -83,10 +87,14 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
const panelWidth = Math.floor(doublePanelWidth / 2);
|
||||
styles.previewWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
styles.innerWidth;
|
||||
const previewLeftPadding = Math.max(
|
||||
doublePanelWidth + constants.buttonBarWidth;
|
||||
let previewRightPadding = Math.max(
|
||||
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) {
|
||||
previewRightPadding = previewButtonWidth;
|
||||
}
|
||||
@ -94,9 +102,14 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
styles.editorWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
doublePanelWidth;
|
||||
const editorSidePadding = Math.max(
|
||||
const editorRightPadding = Math.max(
|
||||
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 -
|
||||
navigationBarLeftButtonWidth -
|
||||
@ -140,10 +153,7 @@ export default {
|
||||
},
|
||||
getters: {
|
||||
constants: () => constants,
|
||||
styles: (state, getters, rootState, rootGetters) => {
|
||||
const localSettings = rootGetters['data/localSettings'];
|
||||
return computeStyles(state, localSettings, rootGetters);
|
||||
},
|
||||
styles: (state, getters, rootState, rootGetters) => computeStyles(rootState, rootGetters),
|
||||
},
|
||||
actions: {
|
||||
updateBodySize({ commit, dispatch, rootGetters }) {
|
@ -1,5 +1,5 @@
|
||||
import Vue from 'vue';
|
||||
import utils from '../../services/utils';
|
||||
import utils from '../services/utils';
|
||||
|
||||
export default (empty, simpleHash = false) => {
|
||||
// Use Date.now as a simple hash function, which is ok for not-synced types
|
@ -1,6 +1,6 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptyPublishLocation';
|
||||
import providerRegistry from '../../services/providers/providerRegistry';
|
||||
import empty from '../data/emptyPublishLocation';
|
||||
import providerRegistry from '../services/providers/providerRegistry';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptySyncLocation';
|
||||
import providerRegistry from '../../services/providers/providerRegistry';
|
||||
import empty from '../data/emptySyncLocation';
|
||||
import providerRegistry from '../services/providers/providerRegistry';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptySyncedContent';
|
||||
import empty from '../data/emptySyncedContent';
|
||||
|
||||
const module = moduleTemplate(empty, true);
|
||||
|
Loading…
Reference in New Issue
Block a user