client/posts: add note editing
This commit is contained in:
		
							parent
							
								
									9e2dace73f
								
							
						
					
					
						commit
						d5a00fe4b9
					
				@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user