client/posts: add note editing
This commit is contained in:
parent
9e2dace73f
commit
d5a00fe4b9
15 changed files with 954 additions and 94 deletions
|
@ -38,8 +38,13 @@ $tag-suggestions-header-color = #EEE
|
|||
$tag-suggestions-border-color = #AAA
|
||||
$duplicate-tag-background-color = #FDC
|
||||
$duplicate-tag-text-color = black
|
||||
$note-overlay-background-color = rgba(255, 255, 255, 0.3)
|
||||
$note-overlay-border-color = rgba(0, 0, 0, 0.3)
|
||||
$active-note-overlay-background-color = rgba(255, 255, 255, 0.3)
|
||||
$active-note-overlay-border-color = rgba(62, 255, 62, 0.8)
|
||||
$note-background-color = rgba(255, 255, 205, 0.3)
|
||||
$note-border-color = rgba(0, 0, 0, 0.2)
|
||||
$edited-note-background-color = rgba(222, 255, 222, 0.3)
|
||||
$edited-note-border-color = rgba(0, 200, 0, 0.9)
|
||||
$note-text-background-color = lemonchiffon
|
||||
$note-text-border-color = black
|
||||
$note-text-text-color = black
|
||||
$hovered-note-point-color = red
|
||||
|
|
63
client/css/notes.styl
Normal file
63
client/css/notes.styl
Normal file
|
@ -0,0 +1,63 @@
|
|||
@import colors
|
||||
|
||||
.post-overlay
|
||||
&[data-state=ready-to-draw],
|
||||
&[data-state=drawing-rectangle],
|
||||
&[data-state=drawing-polygon]
|
||||
&:after
|
||||
box-sizing: border-box
|
||||
border: 0.3em dashed $active-note-overlay-border-color
|
||||
background: $active-note-overlay-background-color
|
||||
display: block
|
||||
content: ' '
|
||||
pointer-events: none
|
||||
position: absolute
|
||||
width: 100%
|
||||
height: 100%
|
||||
left: 0
|
||||
right: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
|
||||
.notes-overlay
|
||||
g
|
||||
stroke-width: 1px
|
||||
|
||||
polygon
|
||||
fill: $note-background-color
|
||||
stroke: $note-border-color
|
||||
pointer-events: auto
|
||||
ellipse
|
||||
display: none
|
||||
|
||||
g[data-state=editing], g[data-state=drawing]
|
||||
stroke-width: 2px
|
||||
|
||||
polygon
|
||||
fill: $edited-note-background-color
|
||||
stroke: $edited-note-border-color
|
||||
ellipse
|
||||
fill: $edited-note-border-color
|
||||
display: block
|
||||
&.nearby
|
||||
fill: $hovered-note-point-color
|
||||
|
||||
.note-text
|
||||
position: absolute
|
||||
max-width: 22.5em
|
||||
display: none
|
||||
|
||||
&:not([data-state=read-only])
|
||||
pointer-events: none
|
||||
|
||||
&>.wrapper
|
||||
background: $note-text-background-color
|
||||
padding: 0.3em 0.6em
|
||||
border: 1px solid $note-text-border-color
|
||||
color: $note-text-text-color
|
||||
box-sizing: border-box
|
||||
|
||||
p:last-of-type
|
||||
margin-bottom: 0
|
||||
p:first-of-type
|
||||
margin-top: 0
|
|
@ -204,9 +204,6 @@ $safety-unsafe = #F3985F
|
|||
top: 0
|
||||
bottom: 0
|
||||
|
||||
.post-overlay
|
||||
pointer-events: none
|
||||
|
||||
.post-overlay>*
|
||||
position: absolute
|
||||
left: 0
|
||||
|
@ -216,31 +213,6 @@ $safety-unsafe = #F3985F
|
|||
width: 100%
|
||||
height: 100%
|
||||
|
||||
.notes
|
||||
stroke-width: 1px
|
||||
polygon
|
||||
fill: $note-overlay-background-color
|
||||
stroke: $note-overlay-border-color
|
||||
pointer-events: auto
|
||||
|
||||
.note-text
|
||||
position: absolute
|
||||
max-width: 22.5em
|
||||
margin-top: -0.5em
|
||||
display: none
|
||||
|
||||
&>.wrapper
|
||||
background: $note-text-background-color
|
||||
padding: 0.5em
|
||||
border: 1px solid $note-text-border-color
|
||||
color: $note-text-text-color
|
||||
margin-top: 1em
|
||||
|
||||
p:last-of-type
|
||||
margin-bottom: 0
|
||||
p:first-of-type
|
||||
margin-top: 0
|
||||
|
||||
.post-view .readonly-sidebar
|
||||
.details
|
||||
i
|
||||
|
|
|
@ -55,6 +55,14 @@
|
|||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canEditPostNotes) { %>
|
||||
<section class='notes'>
|
||||
<a class='add'>Add a note</a>
|
||||
<%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %>
|
||||
<a class='delete inactive'>Delete selected note</a>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canEditPostContent) { %>
|
||||
<section class='post-content'>
|
||||
<label>Content</label>
|
||||
|
|
|
@ -11,13 +11,16 @@ const FileDropperControl = require('../controls/file_dropper_control.js');
|
|||
const template = views.getTemplate('post-edit-sidebar');
|
||||
|
||||
class PostEditSidebarControl extends events.EventTarget {
|
||||
constructor(hostNode, post, postContentControl) {
|
||||
constructor(hostNode, post, postContentControl, postNotesOverlayControl) {
|
||||
super();
|
||||
this._hostNode = hostNode;
|
||||
this._post = post;
|
||||
this._postContentControl = postContentControl;
|
||||
this._postNotesOverlayControl = postNotesOverlayControl;
|
||||
this._newPostContent = null;
|
||||
|
||||
this._postNotesOverlayControl.switchToPassiveEdit();
|
||||
|
||||
views.replaceContent(this._hostNode, template({
|
||||
post: this._post,
|
||||
canEditPostSafety: api.hasPrivilege('posts:edit:safety'),
|
||||
|
@ -39,6 +42,9 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
new ExpanderControl(
|
||||
'Tags',
|
||||
this._hostNode.querySelectorAll('.tags'));
|
||||
new ExpanderControl(
|
||||
'Notes',
|
||||
this._hostNode.querySelectorAll('.notes'));
|
||||
new ExpanderControl(
|
||||
'Content',
|
||||
this._hostNode.querySelectorAll('.post-content, .post-thumbnail'));
|
||||
|
@ -84,6 +90,16 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
this._post.hasCustomThumbnail ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (this._addNoteLinkNode) {
|
||||
this._addNoteLinkNode.addEventListener(
|
||||
'click', e => this._evtAddNoteClick(e));
|
||||
}
|
||||
|
||||
if (this._deleteNoteLinkNode) {
|
||||
this._deleteNoteLinkNode.addEventListener(
|
||||
'click', e => this._evtDeleteNoteClick(e));
|
||||
}
|
||||
|
||||
if (this._featureLinkNode) {
|
||||
this._featureLinkNode.addEventListener(
|
||||
'click', e => this._evtFeatureClick(e));
|
||||
|
@ -94,6 +110,12 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
'click', e => this._evtDeleteClick(e));
|
||||
}
|
||||
|
||||
this._postNotesOverlayControl.addEventListener(
|
||||
'blur', e => this._evtNoteBlur(e));
|
||||
|
||||
this._postNotesOverlayControl.addEventListener(
|
||||
'focus', e => this._evtNoteFocus(e));
|
||||
|
||||
this._post.addEventListener(
|
||||
'changeContent', e => this._evtPostContentChange(e));
|
||||
|
||||
|
@ -110,6 +132,11 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._noteTextareaNode) {
|
||||
this._noteTextareaNode.addEventListener(
|
||||
'change', e => this._evtNoteTextChangeRequest(e));
|
||||
}
|
||||
}
|
||||
|
||||
_evtPostContentChange(e) {
|
||||
|
@ -146,6 +173,45 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
_evtNoteTextChangeRequest(e) {
|
||||
if (this._editedNote) {
|
||||
this._editedNote.text = this._noteTextareaNode.value;
|
||||
}
|
||||
}
|
||||
|
||||
_evtNoteFocus(e) {
|
||||
this._editedNote = e.detail.note;
|
||||
this._addNoteLinkNode.classList.remove('inactive');
|
||||
this._deleteNoteLinkNode.classList.remove('inactive');
|
||||
this._noteTextareaNode.removeAttribute('disabled');
|
||||
this._noteTextareaNode.value = e.detail.note.text;
|
||||
}
|
||||
|
||||
_evtNoteBlur(e) {
|
||||
this._evtNoteTextChangeRequest(null);
|
||||
this._addNoteLinkNode.classList.remove('inactive');
|
||||
this._deleteNoteLinkNode.classList.add('inactive');
|
||||
this._noteTextareaNode.blur();
|
||||
this._noteTextareaNode.setAttribute('disabled', 'disabled');
|
||||
this._noteTextareaNode.value = '';
|
||||
}
|
||||
|
||||
_evtAddNoteClick(e) {
|
||||
if (e.target.classList.contains('inactive')) {
|
||||
return;
|
||||
}
|
||||
this._addNoteLinkNode.classList.add('inactive');
|
||||
this._postNotesOverlayControl.switchToDrawing();
|
||||
}
|
||||
|
||||
_evtDeleteNoteClick(e) {
|
||||
if (e.target.classList.contains('inactive')) {
|
||||
return;
|
||||
}
|
||||
this._post.notes.remove(this._editedNote);
|
||||
this._postNotesOverlayControl.switchToPassiveEdit();
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
|
@ -226,6 +292,18 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
return this._formNode.querySelector('.management .delete');
|
||||
}
|
||||
|
||||
get _addNoteLinkNode() {
|
||||
return this._formNode.querySelector('.notes .add');
|
||||
}
|
||||
|
||||
get _deleteNoteLinkNode() {
|
||||
return this._formNode.querySelector('.notes .delete');
|
||||
}
|
||||
|
||||
get _noteTextareaNode() {
|
||||
return this._formNode.querySelector('.notes textarea');
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
|
|
@ -1,63 +1,493 @@
|
|||
'use strict';
|
||||
|
||||
const keyboard = require('../util/keyboard.js');
|
||||
const views = require('../util/views.js');
|
||||
const events = require('../events.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const Note = require('../models/note.js');
|
||||
const Point = require('../models/point.js');
|
||||
|
||||
const svgNS = 'http://www.w3.org/2000/svg';
|
||||
const snapThreshold = 10;
|
||||
const circleSize = 10;
|
||||
|
||||
class PostNotesOverlayControl {
|
||||
constructor(postOverlayNode, post) {
|
||||
this._post = post;
|
||||
this._postOverlayNode = postOverlayNode;
|
||||
this._install();
|
||||
const MOUSE_BUTTON_LEFT = 1;
|
||||
|
||||
const KEY_LEFT = 37;
|
||||
const KEY_UP = 38;
|
||||
const KEY_RIGHT = 39;
|
||||
const KEY_DOWN = 40;
|
||||
const KEY_ESCAPE = 27;
|
||||
const KEY_RETURN = 13;
|
||||
|
||||
function _getDistance(point1, point2) {
|
||||
return Math.sqrt(
|
||||
Math.pow(point1.x - point2.x, 2) +
|
||||
Math.pow(point1.y - point2.y, 2));
|
||||
}
|
||||
|
||||
function _setNodeState(node, stateName) {
|
||||
if (node === null) {
|
||||
return;
|
||||
}
|
||||
node.setAttribute('data-state', stateName);
|
||||
}
|
||||
|
||||
function _clearEditedNote(hostNode) {
|
||||
const node = hostNode.querySelector('[data-state=\'editing\']');
|
||||
_setNodeState(node, null);
|
||||
return node !== null;
|
||||
}
|
||||
|
||||
class State {
|
||||
constructor(control) {
|
||||
this._control = control;
|
||||
const stateName = misc.decamelize(
|
||||
this.constructor.name.replace(/State/, ''));
|
||||
_setNodeState(control._hostNode, stateName);
|
||||
_setNodeState(control._textNode, stateName);
|
||||
}
|
||||
|
||||
_evtMouseEnter(e) {
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const svgRect = this._svgNode.getBoundingClientRect();
|
||||
const polygonRect = e.target.getBBox();
|
||||
this._textNode.querySelector('.wrapper').innerHTML =
|
||||
misc.formatMarkdown(e.target.getAttribute('data-text'));
|
||||
const x = (
|
||||
-bodyRect.left + svgRect.left + svgRect.width * polygonRect.x);
|
||||
const y = (
|
||||
-bodyRect.top + svgRect.top + svgRect.height * (
|
||||
polygonRect.y + polygonRect.height));
|
||||
this._textNode.style.left = x + 'px';
|
||||
this._textNode.style.top = y + 'px';
|
||||
this._textNode.style.display = 'block';
|
||||
get canShowNoteText() {
|
||||
return false;
|
||||
}
|
||||
|
||||
_evtMouseLeave(e) {
|
||||
const newElement = e.relatedTarget;
|
||||
if (newElement === this._svgNode ||
|
||||
(!this._svgNode.contains(newElement) &&
|
||||
!this._textNode.contains(newElement) &&
|
||||
newElement !== this._textNode)) {
|
||||
this._textNode.style.display = 'none';
|
||||
evtCanvasKeyDown(e) {
|
||||
}
|
||||
|
||||
evtNoteMouseDown(e, hoveredNote) {
|
||||
}
|
||||
|
||||
evtCanvasMouseDown(e) {
|
||||
}
|
||||
|
||||
evtCanvasMouseMove(e) {
|
||||
}
|
||||
|
||||
evtCanvasMouseUp(e) {
|
||||
}
|
||||
|
||||
_getScreenPoint(point) {
|
||||
return new Point(
|
||||
point.x * this._control.boundingBox.width,
|
||||
point.y * this._control.boundingBox.height);
|
||||
}
|
||||
|
||||
_snapPoints(targetPoint, referencePoint) {
|
||||
const targetScreenPoint = this._getScreenPoint(targetPoint);
|
||||
const referenceScreenPoint = this._getScreenPoint(referencePoint);
|
||||
if (_getDistance(targetScreenPoint, referenceScreenPoint) <
|
||||
snapThreshold) {
|
||||
targetPoint.x = referencePoint.x;
|
||||
targetPoint.y = referencePoint.y;
|
||||
}
|
||||
}
|
||||
|
||||
_install() {
|
||||
_createNote() {
|
||||
const note = new Note();
|
||||
this._control._createPolygonNode(note);
|
||||
return note;
|
||||
}
|
||||
|
||||
_getPointFromEvent(e) {
|
||||
return new Point(
|
||||
(e.clientX - this._control.boundingBox.left) /
|
||||
this._control.boundingBox.width,
|
||||
(e.clientY - this._control.boundingBox.top) /
|
||||
this._control.boundingBox.height);
|
||||
}
|
||||
}
|
||||
|
||||
class ReadOnlyState extends State {
|
||||
constructor(control) {
|
||||
super(control);
|
||||
if (_clearEditedNote(control._hostNode)) {
|
||||
this._control.dispatchEvent(new CustomEvent('blur'));
|
||||
}
|
||||
keyboard.unpause();
|
||||
}
|
||||
|
||||
get canShowNoteText() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class PassiveState extends State {
|
||||
constructor(control) {
|
||||
super(control);
|
||||
if (_clearEditedNote(control._hostNode)) {
|
||||
this._control.dispatchEvent(new CustomEvent('blur'));
|
||||
}
|
||||
keyboard.unpause();
|
||||
}
|
||||
|
||||
get canShowNoteText() {
|
||||
return true;
|
||||
}
|
||||
|
||||
evtNoteMouseDown(e, hoveredNote) {
|
||||
this._control._state = new SelectedState(this._control, hoveredNote);
|
||||
}
|
||||
}
|
||||
|
||||
class ActiveState extends State {
|
||||
constructor(control, note) {
|
||||
super(control);
|
||||
if (_clearEditedNote(control._hostNode)) {
|
||||
this._control.dispatchEvent(new CustomEvent('blur'));
|
||||
}
|
||||
keyboard.pause();
|
||||
if (note !== undefined) {
|
||||
this._note = note;
|
||||
this._control.dispatchEvent(
|
||||
new CustomEvent('focus', {
|
||||
detail: {note: note},
|
||||
}));
|
||||
_setNodeState(this._note.groupNode, 'editing');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SelectedState extends ActiveState {
|
||||
constructor(control, note) {
|
||||
super(control, note);
|
||||
this._clickTimeout = null;
|
||||
this._control._hideNoteText();
|
||||
}
|
||||
|
||||
evtCanvasKeyDown(e) {
|
||||
const delta = e.ctrlKey ? 10 : 1;
|
||||
const offsetMap = {
|
||||
[KEY_LEFT]: [-delta, 0],
|
||||
[KEY_UP]: [0, -delta],
|
||||
[KEY_DOWN]: [0, delta],
|
||||
[KEY_RIGHT]: [delta, 0],
|
||||
};
|
||||
if (offsetMap.hasOwnProperty(e.which)) {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this._scaleEditedNote(...offsetMap[e.which]);
|
||||
} else {
|
||||
this._moveEditedNote(...offsetMap[e.which]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evtNoteMouseDown(e, hoveredNote) {
|
||||
if (this._note !== hoveredNote) {
|
||||
this._control._state =
|
||||
new SelectedState(this._control, hoveredNote);
|
||||
return;
|
||||
}
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
const mouseScreenPoint = this._getScreenPoint(mousePoint);
|
||||
this._clickTimeout = window.setTimeout(() => {
|
||||
for (let polygonPoint of this._note.polygon) {
|
||||
const distance = _getDistance(
|
||||
mouseScreenPoint,
|
||||
this._getScreenPoint(polygonPoint));
|
||||
if (distance < circleSize) {
|
||||
this._control._state = new MovingPointState(
|
||||
this._control, this._note, polygonPoint, mousePoint);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._control._state = new MovingNoteState(
|
||||
this._control, this._note, mousePoint);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
evtCanvasMouseMove(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
const mouseScreenPoint = this._getScreenPoint(mousePoint);
|
||||
for (let polygonPoint of this._note.polygon) {
|
||||
const distance = _getDistance(
|
||||
mouseScreenPoint,
|
||||
this._getScreenPoint(polygonPoint));
|
||||
polygonPoint.edgeNode.classList.toggle(
|
||||
'nearby', distance < circleSize);
|
||||
}
|
||||
}
|
||||
|
||||
evtCanvasMouseDown(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
const mouseScreenPoint = this._getScreenPoint(mousePoint);
|
||||
for (let polygonPoint of this._note.polygon) {
|
||||
const distance = _getDistance(
|
||||
mouseScreenPoint,
|
||||
this._getScreenPoint(polygonPoint));
|
||||
if (distance < circleSize) {
|
||||
this._control._state = new MovingPointState(
|
||||
this._control, this._note, polygonPoint, mousePoint);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._control._state = new PassiveState(this._control);
|
||||
}
|
||||
|
||||
evtCanvasMouseUp(e) {
|
||||
window.clearTimeout(this._clickTimeout);
|
||||
}
|
||||
|
||||
_moveEditedNote(x, y) {
|
||||
for (let point of this._note.polygon) {
|
||||
point.x += x / this._control.boundingBox.width;
|
||||
point.y += y / this._control.boundingBox.height;
|
||||
}
|
||||
}
|
||||
|
||||
_scaleEditedNote(x, y) {
|
||||
const min = new Point(Infinity, Infinity);
|
||||
const max = new Point(-Infinity, -Infinity);
|
||||
for (let point of this._note.polygon) {
|
||||
min.x = Math.min(min.x, point.x);
|
||||
min.y = Math.min(min.y, point.y);
|
||||
max.x = Math.max(max.x, point.x);
|
||||
max.y = Math.max(max.y, point.y);
|
||||
}
|
||||
const originalWidth = max.x - min.x;
|
||||
const originalHeight = max.y - min.y;
|
||||
const targetWidth = originalWidth +
|
||||
x / this._control.boundingBox.width;
|
||||
const targetHeight = originalHeight +
|
||||
y / this._control.boundingBox.height;
|
||||
const scaleX = targetWidth / originalWidth;
|
||||
const scaleY = targetHeight / originalHeight;
|
||||
for (let point of this._note.polygon) {
|
||||
point.x = min.x + ((point.x - min.x) * scaleX);
|
||||
point.y = min.y + ((point.y - min.y) * scaleY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MovingPointState extends ActiveState {
|
||||
constructor(control, note, notePoint, mousePoint) {
|
||||
super(control, note);
|
||||
this._notePoint = notePoint;
|
||||
this._originalNotePoint = {x: notePoint.x, y: notePoint.y};
|
||||
this._originalPosition = mousePoint;
|
||||
_setNodeState(this._note.groupNode, 'editing');
|
||||
}
|
||||
|
||||
evtCanvasKeyDown(e) {
|
||||
if (e.which == KEY_ESCAPE) {
|
||||
this._notePoint.x = this._originalNotePoint.x;
|
||||
this._notePoint.y = this._originalNotePoint.y;
|
||||
this._control._state = new SelectedState(this._control, this._note);
|
||||
}
|
||||
}
|
||||
|
||||
evtCanvasMouseMove(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
this._notePoint.x += mousePoint.x - this._originalPosition.x;
|
||||
this._notePoint.y += mousePoint.y - this._originalPosition.y;
|
||||
this._originalPosition = mousePoint;
|
||||
}
|
||||
|
||||
evtCanvasMouseUp(e) {
|
||||
this._control._state = new SelectedState(this._control, this._note);
|
||||
}
|
||||
}
|
||||
|
||||
class MovingNoteState extends ActiveState {
|
||||
constructor(control, note, mousePoint) {
|
||||
super(control, note);
|
||||
this._originalPolygon = [...note.polygon].map(
|
||||
point => ({x: point.x, y: point.y}));
|
||||
this._originalPosition = mousePoint;
|
||||
}
|
||||
|
||||
evtCanvasKeyDown(e) {
|
||||
if (e.which == KEY_ESCAPE) {
|
||||
for (let i of misc.range(this._note.polygon.length)) {
|
||||
this._note.polygon.at(i).x = this._originalPolygon[i].x;
|
||||
this._note.polygon.at(i).y = this._originalPolygon[i].y;
|
||||
}
|
||||
this._control._state = new SelectedState(this._control, this._note);
|
||||
}
|
||||
}
|
||||
|
||||
evtCanvasMouseMove(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
for (let point of this._note.polygon) {
|
||||
point.x += mousePoint.x - this._originalPosition.x;
|
||||
point.y += mousePoint.y - this._originalPosition.y;
|
||||
}
|
||||
this._originalPosition = mousePoint;
|
||||
}
|
||||
|
||||
evtCanvasMouseUp(e) {
|
||||
this._control._state = new SelectedState(this._control, this._note);
|
||||
}
|
||||
}
|
||||
|
||||
class ReadyToDrawState extends ActiveState {
|
||||
constructor(control) {
|
||||
super(control);
|
||||
}
|
||||
|
||||
evtNoteMouseDown(e, hoveredNote) {
|
||||
this._control._state = new SelectedState(this._control, hoveredNote);
|
||||
}
|
||||
|
||||
evtCanvasMouseDown(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
if (e.shiftKey) {
|
||||
this._control._state = new DrawingRectangleState(
|
||||
this._control, mousePoint);
|
||||
} else {
|
||||
this._control._state = new DrawingPolygonState(
|
||||
this._control, mousePoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DrawingRectangleState extends ActiveState {
|
||||
constructor(control, mousePoint) {
|
||||
super(control);
|
||||
this._note = this._createNote();
|
||||
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
|
||||
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
|
||||
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
|
||||
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
|
||||
_setNodeState(this._note.groupNode, 'drawing');
|
||||
}
|
||||
|
||||
evtCanvasMouseUp(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
const x1 = this._note.polygon.at(0).x;
|
||||
const y1 = this._note.polygon.at(0).y;
|
||||
const x2 = this._note.polygon.at(2).x;
|
||||
const y2 = this._note.polygon.at(2).y;
|
||||
const width = (x2 - x1) * this._control.boundingBox.width;
|
||||
const height = (y2 - y1) * this._control.boundingBox.height;
|
||||
if (width < 20 && height < 20) {
|
||||
this._control._deletePolygonNode(this._note);
|
||||
this._control._state = new ReadyToDrawState(this._control);
|
||||
} else {
|
||||
this._control._post.notes.add(this._note);
|
||||
this._control._state = new SelectedState(this._control, this._note);
|
||||
}
|
||||
}
|
||||
|
||||
evtCanvasMouseMove(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
this._note.polygon.at(1).x = mousePoint.x;
|
||||
this._note.polygon.at(3).y = mousePoint.y;
|
||||
this._note.polygon.at(2).x = mousePoint.x;
|
||||
this._note.polygon.at(2).y = mousePoint.y;
|
||||
}
|
||||
}
|
||||
|
||||
class DrawingPolygonState extends ActiveState {
|
||||
constructor(control, mousePoint) {
|
||||
super(control);
|
||||
this._note = this._createNote();
|
||||
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
|
||||
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
|
||||
_setNodeState(this._note.groupNode, 'drawing');
|
||||
}
|
||||
|
||||
evtCanvasKeyDown(e) {
|
||||
if (e.which == KEY_ESCAPE) {
|
||||
this._note.polygon.remove(this._note.polygon.secondLastPoint);
|
||||
if (this._note.polygon.length === 1) {
|
||||
this._cancel();
|
||||
}
|
||||
} else if (e.which == KEY_RETURN) {
|
||||
this._finish();
|
||||
}
|
||||
}
|
||||
|
||||
evtNoteMouseDown(e, hoveredNote) {
|
||||
this.evtCanvasMouseDown(e);
|
||||
}
|
||||
|
||||
evtCanvasMouseDown(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
const firstPoint = this._note.polygon.firstPoint;
|
||||
const mouseScreenPoint = this._getScreenPoint(mousePoint);
|
||||
const firstScreenPoint = this._getScreenPoint(firstPoint);
|
||||
if (_getDistance(mouseScreenPoint, firstScreenPoint) < snapThreshold) {
|
||||
this._finish();
|
||||
} else {
|
||||
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
|
||||
}
|
||||
}
|
||||
|
||||
evtCanvasMouseMove(e) {
|
||||
const mousePoint = this._getPointFromEvent(e);
|
||||
const lastPoint = this._note.polygon.lastPoint;
|
||||
const secondLastPoint = this._note.polygon.secondLastPoint;
|
||||
const firstPoint = this._note.polygon.firstPoint;
|
||||
if (!lastPoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey && secondLastPoint) {
|
||||
const direction = (Math.round(
|
||||
Math.atan2(
|
||||
secondLastPoint.y - mousePoint.y,
|
||||
secondLastPoint.x - mousePoint.x) /
|
||||
(2 * Math.PI / 4)) + 4) % 4;
|
||||
if (direction === 0 || direction === 2) {
|
||||
lastPoint.x = mousePoint.x;
|
||||
lastPoint.y = secondLastPoint.y;
|
||||
} else if (direction === 1 || direction === 3) {
|
||||
lastPoint.x = secondLastPoint.x;
|
||||
lastPoint.y = mousePoint.y;
|
||||
}
|
||||
} else {
|
||||
lastPoint.x = mousePoint.x;
|
||||
lastPoint.y = mousePoint.y;
|
||||
}
|
||||
this._snapPoints(lastPoint, firstPoint);
|
||||
}
|
||||
|
||||
_cancel() {
|
||||
this._control._deletePolygonNode(this._note);
|
||||
this._control._state = new ReadyToDrawState(this._control);
|
||||
}
|
||||
|
||||
_finish() {
|
||||
this._note.polygon.remove(this._note.polygon.lastPoint);
|
||||
if (this._note.polygon.length <= 2) {
|
||||
this._cancel();
|
||||
} else {
|
||||
this._control._post.notes.add(this._note);
|
||||
this._control._state = new SelectedState(this._control, this._note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PostNotesOverlayControl extends events.EventTarget {
|
||||
constructor(hostNode, post) {
|
||||
super();
|
||||
this._post = post;
|
||||
this._hostNode = hostNode;
|
||||
|
||||
this._svgNode = document.createElementNS(svgNS, 'svg');
|
||||
this._svgNode.classList.add('notes');
|
||||
this._svgNode.classList.add('notes-overlay');
|
||||
this._svgNode.setAttribute('preserveAspectRatio', 'none');
|
||||
this._svgNode.setAttribute('viewBox', '0 0 1 1');
|
||||
for (let note of this._post.notes) {
|
||||
const polygonNode = document.createElementNS(svgNS, 'polygon');
|
||||
polygonNode.setAttribute(
|
||||
'vector-effect', 'non-scaling-stroke');
|
||||
polygonNode.setAttribute(
|
||||
'stroke-alignment', 'inside');
|
||||
polygonNode.setAttribute(
|
||||
'points', note.polygon.map(point => point.join(',')).join(' '));
|
||||
polygonNode.setAttribute('data-text', note.text);
|
||||
polygonNode.addEventListener(
|
||||
'mouseenter', e => this._evtMouseEnter(e));
|
||||
polygonNode.addEventListener(
|
||||
'mouseleave', e => this._evtMouseLeave(e));
|
||||
this._svgNode.appendChild(polygonNode);
|
||||
this._createPolygonNode(note);
|
||||
}
|
||||
this._postOverlayNode.appendChild(this._svgNode);
|
||||
this._hostNode.appendChild(this._svgNode);
|
||||
this._post.notes.addEventListener('remove', e => {
|
||||
this._deletePolygonNode(e.detail.note);
|
||||
});
|
||||
|
||||
const keyHandler = e => this._evtCanvasKeyDown(e);
|
||||
document.addEventListener('keydown', keyHandler);
|
||||
this._svgNode.addEventListener(
|
||||
'mousedown', e => this._evtCanvasMouseDown(e));
|
||||
this._svgNode.addEventListener(
|
||||
'mouseup', e => this._evtCanvasMouseUp(e));
|
||||
this._svgNode.addEventListener(
|
||||
'mousemove', e => this._evtCanvasMouseMove(e));
|
||||
|
||||
const wrapperNode = document.createElement('div');
|
||||
wrapperNode.classList.add('wrapper');
|
||||
|
@ -65,17 +495,168 @@ class PostNotesOverlayControl {
|
|||
this._textNode.classList.add('note-text');
|
||||
this._textNode.appendChild(wrapperNode);
|
||||
this._textNode.addEventListener(
|
||||
'mouseleave', e => this._evtMouseLeave(e));
|
||||
'mouseleave', e => this._evtNoteMouseLeave(e));
|
||||
document.body.appendChild(this._textNode);
|
||||
|
||||
views.monitorNodeRemoval(
|
||||
this._postOverlayNode, () => { this._uninstall(); });
|
||||
this._hostNode, () => {
|
||||
this._hostNode.removeChild(this._svgNode);
|
||||
document.removeEventListener('keydown', keyHandler);
|
||||
document.body.removeChild(this._textNode);
|
||||
this._state = new ReadOnlyState(this);
|
||||
});
|
||||
|
||||
this._state = new ReadOnlyState(this);
|
||||
}
|
||||
|
||||
_uninstall() {
|
||||
this._postOverlayNode.removeChild(this._svgNode);
|
||||
document.body.removeChild(this._textNode);
|
||||
switchToPassiveEdit() {
|
||||
this._state = new PassiveState(this);
|
||||
}
|
||||
};
|
||||
|
||||
switchToDrawing() {
|
||||
this._state = new ReadyToDrawState(this);
|
||||
}
|
||||
|
||||
get boundingBox() {
|
||||
return this._hostNode.getBoundingClientRect();
|
||||
}
|
||||
|
||||
_evtCanvasKeyDown(e) {
|
||||
this._state.evtCanvasKeyDown(e);
|
||||
}
|
||||
|
||||
_evtCanvasMouseDown(e) {
|
||||
e.preventDefault();
|
||||
if (e.which !== MOUSE_BUTTON_LEFT) {
|
||||
return;
|
||||
}
|
||||
const hoveredNode = document.elementFromPoint(e.clientX, e.clientY);
|
||||
let hoveredNote = null;
|
||||
for (let note of this._post.notes) {
|
||||
if (note.polygonNode === hoveredNode) {
|
||||
hoveredNote = note;
|
||||
}
|
||||
}
|
||||
if (hoveredNote) {
|
||||
this._state.evtNoteMouseDown(e, hoveredNote);
|
||||
} else {
|
||||
this._state.evtCanvasMouseDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
_evtCanvasMouseUp(e) {
|
||||
this._state.evtCanvasMouseUp(e);
|
||||
}
|
||||
|
||||
_evtCanvasMouseMove(e) {
|
||||
this._state.evtCanvasMouseMove(e);
|
||||
}
|
||||
|
||||
_evtNoteMouseEnter(e, note) {
|
||||
if (this._state.canShowNoteText) {
|
||||
this._showNoteText(note);
|
||||
}
|
||||
}
|
||||
|
||||
_evtNoteMouseLeave(e) {
|
||||
const newElement = e.relatedTarget;
|
||||
if (newElement === this._svgNode ||
|
||||
(!this._svgNode.contains(newElement) &&
|
||||
!this._textNode.contains(newElement) &&
|
||||
newElement !== this._textNode)) {
|
||||
this._hideNoteText();
|
||||
}
|
||||
}
|
||||
|
||||
_showNoteText(note) {
|
||||
this._textNode.querySelector('.wrapper').innerHTML =
|
||||
misc.formatMarkdown(note.text);
|
||||
this._textNode.style.display = 'block';
|
||||
const polygonRect = note.polygonNode.getBBox();
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const noteRect = this._textNode.getBoundingClientRect();
|
||||
const svgRect = this.boundingBox;
|
||||
const x = (
|
||||
-bodyRect.left +
|
||||
svgRect.left +
|
||||
svgRect.width * (polygonRect.x + polygonRect.width / 2) -
|
||||
noteRect.width / 2);
|
||||
const y = (
|
||||
-bodyRect.top +
|
||||
svgRect.top +
|
||||
svgRect.height * (polygonRect.y + polygonRect.height / 2) -
|
||||
noteRect.height / 2);
|
||||
this._textNode.style.left = x + 'px';
|
||||
this._textNode.style.top = y + 'px';
|
||||
}
|
||||
|
||||
_hideNoteText() {
|
||||
this._textNode.style.display = 'none';
|
||||
}
|
||||
|
||||
_updatePolygonNotePoints(note) {
|
||||
note.polygonNode.setAttribute(
|
||||
'points',
|
||||
[...note.polygon].map(
|
||||
point => [point.x, point.y].join(',')).join(' '));
|
||||
}
|
||||
|
||||
_createEdgeNode(point, groupNode) {
|
||||
const node = document.createElementNS(svgNS, 'ellipse');
|
||||
node.setAttribute('cx', point.x);
|
||||
node.setAttribute('cy', point.y);
|
||||
node.setAttribute('rx', circleSize / 2 / this.boundingBox.width);
|
||||
node.setAttribute('ry', circleSize / 2 / this.boundingBox.height);
|
||||
point.edgeNode = node;
|
||||
groupNode.appendChild(node);
|
||||
}
|
||||
|
||||
_deleteEdgeNode(point, note) {
|
||||
this._updatePolygonNotePoints(note);
|
||||
point.edgeNode.parentNode.removeChild(point.edgeNode);
|
||||
}
|
||||
|
||||
_updateEdgeNode(point, note) {
|
||||
this._updatePolygonNotePoints(note);
|
||||
point.edgeNode.setAttribute('cx', point.x);
|
||||
point.edgeNode.setAttribute('cy', point.y);
|
||||
}
|
||||
|
||||
_deletePolygonNode(note) {
|
||||
note.polygonNode.parentNode.removeChild(note.polygonNode);
|
||||
}
|
||||
|
||||
_createPolygonNode(note) {
|
||||
const groupNode = document.createElementNS(svgNS, 'g');
|
||||
note.groupNode = groupNode;
|
||||
{
|
||||
const node = document.createElementNS(svgNS, 'polygon');
|
||||
note.polygonNode = node;
|
||||
node.setAttribute('vector-effect', 'non-scaling-stroke');
|
||||
node.setAttribute('stroke-alignment', 'inside');
|
||||
node.addEventListener(
|
||||
'mouseenter', e => this._evtNoteMouseEnter(e, note));
|
||||
node.addEventListener(
|
||||
'mouseleave', e => this._evtNoteMouseLeave(e));
|
||||
this._updatePolygonNotePoints(note);
|
||||
groupNode.appendChild(node);
|
||||
}
|
||||
for (let point of note.polygon) {
|
||||
this._createEdgeNode(point, groupNode);
|
||||
}
|
||||
|
||||
note.polygon.addEventListener('change', e => {
|
||||
this._updateEdgeNode(e.detail.point, note);
|
||||
});
|
||||
note.polygon.addEventListener('remove', e => {
|
||||
this._deleteEdgeNode(e.detail.point, note);
|
||||
});
|
||||
note.polygon.addEventListener('add', e => {
|
||||
this._createEdgeNode(e.detail.point, groupNode);
|
||||
});
|
||||
|
||||
this._svgNode.appendChild(groupNode);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PostNotesOverlayControl;
|
||||
|
|
|
@ -12,18 +12,32 @@ class AbstractList extends events.EventTarget {
|
|||
const ret = new this();
|
||||
for (let item of response) {
|
||||
const addedItem = this._itemClass.fromResponse(item);
|
||||
addedItem.addEventListener('delete', e => {
|
||||
ret.remove(addedItem);
|
||||
});
|
||||
if (addedItem.addEventListener) {
|
||||
addedItem.addEventListener('delete', e => {
|
||||
ret.remove(addedItem);
|
||||
});
|
||||
addedItem.addEventListener('change', e => {
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: e.detail,
|
||||
}));
|
||||
});
|
||||
}
|
||||
ret._list.push(addedItem);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
add(item) {
|
||||
item.addEventListener('delete', e => {
|
||||
this.remove(item);
|
||||
});
|
||||
if (item.addEventListener) {
|
||||
item.addEventListener('delete', e => {
|
||||
this.remove(item);
|
||||
});
|
||||
item.addEventListener('change', e => {
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: e.detail,
|
||||
}));
|
||||
});
|
||||
}
|
||||
this._list.push(item);
|
||||
const detail = {};
|
||||
detail[this.constructor._itemName] = item;
|
||||
|
@ -32,6 +46,12 @@ class AbstractList extends events.EventTarget {
|
|||
}));
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (let item of [...this._list]) {
|
||||
this.remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
remove(itemToRemove) {
|
||||
for (let [index, item] of this._list.entries()) {
|
||||
if (item !== itemToRemove) {
|
||||
|
@ -51,6 +71,10 @@ class AbstractList extends events.EventTarget {
|
|||
return this._list.length;
|
||||
}
|
||||
|
||||
at(index) {
|
||||
return this._list[index];
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._list[Symbol.iterator]();
|
||||
}
|
||||
|
|
34
client/js/models/note.js
Normal file
34
client/js/models/note.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const Point = require('./point.js');
|
||||
const PointList = require('./point_list.js');
|
||||
|
||||
class Note extends events.EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this._text = '…';
|
||||
this._polygon = new PointList();
|
||||
}
|
||||
|
||||
get text() { return this._text; }
|
||||
get polygon() { return this._polygon; }
|
||||
|
||||
set text(value) { this._text = value; }
|
||||
|
||||
static fromResponse(response) {
|
||||
const note = new Note();
|
||||
note._updateFromResponse(response);
|
||||
return note;
|
||||
}
|
||||
|
||||
_updateFromResponse(response) {
|
||||
this._text = response.text;
|
||||
this._polygon.clear();
|
||||
for (let point of response.polygon) {
|
||||
this._polygon.add(new Point(point[0], point[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Note;
|
12
client/js/models/note_list.js
Normal file
12
client/js/models/note_list.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
const AbstractList = require('./abstract_list.js');
|
||||
const Note = require('./note.js');
|
||||
|
||||
class NoteList extends AbstractList {
|
||||
}
|
||||
|
||||
NoteList._itemClass = Note;
|
||||
NoteList._itemName = 'note';
|
||||
|
||||
module.exports = NoteList;
|
26
client/js/models/point.js
Normal file
26
client/js/models/point.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
|
||||
class Point extends events.EventTarget {
|
||||
constructor(x, y) {
|
||||
super();
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
|
||||
get x() { return this._x; }
|
||||
get y() { return this._y; }
|
||||
|
||||
set x(value) {
|
||||
this._x = value;
|
||||
this.dispatchEvent(new CustomEvent('change', {detail: {point: this}}));
|
||||
}
|
||||
|
||||
set y(value) {
|
||||
this._y = value;
|
||||
this.dispatchEvent(new CustomEvent('change', {detail: {point: this}}));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Point;
|
23
client/js/models/point_list.js
Normal file
23
client/js/models/point_list.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const AbstractList = require('./abstract_list.js');
|
||||
const Point = require('./point.js');
|
||||
|
||||
class PointList extends AbstractList {
|
||||
get firstPoint() {
|
||||
return this._list[0];
|
||||
}
|
||||
|
||||
get secondLastPoint() {
|
||||
return this._list[this._list.length - 2];
|
||||
}
|
||||
|
||||
get lastPoint() {
|
||||
return this._list[this._list.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
PointList._itemClass = Point;
|
||||
PointList._itemName = 'point';
|
||||
|
||||
module.exports = PointList;
|
|
@ -3,6 +3,7 @@
|
|||
const api = require('../api.js');
|
||||
const tags = require('../tags.js');
|
||||
const events = require('../events.js');
|
||||
const NoteList = require('./note_list.js');
|
||||
const CommentList = require('./comment_list.js');
|
||||
const misc = require('../util/misc.js');
|
||||
|
||||
|
@ -99,6 +100,13 @@ class Post extends events.EventTarget {
|
|||
if (misc.arraysDiffer(this._relations, this._orig._relations)) {
|
||||
detail.relations = this._relations;
|
||||
}
|
||||
if (misc.arraysDiffer(this._notes, this._orig._notes)) {
|
||||
detail.notes = [...this._notes].map(note => ({
|
||||
polygon: [...note.polygon].map(
|
||||
point => [point.x, point.y]),
|
||||
text: note.text,
|
||||
}));
|
||||
}
|
||||
if (this._content) {
|
||||
files.content = this._content;
|
||||
}
|
||||
|
@ -228,7 +236,7 @@ class Post extends events.EventTarget {
|
|||
}
|
||||
|
||||
_updateFromResponse(response) {
|
||||
const map = {
|
||||
const map = () => ({
|
||||
_id: response.id,
|
||||
_type: response.type,
|
||||
_mimeType: response.mimeType,
|
||||
|
@ -243,7 +251,7 @@ class Post extends events.EventTarget {
|
|||
|
||||
_flags: response.flags || [],
|
||||
_tags: response.tags || [],
|
||||
_notes: response.notes || [],
|
||||
_notes: NoteList.fromResponse(response.notes || []),
|
||||
_comments: CommentList.fromResponse(response.comments || []),
|
||||
_relations: response.relations || [],
|
||||
|
||||
|
@ -252,10 +260,10 @@ class Post extends events.EventTarget {
|
|||
_ownScore: response.ownScore,
|
||||
_ownFavorite: response.ownFavorite,
|
||||
_hasCustomThumbnail: response.hasCustomThumbnail,
|
||||
};
|
||||
});
|
||||
|
||||
Object.assign(this, map);
|
||||
Object.assign(this._orig, map);
|
||||
Object.assign(this, map());
|
||||
Object.assign(this._orig, map());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,16 @@
|
|||
const mousetrap = require('mousetrap');
|
||||
const settings = require('../models/settings.js');
|
||||
|
||||
let paused = false;
|
||||
const _originalStopCallback = mousetrap.prototype.stopCallback;
|
||||
mousetrap.prototype.stopCallback = function(...args) {
|
||||
var self = this;
|
||||
if (paused) {
|
||||
return true;
|
||||
}
|
||||
return _originalStopCallback.call(self, ...args);
|
||||
};
|
||||
|
||||
function bind(hotkey, func) {
|
||||
if (settings.get().keyboardShortcuts) {
|
||||
mousetrap.bind(hotkey, func);
|
||||
|
@ -18,4 +28,6 @@ function unbind(hotkey) {
|
|||
module.exports = {
|
||||
bind: bind,
|
||||
unbind: unbind,
|
||||
pause: () => { paused = true; },
|
||||
unpause: () => { paused = false; },
|
||||
};
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
const marked = require('marked');
|
||||
|
||||
function decamelize(str, sep) {
|
||||
sep = sep === undefined ? '-' : sep;
|
||||
return str
|
||||
.replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2')
|
||||
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function* range(start=0, end=null, step=1) {
|
||||
if (end == null) {
|
||||
end = start;
|
||||
|
@ -245,9 +253,11 @@ function escapeHtml(unsafe) {
|
|||
}
|
||||
|
||||
function arraysDiffer(source1, source2) {
|
||||
source1 = [...source1];
|
||||
source2 = [...source2];
|
||||
return (
|
||||
[...source1].filter(value => !source2.includes(value)).length > 0 ||
|
||||
[...source2].filter(value => !source1.includes(value)).length > 0);
|
||||
source1.filter(value => !source2.includes(value)).length > 0 ||
|
||||
source2.filter(value => !source1.includes(value)).length > 0);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -266,4 +276,5 @@ module.exports = {
|
|||
makeCssName: makeCssName,
|
||||
splitByWhitespace: splitByWhitespace,
|
||||
arraysDiffer: arraysDiffer,
|
||||
decamelize: decamelize,
|
||||
};
|
||||
|
|
|
@ -47,7 +47,7 @@ class PostView {
|
|||
];
|
||||
});
|
||||
|
||||
new PostNotesOverlayControl(
|
||||
this._postNotesOverlayControl = new PostNotesOverlayControl(
|
||||
postContainerNode.querySelector('.post-overlay'),
|
||||
ctx.post);
|
||||
|
||||
|
@ -80,7 +80,10 @@ class PostView {
|
|||
|
||||
if (ctx.editMode) {
|
||||
this.sidebarControl = new PostEditSidebarControl(
|
||||
sidebarContainerNode, ctx.post, this._postContentControl);
|
||||
sidebarContainerNode,
|
||||
ctx.post,
|
||||
this._postContentControl,
|
||||
this._postNotesOverlayControl);
|
||||
} else {
|
||||
this.sidebarControl = new PostReadonlySidebarControl(
|
||||
sidebarContainerNode, ctx.post, this._postContentControl);
|
||||
|
|
Loading…
Reference in a new issue