Commit 07d13a16 authored by chris's avatar chris

initial commit

parents
Pipeline #10469 failed with stages
in 17 seconds
Copyright (c) 2010-2017 Michael Aufreiter, Oliver Buchtala
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
/*
Global variables
TODO: we should move all app-specific styling into app-land, and only keep
styles that are necessary for the editing to work.
-------------------------------------------------- */
:root {
/* Layout */
--small-layout-width: 300px;
--medium-layout-width: 620px;
--large-layout-width: 960px;
/* Normalized heights (used by buttons and inputs) */
--base-height: 40px;
--short-height: 20px;
--border-radius: 5px;
/* Colors */
--dark-bg-color: #2E2E2E;
--border-color: #E0E4E4;
--dark-border-color: #777;
/* Used by Button component */
--button-color: rgba(0,0,0,0.75);
--fill-white-color: #fff;
--fill-light-color: #f7f9f9; /* #f8f8f8; */
--fill-dark-color: #404040;
--default-box-shadow: 0 0 0 0.75pt #d1d1d1, 0 0 3pt 0.75pt #aaa;
/* Depending on a base-color */
--darken-color-1: rgba(0,0,0,0.05);
--darken-color-2: rgba(0,0,0,0.10);
--darken-color-3: rgba(0,0,0,0.25);
--darken-color-4: rgba(0,0,0,0.50);
--darken-color-5: rgba(0,0,0,0.75);
--lighten-color-1: rgba(0,0,0,0.05);
--lighten-color-2: rgba(0,0,0,0.10);
--lighten-color-3: rgba(0,0,0,0.25);
--lighten-color-4: rgba(0,0,0,0.50);
--lighten-color-5: rgba(0,0,0,0.75);
--link-color: #1795CD;
--text-action-color: #2E72EA;
--border-action-color: #2E72EA;
--light-bg-color: #F7F7F9; /* light grey */
--active-color: #2E72EA;
--active-light-bg-color: #2E72EA0a;
--separator-color: rgba(0,0,0,0.05);
/* We disable this for now, as accessibility needs more discussion */
--focus-outline-color: transparent; /* #1795CD;/* #5BE3FF;
/* Font colors */
--default-text-color: rgba(0,0,0,0.75);
--light-text-color: rgba(0,0,0,.40);
/* Default padding */
--default-padding: 20px;
/* Prose font sizes */
--default-font-size: 16px;
--small-font-size: 13px;
--large-font-size: 20px;
--xlarge-font-size: 25px;
/* Title font sizes */
--title-font-size: 38px;
/* Heading font sizes */
--h1-font-size: 26px;
--h2-font-size: 22px;
--h3-font-size: 18px;
--h4-font-size: 16px;
--strong-font-weight: 600;
--highlight-color-1: #0b9dd9;
--highlight-color-2: #91bb04;
--heading-letterspacing: -0.5px;
/* code-font */
--font-family-code: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--font-size-code: 14px;
/* RGB #A3CDFD = HSB 209,29,80 */
--local-selection-color: #2A8CFF;
}
/*
Implements Substance ChangeStore API. This is just a dumb store.
No integrity checks are made, as this is the task of DocumentEngine
*/
class ChangeStore {
constructor (seed) {
this._changes = seed || {}
}
/*
Gets changes for a given document
@param {String} documentId document id
@param {Number} sinceVersion since which change (optional)
@param {Number} toVersion up to and including version (optional)
*/
getChanges (documentId, sinceVersion, toVersion, cb) {
if (typeof sinceVersion === 'function') {
cb = sinceVersion
sinceVersion = 0
} else if (typeof toVersion === 'function') {
cb = toVersion
toVersion = undefined
}
if (!(documentId && sinceVersion >= 0 && cb)) {
throw new Error('Invalid arguments')
}
let version = this._getVersion(documentId)
let changes = this._getChanges(documentId)
changes = changes.slice(sinceVersion, toVersion)
cb(null, changes, version)
}
/*
Add a change object to the database
*/
addChange (documentId, change, cb) {
if (!documentId || !change) {
throw new Error('Invalid arguments')
}
this._addChange(documentId, change)
let newVersion = this._getVersion(documentId)
cb(null, newVersion)
}
/*
Delete changes for a given documentId
*/
deleteChanges (documentId, cb) {
var deletedChanges = this._deleteChanges(documentId)
cb(null, deletedChanges.length)
}
/*
Gets the version number for a document
*/
getVersion (id, cb) {
cb(null, this._getVersion(id))
}
// Handy synchronous helpers
// -------------------------
_deleteChanges (documentId) {
var changes = this._getChanges(documentId)
delete this._changes[documentId]
return changes
}
_getVersion (documentId) {
var changes = this._changes[documentId]
return changes ? changes.length : 0
}
_getChanges (documentId) {
return this._changes[documentId] || []
}
_addChange (documentId, change) {
if (!this._changes[documentId]) {
this._changes[documentId] = []
}
this._changes[documentId].push(change)
}
}
export default ChangeStore
import EventEmitter from '../util/EventEmitter'
import Err from '../util/SubstanceError'
/**
ClientConnection abstraction. Uses websockets internally
*/
class ClientConnection extends EventEmitter {
constructor (config) {
super()
this.config = config
this._onMessage = this._onMessage.bind(this)
this._onConnectionOpen = this._onConnectionOpen.bind(this)
this._onConnectionClose = this._onConnectionClose.bind(this)
// Establish websocket connection
this._connect()
}
_createWebSocket () {
throw Err('AbstractMethodError')
}
/*
Initializes a new websocket connection
*/
_connect () {
this.ws = this._createWebSocket()
this.ws.addEventListener('open', this._onConnectionOpen)
this.ws.addEventListener('close', this._onConnectionClose)
this.ws.addEventListener('message', this._onMessage)
}
/*
Disposes the current websocket connection
*/
_disconnect () {
this.ws.removeEventListener('message', this._onMessage)
this.ws.removeEventListener('open', this._onConnectionOpen)
this.ws.removeEventListener('close', this._onConnectionClose)
this.ws = null
}
/*
Emits open event when connection has been established
*/
_onConnectionOpen () {
this.emit('open')
}
/*
Trigger reconnect on connection close
*/
_onConnectionClose () {
this._disconnect()
this.emit('close')
console.info('websocket connection closed. Attempting to reconnect in 5s.')
setTimeout(function () {
this._connect()
}.bind(this), 5000)
}
/*
Delegate incoming websocket messages
*/
_onMessage (msg) {
msg = this.deserializeMessage(msg.data)
this.emit('message', msg)
}
/*
Send message via websocket channel
*/
send (msg) {
if (!this.isOpen()) {
console.warn('Message could not be sent. Connection is not open.', msg)
return
}
this.ws.send(this.serializeMessage(msg))
}
/*
Returns true if websocket connection is open
*/
isOpen () {
return this.ws && this.ws.readyState === 1
}
serializeMessage (msg) {
return JSON.stringify(msg)
}
deserializeMessage (msg) {
return JSON.parse(msg)
}
}
export default ClientConnection
import EventEmitter from '../util/EventEmitter'
/**
Client for CollabServer API
Communicates via websocket for real-time operations
*/
class CollabClient extends EventEmitter {
constructor (config) {
super()
this.config = config
this.connection = config.connection
// Hard-coded for now
this.scope = 'substance/collab'
// Bind handlers
this._onMessage = this._onMessage.bind(this)
this._onConnectionOpen = this._onConnectionOpen.bind(this)
this._onConnectionClose = this._onConnectionClose.bind(this)
// Connect handlers
this.connection.on('open', this._onConnectionOpen)
this.connection.on('close', this._onConnectionClose)
this.connection.on('message', this._onMessage)
}
_onConnectionClose () {
this.emit('disconnected')
}
_onConnectionOpen () {
this.emit('connected')
}
/*
Delegate incoming messages from the connection
*/
_onMessage (msg) {
if (msg.scope === this.scope) {
this.emit('message', msg)
} else if (msg.scope !== '_internal') {
console.info('Message ignored. Not sent in hub scope', msg)
}
}
/*
Send message via websocket channel
*/
send (msg) {
if (!this.connection.isOpen()) {
console.warn('Message could not be sent. Connection not open.', msg)
return
}
msg.scope = this.scope
if (this.config.enhanceMessage) {
msg = this.config.enhanceMessage(msg)
}
this.connection.send(msg)
}
/*
Returns true if websocket connection is open
*/
isConnected () {
return this.connection.isOpen()
}
dispose () {
this.connection.off(this)
}
}
export default CollabClient
import EventEmitter from '../util/EventEmitter'
import forEach from '../util/forEach'
import map from '../util/map'
import Err from '../util/SubstanceError'
import DocumentChange from '../model/DocumentChange'
import * as operationHelpers from '../model/operationHelpers'
/*
Engine for realizing collaborative editing. Implements the server-methods of
real time editing as a reusable library.
*/
class CollabEngine extends EventEmitter {
constructor (documentEngine) {
super()
this.documentEngine = documentEngine
// Active collaborators
this._collaborators = {}
}
/*
Register collaborator for a given documentId
*/
_register (collaboratorId, documentId, collaboratorInfo) {
let collaborator = this._collaborators[collaboratorId]
if (!collaborator) {
collaborator = this._collaborators[collaboratorId] = {
collaboratorId: collaboratorId,
documents: {}
}
}
// Extend with collaboratorInfo if available
collaborator.info = collaboratorInfo
// Register document
collaborator.documents[documentId] = {}
}
/*
Unregister collaborator id from document
*/
_unregister (collaboratorId, documentId) {
let collaborator = this._collaborators[collaboratorId]
delete collaborator.documents[documentId]
let docCount = Object.keys(collaborator.documents).length
// If there is no doc left, we can remove the entire collaborator entry
if (docCount === 0) {
delete this._collaborators[collaboratorId]
}
}
/*
Get list of active documents for a given collaboratorId
*/
getDocumentIds (collaboratorId) {
let collaborator = this._collaborators[