New discussion button. Discussion highlighter.

This commit is contained in:
Benoit Schweblin 2017-11-10 23:39:51 +00:00
parent 167f3f50bc
commit 8767adc505
48 changed files with 982 additions and 484 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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();

View File

@ -1,5 +1,5 @@
<template>
<div class="user-image" :style="{'background-image': url}">
<div class="user-image" :style="{backgroundImage: url}">
</div>
</template>

View File

@ -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());
}
}

View 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());
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View 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>

View 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>

View File

@ -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
View 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>

View File

@ -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);

View File

@ -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))

View File

@ -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;

View File

@ -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);
}
}
},
};

View File

@ -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();
},
});

View 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();
}
});
},
);
},
};

View 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();
},
};

View File

@ -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));
});

View File

@ -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);

View File

@ -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);

View File

@ -1,5 +1,5 @@
import moduleTemplate from './moduleTemplate';
import empty from '../../data/emptyContentState';
import empty from '../data/emptyContentState';
const module = moduleTemplate(empty, true);

View File

@ -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
View 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 });
}
},
},
};

View File

@ -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;

View File

@ -1,5 +1,5 @@
import moduleTemplate from './moduleTemplate';
import empty from '../../data/emptyFile';
import empty from '../data/emptyFile';
const module = moduleTemplate(empty);

View File

@ -1,5 +1,5 @@
import moduleTemplate from './moduleTemplate';
import empty from '../../data/emptyFolder';
import empty from '../data/emptyFolder';
const module = moduleTemplate(empty);

View File

@ -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()] : [],
});

View File

@ -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 }) {

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -1,5 +1,5 @@
import moduleTemplate from './moduleTemplate';
import empty from '../../data/emptySyncedContent';
import empty from '../data/emptySyncedContent';
const module = moduleTemplate(empty, true);