├── nodes ├── code │ ├── code.ejs │ ├── schema.json │ └── code.js ├── cover │ ├── cover.ejs │ ├── schema.json │ └── cover.js ├── image │ ├── image.ejs │ ├── schema.json │ └── image.js ├── text │ ├── text.ejs │ ├── schema.json │ └── text.js ├── section │ ├── section.ejs │ ├── schema.json │ └── section.js └── map │ ├── map.ejs │ ├── schema.json │ └── map.js ├── styles ├── comments.less ├── patches.less ├── document │ ├── node │ │ ├── text.less │ │ ├── document.less │ │ ├── section.less │ │ ├── code.less │ │ ├── image.less │ │ └── map.less │ ├── node.less │ └── document.less ├── main.less ├── tools.less ├── history.less ├── mixins.less ├── reset.less └── layout.less ├── templates ├── patches.ejs ├── document.ejs ├── outline.ejs ├── composer.ejs ├── history.ejs ├── tools.ejs └── controls_insert.ejs ├── public └── images │ ├── layers.png │ ├── marker.png │ ├── zoom-in.png │ ├── zoom-out.png │ ├── popup-close.png │ ├── marker-shadow.png │ └── header_background.png ├── source-graphics └── renders │ └── composer-comments.png ├── .gitignore ├── src ├── client │ ├── views │ │ ├── history.js │ │ ├── outline.js │ │ ├── comments.js │ │ ├── patches.js │ │ ├── tools.js │ │ ├── node.js │ │ └── document.js │ ├── instructors │ │ └── instructor.js │ ├── state_machine.js │ ├── util.js │ ├── persistence.js │ ├── composer.js │ └── boot.js ├── server │ ├── document_storage.js │ ├── util.js │ └── document_manager.js └── shared │ └── model │ └── document.js ├── lib ├── codemirror │ ├── util │ │ ├── simple-hint.css │ │ ├── dialog.css │ │ ├── runmode.js │ │ ├── match-highlighter.js │ │ ├── overlay.js │ │ ├── dialog.js │ │ ├── simple-hint.js │ │ ├── search.js │ │ ├── searchcursor.js │ │ ├── javascript-hint.js │ │ ├── closetag.js │ │ ├── foldcode.js │ │ └── formatting.js │ └── codemirror.css ├── keymaster.min.js ├── jquery.timeago.js ├── head.min.js ├── remotestorage.js └── jquery.transloadit2.js ├── package.json ├── data ├── example.json └── schema.json ├── test ├── document-test.js ├── model │ └── index.html └── vendor │ └── mocha.css ├── testsuites └── concurrent_editing.json ├── settings.json ├── LICENSE ├── layouts └── app.html ├── server.js └── README.md /nodes/code/code.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/cover/cover.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/image/image.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/text/text.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/comments.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/patches.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/section/section.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/patches.ejs: -------------------------------------------------------------------------------- 1 |

Patches

2 |
3 | 4 |
-------------------------------------------------------------------------------- /templates/document.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
-------------------------------------------------------------------------------- /public/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/public/images/layers.png -------------------------------------------------------------------------------- /public/images/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/public/images/marker.png -------------------------------------------------------------------------------- /public/images/zoom-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/public/images/zoom-in.png -------------------------------------------------------------------------------- /public/images/zoom-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/public/images/zoom-out.png -------------------------------------------------------------------------------- /public/images/popup-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/public/images/popup-close.png -------------------------------------------------------------------------------- /public/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/public/images/marker-shadow.png -------------------------------------------------------------------------------- /templates/outline.ejs: -------------------------------------------------------------------------------- 1 |

Outline

2 |
3 | Document outline. To be implemented. 4 |
-------------------------------------------------------------------------------- /public/images/header_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/public/images/header_background.png -------------------------------------------------------------------------------- /source-graphics/renders/composer-comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/composer/master/source-graphics/renders/composer-comments.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config/client_session_key.aes 2 | cabal-dev 3 | dist 4 | static/tmp 5 | substantial.sqlite3 6 | .DS_Store 7 | config.json 8 | node_modules/ -------------------------------------------------------------------------------- /templates/composer.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | THE DOCUMENT 8 |
9 |
-------------------------------------------------------------------------------- /styles/document/node/text.less: -------------------------------------------------------------------------------- 1 | .content-node.selected.text > .operations { background: #459fc9; } 2 | 3 | .content-node.text .content { 4 | padding: 10px 0; 5 | /*margin: 0 100px;*/ 6 | } -------------------------------------------------------------------------------- /nodes/map/map.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
-------------------------------------------------------------------------------- /src/client/views/history.js: -------------------------------------------------------------------------------- 1 | sc.views.History = Dance.Performer.extend({ 2 | 3 | // Events 4 | // ------ 5 | 6 | events: { 7 | 8 | }, 9 | 10 | // Handlers 11 | // -------- 12 | 13 | render: function () { 14 | this.$el.html(_.tpl('history', this.model)); 15 | return this; 16 | } 17 | }); -------------------------------------------------------------------------------- /src/client/views/outline.js: -------------------------------------------------------------------------------- 1 | sc.views.Outline = Dance.Performer.extend({ 2 | 3 | // Events 4 | // ------ 5 | 6 | events: { 7 | 8 | }, 9 | 10 | // Handlers 11 | // -------- 12 | 13 | render: function () { 14 | this.$el.html(_.tpl('outline', this.model)); 15 | return this; 16 | } 17 | }); -------------------------------------------------------------------------------- /src/client/views/comments.js: -------------------------------------------------------------------------------- 1 | sc.views.Comments = Dance.Performer.extend({ 2 | 3 | // Events 4 | // ------ 5 | 6 | events: { 7 | 8 | }, 9 | 10 | // Handlers 11 | // -------- 12 | 13 | render: function () { 14 | this.$el.html(_.tpl('comments', this.model)); 15 | return this; 16 | } 17 | }); -------------------------------------------------------------------------------- /src/client/views/patches.js: -------------------------------------------------------------------------------- 1 | sc.views.Patches = Dance.Performer.extend({ 2 | 3 | // Events 4 | // ------ 5 | 6 | events: { 7 | 8 | }, 9 | 10 | // Handlers 11 | // -------- 12 | 13 | render: function () { 14 | this.$el.html(_.tpl('patches', this.model)); 15 | return this; 16 | } 17 | }); -------------------------------------------------------------------------------- /templates/history.ejs: -------------------------------------------------------------------------------- 1 |

Operations

2 |
3 | <% _.each(_.clone(operations).reverse(), function(o) { %> 4 |
5 | <% var cmd = o.command.split(':') %> 6 |
<%= cmd[0] %>
<%= cmd[1] %>
7 |
<%= JSON.stringify(o, undefined, 2) %>
8 |
9 | <% }); %> 10 |
-------------------------------------------------------------------------------- /styles/document/node.less: -------------------------------------------------------------------------------- 1 | /* Generic node styles */ 2 | 3 | .content-node { 4 | position: relative; 5 | 6 | .handle { 7 | position: absolute; 8 | left: -20px; 9 | top: 0px; 10 | bottom: 1px; 11 | width: 20px; 12 | background: #747260; 13 | opacity: 0.1; 14 | } 15 | 16 | &:hover .handle { 17 | background: #747260; 18 | opacity: 0.3; 19 | } 20 | 21 | &.selected .handle { 22 | opacity: 1.0; 23 | } 24 | } -------------------------------------------------------------------------------- /nodes/section/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "/type/section": { 3 | "_id": "/type/section", 4 | "type": "/type/type", 5 | "name": "Section", 6 | "properties": { 7 | "name": { 8 | "name": "Name", 9 | "unique": true, 10 | "type": "string", 11 | "default": "" 12 | }, 13 | "direction": { 14 | "name": "Direction", 15 | "unique": true, 16 | "type": "string" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /nodes/text/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "/type/text": { 3 | "_id": "/type/text", 4 | "type": "/type/type", 5 | "name": "Text", 6 | "properties": { 7 | "content": { 8 | "name": "Content", 9 | "unique": true, 10 | "type": "string", 11 | "default": "

" 12 | }, 13 | "direction": { 14 | "name": "Direction", 15 | "unique": true, 16 | "type": "string" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /lib/codemirror/util/simple-hint.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-completions { 2 | position: absolute; 3 | z-index: 10; 4 | overflow: hidden; 5 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 6 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 7 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 8 | } 9 | .CodeMirror-completions select { 10 | background: #fafafa; 11 | outline: none; 12 | border: none; 13 | padding: 0; 14 | margin: 0; 15 | font-family: monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/client/instructors/instructor.js: -------------------------------------------------------------------------------- 1 | sc.instructors.Instructor = Dance.Instructor.extend({ 2 | initialize: function() { 3 | // Using this.route, because order matters 4 | this.route(':document', 'loadDocument', this.loadDocument); 5 | this.route('new', 'newDocument', this.newDocument); 6 | }, 7 | 8 | newDocument: function() { 9 | composer.newDocument(); 10 | }, 11 | 12 | loadDocument: function(id) { 13 | composer.read(id); 14 | } 15 | }); -------------------------------------------------------------------------------- /nodes/code/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "/type/code": { 3 | "_id": "/type/code", 4 | "type": "/type/type", 5 | "name": "Code", 6 | "properties": { 7 | "content": { 8 | "name": "Content", 9 | "unique": true, 10 | "type": "string", 11 | "default": "" 12 | }, 13 | "language" : { 14 | "name": "Language", 15 | "unique": true, 16 | "type": "string", 17 | "default": "javascript" 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /nodes/cover/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "/type/cover": { 3 | "_id": "/type/cover", 4 | "type": "/type/type", 5 | "name": "Cover", 6 | "properties": { 7 | "title": { 8 | "name": "Document Title", 9 | "unique": true, 10 | "type": "string", 11 | "default": "" 12 | }, 13 | "abstract": { 14 | "name": "Abstract", 15 | "unique": true, 16 | "type": "string", 17 | "default": "" 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /lib/codemirror/util/dialog.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-dialog { 2 | position: relative; 3 | } 4 | 5 | .CodeMirror-dialog > div { 6 | position: absolute; 7 | top: 0; left: 0; right: 0; 8 | background: white; 9 | border-bottom: 1px solid #eee; 10 | z-index: 15; 11 | padding: .1em .8em; 12 | overflow: hidden; 13 | color: #333; 14 | } 15 | 16 | .CodeMirror-dialog input { 17 | border: none; 18 | outline: none; 19 | background: transparent; 20 | width: 20em; 21 | color: inherit; 22 | font-family: monospace; 23 | } 24 | -------------------------------------------------------------------------------- /src/client/views/tools.js: -------------------------------------------------------------------------------- 1 | sc.views.Tools = Dance.Performer.extend({ 2 | 3 | // Events 4 | // ------ 5 | 6 | events: { 7 | 8 | }, 9 | 10 | // Handlers 11 | // -------- 12 | 13 | initialize: function() { 14 | 15 | // Views 16 | this.views = {}; 17 | this.views.tool = new sc.views.Patches({model: this.model}); 18 | }, 19 | 20 | render: function() { 21 | this.$el.html(_.tpl('tools', this.model)); 22 | this.$('.tool').html(this.views.tool.render().el); 23 | return this; 24 | } 25 | }); -------------------------------------------------------------------------------- /styles/main.less: -------------------------------------------------------------------------------- 1 | @images: "images"; 2 | 3 | @import "reset.less"; 4 | @import "mixins.less"; 5 | 6 | @import "layout.less"; 7 | @import "tools.less"; 8 | @import "comments.less"; 9 | @import "patches.less"; 10 | @import "history.less"; 11 | 12 | 13 | @import "document/document.less"; 14 | @import "document/node.less"; 15 | @import "document/node/document.less"; 16 | @import "document/node/text.less"; 17 | @import "document/node/section.less"; 18 | @import "document/node/map.less"; 19 | @import "document/node/code.less"; 20 | @import "document/node/image.less"; -------------------------------------------------------------------------------- /nodes/image/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "/type/image": { 3 | "_id": "/type/image", 4 | "type": "/type/type", 5 | "name": "Image", 6 | "properties": { 7 | "caption": { 8 | "name": "Image Caption", 9 | "unique": true, 10 | "type": "string" 11 | }, 12 | "url": { 13 | "name": "Image URL", 14 | "unique": true, 15 | "type": "string" 16 | }, 17 | "original_url": { 18 | "name": "Original Image URL", 19 | "unique": true, 20 | "type": "string" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /templates/tools.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 10 | 11 |
12 | Save 13 | New 14 |
15 | 16 |
17 | This is the place where tools go. 18 |
19 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "composer", 3 | "description": "A content composition engine", 4 | "version": "0.1.0", 5 | "homepage": "http://substance.io/", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/substance/composer" 9 | }, 10 | "dependencies": { 11 | "underscore": "1.3.1", 12 | "cookie-sessions": "0.0.2", 13 | "express": "2.5.9", 14 | "less": "1.3.0", 15 | "request": "2.9.3", 16 | "riak-js": "latest", 17 | "socket.io": "0.9.5" 18 | }, 19 | "devDependencies": { 20 | "coffee-script": "1.2.0" 21 | } 22 | } -------------------------------------------------------------------------------- /src/server/document_storage.js: -------------------------------------------------------------------------------- 1 | var child_process = require('child_process'), 2 | fs = require('fs'), 3 | db = require('riak-js').getClient(), 4 | _ = require('underscore'); 5 | 6 | 7 | var DocumentStorage = function() { 8 | 9 | }; 10 | 11 | _.extend(DocumentStorage.prototype, { 12 | write: function(document, cb) { 13 | db.save('documents', document.id, document, function(err) { 14 | cb(err); 15 | }); 16 | }, 17 | 18 | read: function(id, rev, cb) { 19 | db.get('documents', id, function(err, doc) { 20 | cb(err, doc); 21 | }); 22 | } 23 | }); 24 | 25 | module.exports = DocumentStorage; -------------------------------------------------------------------------------- /styles/tools.less: -------------------------------------------------------------------------------- 1 | /* Tools 2 | -------------------------------------------------------------------------------*/ 3 | 4 | #tools { 5 | background: #E9EAE5; 6 | overflow: auto; 7 | position: fixed; 8 | left: 1050px; 9 | top: 80px; 10 | bottom: 30px; 11 | width: 300px; 12 | } 13 | 14 | .tools .navigation { 15 | background: #555; 16 | overflow: auto; 17 | a { 18 | display: block; 19 | overflow: auto; 20 | float: left; 21 | border: none; 22 | line-height: 40px; 23 | padding: 0 5px; 24 | border-right: 1px solid #777; 25 | color: #eee; 26 | 27 | &:hover { 28 | background: #333; 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /data/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "substance-composer", 3 | "created_at": "2012-04-10T15:17:28.946Z", 4 | "updated_at": "2012-04-10T15:17:28.946Z", 5 | "head": "/cover/1", 6 | "tail": "/section/2", 7 | "rev": 3, 8 | "nodes": { 9 | "/cover/1": { 10 | "type": ["/type/node", "/type/cover"], 11 | "title": "The Substance Composer", 12 | "abstract": "The Substance Composer is flexible editing component to be used by applications such as Substance.io for collaborative content composition.", 13 | "next": "/section/2", 14 | "prev": null 15 | }, 16 | "/section/2": { 17 | "type": ["/type/node", "/type/section"], 18 | "name": "Plugins", 19 | "prev": "/cover/1", 20 | "next": null 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /test/document-test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | suite('Composer', function(){ 4 | setup(function() { 5 | // ... 6 | }); 7 | 8 | suite('#Document API', function() { 9 | var document; 10 | 11 | setup(function() { 12 | document = new Document(); 13 | }); 14 | 15 | test('should create a section', function() { 16 | var section = document.create('section', 'after', '/text/1'); 17 | }); 18 | 19 | test('should create an image', function() { 20 | document.create('section', {name: 'Hello World'}, 'after', '/text/1'); 21 | }); 22 | 23 | test('should update text', function() { 24 | document.update('/text/1', "ret(5) del(2) ret(4)"); 25 | }); 26 | 27 | }); 28 | }); 29 | }).call(this); 30 | -------------------------------------------------------------------------------- /styles/document/node/document.less: -------------------------------------------------------------------------------- 1 | .content-node.document { 2 | margin: 0; 3 | padding-bottom: 80px; 4 | 5 | &.something-selected { background: #F5F5F3; } 6 | 7 | .document-title { 8 | padding-top: 40px; 9 | font-size: 55px; 10 | font-weight: bold; 11 | line-height: 70px; 12 | margin: 0 100px 20px 100px; 13 | } 14 | 15 | .author { 16 | margin-top: 30px; 17 | font-size: 25px; 18 | text-align: center; 19 | } 20 | 21 | .published { 22 | margin-top: 10px; 23 | text-align: center; 24 | margin-bottom: 50px; 25 | } 26 | 27 | .document-abstract { 28 | font-size: 20px; 29 | line-height: 1.5; 30 | color: #999; 31 | padding: 0 100px 0 100px; 32 | &.empty { display: none; } 33 | } 34 | &.edit #document_lead.empty { display: block; } 35 | } 36 | -------------------------------------------------------------------------------- /styles/history.less: -------------------------------------------------------------------------------- 1 | /* Common styles 2 | -------------------------------------------------------------------------------*/ 3 | 4 | .operations { 5 | 6 | } 7 | 8 | .operations .operation { 9 | border-left: 10px solid #444; 10 | padding: 0px 0px; 11 | margin-bottom: 1px; 12 | 13 | .opcode { 14 | overflow: auto; 15 | } 16 | 17 | .opcode .scope { 18 | float: left; 19 | background: #ccc; 20 | width: 50px; 21 | text-align: center; 22 | } 23 | 24 | .opcode .op { 25 | background: #ccc; 26 | float: left; 27 | margin-left: 1px; 28 | width: 150px; 29 | padding: 0 10px; 30 | } 31 | .command { 32 | display: none; 33 | position: absolute; 34 | left: 50px; 35 | background: white; 36 | } 37 | 38 | &:hover .command { 39 | display: block; 40 | } 41 | } -------------------------------------------------------------------------------- /src/client/state_machine.js: -------------------------------------------------------------------------------- 1 | s.StateMachine = { 2 | transitionTo: function (state) { 3 | if (this.state !== state && this.invokeForState('leave', state) !== false) { 4 | this.state = state; 5 | this.invokeForState('enter'); 6 | } 7 | }, 8 | 9 | invokeForState: function (method) { 10 | var args = Array.prototype.slice.call(arguments, 1); 11 | 12 | var parent = this; 13 | while (parent) { 14 | var constructor = parent.constructor; 15 | if (constructor.states && 16 | constructor.states[this.state] && 17 | constructor.states[this.state][method]) { 18 | return constructor.states[this.state][method].apply(this, args); 19 | } 20 | // Inheritance is set up by Backbone's extend method 21 | parent = constructor.__super__; 22 | } 23 | } 24 | }; -------------------------------------------------------------------------------- /nodes/section/section.js: -------------------------------------------------------------------------------- 1 | sc.views.Node.define('/type/section', { 2 | 3 | className: 'content-node section', 4 | 5 | initialize: function (options) { 6 | sc.views.Node.prototype.initialize.apply(this, arguments); 7 | }, 8 | 9 | focus: function () { 10 | this.headerEl.click(); 11 | }, 12 | 13 | remove: function () { 14 | this.nodeList.remove(); 15 | $(this.el).remove(); 16 | }, 17 | 18 | transitionTo: function (state) { 19 | sc.views.Node.prototype.transitionTo.call(this, state); 20 | if (this.state === state) { 21 | this.nodeList.transitionTo(state); 22 | } 23 | }, 24 | 25 | render: function () { 26 | sc.views.Node.prototype.render.apply(this, arguments); 27 | $(this.contentEl).html(this.model.get('name')); 28 | $(this.contentEl).attr('contenteditable', true); 29 | return this; 30 | } 31 | }); -------------------------------------------------------------------------------- /testsuites/concurrent_editing.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": [ 3 | {"command": "user:announce", "params": {"user": "michael", "color": "#82AA15"}}, 4 | {"command": "node:insert", "params": {"user": "michael", "type": "text", "rev": 3, "attributes": {"content": "I'm a new text node"}}}, 5 | {"command": "node:insert", "params": {"user": "michael", "type": "section", "rev": 4, "attributes": {"name": "Operations"}}}, 6 | {"command": "user:announce", "params": {"user": "john", "color": "#4da6c7"}}, 7 | {"command": "node:insert", "params": {"user": "michael", "type": "text", "rev": 5, "attributes": {"content": "Documents are manipulated through atomic operations."}}}, 8 | {"command": "node:select", "params": {"user": "john", "nodes": ["/cover/1"], "rev": 5}}, 9 | {"command": "node:select", "params": {"user": "michael", "nodes": ["/section/2"], "rev": 5}} 10 | ] 11 | } -------------------------------------------------------------------------------- /styles/mixins.less: -------------------------------------------------------------------------------- 1 | .ui-font { 2 | 3 | } 4 | 5 | .document-font { 6 | 7 | } 8 | 9 | .border-radius (@radius) { 10 | border-radius: @radius; 11 | -moz-border-radius: @radius; 12 | -webkit-border-radius: @radius; 13 | } 14 | 15 | .border-top-radius (@radius) { 16 | border-top-left-radius: @radius; 17 | border-top-right-radius: @radius; 18 | -moz-border-top-left-radius: @radius; 19 | -moz-border-top-right-radius: @radius; 20 | -webkit-border-top-left-radius: @radius; 21 | -webkit-border-top-right-radius: @radius; 22 | } 23 | 24 | .icon (@name, @color: "white", @size: "16") { 25 | .icon { 26 | float: none; margin-right: 0; height: auto; 27 | display: inline-block; 28 | min-width: 16px; 29 | text-decoration: none; border-bottom: none; 30 | text-indent: -1337em; 31 | background: url("@{images}/icons/@{color}/@{name}_@{size}x@{size}.png") center no-repeat; 32 | } 33 | } -------------------------------------------------------------------------------- /src/client/util.js: -------------------------------------------------------------------------------- 1 | // Helpers 2 | // --------------- 3 | 4 | s.util = {}; 5 | 6 | // A fake console to calm down some browsers. 7 | if (!window.console) { 8 | window.console = { 9 | log: function(msg) { 10 | // No-op 11 | } 12 | }; 13 | } 14 | 15 | // Render Underscore templates 16 | _.tpl = function (tpl, ctx) { 17 | var source = templates[tpl]; 18 | return _.template(source, ctx); 19 | }; 20 | 21 | 22 | _.htmlId = function(node) { 23 | node = node instanceof Data.Object ? node._id : node; 24 | return node.split('/').join('_'); 25 | }; 26 | 27 | 28 | _.request = function(method, path, data) { 29 | var cb = _.last(arguments); 30 | $.ajax({ 31 | type: method, 32 | url: path, 33 | data: data !== undefined ? JSON.stringify(data) : null, 34 | dataType: 'json', 35 | contentType: "application/json", 36 | success: function(res) { cb(null, res); }, 37 | error: function(err) { cb(err); } 38 | }); 39 | }; -------------------------------------------------------------------------------- /styles/document/document.less: -------------------------------------------------------------------------------- 1 | #document { 2 | .document-font; 3 | 4 | width: 920px; 5 | position:relative; 6 | margin: 0; 7 | margin-left: 80px; 8 | margin-top: 80px; 9 | 10 | padding: 0px 0 0px 0; 11 | -webkit-box-shadow: 0px 1px 4px rgba(0,0,0,0.35), inset 0px 0px 1px rgba(255,255,255,0.15); 12 | -moz-box-shadow: 0px 1px 4px rgba(0,0,0,0.35), inset 0px 0px 1px rgba(255,255,255,0.15); 13 | box-shadow: 0px 1px 4px rgba(0,0,0,0.35), inset 0px 0px 1px rgba(255,255,255,0.15); 14 | background: #fff; 15 | -webkit-font-smoothing: subpixel-antialiased; 16 | 17 | font-size: 1.1em; 18 | line-height: 30px; 19 | 20 | .document-separator { 21 | background: #000; 22 | width: 300px; height: 1px; 23 | margin: 50px auto; 24 | } 25 | } 26 | 27 | /* Static version of document */ 28 | 29 | .content-fragment { 30 | margin: 0 100px; 31 | } 32 | 33 | pre { 34 | padding: 2px 10px 2px 15px; 35 | border-left: 4px solid #BBB; 36 | margin: 10px 100px; 37 | } -------------------------------------------------------------------------------- /styles/reset.less: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, font, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | dl, dt, dd, ol, ul, li, 7 | fieldset, form, label, legend, 8 | table, caption, tbody, tfoot, thead, tr, th, td { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | outline: 0; 13 | font-weight: inherit; 14 | font-style: inherit; 15 | font-size: 100%; 16 | font-family: inherit; 17 | vertical-align: baseline; 18 | } 19 | :focus { 20 | outline: 0; 21 | } 22 | body { 23 | line-height: 1; 24 | color: black; 25 | background: white; 26 | } 27 | /*ol, ul { 28 | list-style: none; 29 | }*/ 30 | table { 31 | border-collapse: separate; 32 | border-spacing: 0; 33 | } 34 | caption, th, td { 35 | text-align: left; 36 | font-weight: normal; 37 | } 38 | blockquote:before, blockquote:after, 39 | q:before, q:after { 40 | content: ""; 41 | } 42 | blockquote, q { 43 | quotes: "" ""; 44 | } -------------------------------------------------------------------------------- /test/model/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Commit.js Tests 7 | sts 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "development": [ 4 | "jquery.min.js", 5 | "jquery.transloadit2.js", 6 | "keymaster.min.js", 7 | "jquery.timeago.js", 8 | "/socket.io/socket.io.js", 9 | "underscore.js", 10 | "leaflet.js", 11 | "data.js", 12 | "dance.js", 13 | "remotestorage.js" 14 | ], 15 | "production": [ 16 | "jquery.min.js", 17 | "jquery.transloadit2.js", 18 | "keymaster.min.js", 19 | "jquery.timeago.js", 20 | "/socket.io/socket.io.js", 21 | "keymaster.min.js", 22 | "chosen.jquery.min.js", 23 | "underscore.js", 24 | "data.js", 25 | "dance.js", 26 | "remotestorage.js" 27 | ], 28 | "source": [ 29 | "composer.js", 30 | "util.js", 31 | "model/document.js", 32 | "instructors/instructor.js", 33 | "views/document.js", 34 | "views/tools.js", 35 | "views/outline.js", 36 | "views/history.js", 37 | "views/patches.js", 38 | "views/comments.js", 39 | "views/node.js", 40 | "persistence.js", 41 | "boot.js" 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /nodes/map/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "/type/map": { 3 | "_id": "/type/map", 4 | "type": "/type/type", 5 | "name": "Map", 6 | "properties": { 7 | "latitude": { 8 | "name": "Latitude", 9 | "unique": true, 10 | "type": "string", 11 | "default": 38.9 12 | }, 13 | "longitude": { 14 | "name": "Longitude", 15 | "unique": true, 16 | "type": "number", 17 | "default": -77.035 18 | }, 19 | "zoom": { 20 | "name": "Zoom Level", 21 | "unique": true, 22 | "type": "number", 23 | "default": 15 24 | }, 25 | "annotations": { 26 | "name": "Annotations", 27 | "unique": true, 28 | "type": "object", 29 | "default": {} 30 | }, 31 | "comment_count": { 32 | "name": "Virtual comment count attribute", 33 | "unique": true, 34 | "type": "number", 35 | "default": [] 36 | }, 37 | "document": { 38 | "name": "Document Membership", 39 | "unique": true, 40 | "required": true, 41 | "type": ["/type/document"] 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /templates/controls_insert.ejs: -------------------------------------------------------------------------------- 1 |
2 |
Insert Content
3 | 33 |
34 |
-------------------------------------------------------------------------------- /nodes/text/text.js: -------------------------------------------------------------------------------- 1 | sc.views.Node.define('/type/text', { 2 | 3 | className: 'content-node text', 4 | 5 | focus: function () { 6 | $(this.textEl).click(); 7 | }, 8 | 9 | select: function () { 10 | sc.views.Node.prototype.select.apply(this); 11 | }, 12 | 13 | deselect: function () { 14 | sc.views.Node.prototype.deselect.apply(this); 15 | }, 16 | 17 | // Deal with incoming update 18 | update: function() { 19 | this.silent = true; 20 | this.editor.setValue(this.model.get('content')); 21 | }, 22 | 23 | // Dispatch local update to server 24 | serializeUpdate: function() { 25 | return { 26 | "command": "node:update", 27 | "params": { 28 | "node": "/text/2", 29 | "user": "michael", 30 | "properties": { "content": this.editor.getValue()} 31 | } 32 | } 33 | }, 34 | 35 | render: function () { 36 | var that = this; 37 | sc.views.Node.prototype.render.apply(this, arguments); 38 | 39 | setTimeout(function() { 40 | that.editor = CodeMirror(that.contentEl[0], { 41 | lineWrapping: true, 42 | value: that.model.get('content'), 43 | onChange: function() { 44 | that.model.set({content: that.editor.getValue()}); 45 | if (!that.silent) that.dispatch(); 46 | that.silent = false; 47 | } 48 | }); 49 | }, 20); 50 | return this; 51 | } 52 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The following license covers this documentation, and the source code, except 2 | where otherwise indicated. 3 | 4 | Copyright 2012, Substance. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 | EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 21 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 22 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /styles/document/node/section.less: -------------------------------------------------------------------------------- 1 | .content-node.selected.section > .operations { background: #a5c54b; } 2 | 3 | .content-node.section { 4 | } 5 | 6 | .content-node.section .content { 7 | font-weight: bold; 8 | font-size: 25px; 9 | line-height: 30px; 10 | 11 | margin: 0 100px; 12 | padding: 10px 0; 13 | } 14 | 15 | #document h1 { 16 | counter-reset: sub-section; 17 | font-size: 30px; 18 | } 19 | 20 | #document h2 { 21 | counter-reset: composite 22 | } 23 | 24 | #document h1:before{ 25 | counter-increment: section; 26 | content: counter(section) " "; 27 | 28 | font-weight: bold; 29 | margin-right: 20px; 30 | } 31 | 32 | #document h2:before{ 33 | counter-increment: sub-section; 34 | content: counter(section) "." counter(sub-section) " "; 35 | 36 | margin-right: 20px; 37 | font-weight: bold; 38 | } 39 | 40 | #document h3:before{ 41 | counter-increment: composite; 42 | content: counter(section) "." counter(sub-section) "." counter(composite) " "; 43 | 44 | margin-right: 20px; 45 | font-weight: bold; 46 | } 47 | 48 | #document h1, #document h2, #document h3, #document h4, #document h5 { 49 | font-weight: normal; 50 | margin: 0em 100px 0 55px; 51 | padding: 0; 52 | line-height: 1.6em; 53 | } 54 | 55 | #document .edit h1, #document .edit h2, #document .edit h3 { 56 | padding-top: 10px; 57 | margin-top: 0px; 58 | padding-bottom: 10px; 59 | margin-bottom: 10px; 60 | } 61 | 62 | #document h2 { font-size: 20px; } 63 | #document h3 { font-size: 16px; } -------------------------------------------------------------------------------- /nodes/cover/cover.js: -------------------------------------------------------------------------------- 1 | sc.views.Node.define([ '/type/cover' ], { 2 | 3 | className: 'content-node document', 4 | 5 | initialize: function (options) { 6 | sc.views.Node.prototype.initialize.apply(this, arguments); 7 | }, 8 | 9 | events: _.extend({ 10 | }, sc.views.Node.prototype.events), 11 | 12 | 13 | transitionTo: function (state) { 14 | StateMachine.transitionTo.call(this, state); 15 | if (this.state === state) { 16 | this.nodeList.transitionTo(state); 17 | } 18 | }, 19 | 20 | render: function () { 21 | sc.views.Node.prototype.render.apply(this, arguments); 22 | this.titleEl = $('
'+this.model.get('title')+'
').appendTo(this.contentEl); 23 | this.leadEl = $('

'+this.model.get('abstract')+'

').appendTo(this.contentEl); 24 | return this; 25 | } 26 | }, { 27 | 28 | states: { 29 | write: { 30 | enter: function () { 31 | s.views.Node.states.write.enter.apply(this); 32 | $(this.el).addClass('edit'); 33 | }, 34 | leave: function () { 35 | s.views.Node.states.write.leave.apply(this); 36 | $(this.el).removeClass('edit'); 37 | window.editor.deactivate(); 38 | } 39 | }, 40 | moveTarget: { 41 | enter: function () { 42 | $('#document').addClass('move-mode'); 43 | }, 44 | leave: function () { 45 | delete this.movedNode; 46 | delete this.movedParent; 47 | $('#document').removeClass('move-mode'); 48 | } 49 | } 50 | } 51 | }); -------------------------------------------------------------------------------- /lib/codemirror/util/runmode.js: -------------------------------------------------------------------------------- 1 | CodeMirror.runMode = function(string, modespec, callback, options) { 2 | var mode = CodeMirror.getMode(CodeMirror.defaults, modespec); 3 | var isNode = callback.nodeType == 1; 4 | var tabSize = (options && options.tabSize) || CodeMirror.defaults.tabSize; 5 | if (isNode) { 6 | var node = callback, accum = [], col = 0; 7 | callback = function(text, style) { 8 | if (text == "\n") { 9 | accum.push("
"); 10 | col = 0; 11 | return; 12 | } 13 | var escaped = ""; 14 | // HTML-escape and replace tabs 15 | for (var pos = 0;;) { 16 | var idx = text.indexOf("\t", pos); 17 | if (idx == -1) { 18 | escaped += CodeMirror.htmlEscape(text.slice(pos)); 19 | col += text.length - pos; 20 | break; 21 | } else { 22 | col += idx - pos; 23 | escaped += CodeMirror.htmlEscape(text.slice(pos, idx)); 24 | var size = tabSize - col % tabSize; 25 | col += size; 26 | for (var i = 0; i < size; ++i) escaped += " "; 27 | pos = idx + 1; 28 | } 29 | } 30 | 31 | if (style) 32 | accum.push("" + escaped + ""); 33 | else 34 | accum.push(escaped); 35 | } 36 | } 37 | var lines = CodeMirror.splitLines(string), state = CodeMirror.startState(mode); 38 | for (var i = 0, e = lines.length; i < e; ++i) { 39 | if (i) callback("\n"); 40 | var stream = new CodeMirror.StringStream(lines[i]); 41 | while (!stream.eol()) { 42 | var style = mode.token(stream, state); 43 | callback(stream.current(), style, i, stream.start); 44 | stream.start = stream.pos; 45 | } 46 | } 47 | if (isNode) 48 | node.innerHTML = accum.join(""); 49 | }; 50 | -------------------------------------------------------------------------------- /lib/codemirror/util/match-highlighter.js: -------------------------------------------------------------------------------- 1 | // Define match-highlighter commands. Depends on searchcursor.js 2 | // Use by attaching the following function call to the onCursorActivity event: 3 | //myCodeMirror.matchHighlight(minChars); 4 | // And including a special span.CodeMirror-matchhighlight css class (also optionally a separate one for .CodeMirror-focused -- see demo matchhighlighter.html) 5 | 6 | (function() { 7 | var DEFAULT_MIN_CHARS = 2; 8 | 9 | function MatchHighlightState() { 10 | this.marked = []; 11 | } 12 | function getMatchHighlightState(cm) { 13 | return cm._matchHighlightState || (cm._matchHighlightState = new MatchHighlightState()); 14 | } 15 | 16 | function clearMarks(cm) { 17 | var state = getMatchHighlightState(cm); 18 | for (var i = 0; i < state.marked.length; ++i) 19 | state.marked[i].clear(); 20 | state.marked = []; 21 | } 22 | 23 | function markDocument(cm, className, minChars) { 24 | clearMarks(cm); 25 | minChars = (typeof minChars !== 'undefined' ? minChars : DEFAULT_MIN_CHARS); 26 | if (cm.somethingSelected() && cm.getSelection().length >= minChars) { 27 | var state = getMatchHighlightState(cm); 28 | var query = cm.getSelection(); 29 | cm.operation(function() { 30 | if (cm.lineCount() < 2000) { // This is too expensive on big documents. 31 | for (var cursor = cm.getSearchCursor(query); cursor.findNext();) { 32 | //Only apply matchhighlight to the matches other than the one actually selected 33 | if (!(cursor.from().line === cm.getCursor(true).line && cursor.from().ch === cm.getCursor(true).ch)) 34 | state.marked.push(cm.markText(cursor.from(), cursor.to(), className)); 35 | } 36 | } 37 | }); 38 | } 39 | } 40 | 41 | CodeMirror.defineExtension("matchHighlight", function(className, minChars) { 42 | markDocument(this, className, minChars); 43 | }); 44 | })(); 45 | -------------------------------------------------------------------------------- /lib/codemirror/util/overlay.js: -------------------------------------------------------------------------------- 1 | // Utility function that allows modes to be combined. The mode given 2 | // as the base argument takes care of most of the normal mode 3 | // functionality, but a second (typically simple) mode is used, which 4 | // can override the style of text. Both modes get to parse all of the 5 | // text, but when both assign a non-null style to a piece of code, the 6 | // overlay wins, unless the combine argument was true, in which case 7 | // the styles are combined. 8 | 9 | CodeMirror.overlayParser = function(base, overlay, combine) { 10 | return { 11 | startState: function() { 12 | return { 13 | base: CodeMirror.startState(base), 14 | overlay: CodeMirror.startState(overlay), 15 | basePos: 0, baseCur: null, 16 | overlayPos: 0, overlayCur: null 17 | }; 18 | }, 19 | copyState: function(state) { 20 | return { 21 | base: CodeMirror.copyState(base, state.base), 22 | overlay: CodeMirror.copyState(overlay, state.overlay), 23 | basePos: state.basePos, baseCur: null, 24 | overlayPos: state.overlayPos, overlayCur: null 25 | }; 26 | }, 27 | 28 | token: function(stream, state) { 29 | if (stream.start == state.basePos) { 30 | state.baseCur = base.token(stream, state.base); 31 | state.basePos = stream.pos; 32 | } 33 | if (stream.start == state.overlayPos) { 34 | stream.pos = stream.start; 35 | state.overlayCur = overlay.token(stream, state.overlay); 36 | state.overlayPos = stream.pos; 37 | } 38 | stream.pos = Math.min(state.basePos, state.overlayPos); 39 | if (stream.eol()) state.basePos = state.overlayPos = 0; 40 | 41 | if (state.overlayCur == null) return state.baseCur; 42 | if (state.baseCur != null && combine) return state.baseCur + " " + state.overlayCur; 43 | else return state.overlayCur; 44 | }, 45 | 46 | indent: function(state, textAfter) { 47 | return base.indent(state.base, textAfter); 48 | }, 49 | electricChars: base.electricChars 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/client/persistence.js: -------------------------------------------------------------------------------- 1 | // RemoteStorageAdapter 2 | // =========== 3 | 4 | var RemoteStorageAdapter = function() { 5 | 6 | this.write = function(document, cb) { 7 | // TODO: implement 8 | }; 9 | 10 | this.open = function(id, rev, cb) { 11 | // TODO: implement 12 | }; 13 | 14 | this.update = function(id, op, cb) { 15 | // TODO: implement 16 | }; 17 | 18 | this.delete = function(id, cb) { 19 | // TODO: implement 20 | }; 21 | }; 22 | 23 | 24 | // AjaxAdapter 25 | // =========== 26 | 27 | var AjaxAdapter = function() { 28 | 29 | this.write = function(document, cb) { 30 | _.request('PUT', '/write', document, function(err) { 31 | cb(err); 32 | }); 33 | }; 34 | 35 | this.open = function(id, rev, cb) { 36 | _.request('GET', '/open/' + id, function(err, data) { 37 | cb(null, data); 38 | }); 39 | }; 40 | 41 | this.delete = function(id, cb) { 42 | // TODO: implement 43 | }; 44 | }; 45 | 46 | 47 | // SocketIOAdapter 48 | // =========== 49 | 50 | var SocketIOAdapter = function() { 51 | var socket = io.connect('http://localhost'); 52 | var document = null; 53 | 54 | function connected() { 55 | console.log('connected'); 56 | } 57 | 58 | // Merge in operations from other clients 59 | // ----------- 60 | 61 | function receiveUpdate(op) { 62 | 63 | } 64 | 65 | // Update document incrementally using operations 66 | // ----------- 67 | 68 | this.update = function(op, cb) { 69 | socket.emit('update:document', op, function (err, data) { 70 | cb(err, data); 71 | }); 72 | }; 73 | 74 | // Create a document 75 | // ----------- 76 | 77 | this.create = function(cb) { 78 | socket.emit('create:document', function(err, doc) { 79 | cb(null, doc); 80 | }); 81 | }, 82 | 83 | // Open a document 84 | // ----------- 85 | 86 | this.open = function(id, rev, cb) { 87 | document = id; 88 | socket.emit('open:document', id, rev, function(err, data) { 89 | console.log('got it', data); 90 | }); 91 | }, 92 | 93 | socket.on('connect', connected); 94 | socket.on('update', receiveUpdate); 95 | }; 96 | 97 | window.store = new SocketIOAdapter(); -------------------------------------------------------------------------------- /layouts/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Substance Composer 5 | 6 | 7 | 8 | 14 | 15 | 24 | 25 | 29 | 30 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 54 | 55 |
56 | 57 |
58 | 59 | 62 | 63 | -------------------------------------------------------------------------------- /data/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "/type/node": { 3 | "_id": "/type/node", 4 | "type": "/type/type", 5 | "name": "Node", 6 | "properties": { 7 | "next": { 8 | "name": "Next Node", 9 | "unique": true, 10 | "type": "/type/node" 11 | }, 12 | "prev": { 13 | "name": "Previous Node", 14 | "unique": true, 15 | "type": "/type/node" 16 | } 17 | } 18 | }, 19 | 20 | "/type/comment": { 21 | "_id": "/type/comment", 22 | "type": "/type/type", 23 | "properties": { 24 | "node": { 25 | "name": "Node", 26 | "type": ["/type/node"], 27 | "unique": true, 28 | "required": true 29 | }, 30 | "document": { 31 | "name": "Document", 32 | "type": ["/type/document"], 33 | "unique": true, 34 | "required": true 35 | }, 36 | "version": { 37 | "name": "Version", 38 | "type": ["/type/version"], 39 | "unique": true, 40 | "required": false 41 | }, 42 | "creator": { 43 | "name": "Creator", 44 | "type": "/type/user", 45 | "unique": true, 46 | "required": true 47 | }, 48 | "created_at": { 49 | "name": "Created at", 50 | "unique": true, 51 | "type": "date", 52 | "required": true 53 | }, 54 | "content": { 55 | "name": "Content", 56 | "type": "string", 57 | "unique": true, 58 | "required": true 59 | } 60 | }, 61 | "indexes": { 62 | "by_node": ["node"], 63 | "by_user": ["user"] 64 | } 65 | }, 66 | 67 | "/type/user": { 68 | "_id": "/type/user", 69 | "type": "/type/type", 70 | "name": "User", 71 | "properties": { 72 | "username": { 73 | "name": "Username", 74 | "unique": true, 75 | "type": "string", 76 | "required": true, 77 | "validator": "^[a-zA-Z_]{1}[a-zA-Z_0-9-]{1,20}$" 78 | }, 79 | "name": { 80 | "name": "Full Name", 81 | "unique": true, 82 | "type": "string", 83 | "required": true 84 | } 85 | }, 86 | "indexes": { 87 | "by_email": ["email"] 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /lib/codemirror/util/dialog.js: -------------------------------------------------------------------------------- 1 | // Open simple dialogs on top of an editor. Relies on dialog.css. 2 | 3 | (function() { 4 | function dialogDiv(cm, template) { 5 | var wrap = cm.getWrapperElement(); 6 | var dialog = wrap.insertBefore(document.createElement("div"), wrap.firstChild); 7 | dialog.className = "CodeMirror-dialog"; 8 | dialog.innerHTML = '
' + template + '
'; 9 | return dialog; 10 | } 11 | 12 | CodeMirror.defineExtension("openDialog", function(template, callback) { 13 | var dialog = dialogDiv(this, template); 14 | var closed = false, me = this; 15 | function close() { 16 | if (closed) return; 17 | closed = true; 18 | dialog.parentNode.removeChild(dialog); 19 | } 20 | var inp = dialog.getElementsByTagName("input")[0]; 21 | if (inp) { 22 | CodeMirror.connect(inp, "keydown", function(e) { 23 | if (e.keyCode == 13 || e.keyCode == 27) { 24 | CodeMirror.e_stop(e); 25 | close(); 26 | me.focus(); 27 | if (e.keyCode == 13) callback(inp.value); 28 | } 29 | }); 30 | inp.focus(); 31 | CodeMirror.connect(inp, "blur", close); 32 | } 33 | return close; 34 | }); 35 | 36 | CodeMirror.defineExtension("openConfirm", function(template, callbacks) { 37 | var dialog = dialogDiv(this, template); 38 | var buttons = dialog.getElementsByTagName("button"); 39 | var closed = false, me = this, blurring = 1; 40 | function close() { 41 | if (closed) return; 42 | closed = true; 43 | dialog.parentNode.removeChild(dialog); 44 | me.focus(); 45 | } 46 | buttons[0].focus(); 47 | for (var i = 0; i < buttons.length; ++i) { 48 | var b = buttons[i]; 49 | (function(callback) { 50 | CodeMirror.connect(b, "click", function(e) { 51 | CodeMirror.e_preventDefault(e); 52 | close(); 53 | if (callback) callback(me); 54 | }); 55 | })(callbacks[i]); 56 | CodeMirror.connect(b, "blur", function() { 57 | --blurring; 58 | setTimeout(function() { if (blurring <= 0) close(); }, 200); 59 | }); 60 | CodeMirror.connect(b, "focus", function() { ++blurring; }); 61 | } 62 | }); 63 | })(); -------------------------------------------------------------------------------- /lib/keymaster.min.js: -------------------------------------------------------------------------------- 1 | // keymaster.js 2 | // (c) 2011 Thomas Fuchs 3 | // keymaster.js may be freely distributed under the MIT license. 4 | (function(a){function h(a,b){var c=a.length;while(c--)if(a[c]===b)return c;return-1}function i(a){var b,g,i,j,k;b=a.keyCode;if(b==93||b==224)b=91;if(b in d){d[b]=!0;for(i in f)f[i]==b&&(l[i]=!0);return}if(!l.filter.call(this,a))return;if(!(b in c))return;for(j=0;j0;for(i in d)if(!d[i]&&h(g.mods,+i)>-1||d[i]&&h(g.mods,+i)==-1)k=!1;(g.mods.length==0&&!d[16]&&!d[18]&&!d[17]&&!d[91]||k)&&g.method(a,g)===!1&&(a.preventDefault?a.preventDefault():a.returnValue=!1,a.stopPropagation&&a.stopPropagation(),a.cancelBubble&&(a.cancelBubble=!0))}}}function j(a){var b=a.keyCode,c;if(b==93||b==224)b=91;if(b in d){d[b]=!1;for(c in f)f[c]==b&&(l[c]=!1)}}function k(){for(b in d)d[b]=!1;for(b in f)l[b]=!1}function l(a,b,d){var e,h,i,j;d===undefined&&(d=b,b="all"),a=a.replace(/\s/g,""),e=a.split(","),e[e.length-1]==""&&(e[e.length-2]+=",");for(i=0;i1){h=a.slice(0,a.length-1);for(j=0;j/g, '>').replace(/"/g, '"'); 11 | }; 12 | 13 | 14 | util.isProduction = function() { 15 | return process.env.NODE_ENV === 'production'; 16 | }; 17 | 18 | 19 | util.scripts = function() { 20 | var scripts = []; 21 | if (util.isProduction()) { 22 | scripts = settings.scripts.production; 23 | } else { 24 | scripts = settings.scripts.development.concat(settings.scripts.source); 25 | } 26 | 27 | // Include nodes 28 | var files = fs.readdirSync(__dirname + '/../../nodes'); 29 | _.each(files, function (file) { 30 | if (file.indexOf('.')>=0) return; 31 | scripts.push(file+"/"+file+".js"); 32 | }); 33 | return scripts; 34 | } 35 | 36 | 37 | util.loadTemplates = function() { 38 | var tpls = {}; 39 | var files = fs.readdirSync(__dirname + '/../../templates'); 40 | _.each(files, function (file) { 41 | var name = file.replace(/\.ejs$/, '') 42 | , content = fs.readFileSync(__dirname + '/../../templates/' + file, 'utf-8'); 43 | tpls[name] = content; 44 | }); 45 | 46 | // Include node templates 47 | var files = fs.readdirSync(__dirname + '/../../nodes'); 48 | _.each(files, function (file) { 49 | if (file.indexOf('.')>=0) return; 50 | tpls[file] = fs.readFileSync(__dirname + '/../../nodes/' + file + '/' + file + '.ejs', 'utf-8');; 51 | // scripts.push(file+"/"+file+".js"); 52 | }); 53 | return tpls; 54 | }; 55 | 56 | 57 | util.schema = function() { 58 | var schema = JSON.parse(fs.readFileSync(__dirname+ '/../../data/schema.json', 'utf-8')); 59 | var files = fs.readdirSync(__dirname + '/../../nodes'); 60 | _.each(files, function (file) { 61 | if (file.indexOf('.')>=0) return; 62 | _.extend(schema, JSON.parse(fs.readFileSync(__dirname + '/../../nodes/' + file + "/schema.json" , 'utf-8'))) 63 | }); 64 | return schema; 65 | }; 66 | 67 | 68 | util.loadStyles = function(cb) { 69 | if (util.isProduction() && styles) return cb(styles); 70 | 71 | var mainFile = 'styles/main.less'; 72 | child_process.exec('node_modules/less/bin/lessc ' + mainFile, function (error, stdout, stderr) { 73 | if (error) { 74 | util.styles = '/* An error occurred: ' + stderr + ' */'; 75 | console.log(stderr); 76 | } else { 77 | util.styles = stdout; 78 | } 79 | cb(util.styles); 80 | }); 81 | }; 82 | 83 | util.templates = util.isProduction() ? _.once(util.loadTemplates) : util.loadTemplates; 84 | 85 | module.exports = util; -------------------------------------------------------------------------------- /src/server/document_manager.js: -------------------------------------------------------------------------------- 1 | var child_process = require('child_process'), 2 | fs = require('fs'), 3 | _ = require('underscore'), 4 | ds = new require('./document_storage.js'); 5 | 6 | 7 | // Document Manager 8 | // ============= 9 | 10 | var DocumentManager = function(server) { 11 | this.io = require('socket.io').listen(server); 12 | this.documents = []; 13 | this.sessions = []; 14 | this.bindHandlers(); 15 | }; 16 | 17 | _.extend(DocumentManager.prototype, { 18 | // Bind Socket.io handlers 19 | bindHandlers: function() { 20 | var that = this; 21 | 22 | this.io.sockets.on('connection', function (socket) { 23 | 24 | // Bind to this and pass through socket instance as the first parameter 25 | function delegate(fn) { 26 | return function() { 27 | _.bind(fn, that); 28 | fn.apply(this, [socket].concat(arguments)); 29 | }; 30 | } 31 | 32 | // Hi user 33 | this.openSession(socket) 34 | 35 | // Do things 36 | socket.on('document:create', delegate(this.createDocument)); 37 | socket.on('document:join', delegate(this.joinDocument)); 38 | socket.on('document:leave', delegate(this.leaveDocument)); 39 | socket.on('document:update', delegate(this.updateDocument)); 40 | socket.on('disconnect', delegate(this.closeSession)); 41 | }); 42 | }, 43 | 44 | // Document 45 | // ------------- 46 | 47 | // Create a document, join the fun 48 | createDocument: function(socket, cb) { 49 | var document = Document.create(util.schema()); 50 | registerDocument(document); 51 | cb(null, { document: doc, sessions: {} }); 52 | }, 53 | 54 | // Join a document editing session, update the session 55 | joinDocument: function(socket, document, cb) { 56 | this.documents[document.id] = { 57 | collaborators: [socket.id], 58 | model: document 59 | } 60 | }, 61 | 62 | updateDocument: function(socket, operation, cb) { 63 | cb(null, 'confirmed'); 64 | }, 65 | 66 | // User closes a particular document 67 | // TODO: should this be explicitly called, or should it 68 | // be just overruled by another call of joinDocument 69 | leaveDocument: function(socket, cb) { 70 | // TODO: implement 71 | }, 72 | 73 | // Session 74 | // ------------- 75 | 76 | // A new user joins the party 77 | openSession: function(socket, cb) { 78 | console.log('clearly a new session.') 79 | this.sessions[sessionId] = { 80 | id: socket.id, 81 | username: socket.id, 82 | document: null, 83 | color: "#82AA15" 84 | }; 85 | }, 86 | 87 | // When a user leaves the party 88 | closeSession: function(socket, cb) { 89 | console.log('removing session' + socket.id); 90 | delete this.sessions[socket.io]; 91 | } 92 | 93 | }); 94 | 95 | module.exports = DocumentManager; -------------------------------------------------------------------------------- /styles/layout.less: -------------------------------------------------------------------------------- 1 | /* Variables 2 | -------------------------------------------------------------------------------*/ 3 | 4 | 5 | @background: #842210; 6 | 7 | 8 | /* Common styles 9 | -------------------------------------------------------------------------------*/ 10 | 11 | html, body { 12 | height: 100%; 13 | 14 | } 15 | 16 | body, input, textarea { 17 | font: normal 14px/20px DroidSans,Arial,sans-serif; 18 | font-weight: 400; 19 | color: #444; 20 | } 21 | 22 | html { 23 | margin: 0; padding: 0; 24 | -webkit-font-smoothing: antialiased; 25 | } 26 | 27 | img { border: none; } 28 | 29 | body { 30 | .ui-font; 31 | min-width: 1200px; 32 | font-size: 14px; 33 | margin: 0; padding: 0; 34 | background: #F0F1EB; 35 | color:#444; 36 | line-height:1.5em; 37 | } 38 | 39 | a { 40 | text-decoration:none; 41 | color: #51483D; 42 | border-bottom: 1px solid #ccc; 43 | padding: 0 0 1px 0; 44 | } 45 | 46 | a:hover { 47 | color: #000; 48 | border-bottom: 1px solid #999; 49 | } 50 | 51 | 52 | p { margin: 0 0 10px 0; } 53 | p.info { 54 | font-style: italic; 55 | color: rgba(0, 0, 0, 0.7); 56 | } 57 | 58 | h1, h2, h3, h4, h5, h6 { padding: 10px 0 5px 0; } 59 | h3, h4, h5, h6 { padding-top: 20px; } 60 | h2 { font-size: 160%; padding-bottom: 15px; } 61 | 62 | 63 | /* Helpers 64 | -------------------------------------------------------------------------------*/ 65 | 66 | * { 67 | box-sizing:border-box; 68 | -webkit-box-sizing:border-box; 69 | -moz-box-sizing:border-box; 70 | } 71 | 72 | .clear { clear: both; } 73 | .hidden { display: none; } 74 | .invisible { opacity: 0; } 75 | .right { float: right; } 76 | .left { float: left; } 77 | .nobr {white-space: nowrap} 78 | .right-align {text-align: right; } 79 | 80 | .serif { 81 | .document-font; 82 | } 83 | 84 | .italic { 85 | font-style: italic; 86 | } 87 | 88 | 89 | #header { 90 | position: fixed; 91 | top: 0px; 92 | height: 40px; 93 | background: #333; 94 | left: 0px; 95 | right: 0px; 96 | line-height: 40px; 97 | 98 | font-size: 25px; 99 | padding-left: 90px; 100 | color: #eee; 101 | z-index: 1000; 102 | overflow: auto; 103 | 104 | h1 { color: #eee; font-size: 20px; padding: 0; line-height: 40px; float: left; } 105 | 106 | .actions { 107 | float: left; 108 | line-height: 40px; 109 | margin-left: 20px; 110 | a { 111 | font-size: 16px; 112 | background: #000; 113 | padding: 2px 5px; 114 | border: none; 115 | } 116 | } 117 | } 118 | 119 | /* body > #container 120 | -------------------------------------------------------------------------------*/ 121 | 122 | #container { 123 | min-height: 100%; 124 | padding-bottom: 180px; 125 | } 126 | 127 | #main { 128 | padding-top: 35px; 129 | } 130 | 131 | -------------------------------------------------------------------------------- /styles/document/node/code.less: -------------------------------------------------------------------------------- 1 | .content-node.code select { 2 | position: absolute; 3 | top: 20px; 4 | right: 100px; 5 | z-index: 1000; 6 | display: none; 7 | } 8 | 9 | .content-node.code.selected select { 10 | display: inline; 11 | } 12 | 13 | .content-node.code { 14 | padding: 10px 0; 15 | } 16 | 17 | /* CodeMirror Styles 18 | -------------------------------------------------------------------------------*/ 19 | 20 | .CodeMirror { 21 | margin: 10px 100px; 22 | line-height: 1.7em; 23 | font-size: 12px; 24 | font-family: Monaco, Consolas, "Lucida Console", monospace; 25 | } 26 | 27 | .CodeMirror-scroll { 28 | overflow: auto; 29 | height: auto; overflow-y: visible; /* grow with content; source: manual */ 30 | overflow-x: auto; 31 | /* This is needed to prevent an IE[67] bug where the scrolled content 32 | is visible outside of the scrolling box. */ 33 | position: relative; 34 | } 35 | 36 | .CodeMirror-gutter { 37 | position: absolute; left: 0; top: 0; 38 | background-color: #f7f7f7; 39 | border-right: 1px solid #eee; 40 | min-width: 2em; 41 | height: 100%; 42 | } 43 | .CodeMirror-gutter-text { 44 | color: #aaa; 45 | text-align: right; 46 | padding: .4em .2em .4em .4em; 47 | } 48 | .CodeMirror-lines { 49 | padding: .4em; 50 | } 51 | 52 | .CodeMirror pre { 53 | -moz-border-radius: 0; 54 | -webkit-border-radius: 0; 55 | -o-border-radius: 0; 56 | border-radius: 0; 57 | border-width: 0; margin: 0; padding: 0; background: transparent; 58 | font-family: inherit; 59 | font-size: inherit; 60 | padding: 0; margin: 0; 61 | white-space: pre; 62 | word-wrap: normal; 63 | } 64 | 65 | .CodeMirror textarea { 66 | font-family: inherit !important; 67 | font-size: inherit !important; 68 | } 69 | 70 | .CodeMirror-cursor { 71 | z-index: 10; 72 | position: absolute; 73 | visibility: hidden; 74 | border-left: 1px solid black !important; 75 | } 76 | .CodeMirror-focused .CodeMirror-cursor { 77 | visibility: visible; 78 | } 79 | 80 | span.CodeMirror-selected { 81 | background: #ccc !important; 82 | color: HighlightText !important; 83 | } 84 | .CodeMirror-focused span.CodeMirror-selected { 85 | background: Highlight !important; 86 | } 87 | 88 | .CodeMirror-matchingbracket {color: #0f0 !important;} 89 | .CodeMirror-nonmatchingbracket {color: #f22 !important;} 90 | 91 | 92 | .cm-s-elegant span.cm-number, .cm-s-elegant span.cm-string, .cm-s-elegant span.cm-atom {color: #219161;} 93 | .cm-s-elegant span.cm-comment {color: #888;} 94 | .cm-s-elegant span.cm-meta {color: #555;font-style: italic;} 95 | .cm-s-elegant span.cm-variable {color: #19469D;} 96 | .cm-s-elegant span.cm-variable-2 {color: #b11;} 97 | .cm-s-elegant span.cm-qualifier {color: #555;} 98 | .cm-s-elegant span.cm-keyword {color: #954121;} 99 | .cm-s-elegant span.cm-builtin {color: #30a;} 100 | .cm-s-elegant span.cm-error {background-color: #fdd;} -------------------------------------------------------------------------------- /lib/codemirror/util/simple-hint.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | CodeMirror.simpleHint = function(editor, getHints) { 3 | // We want a single cursor position. 4 | if (editor.somethingSelected()) return; 5 | var result = getHints(editor); 6 | if (!result || !result.list.length) return; 7 | var completions = result.list; 8 | function insert(str) { 9 | editor.replaceRange(str, result.from, result.to); 10 | } 11 | // When there is only one completion, use it directly. 12 | if (completions.length == 1) {insert(completions[0]); return true;} 13 | 14 | // Build the select widget 15 | var complete = document.createElement("div"); 16 | complete.className = "CodeMirror-completions"; 17 | var sel = complete.appendChild(document.createElement("select")); 18 | // Opera doesn't move the selection when pressing up/down in a 19 | // multi-select, but it does properly support the size property on 20 | // single-selects, so no multi-select is necessary. 21 | if (!window.opera) sel.multiple = true; 22 | for (var i = 0; i < completions.length; ++i) { 23 | var opt = sel.appendChild(document.createElement("option")); 24 | opt.appendChild(document.createTextNode(completions[i])); 25 | } 26 | sel.firstChild.selected = true; 27 | sel.size = Math.min(10, completions.length); 28 | var pos = editor.cursorCoords(); 29 | complete.style.left = pos.x + "px"; 30 | complete.style.top = pos.yBot + "px"; 31 | document.body.appendChild(complete); 32 | // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. 33 | var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); 34 | if(winW - pos.x < sel.clientWidth) 35 | complete.style.left = (pos.x - sel.clientWidth) + "px"; 36 | // Hack to hide the scrollbar. 37 | if (completions.length <= 10) 38 | complete.style.width = (sel.clientWidth - 1) + "px"; 39 | 40 | var done = false; 41 | function close() { 42 | if (done) return; 43 | done = true; 44 | complete.parentNode.removeChild(complete); 45 | } 46 | function pick() { 47 | insert(completions[sel.selectedIndex]); 48 | close(); 49 | setTimeout(function(){editor.focus();}, 50); 50 | } 51 | CodeMirror.connect(sel, "blur", close); 52 | CodeMirror.connect(sel, "keydown", function(event) { 53 | var code = event.keyCode; 54 | // Enter 55 | if (code == 13) {CodeMirror.e_stop(event); pick();} 56 | // Escape 57 | else if (code == 27) {CodeMirror.e_stop(event); close(); editor.focus();} 58 | else if (code != 38 && code != 40) { 59 | close(); editor.focus(); 60 | // Pass the event to the CodeMirror instance so that it can handle things like backspace properly. 61 | editor.triggerOnKeyDown(event); 62 | setTimeout(function(){CodeMirror.simpleHint(editor, getHints);}, 50); 63 | } 64 | }); 65 | CodeMirror.connect(sel, "dblclick", pick); 66 | 67 | sel.focus(); 68 | // Opera sometimes ignores focusing a freshly created node 69 | if (window.opera) setTimeout(function(){if (!done) sel.focus();}, 100); 70 | return true; 71 | }; 72 | })(); 73 | -------------------------------------------------------------------------------- /styles/document/node/image.less: -------------------------------------------------------------------------------- 1 | .content-node.selected.image > .operations { background: #c35a3a; } 2 | 3 | /* Numbering */ 4 | 5 | #document { 6 | counter-reset: figure 0 section 0; 7 | } 8 | 9 | /* Increment Figure Counter */ 10 | .content-node.image, .content-node.resource { 11 | counter-increment: figure; 12 | } 13 | 14 | .content-node { 15 | .caption { 16 | margin: 0px 100px; 17 | padding: 15px 0; 18 | &.empty { display: none; } 19 | &:before { content: "Fig. " counter(figure) " "; } 20 | } 21 | &.selected .caption { display: block; } 22 | } 23 | 24 | .content-node.image { 25 | text-align: center; 26 | &.selected { 27 | .image-editor { display: block; } 28 | } 29 | } 30 | 31 | .image-content { 32 | display: inline-block; 33 | position: relative; 34 | text-align: center; 35 | padding: 0; 36 | margin: 0; 37 | img { 38 | padding: 0; 39 | margin: 0; 40 | max-width: 720px; 41 | min-width: 200px; 42 | min-height: 150px; 43 | display: block; 44 | padding: 5px 0; 45 | } 46 | 47 | &.placeholder { 48 | .image-drop-area .credits { display: block; } 49 | .image-file { bottom: 30px; } 50 | } 51 | 52 | .image-editor .heading { 53 | .document-font; 54 | font-size: 20px; 55 | text-align: center; 56 | } 57 | } 58 | 59 | .content-node.document.edit .content-node.image.selected .image-content img { 60 | opacity: 0.1; 61 | } 62 | 63 | .image-editor { 64 | .ui-font; 65 | display: none; 66 | position: absolute; 67 | margin: 0 auto; 68 | top: 0px; 69 | bottom: 0px; 70 | right: 0px; 71 | left: 0px; 72 | 73 | .image-drop-area { 74 | position: absolute; 75 | top: 20px; right: 20px; left: 20px; bottom: 20px; 76 | 77 | border: 2px dashed #aaa; 78 | -webkit-border-radius:10px; 79 | -moz-border-radius: 10px; 80 | border-radius: 10px; 81 | 82 | .info { 83 | color: #666; 84 | text-align: center; 85 | line-height: 45px; 86 | } 87 | 88 | .credits { 89 | display: none; 90 | color: #666; 91 | font-size: 12px; 92 | text-align: center; 93 | position: absolute; 94 | left: -150px; 95 | right: -150px; 96 | bottom: 5px; 97 | } 98 | } 99 | 100 | .image-file { 101 | display: block; 102 | position: absolute; 103 | top: 0px; 104 | bottom: 0px; 105 | left: 0px; 106 | right: 0px; 107 | opacity: 0; 108 | padding: 0; 109 | margin: 0; 110 | } 111 | 112 | .image-url-area { 113 | padding-top: 40px; 114 | 115 | input { 116 | width: 100%; 117 | background: #fff; 118 | border: 1px solid #ccc; 119 | } 120 | } 121 | } 122 | 123 | .image-progress { 124 | display: none; 125 | .label { padding: 10px; text-transform: uppercase; font-size: 12px; color: #777; text-align: center;} 126 | } 127 | 128 | /* ImageEditor Widget */ 129 | .progress-container { 130 | .border-radius(3px); 131 | margin: 0 10px; 132 | background: #fff; 133 | height: 10px; 134 | max-width: 200px; 135 | margin: 0 auto; 136 | } 137 | 138 | .progress-bar { 139 | .border-radius(3px); 140 | background: #82AA15; 141 | height: 10px; 142 | } -------------------------------------------------------------------------------- /src/client/composer.js: -------------------------------------------------------------------------------- 1 | (function(exports) { 2 | 3 | // The Substance Namespace 4 | var Substance = {}; 5 | 6 | var Composer = Dance.Performer.extend({ 7 | el: 'container', 8 | 9 | events: { 10 | 'click .save-document': function() { 11 | store.write(this.model.toJSON(), function() { 12 | console.log('saved.'); 13 | }); 14 | return false; 15 | } 16 | }, 17 | 18 | initialize: function(options) { 19 | // this.user = options.user || this.newUser(); 20 | 21 | // Selection shortcuts 22 | key('shift+down', _.bind(function() { this.views.document.expandSelection(); return false; }, this)); 23 | key('shift+up', _.bind(function() { this.views.document.narrowSelection(); return false; }, this)); 24 | key('esc', _.bind(function() { console.log('clear selection'); return false; }, this)); 25 | 26 | // Move shortcuts 27 | key('down', _.bind(function() { this.views.document.moveDown(); return false; }, this)); 28 | key('up', _.bind(function() { this.views.document.moveUp(); return false; }, this)); 29 | 30 | // Node insertion shortcuts 31 | key('alt+t', _.bind(function() { console.log('insert text node'); }, this)); 32 | 33 | // Initialize Instructor 34 | this.instructor = new Substance.Composer.instructors.Instructor({}); 35 | }, 36 | 37 | // Build a document 38 | build: function(doc) { 39 | // Document Model 40 | this.model = new Composer.models.Document(doc.document); 41 | 42 | // All active sessions (=users on that document) 43 | this.sessions = doc.sessions; 44 | 45 | // Possible modes: edit, view, patch, apply-patch 46 | this.mode = "edit"; 47 | 48 | // Views 49 | this.views = {}; 50 | this.views.document = new Substance.Composer.views.Document({ model: this.model }); 51 | this.views.tools = new Substance.Composer.views.Tools({model: this.model}); 52 | 53 | this.model.on('operation:executed', function() {}, this); 54 | this.renderDoc(); 55 | }, 56 | 57 | // Dispatch Operation 58 | execute: function(op) { 59 | this.model.execute(op); 60 | }, 61 | 62 | start: function() { 63 | Dance.history.start(); 64 | this.render(); 65 | }, 66 | 67 | read: function(id, rev) { 68 | store.open(id, rev, function(err, doc) { 69 | console.log('loaded:', doc); 70 | }); 71 | }, 72 | 73 | newDocument: function() { 74 | var that = this; 75 | store.create(function(err, doc) { 76 | that.build(doc); 77 | }); 78 | }, 79 | 80 | // Store document on the server 81 | save: function() { 82 | 83 | }, 84 | 85 | render: function() { 86 | this.$el.html(_.tpl('composer')); 87 | }, 88 | 89 | renderDoc: function() { 90 | this.$('#document').replaceWith(this.views.document.render().el); 91 | this.$('#tools').html(this.views.tools.render().el); 92 | } 93 | }, 94 | // Class Variables 95 | { 96 | models: {}, 97 | views: {}, 98 | instructors: {}, 99 | utils: {} 100 | }); 101 | 102 | // Exports 103 | Substance.Composer = Composer; 104 | exports.Substance = Substance; 105 | exports.s = Substance; 106 | exports.sc = Substance.Composer; 107 | 108 | })(window); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | app = express.createServer(), 3 | fs = require('fs'), 4 | url = require('url'), 5 | Data = require('./lib/data'), 6 | _ = require('underscore'), 7 | util = require('./src/server/util.js'), 8 | Document = require('./src/shared/model/document.js'), 9 | ds = new (require('./src/server/document_storage.js'))(), 10 | dm = new (require('./src/server/document_manager.js'))(app); 11 | 12 | 13 | // App config 14 | // =========== 15 | 16 | global.config = JSON.parse(fs.readFileSync(__dirname+ '/config.json', 'utf-8')); 17 | global.example = fs.readFileSync(__dirname+ '/data/example.json', 'utf-8'); 18 | 19 | 20 | // Express.js Configuration 21 | // ----------- 22 | 23 | app.configure(function() { 24 | var CookieStore = require('cookie-sessions'); 25 | app.use(express.bodyParser()); 26 | app.use(express.methodOverride()); 27 | app.use(CookieStore({secret: config.secret})); 28 | app.use(app.router); 29 | app.use(express.static(__dirname+"/public", { maxAge: 41 })); 30 | app.use(express.static(__dirname+"/test", { maxAge: 41 })); 31 | app.use(express.static(__dirname+"/src/client", { maxAge: 41 })); 32 | app.use(express.static(__dirname+"/src/shared", { maxAge: 41 })); 33 | app.use(express.static(__dirname+"/lib", { maxAge: 41 })); 34 | app.use(express.static(__dirname+"/data", { maxAge: 41 })); 35 | app.use(express.static(__dirname+"/nodes", { maxAge: 41 })); 36 | app.use(express.logger({ format: ':method :url' })); 37 | }); 38 | 39 | app.enable("jsonp callback"); 40 | 41 | function serveStartpage(req, res) { 42 | html = fs.readFileSync(__dirname+ '/layouts/app.html', 'utf-8'); 43 | res.send(html.replace('{{{{seed}}}}', JSON.stringify(util.schema())) 44 | .replace('{{{{scripts}}}}', JSON.stringify(util.scripts())) 45 | .replace('{{{{example}}}}', example) 46 | .replace('{{{{templates}}}}', JSON.stringify(util.templates()))); 47 | } 48 | 49 | // Web server 50 | // =========== 51 | 52 | 53 | // Style sheets 54 | // ----------- 55 | 56 | app.get('/styles.css', function(req, res) { 57 | res.writeHead(200, {'Content-Type': 'text/css'}); 58 | util.loadStyles(function(css) { 59 | res.write(css); 60 | res.end(); 61 | }); 62 | }); 63 | 64 | // Read document from store 65 | // ----------- 66 | 67 | app.get('/read/:id', function(req, res) { 68 | ds.read(req.params.id, function(err, data) { 69 | res.send(data); 70 | }); 71 | }); 72 | 73 | // Incrementally update document 74 | // ----------- 75 | 76 | app.put('/update', function(req, res) { 77 | var data = req.body; 78 | ds.read(req.params.id, function(err, data) { 79 | res.send(data); 80 | }); 81 | }); 82 | 83 | // Store a document 84 | // ----------- 85 | 86 | app.post('/write', function(req, res) { 87 | var doc = req.body; 88 | ds.write(doc, function(err, rev) { 89 | res.send('Document successfully stored. New revision: '+doc.rev); 90 | }); 91 | }); 92 | 93 | // Serve startpage 94 | // ----------- 95 | 96 | app.get('/', serveStartpage); 97 | 98 | 99 | // Start server 100 | // ----------- 101 | 102 | app.listen(config['server_port'], config['server_host'], function (err) { 103 | console.log('Substance Library is listening at http://'+config['server_host']+':'+config['server_port']); 104 | }); 105 | -------------------------------------------------------------------------------- /src/client/views/node.js: -------------------------------------------------------------------------------- 1 | sc.views.Node = Dance.Performer.extend(_.extend({}, s.StateMachine, { 2 | 3 | className: 'content-node', 4 | 5 | attributes: { 6 | draggable: 'false' 7 | }, 8 | 9 | initialize: function (options) { 10 | this.state = 'read'; 11 | this.document = options.document; 12 | $(this.el).attr({ id: _.htmlId(this.model) }); 13 | }, 14 | 15 | transitionTo: function (state) { 16 | StateMachine.transitionTo.call(this, state); 17 | if (this.state === state) { 18 | this.afterControls.transitionTo(state); 19 | } 20 | }, 21 | 22 | // Dispatching a change 23 | dispatch: function() { 24 | dispatch(this.serializeUpdate()); 25 | }, 26 | 27 | // Events 28 | // ------ 29 | 30 | events: { 31 | 'click .toggle-move-node': 'toggleMoveNode', 32 | 'click': 'select' 33 | }, 34 | 35 | toggleMoveNode: function (e) { 36 | e.preventDefault(); 37 | e.stopPropagation(); 38 | 39 | if (this.state === 'move') { 40 | this.root.transitionTo('write'); 41 | } else { 42 | // There could be another node that is currently in move state. 43 | // Transition to read state to make sure that no node is in move state. 44 | this.root.transitionTo('read'); 45 | this.transitionTo('move'); 46 | 47 | this.root.movedNode = this.model; 48 | this.root.movedParent = this.parent; 49 | this.root.transitionTo('moveTarget'); 50 | } 51 | }, 52 | 53 | // TODO: move to document level ? 54 | select: function (e) { 55 | this.document.execute({command:"node:select", params: { user: "michael", nodes: [this.model._id] }}); 56 | }, 57 | 58 | focus: function () {}, 59 | 60 | render: function () { 61 | this.contentEl = $('
').appendTo(this.el); 62 | this.handleEl = $('
').appendTo(this.el); 63 | return this; 64 | } 65 | 66 | }), { 67 | 68 | 69 | // States 70 | // ------ 71 | 72 | states: { 73 | read: { 74 | enter: function () {}, 75 | leave: function () {} 76 | }, 77 | 78 | write: { 79 | enter: function () {}, 80 | leave: function () {} 81 | }, 82 | 83 | move: { 84 | enter: function () { 85 | $(this.el).addClass('being-moved'); // TODO 86 | }, 87 | leave: function (nextState) { 88 | if (nextState === 'moveTarget') { return false; } 89 | $(this.el).removeClass('being-moved'); // TODO 90 | } 91 | }, 92 | 93 | moveTarget: { 94 | enter: function () {}, 95 | leave: function () {} 96 | } 97 | }, 98 | 99 | 100 | // Inheritance & Instantiation 101 | // --------------------------- 102 | 103 | subclasses: {}, 104 | 105 | define: function (types, protoProps, classProps) { 106 | classProps = classProps || {}; 107 | var subclass = this.extend(protoProps, classProps); 108 | 109 | function toArray (a) { return _.isArray(a) ? a : [a] } 110 | _.each(toArray(types), function (type) { 111 | this.subclasses[type] = subclass; 112 | }, this); 113 | 114 | return subclass; 115 | }, 116 | 117 | create: function (options) { 118 | var model = options.model 119 | , type = model.type()._id 120 | , Subclass = this.subclasses[type]; 121 | 122 | if (!Subclass) { throw new Error("Node has no subclass for type '"+type+"'"); } 123 | return new Subclass(options); 124 | } 125 | 126 | }); -------------------------------------------------------------------------------- /lib/codemirror/codemirror.css: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | 3 | font-size: 20px; 4 | line-height: 20px; 5 | font-family: monospace; 6 | } 7 | 8 | .CodeMirror-scroll { 9 | overflow: auto; 10 | height:; 11 | /* This is needed to prevent an IE[67] bug where the scrolled content 12 | is visible outside of the scrolling box. */ 13 | position: relative; 14 | outline: none; 15 | } 16 | 17 | .CodeMirror-gutter { 18 | position: absolute; left: 0; top: 0; 19 | z-index: 10; 20 | background-color: #f7f7f7; 21 | border-right: 1px solid #eee; 22 | min-width: 2em; 23 | height: 100%; 24 | } 25 | .CodeMirror-gutter-text { 26 | color: #aaa; 27 | text-align: right; 28 | padding: .4em .2em .4em .4em; 29 | white-space: pre !important; 30 | } 31 | .CodeMirror-lines { 32 | padding: .4em; 33 | white-space: pre; 34 | } 35 | 36 | .CodeMirror pre { 37 | -moz-border-radius: 0; 38 | -webkit-border-radius: 0; 39 | -o-border-radius: 0; 40 | border-radius: 0; 41 | border-width: 0; margin: 0; padding: 0; background: transparent; 42 | font-family: inherit; 43 | font-size: inherit; 44 | padding: 0; margin: 0; 45 | white-space: pre; 46 | word-wrap: normal; 47 | } 48 | 49 | .CodeMirror-wrap pre { 50 | word-wrap: break-word; 51 | white-space: pre-wrap; 52 | } 53 | .CodeMirror-wrap .CodeMirror-scroll { 54 | overflow-x: hidden; 55 | } 56 | 57 | .CodeMirror textarea { 58 | outline: none !important; 59 | } 60 | 61 | .CodeMirror pre.CodeMirror-cursor { 62 | z-index: 10; 63 | position: absolute; 64 | visibility: hidden; 65 | border-left: 1px solid red; 66 | border-right:none; 67 | width:0; 68 | } 69 | .CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {} 70 | .CodeMirror-focused pre.CodeMirror-cursor { 71 | visibility: visible; 72 | } 73 | 74 | div.CodeMirror-selected { background: #ccc; } 75 | .CodeMirror-focused div.CodeMirror-selected { background: #ccc; } 76 | 77 | .CodeMirror-searching { 78 | background: #ffa; 79 | background: rgba(255, 255, 0, .4); 80 | } 81 | 82 | /* Default theme */ 83 | 84 | .cm-s-default span.cm-keyword {color: #708;} 85 | .cm-s-default span.cm-atom {color: #219;} 86 | .cm-s-default span.cm-number {color: #164;} 87 | .cm-s-default span.cm-def {color: #00f;} 88 | .cm-s-default span.cm-variable {color: black;} 89 | .cm-s-default span.cm-variable-2 {color: #05a;} 90 | .cm-s-default span.cm-variable-3 {color: #085;} 91 | .cm-s-default span.cm-property {color: black;} 92 | .cm-s-default span.cm-operator {color: black;} 93 | .cm-s-default span.cm-comment {color: #a50;} 94 | .cm-s-default span.cm-string {color: #a11;} 95 | .cm-s-default span.cm-string-2 {color: #f50;} 96 | .cm-s-default span.cm-meta {color: #555;} 97 | .cm-s-default span.cm-error {color: #f00;} 98 | .cm-s-default span.cm-qualifier {color: #555;} 99 | .cm-s-default span.cm-builtin {color: #30a;} 100 | .cm-s-default span.cm-bracket {color: #cc7;} 101 | .cm-s-default span.cm-tag {color: #170;} 102 | .cm-s-default span.cm-attribute {color: #00c;} 103 | .cm-s-default span.cm-header {color: #a0a;} 104 | .cm-s-default span.cm-quote {color: #090;} 105 | .cm-s-default span.cm-hr {color: #999;} 106 | .cm-s-default span.cm-link {color: #00c;} 107 | 108 | span.cm-header, span.cm-strong {font-weight: bold;} 109 | span.cm-em {font-style: italic;} 110 | span.cm-emstrong {font-style: italic; font-weight: bold;} 111 | span.cm-link {text-decoration: underline;} 112 | 113 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 114 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 115 | -------------------------------------------------------------------------------- /nodes/code/code.js: -------------------------------------------------------------------------------- 1 | // s.views.Node.define('/type/code', { 2 | 3 | // className: 'content-node code', 4 | 5 | // events: _.extend({ 6 | // 'change select': 'changeLanguageSelect' 7 | // }, s.views.Node.prototype.events), 8 | 9 | // languages: [ 'JavaScript', 'Python', 'Ruby', 'PHP', 'HTML', 'CSS', 'Haskell' 10 | // , 'CoffeeScript', 'Java', 'C', 'C++', 'C#', 'Other' 11 | // ], 12 | 13 | // modeForLanguage: function (language) { 14 | // return { 15 | // javascript: 'javascript', 16 | // python: { name: 'python', version: 3 }, 17 | // ruby: 'ruby', 18 | // php: 'php', 19 | // html: 'htmlmixed', 20 | // css: 'css', 21 | // haskell: 'haskell', 22 | // coffeescript: 'coffeescript', 23 | // java: 'text/x-java', 24 | // c: 'text/x-csrc', 25 | // 'c++': 'text/x-c++src', 26 | // 'c#': 'text/x-csharp' 27 | // }[language] || 'null'; 28 | // }, 29 | 30 | // changeLanguageSelect: function () { 31 | // var newLanguage = this.languageSelect.val(); 32 | // updateNode(this.model, { language: newLanguage }); 33 | // this.codeMirror.setOption('mode', this.modeForLanguage(newLanguage)); 34 | // }, 35 | 36 | // focus: function () { 37 | // this.codeMirror.focus(); 38 | // }, 39 | 40 | // codeMirrorConfig: { 41 | // lineNumbers: true, 42 | // theme: 'elegant', 43 | // indentUnit: 2, 44 | // indentWithTabs: false, 45 | // tabMode: 'shift' 46 | // }, 47 | 48 | // render: function () { 49 | // function createSelect (dflt, opts) { 50 | // var html = ''; 57 | // return html; 58 | // } 59 | 60 | // var self = this; 61 | 62 | // s.views.Node.prototype.render.apply(this, arguments); 63 | // this.languageSelect = $(createSelect(this.model.get('language'), this.languages)).appendTo(this.contentEl); 64 | // var codeMirrorConfig = _.extend({}, this.codeMirrorConfig, { 65 | // mode: this.modeForLanguage(this.model.get('language')), 66 | // value: s.util.unescape(this.model.get('content') || ''), 67 | // readOnly: true, 68 | // onFocus: function () { 69 | // // Without this, there is the possibility to focus the editor without 70 | // // activating the code node. Don't ask me why. 71 | // self.selectThis(); 72 | // }, 73 | // onBlur: function () { 74 | // // Try to prevent multiple selections in multiple CodeMirror instances 75 | // self.codeMirror.setSelection({ line:0, ch:0 }, { line:0, ch:0 }); 76 | // }, 77 | // onChange: _.throttle(function () { 78 | // updateNode(self.model, { content: s.util.escape(self.codeMirror.getValue()) }); 79 | // }, 500) 80 | // }); 81 | // this.codeMirror = CodeMirror(this.contentEl.get(0), codeMirrorConfig); 82 | 83 | // setTimeout(function () { 84 | // // after dom insertion 85 | // self.codeMirror.refresh(); 86 | // }, 10); 87 | 88 | // return this; 89 | // } 90 | 91 | // }, { 92 | 93 | // states: { 94 | // write: { 95 | // enter: function () { 96 | // s.views.Node.states.write.enter.apply(this); 97 | // this.codeMirror.setOption('readOnly', false); 98 | // }, 99 | // leave: function () { 100 | // s.views.Node.states.write.leave.apply(this); 101 | // this.codeMirror.setOption('readOnly', true); 102 | // } 103 | // } 104 | // } 105 | 106 | // }); -------------------------------------------------------------------------------- /src/client/boot.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | var commands = [ 4 | {"command": "user:announce", "params": {"user": "michael", "color": "#82AA15"}}, 5 | {"command": "node:insert", "params": {"user": "michael", "type": "text", "rev": 3, "attributes": {"content": "It's literally impossible to build an editor that can be used across different disciplines. Scientists, writers and journalists all have different needs. That's why Substance just provides the core infrastructure, and introduces Content Types that can be developed individually by the community, tailored to their specific needs."}}}, 6 | // {"command": "node:insert", "params": {"user": "michael", "type": "map", "rev": 3, "attributes": {"content": "Hey! I'm a map."}}}, 7 | {"command": "node:insert", "params": {"user": "michael", "type": "section", "rev": 4, "attributes": {"name": "Structured Composition"}}}, 8 | {"command": "node:insert", "params": {"user": "michael", "type": "text", "rev": 5, "attributes": {"content": "Instead of conventional sequential text-editing, documents are composed of Content Nodes in a structured manner. The composer focuses on content, by leaving the layout part to the system, not the user. Because of the absence of formatting utilities, it suggests structured, content-oriented writing."}}}, 9 | {"command": "node:insert", "params": {"user": "michael", "type": "section", "rev": 6, "attributes": {"name": "Open Collaboration"}}}, 10 | {"command": "node:insert", "params": {"user": "michael", "type": "text", "rev": 7, "attributes": {"content": "The Substance Composer targets open collaboration. Co-authors can edit one document at the same time, while content is synchronized among users in realtime. There's a strong focus on reader collaboration as well. They can easily participate and comment on certain text passages or suggest a patch."}}}, 11 | {"command": "node:insert", "params": {"user": "michael", "type": "section", "rev": 8, "attributes": {"name": "Patches"}}}, 12 | {"command": "node:insert", "params": {"user": "michael", "type": "text", "rev": 9, "attributes": {"content": "Readers will be able to contribute right away by submitting patches, which can be applied to the document at a later time. Patches are an important concept to realize a peer-review process."}}}, 13 | {"command": "node:insert", "params": {"user": "michael", "type": "section", "rev": 10, "attributes": {"name": "Operations"}}}, 14 | {"command": "node:insert", "params": {"user": "michael", "type": "text", "rev": 11, "attributes": {"content": "The Substance Composer uses atomic operations to transform documents. This is a fundamental concept that allows collaborative editing of one document (even at the same time). The technique behind it is called Operational Transformation. Based on all recorded operations, the complete document history can be reproduced at any time. In other words. This is the best thing since sliced bread."}}}, 15 | {"command": "user:announce", "params": {"user": "john", "color": "#4da6c7"}}, 16 | // {"command": "node:select", "params": {"user": "john", "nodes": ["/cover/1"], "rev": 12}}, 17 | {"command": "node:select", "params": {"user": "michael", "nodes": ["/section/2", "/text/3"], "rev": 12}}, 18 | // {"command": "node:move", "params": {"user": "michael", "nodes": ["/section/2", "/text/3"], "target": "/text/5", "rev": 12}} 19 | ]; 20 | 21 | // Executes commands in serial 22 | function execCommands() { 23 | var index = 0; 24 | function next() { 25 | if (index >= commands.length) return; 26 | composer.execute(commands[index]); 27 | index += 1; 28 | _.delay(next, 1); 29 | } 30 | _.delay(next, 1); 31 | } 32 | 33 | // var doc = sc.models.Document.create(); 34 | window.composer = new Substance.Composer({el: '#container'}); 35 | 36 | // composer.execute({"command": "user:announce", "params": {"user": "michael", "color": "#82AA15"}}); 37 | // Update a node 38 | // _.delay(function() { 39 | // composer.execute({"command": "node:update", "params": {"node": "/text/2", "user": "michael", "properties": { "content": "All new content"} } }); 40 | // }, 800); 41 | composer.start(); 42 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hi! We are the team of [Substance](http://substance.io), and we're passionate about making web-based content composition easy. We believe that [Content is Data](http://www.slideshare.net/_mql/substanceio-content-is-data) and should be separated from presentation. Authors want to create meaningful content in the first place, they don't want to spend time with aligning text, choosing fonts and resizing images. 2 | 3 | Also we've made a claim: 4 | 5 | > Building an editor for everyone is impossible 6 | 7 | And we've proposed a solution: 8 | 9 | > Provide an easy way for communities to build their own editor 10 | 11 | 12 | The Substance Composer 13 | ========= 14 | 15 | The Substance Composer is a foundation for building your own editor tailored for you particular usecase. You can extend basic content types such as Text, Sections and Images with custom types such as Maps, Formulas, or pre-structured types such as an Event content type that allows you entering name, date, organizer etc. You can add whatever you can imagine, the sky is the limit. But here comes the bummer: You need to do it yourself. Our mission is to make it very easy for you, by creating an infrastructure for basic operations such as inserting, moving and deleting nodes, and a generic UI for dealing with patches and comments. 16 | 17 | ![Composer](http://f.cl.ly/items/2j0g3c0S0E290p3d3E2E/Screen%20Shot%202012-05-08%20at%2010.48.09%20PM.png) 18 | 19 | Collaboration 20 | ========= 21 | 22 | Since collaboration is more imporantant than ever before to create high quality content we've added the concept of patches to turn every readers into a potential collaborator. 23 | 24 | ![Patches](http://f.cl.ly/items/1q2w2W0F0Q06043Y3346/Screen%20Shot%202012-05-08%20at%2011.08.23%20PM.png) 25 | 26 | The Substance Composer uses operations to transform documents. By keeping track of atomic document operations, the complete history can be replayed and allows users to go back and forth in time. You can either use the web-based editor for manipulating documents, or do it programmatically using the API. 27 | 28 | Why should we consider content as data? 29 | ========= 30 | 31 | ![Content is data](http://f.cl.ly/items/2o2f2c3x0C0L392H2w0c/Screen%20Shot%202012-05-08%20at%2010.55.10%20PM.png) 32 | 33 | Extensions 34 | ========= 35 | 36 | You can implement your own content types. We'll provide a tutorial once the editor stable enough. 37 | 38 | ![Composer](http://f.cl.ly/items/0w1D1u203D120j1R2938/Screen%20Shot%202012-05-08%20at%2010.52.02%20PM.png) 39 | 40 | 41 | API 42 | ===================== 43 | 44 | Document Manipulation 45 | --------------------- 46 | 47 | Documents are manipulated using commands. Commands are represented as JSON. 48 | 49 | 50 | User API 51 | --------------------- 52 | 53 | ### user:announce 54 | 55 | Announce a new author collaborating on the document. 56 | 57 | ```js 58 | {"command": "user:announce", "params": {"user": "michael", "color": "#82AA15"}} 59 | ``` 60 | 61 | Node API 62 | --------------------- 63 | 64 | Commands for inserting, updating, moving and deleting content nodes. 65 | 66 | ### node:insert 67 | 68 | Insert a new node. 69 | 70 | ```js 71 | { 72 | "command": "node:insert", 73 | "params": { 74 | "user": "michael", 75 | "type": "text", 76 | "rev": 3, 77 | "attributes": {"content": "Text goes here."} 78 | } 79 | } 80 | ``` 81 | 82 | ### node:move 83 | 84 | Move node(s). They are inserted after a specified target node. 85 | 86 | ```js 87 | { 88 | "command": "node:insert", 89 | "params": { 90 | "user": "michael", 91 | "nodes": ["/section/2", "/text/3"], 92 | "target": "/text/5" 93 | "rev": 12 94 | } 95 | } 96 | ``` 97 | 98 | ### node:select 99 | 100 | Make a new node selection. 101 | 102 | ```js 103 | { 104 | "command": "node:select", 105 | "params": { 106 | "user": "michael", 107 | "nodes": ["/section/2", "/text/3"], 108 | "rev": 12 109 | } 110 | } 111 | ``` 112 | 113 | ### node:update 114 | 115 | To be implemented. 116 | 117 | ### node:delete 118 | 119 | To be implemented. 120 | 121 | 122 | Patch API 123 | --------------------- 124 | 125 | To be implemented. 126 | 127 | Comment API 128 | --------------------- 129 | 130 | To be implemented. 131 | 132 | -------------------------------------------------------------------------------- /src/client/views/document.js: -------------------------------------------------------------------------------- 1 | sc.views.Document = Dance.Performer.extend({ 2 | id: 'document', 3 | 4 | // Events 5 | // ------ 6 | 7 | events: { 8 | 9 | }, 10 | 11 | // Handlers 12 | // -------- 13 | 14 | initialize: function (options) { 15 | _.bindAll(this, 'insertNode'); 16 | 17 | this.model.on('node:insert', this.insertNode, this); 18 | this.model.on('node:update', this.updateNode, this); 19 | this.model.on('node:select', this.updateSelections, this); 20 | this.model.on('node:move', this.move, this); 21 | 22 | this.build(); 23 | 24 | $(document.body).keydown(this.onKeydown); 25 | }, 26 | 27 | build: function() { 28 | this.nodes = {}; 29 | this.model.each(function(node) { 30 | this.nodes[node._id] = this.createNodeView(node); 31 | }, this); 32 | }, 33 | 34 | // UI updates 35 | // -------- 36 | 37 | insertNode: function(node, options) { 38 | var view = this.createNodeView(node); 39 | this.nodes[node._id] = view; 40 | $(view.render().el).appendTo(this.el); 41 | }, 42 | 43 | updateNode: function(node, properties) { 44 | this.nodes[node].update(properties); 45 | }, 46 | 47 | // Incoming move node operation 48 | move: function(options) { 49 | var $selection = $(_.map(options.nodes, function(n) { return '#'+_.htmlId(n); }).join(', ')); 50 | $selection.insertAfter($('#'+_.htmlId(options.target))); 51 | }, 52 | 53 | updateSelections: function(selections) { 54 | $('.content-node.selected .handle').css('background', ''); 55 | $('.content-node.selected').removeClass('selected'); 56 | 57 | _.each(selections, function(user, node) { 58 | $('#'+_.htmlId(node)).addClass('selected') 59 | .find('.handle').css('background', this.model.users[user].color); 60 | }, this); 61 | }, 62 | 63 | 64 | // Issue commands 65 | // -------- 66 | 67 | expandSelection: function() { 68 | var lastnode = _.last(this.model.users[composer.user].selection); 69 | if (lastnode) { 70 | var next = this.model.get(lastnode).get('next'); 71 | if (next) { 72 | var newSelection = this.model.users[composer.user].selection.concat([next._id]); 73 | this.model.execute({command:"node:select", params: { user: "michael", nodes: newSelection }}); 74 | } 75 | } 76 | }, 77 | 78 | narrowSelection: function() { 79 | var selection = this.model.users[composer.user].selection; 80 | selection = _.clone(selection).splice(0, selection.length-1); 81 | this.model.execute({command:"node:select", params: { user: "michael", nodes: selection }}); 82 | }, 83 | 84 | moveDown: function() { 85 | var selection = this.model.users[composer.user].selection; 86 | var last = this.model.get(_.last(selection)); 87 | if (last.get('next')) { 88 | this.model.execute({command:"node:move", params: { user: "michael", nodes: selection, target: last.get('next')._id, rev: this.model.rev }}); 89 | } 90 | }, 91 | 92 | moveUp: function() { 93 | var selection = this.model.users[composer.user].selection; 94 | var first = this.model.get(_.first(selection)); 95 | 96 | // 1st node (cover) stays on top 97 | if (first.get('prev') && first.get('prev').get('prev')) { 98 | this.model.execute({command:"node:move", params: { 99 | user: "michael", 100 | nodes: selection, 101 | target: first.get('prev').get('prev')._id, 102 | rev: this.model.rev 103 | }}); 104 | } 105 | }, 106 | 107 | createNodeView: function(node) { 108 | return sc.views.Node.create({ 109 | document: this.model, 110 | model: node 111 | }); 112 | }, 113 | 114 | selectNode: function (view) { 115 | this.deselectNode(); 116 | $(this.el).addClass('something-selected'); 117 | view.select(); 118 | this.selected = view; 119 | }, 120 | 121 | deselectNode: function () { 122 | if (this.selected) { 123 | $(this.el).removeClass('something-selected'); 124 | this.selected.deselect(); 125 | delete this.selected; 126 | } 127 | }, 128 | 129 | render: function () { 130 | this.model.each(function(node) { 131 | $(this.nodes[node._id].render().el).appendTo(this.el); 132 | }, this); 133 | return this; 134 | }, 135 | 136 | // Helpers 137 | // ------- 138 | 139 | edit: function () { 140 | this.node.transitionTo('write'); 141 | }, 142 | 143 | deselect: function () { 144 | // TODO 145 | } 146 | 147 | }); -------------------------------------------------------------------------------- /nodes/image/image.js: -------------------------------------------------------------------------------- 1 | // s.views.Node.define('/type/image', { 2 | 3 | // className: 'content-node image', 4 | 5 | // events: _.extend({ 6 | // 'change .image-file': 'upload' 7 | // }, s.views.Node.prototype.events), 8 | 9 | // focus: function () { 10 | // this.caption.click(); 11 | // }, 12 | 13 | // initializeUploadForm: function () { 14 | // _.bindAll(this, 'onStart', 'onProgress', 'onError'); 15 | 16 | // this.$('.upload-image-form').transloadit({ 17 | // modal: false, 18 | // wait: true, 19 | // autoSubmit: false, 20 | // onStart: this.onStart, 21 | // onProgress: this.onProgress, 22 | // onError: this.onError, 23 | // onSuccess: _.bind(function (assembly) { 24 | // if (assembly.results.web_version && 25 | // assembly.results.web_version[1] && 26 | // assembly.results.web_version[1].url) { 27 | // this.onSuccess(assembly); 28 | // } else { 29 | // this.onInvalid(); 30 | // } 31 | // }, this) 32 | // }); 33 | // }, 34 | 35 | // onStart: function () { 36 | // this.$('.image-progress').show(); 37 | // this.$('.info').hide(); 38 | // this.$('.image-progress .label').html("Uploading …"); 39 | // this.$('.progress-bar').css('width', '0%'); 40 | // }, 41 | 42 | // onProgress: function (bytesReceived, bytesExpected) { 43 | // var percentage = Math.max(0, parseInt(bytesReceived / bytesExpected * 100)); 44 | // if (!percentage) percentage = 0; 45 | // this.$('.image-progress .label').html("Uploading … " + percentage + "%"); 46 | // this.$('.progress-bar').css('width', percentage + '%'); 47 | // }, 48 | 49 | // onSuccess: function (assembly) { 50 | 51 | // updateNode(this.model, { 52 | // url: assembly.results.web_version[1].url, 53 | // original_url: assembly.results.print_version[1].url, 54 | // dirty: true 55 | // }); 56 | 57 | // this.img.attr({src: this.model.get('url')}); 58 | 59 | // this.img[0].onload = _.bind(function() { 60 | // // Re-render node once ready 61 | // this.root.document.deselect() 62 | // this.$('.progress-container').hide(); 63 | // this.$('.info').show(); 64 | // }, this); 65 | // }, 66 | 67 | // onError: function (assembly) { 68 | // // TODO 69 | // //alert(JSON.stringify(assembly)); 70 | // //this.$('.image-progress .label').html("Invalid image. Skipping …"); 71 | // //this.$('.progress-container').hide(); 72 | // // 73 | // //setTimeout(_.bind(function () { 74 | // // app.document.reset(); 75 | // // this.$('.info').show(); 76 | // //}, this), 3000); 77 | // }, 78 | 79 | // onInvalid: function () { 80 | // this.$('.image-progress .label').html("Invalid image. Skipping …"); 81 | // this.$('.progress-container').hide(); 82 | 83 | // setTimeout(_.bind(function () { 84 | // this.$('.info').show(); 85 | // }, this), 3000); 86 | // }, 87 | 88 | // upload: function () { 89 | // this.$('.upload-image-form').submit(); 90 | // }, 91 | 92 | // render: function () { 93 | // s.views.Node.prototype.render.apply(this); 94 | 95 | // this.imageContent = $('
').appendTo(this.contentEl); 96 | // if (!this.model.get('url')) { this.imageContent.addClass('placeholder'); } 97 | 98 | // this.img = $('') 99 | // .attr({ src: this.model.get('url') || '/images/image_placeholder.png' }); 100 | 101 | // $('') 102 | // .attr({ href: this.model.get('original_url') }) 103 | // .append(this.img) 104 | // .appendTo(this.imageContent); 105 | 106 | // this.imageEditor = $(s.util.tpl('image_editor', { 107 | // transloadit_params: config.transloadit.image 108 | // })).appendTo(this.imageContent); 109 | // this.initializeUploadForm(); 110 | 111 | // this.caption = this.makeEditable($('
'), 'caption', "Enter Caption") 112 | // .insertAfter(this.contentEl); 113 | 114 | // return this; 115 | // } 116 | 117 | // }, { 118 | 119 | // states: { 120 | // write: { 121 | // enter: function () { 122 | // s.views.Node.states.write.enter.apply(this); 123 | 124 | // this.img.unwrap(); 125 | // }, 126 | // leave: function () { 127 | // s.views.Node.states.write.leave.apply(this); 128 | 129 | // this.img.wrap($('') 130 | // .attr({ href: this.model.get('original_url') })); 131 | // } 132 | // } 133 | // } 134 | 135 | // }); -------------------------------------------------------------------------------- /lib/codemirror/util/search.js: -------------------------------------------------------------------------------- 1 | // Define search commands. Depends on dialog.js or another 2 | // implementation of the openDialog method. 3 | 4 | // Replace works a little oddly -- it will do the replace on the next 5 | // Ctrl-G (or whatever is bound to findNext) press. You prevent a 6 | // replace by making sure the match is no longer selected when hitting 7 | // Ctrl-G. 8 | 9 | (function() { 10 | function SearchState() { 11 | this.posFrom = this.posTo = this.query = null; 12 | this.marked = []; 13 | } 14 | function getSearchState(cm) { 15 | return cm._searchState || (cm._searchState = new SearchState()); 16 | } 17 | function dialog(cm, text, shortText, f) { 18 | if (cm.openDialog) cm.openDialog(text, f); 19 | else f(prompt(shortText, "")); 20 | } 21 | function confirmDialog(cm, text, shortText, fs) { 22 | if (cm.openConfirm) cm.openConfirm(text, fs); 23 | else if (confirm(shortText)) fs[0](); 24 | } 25 | function parseQuery(query) { 26 | var isRE = query.match(/^\/(.*)\/$/); 27 | return isRE ? new RegExp(isRE[1]) : query; 28 | } 29 | var queryDialog = 30 | 'Search: (Use /re/ syntax for regexp search)'; 31 | function doSearch(cm, rev) { 32 | var state = getSearchState(cm); 33 | if (state.query) return findNext(cm, rev); 34 | dialog(cm, queryDialog, "Search for:", function(query) { 35 | cm.operation(function() { 36 | if (!query || state.query) return; 37 | state.query = parseQuery(query); 38 | if (cm.lineCount() < 2000) { // This is too expensive on big documents. 39 | for (var cursor = cm.getSearchCursor(query); cursor.findNext();) 40 | state.marked.push(cm.markText(cursor.from(), cursor.to(), "CodeMirror-searching")); 41 | } 42 | state.posFrom = state.posTo = cm.getCursor(); 43 | findNext(cm, rev); 44 | }); 45 | }); 46 | } 47 | function findNext(cm, rev) {cm.operation(function() { 48 | var state = getSearchState(cm); 49 | var cursor = cm.getSearchCursor(state.query, rev ? state.posFrom : state.posTo); 50 | if (!cursor.find(rev)) { 51 | cursor = cm.getSearchCursor(state.query, rev ? {line: cm.lineCount() - 1} : {line: 0, ch: 0}); 52 | if (!cursor.find(rev)) return; 53 | } 54 | cm.setSelection(cursor.from(), cursor.to()); 55 | state.posFrom = cursor.from(); state.posTo = cursor.to(); 56 | })} 57 | function clearSearch(cm) {cm.operation(function() { 58 | var state = getSearchState(cm); 59 | if (!state.query) return; 60 | state.query = null; 61 | for (var i = 0; i < state.marked.length; ++i) state.marked[i].clear(); 62 | state.marked.length = 0; 63 | })} 64 | 65 | var replaceQueryDialog = 66 | 'Replace: (Use /re/ syntax for regexp search)'; 67 | var replacementQueryDialog = 'With: '; 68 | var doReplaceConfirm = "Replace? "; 69 | function replace(cm, all) { 70 | dialog(cm, replaceQueryDialog, "Replace:", function(query) { 71 | if (!query) return; 72 | query = parseQuery(query); 73 | dialog(cm, replacementQueryDialog, "Replace with:", function(text) { 74 | if (all) { 75 | cm.operation(function() { 76 | for (var cursor = cm.getSearchCursor(query); cursor.findNext();) { 77 | if (typeof query != "string") { 78 | var match = cm.getRange(cursor.from(), cursor.to()).match(query); 79 | cursor.replace(text.replace(/\$(\d)/, function(w, i) {return match[i];})); 80 | } else cursor.replace(text); 81 | } 82 | }); 83 | } else { 84 | clearSearch(cm); 85 | var cursor = cm.getSearchCursor(query, cm.getCursor()); 86 | function advance() { 87 | var start = cursor.from(), match; 88 | if (!(match = cursor.findNext())) { 89 | cursor = cm.getSearchCursor(query); 90 | if (!(match = cursor.findNext()) || 91 | (cursor.from().line == start.line && cursor.from().ch == start.ch)) return; 92 | } 93 | cm.setSelection(cursor.from(), cursor.to()); 94 | confirmDialog(cm, doReplaceConfirm, "Replace?", 95 | [function() {doReplace(match);}, advance]); 96 | } 97 | function doReplace(match) { 98 | cursor.replace(typeof query == "string" ? text : 99 | text.replace(/\$(\d)/, function(w, i) {return match[i];})); 100 | advance(); 101 | } 102 | advance(); 103 | } 104 | }); 105 | }); 106 | } 107 | 108 | CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; 109 | CodeMirror.commands.findNext = doSearch; 110 | CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; 111 | CodeMirror.commands.clearSearch = clearSearch; 112 | CodeMirror.commands.replace = replace; 113 | CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; 114 | })(); 115 | -------------------------------------------------------------------------------- /lib/jquery.timeago.js: -------------------------------------------------------------------------------- 1 | /* 2 | * timeago: a jQuery plugin, version: 0.9.3 (2011-01-21) 3 | * @requires jQuery v1.2.3 or later 4 | * 5 | * Timeago is a jQuery plugin that makes it easy to support automatically 6 | * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). 7 | * 8 | * For usage and examples, visit: 9 | * http://timeago.yarp.com/ 10 | * 11 | * Licensed under the MIT: 12 | * http://www.opensource.org/licenses/mit-license.php 13 | * 14 | * Copyright (c) 2008-2011, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org) 15 | */ 16 | (function($) { 17 | $.timeago = function(timestamp) { 18 | if (timestamp instanceof Date) { 19 | return inWords(timestamp); 20 | } else if (typeof timestamp === "string") { 21 | return inWords($.timeago.parse(timestamp)); 22 | } else { 23 | return inWords($.timeago.datetime(timestamp)); 24 | } 25 | }; 26 | var $t = $.timeago; 27 | 28 | $.extend($.timeago, { 29 | settings: { 30 | refreshMillis: 60000, 31 | allowFuture: false, 32 | strings: { 33 | prefixAgo: null, 34 | prefixFromNow: null, 35 | suffixAgo: "ago", 36 | suffixFromNow: "from now", 37 | seconds: "less than a minute", 38 | minute: "about a minute", 39 | minutes: "%d minutes", 40 | hour: "about an hour", 41 | hours: "about %d hours", 42 | day: "a day", 43 | days: "%d days", 44 | month: "about a month", 45 | months: "%d months", 46 | year: "about a year", 47 | years: "%d years", 48 | numbers: [] 49 | } 50 | }, 51 | inWords: function(distanceMillis) { 52 | var $l = this.settings.strings; 53 | var prefix = $l.prefixAgo; 54 | var suffix = $l.suffixAgo; 55 | if (this.settings.allowFuture) { 56 | if (distanceMillis < 0) { 57 | prefix = $l.prefixFromNow; 58 | suffix = $l.suffixFromNow; 59 | } 60 | distanceMillis = Math.abs(distanceMillis); 61 | } 62 | 63 | var seconds = distanceMillis / 1000; 64 | var minutes = seconds / 60; 65 | var hours = minutes / 60; 66 | var days = hours / 24; 67 | var years = days / 365; 68 | 69 | function substitute(stringOrFunction, number) { 70 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; 71 | var value = ($l.numbers && $l.numbers[number]) || number; 72 | return string.replace(/%d/i, value); 73 | } 74 | 75 | var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || 76 | seconds < 90 && substitute($l.minute, 1) || 77 | minutes < 45 && substitute($l.minutes, Math.round(minutes)) || 78 | minutes < 90 && substitute($l.hour, 1) || 79 | hours < 24 && substitute($l.hours, Math.round(hours)) || 80 | hours < 48 && substitute($l.day, 1) || 81 | days < 30 && substitute($l.days, Math.floor(days)) || 82 | days < 60 && substitute($l.month, 1) || 83 | days < 365 && substitute($l.months, Math.floor(days / 30)) || 84 | years < 2 && substitute($l.year, 1) || 85 | substitute($l.years, Math.floor(years)); 86 | 87 | return $.trim([prefix, words, suffix].join(" ")); 88 | }, 89 | parse: function(iso8601) { 90 | var s = $.trim(iso8601); 91 | s = s.replace(/\.\d\d\d+/,""); // remove milliseconds 92 | s = s.replace(/-/,"/").replace(/-/,"/"); 93 | s = s.replace(/T/," ").replace(/Z/," UTC"); 94 | s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 95 | return new Date(s); 96 | }, 97 | datetime: function(elem) { 98 | // jQuery's `is()` doesn't play well with HTML5 in IE 99 | var isTime = $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); 100 | var iso8601 = isTime ? $(elem).attr("datetime") : $(elem).attr("title"); 101 | return $t.parse(iso8601); 102 | } 103 | }); 104 | 105 | $.fn.timeago = function() { 106 | var self = this; 107 | self.each(refresh); 108 | 109 | var $s = $t.settings; 110 | if ($s.refreshMillis > 0) { 111 | setInterval(function() { self.each(refresh); }, $s.refreshMillis); 112 | } 113 | return self; 114 | }; 115 | 116 | function refresh() { 117 | var data = prepareData(this); 118 | if (!isNaN(data.datetime)) { 119 | $(this).text(inWords(data.datetime)); 120 | } 121 | return this; 122 | } 123 | 124 | function prepareData(element) { 125 | element = $(element); 126 | if (!element.data("timeago")) { 127 | element.data("timeago", { datetime: $t.datetime(element) }); 128 | var text = $.trim(element.text()); 129 | if (text.length > 0) { 130 | element.attr("title", text); 131 | } 132 | } 133 | return element.data("timeago"); 134 | } 135 | 136 | function inWords(date) { 137 | return $t.inWords(distance(date)); 138 | } 139 | 140 | function distance(date) { 141 | return (new Date().getTime() - date.getTime()); 142 | } 143 | 144 | // fix for IE6 suckage 145 | document.createElement("abbr"); 146 | document.createElement("time"); 147 | }(jQuery)); -------------------------------------------------------------------------------- /lib/codemirror/util/searchcursor.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function SearchCursor(cm, query, pos, caseFold) { 3 | this.atOccurrence = false; this.cm = cm; 4 | if (caseFold == null) caseFold = typeof query == "string" && query == query.toLowerCase(); 5 | 6 | pos = pos ? cm.clipPos(pos) : {line: 0, ch: 0}; 7 | this.pos = {from: pos, to: pos}; 8 | 9 | // The matches method is filled in based on the type of query. 10 | // It takes a position and a direction, and returns an object 11 | // describing the next occurrence of the query, or null if no 12 | // more matches were found. 13 | if (typeof query != "string") // Regexp match 14 | this.matches = function(reverse, pos) { 15 | if (reverse) { 16 | var line = cm.getLine(pos.line).slice(0, pos.ch), match = line.match(query), start = 0; 17 | while (match) { 18 | var ind = line.indexOf(match[0]); 19 | start += ind; 20 | line = line.slice(ind + 1); 21 | var newmatch = line.match(query); 22 | if (newmatch) match = newmatch; 23 | else break; 24 | start++; 25 | } 26 | } 27 | else { 28 | var line = cm.getLine(pos.line).slice(pos.ch), match = line.match(query), 29 | start = match && pos.ch + line.indexOf(match[0]); 30 | } 31 | if (match) 32 | return {from: {line: pos.line, ch: start}, 33 | to: {line: pos.line, ch: start + match[0].length}, 34 | match: match}; 35 | }; 36 | else { // String query 37 | if (caseFold) query = query.toLowerCase(); 38 | var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;}; 39 | var target = query.split("\n"); 40 | // Different methods for single-line and multi-line queries 41 | if (target.length == 1) 42 | this.matches = function(reverse, pos) { 43 | var line = fold(cm.getLine(pos.line)), len = query.length, match; 44 | if (reverse ? (pos.ch >= len && (match = line.lastIndexOf(query, pos.ch - len)) != -1) 45 | : (match = line.indexOf(query, pos.ch)) != -1) 46 | return {from: {line: pos.line, ch: match}, 47 | to: {line: pos.line, ch: match + len}}; 48 | }; 49 | else 50 | this.matches = function(reverse, pos) { 51 | var ln = pos.line, idx = (reverse ? target.length - 1 : 0), match = target[idx], line = fold(cm.getLine(ln)); 52 | var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match)); 53 | if (reverse ? offsetA >= pos.ch || offsetA != match.length 54 | : offsetA <= pos.ch || offsetA != line.length - match.length) 55 | return; 56 | for (;;) { 57 | if (reverse ? !ln : ln == cm.lineCount() - 1) return; 58 | line = fold(cm.getLine(ln += reverse ? -1 : 1)); 59 | match = target[reverse ? --idx : ++idx]; 60 | if (idx > 0 && idx < target.length - 1) { 61 | if (line != match) return; 62 | else continue; 63 | } 64 | var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length); 65 | if (reverse ? offsetB != line.length - match.length : offsetB != match.length) 66 | return; 67 | var start = {line: pos.line, ch: offsetA}, end = {line: ln, ch: offsetB}; 68 | return {from: reverse ? end : start, to: reverse ? start : end}; 69 | } 70 | }; 71 | } 72 | } 73 | 74 | SearchCursor.prototype = { 75 | findNext: function() {return this.find(false);}, 76 | findPrevious: function() {return this.find(true);}, 77 | 78 | find: function(reverse) { 79 | var self = this, pos = this.cm.clipPos(reverse ? this.pos.from : this.pos.to); 80 | function savePosAndFail(line) { 81 | var pos = {line: line, ch: 0}; 82 | self.pos = {from: pos, to: pos}; 83 | self.atOccurrence = false; 84 | return false; 85 | } 86 | 87 | for (;;) { 88 | if (this.pos = this.matches(reverse, pos)) { 89 | this.atOccurrence = true; 90 | return this.pos.match || true; 91 | } 92 | if (reverse) { 93 | if (!pos.line) return savePosAndFail(0); 94 | pos = {line: pos.line-1, ch: this.cm.getLine(pos.line-1).length}; 95 | } 96 | else { 97 | var maxLine = this.cm.lineCount(); 98 | if (pos.line == maxLine - 1) return savePosAndFail(maxLine); 99 | pos = {line: pos.line+1, ch: 0}; 100 | } 101 | } 102 | }, 103 | 104 | from: function() {if (this.atOccurrence) return this.pos.from;}, 105 | to: function() {if (this.atOccurrence) return this.pos.to;}, 106 | 107 | replace: function(newText) { 108 | var self = this; 109 | if (this.atOccurrence) 110 | self.pos.to = this.cm.replaceRange(newText, self.pos.from, self.pos.to); 111 | } 112 | }; 113 | 114 | CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { 115 | return new SearchCursor(this, query, pos, caseFold); 116 | }); 117 | })(); 118 | -------------------------------------------------------------------------------- /lib/codemirror/util/javascript-hint.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function forEach(arr, f) { 3 | for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); 4 | } 5 | 6 | function arrayContains(arr, item) { 7 | if (!Array.prototype.indexOf) { 8 | var i = arr.length; 9 | while (i--) { 10 | if (arr[i] === item) { 11 | return true; 12 | } 13 | } 14 | return false; 15 | } 16 | return arr.indexOf(item) != -1; 17 | } 18 | 19 | function scriptHint(editor, keywords, getToken) { 20 | // Find the token at the cursor 21 | var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; 22 | // If it's not a 'word-style' token, ignore the token. 23 | if (!/^[\w$_]*$/.test(token.string)) { 24 | token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, 25 | className: token.string == "." ? "property" : null}; 26 | } 27 | // If it is a property, find out what it is a property of. 28 | while (tprop.className == "property") { 29 | tprop = getToken(editor, {line: cur.line, ch: tprop.start}); 30 | if (tprop.string != ".") return; 31 | tprop = getToken(editor, {line: cur.line, ch: tprop.start}); 32 | if (tprop.string == ')') { 33 | var level = 1; 34 | do { 35 | tprop = getToken(editor, {line: cur.line, ch: tprop.start}); 36 | switch (tprop.string) { 37 | case ')': level++; break; 38 | case '(': level--; break; 39 | default: break; 40 | } 41 | } while (level > 0) 42 | tprop = getToken(editor, {line: cur.line, ch: tprop.start}); 43 | if (tprop.className == 'variable') 44 | tprop.className = 'function'; 45 | else return; // no clue 46 | } 47 | if (!context) var context = []; 48 | context.push(tprop); 49 | } 50 | return {list: getCompletions(token, context, keywords), 51 | from: {line: cur.line, ch: token.start}, 52 | to: {line: cur.line, ch: token.end}}; 53 | } 54 | 55 | CodeMirror.javascriptHint = function(editor) { 56 | return scriptHint(editor, javascriptKeywords, 57 | function (e, cur) {return e.getTokenAt(cur);}); 58 | } 59 | 60 | function getCoffeeScriptToken(editor, cur) { 61 | // This getToken, it is for coffeescript, imitates the behavior of 62 | // getTokenAt method in javascript.js, that is, returning "property" 63 | // type and treat "." as indepenent token. 64 | var token = editor.getTokenAt(cur); 65 | if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') { 66 | token.end = token.start; 67 | token.string = '.'; 68 | token.className = "property"; 69 | } 70 | else if (/^\.[\w$_]*$/.test(token.string)) { 71 | token.className = "property"; 72 | token.start++; 73 | token.string = token.string.replace(/\./, ''); 74 | } 75 | return token; 76 | } 77 | 78 | CodeMirror.coffeescriptHint = function(editor) { 79 | return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken); 80 | } 81 | 82 | var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " + 83 | "toUpperCase toLowerCase split concat match replace search").split(" "); 84 | var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " + 85 | "lastIndexOf every some filter forEach map reduce reduceRight ").split(" "); 86 | var funcProps = "prototype apply call bind".split(" "); 87 | var javascriptKeywords = ("break case catch continue debugger default delete do else false finally for function " + 88 | "if in instanceof new null return switch throw true try typeof var void while with").split(" "); 89 | var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " + 90 | "if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" "); 91 | 92 | function getCompletions(token, context, keywords) { 93 | var found = [], start = token.string; 94 | function maybeAdd(str) { 95 | if (str.indexOf(start) == 0 && !arrayContains(found, str)) found.push(str); 96 | } 97 | function gatherCompletions(obj) { 98 | if (typeof obj == "string") forEach(stringProps, maybeAdd); 99 | else if (obj instanceof Array) forEach(arrayProps, maybeAdd); 100 | else if (obj instanceof Function) forEach(funcProps, maybeAdd); 101 | for (var name in obj) maybeAdd(name); 102 | } 103 | 104 | if (context) { 105 | // If this is a property, see if it belongs to some object we can 106 | // find in the current environment. 107 | var obj = context.pop(), base; 108 | if (obj.className == "variable") 109 | base = window[obj.string]; 110 | else if (obj.className == "string") 111 | base = ""; 112 | else if (obj.className == "atom") 113 | base = 1; 114 | else if (obj.className == "function") { 115 | if (window.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') && 116 | (typeof window.jQuery == 'function')) 117 | base = window.jQuery(); 118 | else if (window._ != null && (obj.string == '_') && (typeof window._ == 'function')) 119 | base = window._(); 120 | } 121 | while (base != null && context.length) 122 | base = base[context.pop().string]; 123 | if (base != null) gatherCompletions(base); 124 | } 125 | else { 126 | // If not, just look in the window object and any local scope 127 | // (reading into JS mode internals to get at the local variables) 128 | for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name); 129 | gatherCompletions(window); 130 | forEach(keywords, maybeAdd); 131 | } 132 | return found; 133 | } 134 | })(); 135 | -------------------------------------------------------------------------------- /lib/head.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | Head JS The only script in your 3 | Copyright Tero Piirainen (tipiirai) 4 | License MIT / http://bit.ly/mit-license 5 | Version 0.96 6 | 7 | http://headjs.com 8 | */(function(a){function l(){var a=window.outerWidth||b.clientWidth;b.className=b.className.replace(/ (w|lt)-\d+/g,""),f("w-"+Math.round(a/100)*100),h(c.screens,function(b){a<=b&&f("lt-"+b)}),i.feature()}function h(a,b){for(var c=0,d=a.length;c2&&this[d+1]!==undefined)d&&f(this.slice(1,d+1).join("-")+c.section);else{var e=a||"index",g=e.indexOf(".");g>0&&(e=e.substring(0,g)),b.id=e+c.page,d||f("root"+c.section)}}),l(),window.onresize=l,i.feature("js",!0).feature()})(document),function(){function h(a){var b=a.charAt(0).toUpperCase()+a.substr(1),c=(a+" "+d.join(b+" ")+b).split(" ");return!!g(c)}function g(a){for(var c in a)if(b[a[c]]!==undefined)return!0}var a=document.createElement("i"),b=a.style,c=" -o- -moz- -ms- -webkit- -khtml- ".split(" "),d="Webkit Moz O ms Khtml".split(" "),e=window.head_conf&&head_conf.head||"head",f=window[e],i={gradient:function(){var a="background-image:",d="gradient(linear,left top,right bottom,from(#9f9),to(#fff));",e="linear-gradient(left top,#eee,#fff);";b.cssText=(a+c.join(d+a)+c.join(e+a)).slice(0,-a.length);return!!b.backgroundImage},rgba:function(){b.cssText="background-color:rgba(0,0,0,0.5)";return!!b.backgroundColor},opacity:function(){return a.style.opacity===""},textshadow:function(){return b.textShadow===""},multiplebgs:function(){b.cssText="background:url(//:),url(//:),red url(//:)";return(new RegExp("(url\\s*\\(.*?){3}")).test(b.background)},boxshadow:function(){return h("boxShadow")},borderimage:function(){return h("borderImage")},borderradius:function(){return h("borderRadius")},cssreflections:function(){return h("boxReflect")},csstransforms:function(){return h("transform")},csstransitions:function(){return h("transition")},fontface:function(){var a=navigator.userAgent,b;if(0)return!0;if(b=a.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/))return b[1]>="4.0.249.4"||1*b[1].split(".")[0]>5;if((b=a.match(/Safari\/(\d+\.\d+)/))&&!/iPhone/.test(a))return b[1]>="525.13";if(/Opera/.test({}.toString.call(window.opera)))return opera.version()>="10.00";if(b=a.match(/rv:(\d+\.\d+\.\d+)[^b].*Gecko\//))return b[1]>="1.9.1";return!1}};for(var j in i)i[j]&&f.feature(j,i[j].call(),!0);f.feature()}(),function(a){function z(){d||(d=!0,s(e,function(a){p(a)}))}function y(c,d){var e=a.createElement("script");e.type="text/"+(c.type||"javascript"),e.src=c.src||c,e.async=!1,e.onreadystatechange=e.onload=function(){var a=e.readyState;!d.done&&(!a||/loaded|complete/.test(a))&&(d.done=!0,d())},(a.body||b).appendChild(e)}function x(a,b){if(a.state==o)return b&&b();if(a.state==n)return k.ready(a.name,b);if(a.state==m)return a.onpreload.push(function(){x(a,b)});a.state=n,y(a.url,function(){a.state=o,b&&b(),s(g[a.name],function(a){p(a)}),u()&&d&&s(g.ALL,function(a){p(a)})})}function w(a,b){a.state===undefined&&(a.state=m,a.onpreload=[],y({src:a.url,type:"cache"},function(){v(a)}))}function v(a){a.state=l,s(a.onpreload,function(a){a.call()})}function u(a){a=a||h;var b;for(var c in a){if(a.hasOwnProperty(c)&&a[c].state!=o)return!1;b=!0}return b}function t(a){return Object.prototype.toString.call(a)=="[object Function]"}function s(a,b){if(!!a){typeof a=="object"&&(a=[].slice.call(a));for(var c=0;c" character of a start tag has been typed. It can 6 | * also complete " 18 | * Contributed under the same license terms as CodeMirror. 19 | */ 20 | (function() { 21 | /** Option that allows tag closing behavior to be toggled. Default is true. */ 22 | CodeMirror.defaults['closeTagEnabled'] = true; 23 | 24 | /** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */ 25 | CodeMirror.defaults['closeTagIndent'] = ['applet', 'blockquote', 'body', 'button', 'div', 'dl', 'fieldset', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'iframe', 'layer', 'legend', 'object', 'ol', 'p', 'select', 'table', 'ul']; 26 | 27 | /** 28 | * Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass. 29 | * - cm: The editor instance. 30 | * - ch: The character being processed. 31 | * - indent: Optional. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option. 32 | * Pass false to disable indentation. Pass an array to override the default list of tag names. 33 | */ 34 | CodeMirror.defineExtension("closeTag", function(cm, ch, indent) { 35 | if (!cm.getOption('closeTagEnabled')) { 36 | throw CodeMirror.Pass; 37 | } 38 | 39 | var mode = cm.getOption('mode'); 40 | 41 | if (mode == 'text/html') { 42 | 43 | /* 44 | * Relevant structure of token: 45 | * 46 | * htmlmixed 47 | * className 48 | * state 49 | * htmlState 50 | * type 51 | * context 52 | * tagName 53 | * mode 54 | * 55 | * xml 56 | * className 57 | * state 58 | * tagName 59 | * type 60 | */ 61 | 62 | var pos = cm.getCursor(); 63 | var tok = cm.getTokenAt(pos); 64 | var state = tok.state; 65 | 66 | if (state.mode && state.mode != 'html') { 67 | throw CodeMirror.Pass; // With htmlmixed, we only care about the html sub-mode. 68 | } 69 | 70 | if (ch == '>') { 71 | var type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml 72 | 73 | if (tok.className == 'tag' && type == 'closeTag') { 74 | throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag. 75 | } 76 | 77 | cm.replaceSelection('>'); // Mode state won't update until we finish the tag. 78 | pos = {line: pos.line, ch: pos.ch + 1}; 79 | cm.setCursor(pos); 80 | 81 | tok = cm.getTokenAt(cm.getCursor()); 82 | state = tok.state; 83 | type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml 84 | 85 | if (tok.className == 'tag' && type != 'selfcloseTag') { 86 | var tagName = state.htmlState ? state.htmlState.context.tagName : state.tagName; // htmlmixed : xml 87 | if (tagName.length > 0) { 88 | insertEndTag(cm, indent, pos, tagName); 89 | } 90 | return; 91 | } 92 | 93 | // Undo the '>' insert and allow cm to handle the key instead. 94 | cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos); 95 | cm.replaceSelection(""); 96 | 97 | } else if (ch == '/') { 98 | if (tok.className == 'tag' && tok.string == '<') { 99 | var tagName = state.htmlState ? (state.htmlState.context ? state.htmlState.context.tagName : '') : state.context.tagName; // htmlmixed : xml # extra htmlmized check is for ' 0) { 101 | completeEndTag(cm, pos, tagName); 102 | return; 103 | } 104 | } 105 | } 106 | 107 | } else if (mode == 'xmlpure') { 108 | 109 | var pos = cm.getCursor(); 110 | var tok = cm.getTokenAt(pos); 111 | var tagName = tok.state.context.tagName; 112 | 113 | if (ch == '>') { 114 | // tagName=foo, string=foo 115 | // tagName=foo, string=/ # ignore 116 | // tagName=foo, string=/foo # ignore 117 | if (tok.string == tagName) { 118 | cm.replaceSelection('>'); // parity w/html modes 119 | pos = {line: pos.line, ch: pos.ch + 1}; 120 | cm.setCursor(pos); 121 | 122 | insertEndTag(cm, indent, pos, tagName); 123 | return; 124 | } 125 | 126 | } else if (ch == '/') { 127 | // ', 'end'); 142 | cm.indentLine(pos.line + 1); 143 | cm.indentLine(pos.line + 2); 144 | cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length}); 145 | } else { 146 | cm.replaceSelection(''); 147 | cm.setCursor(pos); 148 | } 149 | } 150 | 151 | function shouldIndent(cm, indent, tagName) { 152 | if (typeof indent == 'undefined' || indent == null || indent == true) { 153 | indent = cm.getOption('closeTagIndent'); 154 | } 155 | if (!indent) { 156 | indent = []; 157 | } 158 | return indexOf(indent, tagName.toLowerCase()) != -1; 159 | } 160 | 161 | // C&P from codemirror.js...would be nice if this were visible to utilities. 162 | function indexOf(collection, elt) { 163 | if (collection.indexOf) return collection.indexOf(elt); 164 | for (var i = 0, e = collection.length; i < e; ++i) 165 | if (collection[i] == elt) return i; 166 | return -1; 167 | } 168 | 169 | function completeEndTag(cm, pos, tagName) { 170 | cm.replaceSelection('/' + tagName + '>'); 171 | cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 }); 172 | } 173 | 174 | })(); 175 | -------------------------------------------------------------------------------- /lib/codemirror/util/foldcode.js: -------------------------------------------------------------------------------- 1 | // the tagRangeFinder function is 2 | // Copyright (C) 2011 by Daniel Glazman 3 | // released under the MIT license (../../LICENSE) like the rest of CodeMirror 4 | CodeMirror.tagRangeFinder = function(cm, line) { 5 | var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; 6 | var nameChar = nameStartChar + "\-\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; 7 | var xmlNAMERegExp = new RegExp("^[" + nameStartChar + "][" + nameChar + "]*"); 8 | 9 | var lineText = cm.getLine(line); 10 | var found = false; 11 | var tag = null; 12 | var pos = 0; 13 | while (!found) { 14 | pos = lineText.indexOf("<", pos); 15 | if (-1 == pos) // no tag on line 16 | return; 17 | if (pos + 1 < lineText.length && lineText[pos + 1] == "/") { // closing tag 18 | pos++; 19 | continue; 20 | } 21 | // ok we weem to have a start tag 22 | if (!lineText.substr(pos + 1).match(xmlNAMERegExp)) { // not a tag name... 23 | pos++; 24 | continue; 25 | } 26 | var gtPos = lineText.indexOf(">", pos + 1); 27 | if (-1 == gtPos) { // end of start tag not in line 28 | var l = line + 1; 29 | var foundGt = false; 30 | var lastLine = cm.lineCount(); 31 | while (l < lastLine && !foundGt) { 32 | var lt = cm.getLine(l); 33 | var gt = lt.indexOf(">"); 34 | if (-1 != gt) { // found a > 35 | foundGt = true; 36 | var slash = lt.lastIndexOf("/", gt); 37 | if (-1 != slash && slash < gt) { 38 | var str = lineText.substr(slash, gt - slash + 1); 39 | if (!str.match( /\/\s*\>/ )) // yep, that's the end of empty tag 40 | return l+1; 41 | } 42 | } 43 | l++; 44 | } 45 | found = true; 46 | } 47 | else { 48 | var slashPos = lineText.lastIndexOf("/", gtPos); 49 | if (-1 == slashPos) { // cannot be empty tag 50 | found = true; 51 | // don't continue 52 | } 53 | else { // empty tag? 54 | // check if really empty tag 55 | var str = lineText.substr(slashPos, gtPos - slashPos + 1); 56 | if (!str.match( /\/\s*\>/ )) { // finally not empty 57 | found = true; 58 | // don't continue 59 | } 60 | } 61 | } 62 | if (found) { 63 | var subLine = lineText.substr(pos + 1); 64 | tag = subLine.match(xmlNAMERegExp); 65 | if (tag) { 66 | // we have an element name, wooohooo ! 67 | tag = tag[0]; 68 | // do we have the close tag on same line ??? 69 | if (-1 != lineText.indexOf("", pos)) // yep 70 | { 71 | found = false; 72 | } 73 | // we don't, so we have a candidate... 74 | } 75 | else 76 | found = false; 77 | } 78 | if (!found) 79 | pos++; 80 | } 81 | 82 | if (found) { 83 | var startTag = "(\\<\\/" + tag + "\\>)|(\\<" + tag + "\\>)|(\\<" + tag + "\\s)|(\\<" + tag + "$)"; 84 | var startTagRegExp = new RegExp(startTag, "g"); 85 | var endTag = ""; 86 | var depth = 1; 87 | var l = line + 1; 88 | var lastLine = cm.lineCount(); 89 | while (l < lastLine) { 90 | lineText = cm.getLine(l); 91 | var match = lineText.match(startTagRegExp); 92 | if (match) { 93 | for (var i = 0; i < match.length; i++) { 94 | if (match[i] == endTag) 95 | depth--; 96 | else 97 | depth++; 98 | if (!depth) 99 | return l+1; 100 | } 101 | } 102 | l++; 103 | } 104 | return; 105 | } 106 | }; 107 | 108 | CodeMirror.braceRangeFinder = function(cm, line) { 109 | var lineText = cm.getLine(line); 110 | var startChar = lineText.lastIndexOf("{"); 111 | if (startChar < 0 || lineText.lastIndexOf("}") > startChar) return; 112 | var tokenType = cm.getTokenAt({line: line, ch: startChar}).className; 113 | var count = 1, lastLine = cm.lineCount(), end; 114 | outer: for (var i = line + 1; i < lastLine; ++i) { 115 | var text = cm.getLine(i), pos = 0; 116 | for (;;) { 117 | var nextOpen = text.indexOf("{", pos), nextClose = text.indexOf("}", pos); 118 | if (nextOpen < 0) nextOpen = text.length; 119 | if (nextClose < 0) nextClose = text.length; 120 | pos = Math.min(nextOpen, nextClose); 121 | if (pos == text.length) break; 122 | if (cm.getTokenAt({line: i, ch: pos + 1}).className == tokenType) { 123 | if (pos == nextOpen) ++count; 124 | else if (!--count) { end = i; break outer; } 125 | } 126 | ++pos; 127 | } 128 | } 129 | if (end == null || end == line + 1) return; 130 | return end; 131 | }; 132 | 133 | CodeMirror.indentRangeFinder = function(cm, line) { 134 | var tabSize = cm.getOption("tabSize"); 135 | var myIndent = cm.getLineHandle(line).indentation(tabSize), last; 136 | for (var i = line + 1, end = cm.lineCount(); i < end; ++i) { 137 | var handle = cm.getLineHandle(i); 138 | if (!/^\s*$/.test(handle.text)) { 139 | if (handle.indentation(tabSize) <= myIndent) break; 140 | last = i; 141 | } 142 | } 143 | if (!last) return null; 144 | return last + 1; 145 | }; 146 | 147 | CodeMirror.newFoldFunction = function(rangeFinder, markText) { 148 | var folded = []; 149 | if (markText == null) markText = '
%N%'; 150 | 151 | function isFolded(cm, n) { 152 | for (var i = 0; i < folded.length; ++i) { 153 | var start = cm.lineInfo(folded[i].start); 154 | if (!start) folded.splice(i--, 1); 155 | else if (start.line == n) return {pos: i, region: folded[i]}; 156 | } 157 | } 158 | 159 | function expand(cm, region) { 160 | cm.clearMarker(region.start); 161 | for (var i = 0; i < region.hidden.length; ++i) 162 | cm.showLine(region.hidden[i]); 163 | } 164 | 165 | return function(cm, line) { 166 | cm.operation(function() { 167 | var known = isFolded(cm, line); 168 | if (known) { 169 | folded.splice(known.pos, 1); 170 | expand(cm, known.region); 171 | } else { 172 | var end = rangeFinder(cm, line); 173 | if (end == null) return; 174 | var hidden = []; 175 | for (var i = line + 1; i < end; ++i) { 176 | var handle = cm.hideLine(i); 177 | if (handle) hidden.push(handle); 178 | } 179 | var first = cm.setMarker(line, markText); 180 | var region = {start: first, hidden: hidden}; 181 | cm.onDeleteLine(first, function() { expand(cm, region); }); 182 | folded.push(region); 183 | } 184 | }); 185 | }; 186 | }; 187 | -------------------------------------------------------------------------------- /src/shared/model/document.js: -------------------------------------------------------------------------------- 1 | if (typeof exports !== 'undefined') { 2 | var Data = require ('../../../lib/data'), 3 | _ = require('underscore'); 4 | } 5 | 6 | // Document 7 | // -------- 8 | // 9 | // A generic model for representing and transforming digital documents 10 | 11 | var Document = function(document, schema) { 12 | var that = this; 13 | 14 | this.id = document.id; 15 | 16 | // Initialize document 17 | this.nodes = new Data.Graph(schema); 18 | this.nodes.merge(document.nodes); 19 | 20 | this.head = this.nodes.get(document.head); 21 | this.tail = this.nodes.get(document.tail); 22 | 23 | this.rev = document.rev; 24 | 25 | this.selections = {}; 26 | this.users = {}; 27 | 28 | // Operations History 29 | this.operations = []; 30 | 31 | function checkRev(rev) { 32 | return that.rev === rev; 33 | } 34 | 35 | // Node API 36 | // -------- 37 | 38 | this.node = { 39 | 40 | // Process update command 41 | update: function(options) { 42 | that.nodes.get(options.node).set(options.properties); 43 | that.trigger('node:update', options.node); 44 | }, 45 | 46 | // Update selection 47 | select: function(options) { 48 | if (that.users[options.user].selection) { 49 | _.each(that.users[options.user].selection, function(node) { 50 | delete that.selections[node]; 51 | }); 52 | } 53 | 54 | that.users[options.user].selection = options.nodes; 55 | _.each(options.nodes, function(node) { 56 | that.selections[node] = options.user; 57 | }); 58 | 59 | that.trigger('node:select', that.selections); 60 | }, 61 | 62 | // Insert a new node 63 | insert: function(options) { 64 | if (checkRev(options.rev)) { 65 | 66 | var node = that.nodes.set(_.extend({ 67 | "type": ["/type/node", "/type/"+options.type], 68 | _id: ["", options.type, options.rev].join('/'), 69 | prev: that.tail._id 70 | }, options.attributes)); 71 | that.tail.set({next: node._id}); 72 | that.tail = node; 73 | if (node) { 74 | that.rev += 1; 75 | that.trigger('node:insert', node); 76 | return node; 77 | } 78 | } 79 | return null; 80 | }, 81 | 82 | // Move selected nodes 83 | move: function(options) { 84 | if (checkRev(options.rev)) { 85 | var f = that.get(_.first(options.nodes)), // first node of selection 86 | l = that.get(_.last(options.nodes)), // last node of selection 87 | t = that.get(options.target), // target node 88 | fp = f.get('prev'), // first-previous 89 | ln = l.get('next'), // last-next 90 | tn = t.get('next'); // target-next 91 | 92 | t.set({ 93 | next: f._id, 94 | prev: t.get('prev') === l ? (fp ? fp._id : null) 95 | : (t.get('prev') ? t.get('prev')._id : null) 96 | }); 97 | 98 | if (fp) { 99 | fp.set({next: ln ? ln._id : null}); 100 | } else { 101 | // dealing with the first node 102 | that.head = t; 103 | console.log('dealing with the first elem'); 104 | } 105 | 106 | // First node of the selection is now preceded by the target node 107 | f.set({prev: t._id}); 108 | 109 | if (ln) ln.set({prev: fp ? fp._id : null}); 110 | 111 | l.set({next: tn ? tn._id : null}); 112 | 113 | if (tn) { 114 | tn.set({prev: l._id}); 115 | } else { 116 | // Special case: target is tail node 117 | that.tail = l; 118 | } 119 | 120 | that.trigger('node:move', options); 121 | that.rev += 1; 122 | } 123 | }, 124 | 125 | // Delete node by id 126 | delete: function(node) { 127 | 128 | } 129 | }; 130 | 131 | 132 | // Patch API 133 | // -------- 134 | 135 | this.patch = { 136 | 137 | }; 138 | 139 | // Comment API 140 | // -------- 141 | 142 | this.comment = { 143 | 144 | }; 145 | 146 | // User API 147 | // -------- 148 | 149 | this.user = { 150 | enter: function(user) { 151 | this.users[user.id] = { id: user.id, username: user.username, color: user.color || "red"}; 152 | }, 153 | 154 | leave: function(id) { 155 | delete this.users[id]; 156 | } 157 | }; 158 | 159 | 160 | // Document API 161 | // -------- 162 | 163 | // Iterate over all nodes 164 | this.each = function(fn, ctx) { 165 | var current = this.head; 166 | var index = 0; 167 | 168 | fn.call(ctx || this, current, current._id, index); 169 | while (current = current.get('next')) { 170 | index += 1; 171 | fn.call(ctx || this, current, current._id, index); 172 | } 173 | }; 174 | 175 | this.logOperation = function(op) { 176 | this.operations.push(op); 177 | }; 178 | 179 | this.execute = function(op, silent) { 180 | var command = op.command.split(':'); 181 | this[command[0]][command[1]](op.params); 182 | if (!silent) this.trigger('operation:executed'); 183 | this.logOperation(op); 184 | }; 185 | 186 | // Get a specific node 187 | this.get = function(id) { 188 | return this.nodes.get(id); 189 | }; 190 | 191 | // Serialize document state to JSON 192 | this.toJSON = function() { 193 | return { 194 | id: this.id, 195 | operations: this.operations, 196 | nodes: this.nodes.toJSON(), 197 | head: this.head._id, 198 | tail: this.tail._id 199 | } 200 | }; 201 | }; 202 | 203 | 204 | // Create a new (empty) document 205 | // -------- 206 | 207 | Document.create = function(schema) { 208 | var doc = { 209 | "id": Data.uuid(), 210 | "created_at": "2012-04-10T15:17:28.946Z", 211 | "updated_at": "2012-04-10T15:17:28.946Z", 212 | "head": "/cover/1", 213 | "tail": "/text/3", 214 | "rev": 3, 215 | "nodes": { 216 | "/cover/1": { 217 | "type": ["/type/node", "/type/cover"], 218 | "title": "A new document", 219 | "abstract": "The Substance Composer is flexible editing component to be used by applications such as Substance.io for collaborative content composition.", 220 | "next": "/section/2", 221 | "prev": null 222 | }, 223 | "/section/2": { 224 | "type": ["/type/node", "/type/section"], 225 | "name": "Plugins", 226 | "prev": "/cover/1", 227 | "next": "/text/3" 228 | }, 229 | "/text/3": { 230 | "type": ["/type/node", "/type/text"], 231 | "content": "Enter some text.", 232 | "prev": "/section/2", 233 | "next": null 234 | } 235 | } 236 | }; 237 | return new Document(doc, schema); 238 | }; 239 | 240 | _.extend(Document.prototype, _.Events); 241 | 242 | // Export for browser 243 | if (typeof exports !== 'undefined') { 244 | module.exports = Document; 245 | } else { 246 | sc.models.Document = Document; 247 | } 248 | -------------------------------------------------------------------------------- /nodes/map/map.js: -------------------------------------------------------------------------------- 1 | var collections = {}; 2 | 3 | collections["spots"] = { 4 | 5 | // New Spots 6 | // ------------------- 7 | 8 | enter: function(spots) { 9 | var that = this; 10 | spots.each(function(spot, key, index) { 11 | var spot = $(_.template($('script[name=nav_spot]').html(), { 12 | spot: spot, 13 | active: that.activeSpot === spot 14 | })).css('left', spot.pos.x) 15 | .css('bottom', -70) 16 | .appendTo($('.spots-navigation')); 17 | 18 | }); 19 | _.delay(this.collections["spots"].update, 0, spots) 20 | }, 21 | 22 | // Existing Spots 23 | // ------------------- 24 | 25 | update: function(spots) { 26 | spots.each(function(spot) { 27 | $('#'+_.htmlId(spot._id)) 28 | .css('left', spot.pos.x) 29 | .css('bottom', 10) 30 | .css('background-image', "url('http://a.tiles.mapbox.com/v3/mapbox.mapbox-streets/"+spot.get('longitude')+","+spot.get('latitude')+",11/100x100.png')") 31 | }); 32 | }, 33 | 34 | // Removed Spots 35 | // ------------------- 36 | 37 | exit: function(spots) { 38 | spots.each(function(spot) { 39 | $('#'+_.htmlId(spot._id)).remove(); 40 | }); 41 | } 42 | }; 43 | 44 | 45 | sc.views.Node.define('/type/map', { 46 | 47 | className: 'content-node map', 48 | 49 | collections: collections, 50 | 51 | events: { 52 | 'change .name': '_updateData', 53 | 'change .descr': '_updateData', 54 | 'click .spots-navigation .spot': '_gotoSpot', 55 | 'click .remove-spot': '_removeSpot' 56 | }, 57 | 58 | _removeSpot: function(e) { 59 | var spot = this.spots.get($(e.currentTarget).parent().attr('data-id')); 60 | this.map.removeLayer(spot.marker); 61 | this.spots.del(spot._id); 62 | this.activeSpot = null; 63 | this.trigger('update', this.spots); 64 | this.render(); 65 | return false; 66 | }, 67 | 68 | _gotoSpot: function(e) { 69 | this.gotoSpot(this.spots.get($(e.currentTarget).attr('data-id'))); 70 | }, 71 | 72 | _updateData: function(e) { 73 | this.activeSpot.name = this.$('.name').val(); 74 | this.activeSpot.descr = this.$('.descr').val(); 75 | }, 76 | 77 | 78 | // Contructor 79 | // ------------------- 80 | 81 | initialize: function(options) { 82 | 83 | }, 84 | 85 | 86 | // Calculating layout 87 | // ------------------- 88 | 89 | layout: function(property) { 90 | this.data["spots"].each(function(spot, key, index) { 91 | spot.pos = { 92 | x: index*100, 93 | }; 94 | }); 95 | }, 96 | 97 | // Jump to Spot 98 | // ------------------- 99 | 100 | gotoSpot: function(spot) { 101 | this.activeSpot = spot; 102 | this.render(); 103 | this.map.setView(new L.LatLng(spot.get('latitude'), spot.get('longitude')), 15); 104 | }, 105 | 106 | // Jump to next Spot 107 | // ------------------- 108 | 109 | nextSpot: function() { 110 | if (!this.activeSpot) return this.gotoSpot(this.spots.first()); 111 | var currentIndex = this.spots.index(this.activeSpot.id); 112 | this.gotoSpot(this.spots.at((currentIndex + 1) % this.spots.length)); 113 | }, 114 | 115 | // Jump to previous Spot 116 | // ------------------- 117 | 118 | prevSpot: function() { 119 | if (!this.activeSpot) return this.gotoSpot(this.spots.last()); 120 | var currentIndex = this.spots.index(this.activeSpot.id); 121 | if (currentIndex<=0) return this.gotoSpot(this.spots.last()); 122 | this.gotoSpot(this.spots.at((currentIndex - 1) % this.spots.length)); 123 | }, 124 | 125 | // Add a new spot 126 | // ------------------- 127 | 128 | addSpot: function(lat, lng, name, descr, id, silent) { 129 | var spot = { 130 | id: id ? id : this.map._container.id + Data.uuid(), 131 | name: name || 'Untitled', 132 | descr: descr || 'Undescribed.' , 133 | latitude: lat, 134 | longitude: lng 135 | }; 136 | 137 | var s = this.data["spots"].add(spot); 138 | s.marker = new L.Marker(new L.LatLng(lat, lng), { draggable: true }); 139 | 140 | // Update when dragged. 141 | function drag(e) { 142 | var pos = e.target._latlng; 143 | spot.latitude = pos.lat; 144 | spot.longitude = pos.lng; 145 | this.render(); 146 | this.trigger('update', this.spots); 147 | } 148 | 149 | function click(e) { 150 | this.activeSpot = s; 151 | this.render(); 152 | } 153 | 154 | s.marker.on('drag', _.bind(drag, this)); 155 | s.marker.on('click', _.bind(click, this)); 156 | this.map.addLayer(s.marker); 157 | 158 | if (!silent) this.trigger('update', this.spots); 159 | return s; 160 | }, 161 | 162 | 163 | // Keyboard navigation - a pleasure 164 | // ------------------- 165 | 166 | registerKeyBindings: function() { 167 | // $(document) 168 | // .keydown('right', _.bind(function() { this.nextSpot(); this.render(); }, this)) 169 | // .keydown('left', _.bind(function() { this.prevSpot(); this.render(); }, this)) 170 | // .keydown('esc', _.bind(function() { this.activeSpot = null; this.render(); }, this)); 171 | }, 172 | 173 | 174 | // Register Map Events 175 | // ------------------- 176 | 177 | registerMapEvents: function() { 178 | var that = this; 179 | var clickCount = 0; 180 | // Add new spot, every time the map gets clicked 181 | this.map.on('click', function(e) { 182 | if (that.activeSpot) { } 183 | 184 | clickCount += 1; 185 | if (clickCount <= 1) { 186 | _.delay(function() { 187 | if (clickCount <= 1) { 188 | that.activeSpot = that.addSpot(e.latlng.lat, e.latlng.lng); 189 | that.render(); 190 | } 191 | clickCount = 0; 192 | }, 300); 193 | } 194 | }); 195 | }, 196 | 197 | // Render the beast 198 | // ------------------- 199 | 200 | focus: function () { 201 | $(this.textEl).click(); 202 | }, 203 | 204 | select: function () { 205 | sc.views.Node.prototype.select.apply(this); 206 | }, 207 | 208 | deselect: function () { 209 | sc.views.Node.prototype.deselect.apply(this); 210 | }, 211 | 212 | renderMap: function() { 213 | var that = this; 214 | 215 | _.delay(function() { 216 | that.map = new L.Map('map', { 217 | layers: new L.TileLayer('http://a.tiles.mapbox.com/v3/mapbox.mapbox-streets/{z}/{x}/{y}.png', {}), 218 | center: new L.LatLng(51.505, -0.09), 219 | zoom: 13, 220 | maxZoom: 17, 221 | attributionControl: false 222 | }); 223 | 224 | that.spots = that.data["spots"] = new Data.Collection({ 225 | "type": { 226 | "_id": "/type/spot", 227 | "name": "Spots", 228 | "properties": { 229 | "latitude": {"name": "Latitude", "type": "number" }, 230 | "longitude": {"name": "Longitude", "type": "number" }, 231 | "name": { "name": "Name", "type": "string"}, 232 | "descr": { "name": "Description", "type": "string" } 233 | } 234 | } 235 | }); 236 | 237 | var spots = [ 238 | { 239 | "_id": "/spot/1", 240 | "latitude": 38.91310029076162, 241 | "longitude": -77.03267812728882, 242 | "name": "MapBox", 243 | "descr": "MapBox Headquarters in Washington DC" 244 | }, 245 | { 246 | "_id": "/spot/2", 247 | "latitude": 48.30293692666153, 248 | "longitude": 14.294521808624268, 249 | "name": "Quasipartikel", 250 | "descr": "The Quasipartikel Quasioffice in Linz, Austria" 251 | }, 252 | { 253 | "_id": "/spot/3", 254 | "latitude": 40.689253770619686, 255 | "longitude": -74.04454708099365, 256 | "name": "Statue of Liberty", 257 | "descr": "Sightseeing, anyone?" 258 | } 259 | ]; 260 | 261 | that.activeSpot = null; 262 | 263 | _.each(spots, function(s) { 264 | that.addSpot(s.latitude, s.longitude, s.name, s.descr, s.id, true); 265 | }, that); 266 | 267 | this.activeSpot = that.spots.first(); 268 | that.registerMapEvents(); 269 | that.registerKeyBindings(); 270 | }, 50); 271 | 272 | }, 273 | 274 | // Render the beast 275 | // ------------------- 276 | 277 | render: function () { 278 | var that = this; 279 | if (!this.rendered) { 280 | sc.views.Node.prototype.render.apply(this, arguments); 281 | $(this.contentEl).html(_.tpl('map', this.model)); 282 | this.renderMap(); 283 | this.rendered = true; 284 | } 285 | 286 | _.delay(function() { 287 | that.layout(); 288 | that.refresh(); 289 | 290 | that.$('.spots-navigation .spot.active').removeClass('active'); 291 | if (that.activeSpot) { 292 | that.$('.spot-details').replaceWith(_.template($('script[name=spot]').html(), { 293 | spot: that.activeSpot 294 | })); 295 | that.$('#'+ _.htmlId(that.activeSpot._id)).addClass('active'); 296 | } else { 297 | that.$('.spot-details').empty(); 298 | } 299 | }, 100); 300 | 301 | return this; 302 | } 303 | }); -------------------------------------------------------------------------------- /lib/remotestorage.js: -------------------------------------------------------------------------------- 1 | (function(){function c(c,d,e){a[c]=e;var f=c.substring(0,c.lastIndexOf("/")+1);b[c]=[];for(var g=0;g2?b("That is not a user address. There is more than one @-sign in it"):/^[\.0-9A-Za-z]+$/.test(c[0])?/^[\.0-9A-Za-z\-]+$/.test(c[1])?b(null,["https://"+c[1]+"/.well-known/host-meta","http://"+c[1]+"/.well-known/host-meta"]):b('That is not a user address. There are non-dotalphanumeric symbols after the @-sign: "'+c[1]+'"'):b('That is not a user address. There are non-dotalphanumeric symbols before the @-sign: "'+c[0]+'"')}function c(b,f,g){var h=b.shift();h?a.ajax({url:h,success:function(a){e(a,function(e,h){e?d(a,function(a,d){a?c(b,f,g):g(null,d)}):g(null,h)})},error:function(a){c(b,f,g)},timeout:f}):g("could not fetch xrd")}function d(b,c){a.parseXml(b,function(a,b){if(a)c(a);else if(b&&b.Link){var d={};if(b.Link&&b.Link["@"])b.Link["@"].rel&&(d[b.Link["@"].rel]=b.Link["@"]);else for(var e=0;e=1&&(e[f]=c.links[f][0]);b(null,e)}function f(a,d,e){b(a,function(b,f){b?e(err):c(f,d.timeout,function(b,f){if(b)e("could not fetch host-meta for "+a);else if(f.lrdd&&f.lrdd.template){var g=f.lrdd.template.split("{uri}"),h=[g.join("acct:"+a),g.join(a)];c(h,d.timeout,function(b,c){if(b)e("could not fetch lrdd for "+a);else if(c.remoteStorage&&c.remoteStorage.auth&&c.remoteStorage.api&&c.remoteStorage.template){var d={};if(c["remoteStorage"]["api"]=="simple")d.type="pds-remotestorage-00#simple";else if(c["remoteStorage"]["api"]=="WebDAV")d.type="pds-remotestorage-00#webdav";else if(c["remoteStorage"]["api"]=="CouchDB")d.type="pds-remotestorage-00#couchdb";else{e("api not recognized");return}var f=c.remoteStorage.template.split("{category}");f[0].substring(f[0].length-1)=="/"?d.href=f[0].substring(0,f[0].length-1):d.href=f[0],f.length==2&&f[1]!="/"&&(d.legacySuffix=f[1]),d.auth={type:"pds-oauth2-00",href:c.remoteStorage.auth},e(null,d)}else c.remotestorage&&c.remotestorage.href&&c.remotestorage.type&&c.remotestorage.links&&c.remotestorage.links.auth&&c.remotestorage.links.auth[0]&&c.remotestorage.links.auth[0].href&&c.remotestorage.links.auth[0].type&&c["remotestorage"]["links"]["auth"][0]["type"]=="oauth2-ig"?(c.remotestorage.auth=c.remotestorage.links.auth[0],delete c.remotestorage.links,e(null,c.remotestorage)):e("could not extract storageInfo from lrdd")})}else e("could not extract lrdd template from host-meta")})})}return{getStorageInfo:f}}),c("lib/hardcoded",["./platform"],function(a){function c(b,c,d){a.ajax({url:"http://proxy.unhosted.org/irisCouchCheck?q=acct:"+b,success:function(a){var b;try{b=JSON.parse(a)}catch(c){}b?d(null,b):d("err: unparsable response from IrisCouch check")},error:function(a){d("err: during IrisCouch test:"+a)},timeout:c.timeout})}function d(a){var b=a.split("@");return["libredocs","mail","browserid","me"].indexOf(b[0])==-1?b[0]+"@iriscouch.com":b[2].substring(0,b[2].indexOf("."))+"@iriscouch.com"}function e(a,d,e){var f=a.split("@");if(f.length<2)e("That is not a user address. There is no @-sign in it");else if(f.length>2)e("That is not a user address. There is more than one @-sign in it");else if(!/^[\.0-9A-Za-z]+$/.test(f[0]))e('That is not a user address. There are non-dotalphanumeric symbols before the @-sign: "'+f[0]+'"');else if(!/^[\.0-9A-Za-z\-]+$/.test(f[1]))e('That is not a user address. There are non-dotalphanumeric symbols after the @-sign: "'+f[1]+'"');else{while(f[1].indexOf(".")!=-1){if(b[f[1]]){blueprint=b[f[1]],e(null,{type:"pds-remotestorage-00#"+blueprint.api,auth:{type:"pds-oauth2-00",href:blueprint.authPrefix+a},href:blueprint.templatePrefix+a});return}f[1]=f[1].substring(f[1].indexOf(".")+1)}new Date0){a=location.hash.split("&");for(var c=0;c -1 && endIndex > -1 && endIndex > startIndex) { 40 | // Take string till comment start 41 | selText = selText.substr(0, startIndex) 42 | // From comment start till comment end 43 | + selText.substring(startIndex + curMode.commentStart.length, endIndex) 44 | // From comment end till string end 45 | + selText.substr(endIndex + curMode.commentEnd.length); 46 | } 47 | this.replaceRange(selText, from, to); 48 | } 49 | }); 50 | 51 | // Applies automatic mode-aware indentation to the specified range 52 | CodeMirror.defineExtension("autoIndentRange", function (from, to) { 53 | var cmInstance = this; 54 | this.operation(function () { 55 | for (var i = from.line; i <= to.line; i++) { 56 | cmInstance.indentLine(i, "smart"); 57 | } 58 | }); 59 | }); 60 | 61 | // Applies automatic formatting to the specified range 62 | CodeMirror.defineExtension("autoFormatRange", function (from, to) { 63 | var absStart = this.indexFromPos(from); 64 | var absEnd = this.indexFromPos(to); 65 | // Insert additional line breaks where necessary according to the 66 | // mode's syntax 67 | var res = this.getModeExt().autoFormatLineBreaks(this.getValue(), absStart, absEnd); 68 | var cmInstance = this; 69 | 70 | // Replace and auto-indent the range 71 | this.operation(function () { 72 | cmInstance.replaceRange(res, from, to); 73 | var startLine = cmInstance.posFromIndex(absStart).line; 74 | var endLine = cmInstance.posFromIndex(absStart + res.length).line; 75 | for (var i = startLine; i <= endLine; i++) { 76 | cmInstance.indentLine(i, "smart"); 77 | } 78 | }); 79 | }); 80 | 81 | // Define extensions for a few modes 82 | 83 | CodeMirror.modeExtensions["css"] = { 84 | commentStart: "/*", 85 | commentEnd: "*/", 86 | wordWrapChars: [";", "\\{", "\\}"], 87 | autoFormatLineBreaks: function (text) { 88 | return text.replace(new RegExp("(;|\\{|\\})([^\r\n])", "g"), "$1\n$2"); 89 | } 90 | }; 91 | 92 | CodeMirror.modeExtensions["javascript"] = { 93 | commentStart: "/*", 94 | commentEnd: "*/", 95 | wordWrapChars: [";", "\\{", "\\}"], 96 | 97 | getNonBreakableBlocks: function (text) { 98 | var nonBreakableRegexes = [ 99 | new RegExp("for\\s*?\\(([\\s\\S]*?)\\)"), 100 | new RegExp("'([\\s\\S]*?)('|$)"), 101 | new RegExp("\"([\\s\\S]*?)(\"|$)"), 102 | new RegExp("//.*([\r\n]|$)") 103 | ]; 104 | var nonBreakableBlocks = new Array(); 105 | for (var i = 0; i < nonBreakableRegexes.length; i++) { 106 | var curPos = 0; 107 | while (curPos < text.length) { 108 | var m = text.substr(curPos).match(nonBreakableRegexes[i]); 109 | if (m != null) { 110 | nonBreakableBlocks.push({ 111 | start: curPos + m.index, 112 | end: curPos + m.index + m[0].length 113 | }); 114 | curPos += m.index + Math.max(1, m[0].length); 115 | } 116 | else { // No more matches 117 | break; 118 | } 119 | } 120 | } 121 | nonBreakableBlocks.sort(function (a, b) { 122 | return a.start - b.start; 123 | }); 124 | 125 | return nonBreakableBlocks; 126 | }, 127 | 128 | autoFormatLineBreaks: function (text) { 129 | var curPos = 0; 130 | var reLinesSplitter = new RegExp("(;|\\{|\\})([^\r\n])", "g"); 131 | var nonBreakableBlocks = this.getNonBreakableBlocks(text); 132 | if (nonBreakableBlocks != null) { 133 | var res = ""; 134 | for (var i = 0; i < nonBreakableBlocks.length; i++) { 135 | if (nonBreakableBlocks[i].start > curPos) { // Break lines till the block 136 | res += text.substring(curPos, nonBreakableBlocks[i].start).replace(reLinesSplitter, "$1\n$2"); 137 | curPos = nonBreakableBlocks[i].start; 138 | } 139 | if (nonBreakableBlocks[i].start <= curPos 140 | && nonBreakableBlocks[i].end >= curPos) { // Skip non-breakable block 141 | res += text.substring(curPos, nonBreakableBlocks[i].end); 142 | curPos = nonBreakableBlocks[i].end; 143 | } 144 | } 145 | if (curPos < text.length - 1) { 146 | res += text.substr(curPos).replace(reLinesSplitter, "$1\n$2"); 147 | } 148 | return res; 149 | } 150 | else { 151 | return text.replace(reLinesSplitter, "$1\n$2"); 152 | } 153 | } 154 | }; 155 | 156 | CodeMirror.modeExtensions["xml"] = { 157 | commentStart: "", 159 | wordWrapChars: [">"], 160 | 161 | autoFormatLineBreaks: function (text) { 162 | var lines = text.split("\n"); 163 | var reProcessedPortion = new RegExp("(^\\s*?<|^[^<]*?)(.+)(>\\s*?$|[^>]*?$)"); 164 | var reOpenBrackets = new RegExp("<", "g"); 165 | var reCloseBrackets = new RegExp("(>)([^\r\n])", "g"); 166 | for (var i = 0; i < lines.length; i++) { 167 | var mToProcess = lines[i].match(reProcessedPortion); 168 | if (mToProcess != null && mToProcess.length > 3) { // The line starts with whitespaces and ends with whitespaces 169 | lines[i] = mToProcess[1] 170 | + mToProcess[2].replace(reOpenBrackets, "\n$&").replace(reCloseBrackets, "$1\n$2") 171 | + mToProcess[3]; 172 | continue; 173 | } 174 | } 175 | 176 | return lines.join("\n"); 177 | } 178 | }; 179 | 180 | CodeMirror.modeExtensions["htmlmixed"] = { 181 | commentStart: "", 183 | wordWrapChars: [">", ";", "\\{", "\\}"], 184 | 185 | getModeInfos: function (text, absPos) { 186 | var modeInfos = new Array(); 187 | modeInfos[0] = 188 | { 189 | pos: 0, 190 | modeExt: CodeMirror.modeExtensions["xml"], 191 | modeName: "xml" 192 | }; 193 | 194 | var modeMatchers = new Array(); 195 | modeMatchers[0] = 196 | { 197 | regex: new RegExp("]*>([\\s\\S]*?)(]*>|$)", "i"), 198 | modeExt: CodeMirror.modeExtensions["css"], 199 | modeName: "css" 200 | }; 201 | modeMatchers[1] = 202 | { 203 | regex: new RegExp("]*>([\\s\\S]*?)(]*>|$)", "i"), 204 | modeExt: CodeMirror.modeExtensions["javascript"], 205 | modeName: "javascript" 206 | }; 207 | 208 | var lastCharPos = (typeof (absPos) !== "undefined" ? absPos : text.length - 1); 209 | // Detect modes for the entire text 210 | for (var i = 0; i < modeMatchers.length; i++) { 211 | var curPos = 0; 212 | while (curPos <= lastCharPos) { 213 | var m = text.substr(curPos).match(modeMatchers[i].regex); 214 | if (m != null) { 215 | if (m.length > 1 && m[1].length > 0) { 216 | // Push block begin pos 217 | var blockBegin = curPos + m.index + m[0].indexOf(m[1]); 218 | modeInfos.push( 219 | { 220 | pos: blockBegin, 221 | modeExt: modeMatchers[i].modeExt, 222 | modeName: modeMatchers[i].modeName 223 | }); 224 | // Push block end pos 225 | modeInfos.push( 226 | { 227 | pos: blockBegin + m[1].length, 228 | modeExt: modeInfos[0].modeExt, 229 | modeName: modeInfos[0].modeName 230 | }); 231 | curPos += m.index + m[0].length; 232 | continue; 233 | } 234 | else { 235 | curPos += m.index + Math.max(m[0].length, 1); 236 | } 237 | } 238 | else { // No more matches 239 | break; 240 | } 241 | } 242 | } 243 | // Sort mode infos 244 | modeInfos.sort(function sortModeInfo(a, b) { 245 | return a.pos - b.pos; 246 | }); 247 | 248 | return modeInfos; 249 | }, 250 | 251 | autoFormatLineBreaks: function (text, startPos, endPos) { 252 | var modeInfos = this.getModeInfos(text); 253 | var reBlockStartsWithNewline = new RegExp("^\\s*?\n"); 254 | var reBlockEndsWithNewline = new RegExp("\n\\s*?$"); 255 | var res = ""; 256 | // Use modes info to break lines correspondingly 257 | if (modeInfos.length > 1) { // Deal with multi-mode text 258 | for (var i = 1; i <= modeInfos.length; i++) { 259 | var selStart = modeInfos[i - 1].pos; 260 | var selEnd = (i < modeInfos.length ? modeInfos[i].pos : endPos); 261 | 262 | if (selStart >= endPos) { // The block starts later than the needed fragment 263 | break; 264 | } 265 | if (selStart < startPos) { 266 | if (selEnd <= startPos) { // The block starts earlier than the needed fragment 267 | continue; 268 | } 269 | selStart = startPos; 270 | } 271 | if (selEnd > endPos) { 272 | selEnd = endPos; 273 | } 274 | var textPortion = text.substring(selStart, selEnd); 275 | if (modeInfos[i - 1].modeName != "xml") { // Starting a CSS or JavaScript block 276 | if (!reBlockStartsWithNewline.test(textPortion) 277 | && selStart > 0) { // The block does not start with a line break 278 | textPortion = "\n" + textPortion; 279 | } 280 | if (!reBlockEndsWithNewline.test(textPortion) 281 | && selEnd < text.length - 1) { // The block does not end with a line break 282 | textPortion += "\n"; 283 | } 284 | } 285 | res += modeInfos[i - 1].modeExt.autoFormatLineBreaks(textPortion); 286 | } 287 | } 288 | else { // Single-mode text 289 | res = modeInfos[0].modeExt.autoFormatLineBreaks(text.substring(startPos, endPos)); 290 | } 291 | 292 | return res; 293 | } 294 | }; 295 | -------------------------------------------------------------------------------- /styles/document/node/map.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font: 13px/22px 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | color: #404040; 6 | } 7 | 8 | textarea, 9 | input { font: 13px/20px 'Helvetica Neue', Helvetica, Arial, sans-serif; } 10 | 11 | .header { 12 | position: absolute; 13 | top: 0px; 14 | left: 70px; 15 | color: #fff; 16 | z-index: 500; 17 | } 18 | 19 | .header h1 { 20 | height: 65px; 21 | background: #444; 22 | font-size: 45px; 23 | line-height: 65px; 24 | margin: 0; 25 | padding: 0 5px; 26 | opacity: 0.9; 27 | } 28 | 29 | .header .subtitle { 30 | color: #ddd; 31 | border-top: 1px solid #fff; 32 | background: #444; 33 | padding: 0 5px; 34 | font-size: 18px; 35 | opacity: 0.9; 36 | } 37 | 38 | .spots-container { 39 | border: 1px solid #ccc; 40 | position: relative; 41 | top: -120px; 42 | height: 100px; 43 | margin: 10px; 44 | background: #eee; 45 | opacity: 0.9; 46 | border-radius: 3px; 47 | } 48 | 49 | .spots-container .spot-details { 50 | position: absolute; 51 | top: -140px; 52 | } 53 | 54 | .spots-container .spot-details input.name { 55 | background: #90AF4B; 56 | 57 | width: 280px; 58 | outline: none; 59 | -webkit-appearance: none; 60 | color: #202020; 61 | 62 | font-size: 30px; 63 | font-weight: bold; 64 | color: #fff; 65 | 66 | border: 1px solid transparent; 67 | display: block; 68 | margin-bottom: 5px; 69 | box-shadow: none; 70 | -moz-border-radius: 0; 71 | -webkit-border-radius: 0; 72 | border-radius: 0; 73 | } 74 | 75 | .spots-container .spot-details textarea.descr { 76 | background: #90AF4B; 77 | color: #E7EFAC; 78 | font-size: 18px; 79 | outline: none; 80 | -webkit-appearance: none; 81 | width: 280px; 82 | height: 70px; 83 | resize: none; 84 | border: none; 85 | } 86 | 87 | .spots-container .spots-navigation { 88 | margin-top: 12px; 89 | } 90 | 91 | .spots-container .spots-navigation .spot { 92 | position: absolute; 93 | width: 80px; 94 | height: 80px; 95 | border-radius: 43px; 96 | 97 | border: 3px solid #fff; 98 | margin-left: 20px; 99 | 100 | /*background-image: url('images/map-thumb.png');*/ 101 | 102 | -moz-transition-duration: 0.4s; 103 | -webkit-transition-duration: 0.4s; 104 | transition-duration: 0.4s; 105 | cursor: pointer; 106 | } 107 | 108 | .spots-container .spots-navigation .spot.active { 109 | 110 | border: 3px solid #555; 111 | } 112 | 113 | 114 | .spots-container .spot-details .remove-spot { 115 | background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAACNSURBVBiVjZDBDcMwCEWfPUUHyMFLVMJjZARPlBEyBpG6RA8ZwFvQy3fk9BRO6POAD8nMCrABq7t3pqi1voAdaFmQAa7CDLlqWzKzIRTgC1SxNy1FxNxdgFPgMiB37yki5lUfAajhPXxnHkb+M75o0qn8OvDxMVl/ugR37/JVpRVgz0ADjgENTxN8AO0Hd4JA8B6QADMAAAAASUVORK5CYII=) no-repeat center center; 116 | position: absolute; 117 | top: -10px; 118 | right: 10px; 119 | width: 10px; 120 | height: 10px; 121 | opacity: 0.5; 122 | -moz-transition: opacity 100ms linear; 123 | -o-transition: opacity 100ms linear; 124 | -webkit-transition: opacity 100ms linear; 125 | transition: opacity 100ms linear; 126 | } 127 | 128 | .spots-container .spot-details .remove-spot:hover { 129 | opacity: 1; 130 | } 131 | 132 | .spots-container .spot-details { 133 | padding: 10px; 134 | } 135 | 136 | /* required styles */ 137 | 138 | .leaflet-map-pane, 139 | .leaflet-tile, 140 | .leaflet-marker-icon, 141 | .leaflet-marker-shadow, 142 | .leaflet-tile-pane, 143 | .leaflet-overlay-pane, 144 | .leaflet-shadow-pane, 145 | .leaflet-marker-pane, 146 | .leaflet-popup-pane, 147 | .leaflet-overlay-pane svg, 148 | .leaflet-zoom-box, 149 | .leaflet-image-layer { /* TODO optimize classes */ 150 | position: absolute; 151 | } 152 | .leaflet-container { 153 | overflow: hidden; 154 | } 155 | .leaflet-tile-pane, 156 | .leaflet-container, 157 | .leaflet-corner, 158 | .leaflet-popup { 159 | /* TODO make this configurable */ 160 | -webkit-transform: translate3d(0,0,0); 161 | } 162 | .leaflet-tile, 163 | .leaflet-marker-icon, 164 | .leaflet-marker-shadow { 165 | -moz-user-select: none; 166 | -webkit-user-select: none; 167 | user-select: none; 168 | } 169 | .leaflet-marker-icon, 170 | .leaflet-marker-shadow { 171 | display: block; 172 | } 173 | .leaflet-clickable { 174 | cursor: pointer; 175 | } 176 | .leaflet-dragging { 177 | cursor: move; 178 | } 179 | .leaflet-dragging .leaflet-clickable { 180 | cursor: move; 181 | } 182 | .leaflet-container img { 183 | max-width: none !important; 184 | } 185 | .leaflet-div-icon { 186 | background: #fff; 187 | border: 1px solid #666; 188 | } 189 | .leaflet-editing-icon { 190 | border-radius: 2px; 191 | } 192 | .leaflet-tile-pane { z-index: 2; } 193 | .leaflet-objects-pane { z-index: 3; } 194 | .leaflet-overlay-pane { z-index: 4; } 195 | .leaflet-shadow-pane { z-index: 5; } 196 | .leaflet-marker-pane { z-index: 6; } 197 | .leaflet-popup-pane { z-index: 7; } 198 | 199 | .leaflet-zoom-box { 200 | width: 0; 201 | height: 0; 202 | } 203 | .leaflet-tile { 204 | visibility: hidden; 205 | } 206 | .leaflet-tile-loaded { 207 | visibility: inherit; 208 | } 209 | a.leaflet-active { 210 | outline: 2px solid orange; 211 | } 212 | 213 | /* Leaflet controls */ 214 | .leaflet-control { 215 | position: relative; 216 | z-index: 7; 217 | } 218 | .leaflet-top, 219 | .leaflet-bottom { 220 | position: absolute; 221 | } 222 | .leaflet-top { 223 | top: 0; 224 | } 225 | .leaflet-right { 226 | right: 0; 227 | } 228 | .leaflet-bottom { 229 | bottom: 0; 230 | } 231 | .leaflet-left { 232 | left: 0; 233 | } 234 | .leaflet-control { 235 | float: left; 236 | clear: both; 237 | } 238 | .leaflet-right .leaflet-control { 239 | float: right; 240 | } 241 | .leaflet-top .leaflet-control { 242 | margin-top: 10px; 243 | } 244 | .leaflet-bottom .leaflet-control { 245 | margin-bottom: 10px; 246 | } 247 | .leaflet-left .leaflet-control { 248 | margin-left: 10px; 249 | } 250 | .leaflet-right .leaflet-control { 251 | margin-right: 10px; 252 | } 253 | 254 | .leaflet-control-zoom { 255 | -moz-border-radius: 7px; 256 | -webkit-border-radius: 7px; 257 | border-radius: 7px; 258 | } 259 | .leaflet-control-zoom { 260 | padding: 5px; 261 | background: rgba(0, 0, 0, 0.25); 262 | } 263 | .leaflet-control-zoom a { 264 | background-color: rgba(255, 255, 255, 0.75); 265 | } 266 | .leaflet-control-zoom a, .leaflet-control-layers a { 267 | background-position: 50% 50%; 268 | background-repeat: no-repeat; 269 | display: block; 270 | } 271 | .leaflet-control-zoom a { 272 | -moz-border-radius: 4px; 273 | -webkit-border-radius: 4px; 274 | border-radius: 4px; 275 | width: 19px; 276 | height: 19px; 277 | } 278 | .leaflet-control-zoom a:hover { 279 | background-color: #fff; 280 | } 281 | .leaflet-touch .leaflet-control-zoom a { 282 | width: 27px; 283 | height: 27px; 284 | } 285 | .leaflet-control-zoom-in { 286 | background-image: url(/images/zoom-in.png); 287 | margin-bottom: 5px; 288 | } 289 | .leaflet-control-zoom-out { 290 | background-image: url(/images/zoom-out.png); 291 | } 292 | 293 | .leaflet-control-layers { 294 | box-shadow: 0 1px 7px #999; 295 | background: #f8f8f9; 296 | -moz-border-radius: 8px; 297 | -webkit-border-radius: 8px; 298 | border-radius: 8px; 299 | } 300 | .leaflet-control-layers a { 301 | background-image: url(/images/layers.png); 302 | width: 36px; 303 | height: 36px; 304 | } 305 | .leaflet-touch .leaflet-control-layers a { 306 | width: 44px; 307 | height: 44px; 308 | } 309 | .leaflet-control-layers .leaflet-control-layers-list, 310 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 311 | display: none; 312 | } 313 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 314 | display: block; 315 | position: relative; 316 | } 317 | .leaflet-control-layers-expanded { 318 | padding: 6px 10px 6px 6px; 319 | font: 12px/1.5 'Helvetica Neue', Helvetica, Arial, sans-serif; 320 | color: #333; 321 | background: #fff; 322 | } 323 | .leaflet-control-layers input { 324 | margin-top: 2px; 325 | position: relative; 326 | top: 1px; 327 | } 328 | .leaflet-control-layers label { 329 | display: block; 330 | } 331 | .leaflet-control-layers-separator { 332 | height: 0; 333 | border-top: 1px solid #ddd; 334 | margin: 5px -10px 5px -6px; 335 | } 336 | 337 | .leaflet-container .leaflet-control-attribution { 338 | background-color: rgba(255, 255, 255, 0.7); 339 | box-shadow: 0 0 5px #bbb; 340 | margin: 0; 341 | } 342 | .leaflet-control-attribution, 343 | .leaflet-control-scale-line { 344 | padding: 0 5px; 345 | color: #333; 346 | } 347 | .leaflet-container .leaflet-control-attribution, 348 | .leaflet-container .leaflet-control-scale { 349 | font: 11px/1.5 'Helvetica Neue', Arial, Helvetica, sans-serif; 350 | } 351 | .leaflet-left .leaflet-control-scale { 352 | margin-left: 5px; 353 | } 354 | .leaflet-bottom .leaflet-control-scale { 355 | margin-bottom: 5px; 356 | } 357 | 358 | .leaflet-control-scale-line { 359 | border: 2px solid #777; 360 | border-top: none; 361 | color: black; 362 | line-height: 1; 363 | font-size: 10px; 364 | padding-bottom: 2px; 365 | text-shadow: 1px 1px 1px #fff; 366 | background-color: rgba(255, 255, 255, 0.5); 367 | } 368 | .leaflet-control-scale-line:nth-child(2) { 369 | border-top: 2px solid #777; 370 | padding-top: 1px; 371 | border-bottom: none; 372 | margin-top: -2px; 373 | } 374 | 375 | .leaflet-touch .leaflet-control-attribution, .leaflet-touch .leaflet-control-layers { 376 | box-shadow: none; 377 | } 378 | .leaflet-touch .leaflet-control-layers { 379 | border: 5px solid #bbb; 380 | } 381 | 382 | 383 | /* Fade animations */ 384 | .leaflet-fade-anim .leaflet-tile, .leaflet-fade-anim .leaflet-popup { 385 | opacity: 0; 386 | } 387 | .leaflet-fade-anim .leaflet-tile-loaded, .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 388 | opacity: 1; 389 | } 390 | .leaflet-zoom-anim .leaflet-tile, .leaflet-pan-anim .leaflet-tile { 391 | -webkit-transition: none; 392 | -moz-transition: none; 393 | -o-transition: none; 394 | transition: none; 395 | } 396 | .leaflet-zoom-anim .leaflet-objects-pane { 397 | visibility: hidden; 398 | } 399 | 400 | 401 | /* Popup layout */ 402 | .leaflet-popup { 403 | position: absolute; 404 | text-align: center; 405 | min-width: 300px; 406 | } 407 | .leaflet-popup-content-wrapper { 408 | padding: 1px; 409 | text-align: left; 410 | } 411 | .leaflet-popup-content { 412 | margin: 12px 10px 5px; 413 | } 414 | .leaflet-popup-tip-container { 415 | margin: 0 auto; 416 | width: 40px; 417 | height: 16px; 418 | position: relative; 419 | top: -1px; 420 | overflow: hidden; 421 | } 422 | .leaflet-popup-tip { 423 | width: 18px; 424 | height: 18px; 425 | margin: -10px auto 0; 426 | -moz-transform: rotate(45deg); 427 | -webkit-transform: rotate(45deg); 428 | -ms-transform: rotate(45deg); 429 | -o-transform: rotate(45deg); 430 | transform: rotate(45deg); 431 | } 432 | .leaflet-popup-content p { 433 | margin: 10px 0; 434 | } 435 | .leaflet-popup-scrolled { 436 | overflow: auto; 437 | border-bottom: 1px solid #ddd; 438 | border-top: 1px solid #ddd; 439 | } 440 | 441 | 442 | /* Visual appearance */ 443 | .leaflet-container { 444 | background: #ddd; 445 | } 446 | .leaflet-container a { 447 | color: #0078A8; 448 | } 449 | .leaflet-zoom-box { 450 | border: 3px solid #05f; 451 | background: white; 452 | opacity: 0.45; 453 | } 454 | .leaflet-popup-content-wrapper, 455 | .leaflet-popup-tip { 456 | background: white; 457 | border: 1px solid #a0a0a0; 458 | -moz-box-shadow: 0 0 8px rgba(0,0,0,0.25); 459 | box-shadow: 0 0 8px rgba(0,0,0,0.25); 460 | } 461 | .leaflet-popup-tip { 462 | z-index: 10000; 463 | } 464 | .leaflet-popup-content-wrapper { 465 | -moz-border-radius: 5px; 466 | -webkit-border-radius: 5px; 467 | border-radius: 5px; 468 | } 469 | .leaflet-popup-content { 470 | font: 13px/22px 'Helvetica Neue', Helvetica, Arial, sans-serif; 471 | } 472 | .leaflet-popup-close-button { 473 | display: none; 474 | background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAACNSURBVBiVjZDBDcMwCEWfPUUHyMFLVMJjZARPlBEyBpG6RA8ZwFvQy3fk9BRO6POAD8nMCrABq7t3pqi1voAdaFmQAa7CDLlqWzKzIRTgC1SxNy1FxNxdgFPgMiB37yki5lUfAajhPXxnHkb+M75o0qn8OvDxMVl/ugR37/JVpRVgz0ADjgENTxN8AO0Hd4JA8B6QADMAAAAASUVORK5CYII=) no-repeat center center; 475 | position: absolute; 476 | top: 10px; 477 | right: 10px; 478 | width: 10px; 479 | height: 10px; 480 | opacity: 0.5; 481 | -moz-transition: opacity 100ms linear; 482 | -o-transition: opacity 100ms linear; 483 | -webkit-transition: opacity 100ms linear; 484 | transition: opacity 100ms linear; 485 | } 486 | .leaflet-popup-close-button:active { 487 | opacity: 1; 488 | } -------------------------------------------------------------------------------- /lib/jquery.transloadit2.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery Easing v1.3: Copyright (c) 2008 George McGinley Smith | BSD License: http://www.opensource.org/licenses/bsd-license.php 3 | jQuery JSONP Core Plugin 2.1.2: Copyright (c) 2010 Julian Aubourg | MIT License: http://www.opensource.org/licenses/mit-license.php 4 | json2: Douglas Crockford | Public domain 5 | jQuery Tools 1.2.3: Tero Piirainen | Public domain 6 | jquery.transloadit2.js: Copyright (c) 2010 Felix Geisendörfer | MIT License: http://www.opensource.org/licenses/mit-license.php 7 | 8 | Fork this on Github: http://github.com/transloadit/jquery-sdk 9 | */ 10 | jQuery.easing.jswing=jQuery.easing.swing; 11 | jQuery.extend(jQuery.easing,{def:"easeOutQuad",swing:function(c,a,f,e,g){return jQuery.easing[jQuery.easing.def](c,a,f,e,g)},easeInQuad:function(c,a,f,e,g){return e*(a/=g)*a+f},easeOutQuad:function(c,a,f,e,g){return-e*(a/=g)*(a-2)+f},easeInOutQuad:function(c,a,f,e,g){if((a/=g/2)<1)return e/2*a*a+f;return-e/2*(--a*(a-2)-1)+f},easeInCubic:function(c,a,f,e,g){return e*(a/=g)*a*a+f},easeOutCubic:function(c,a,f,e,g){return e*((a=a/g-1)*a*a+1)+f},easeInOutCubic:function(c,a,f,e,g){if((a/=g/2)<1)return e/ 12 | 2*a*a*a+f;return e/2*((a-=2)*a*a+2)+f},easeInQuart:function(c,a,f,e,g){return e*(a/=g)*a*a*a+f},easeOutQuart:function(c,a,f,e,g){return-e*((a=a/g-1)*a*a*a-1)+f},easeInOutQuart:function(c,a,f,e,g){if((a/=g/2)<1)return e/2*a*a*a*a+f;return-e/2*((a-=2)*a*a*a-2)+f},easeInQuint:function(c,a,f,e,g){return e*(a/=g)*a*a*a*a+f},easeOutQuint:function(c,a,f,e,g){return e*((a=a/g-1)*a*a*a*a+1)+f},easeInOutQuint:function(c,a,f,e,g){if((a/=g/2)<1)return e/2*a*a*a*a*a+f;return e/2*((a-=2)*a*a*a*a+2)+f},easeInSine:function(c, 13 | a,f,e,g){return-e*Math.cos(a/g*(Math.PI/2))+e+f},easeOutSine:function(c,a,f,e,g){return e*Math.sin(a/g*(Math.PI/2))+f},easeInOutSine:function(c,a,f,e,g){return-e/2*(Math.cos(Math.PI*a/g)-1)+f},easeInExpo:function(c,a,f,e,g){return a==0?f:e*Math.pow(2,10*(a/g-1))+f},easeOutExpo:function(c,a,f,e,g){return a==g?f+e:e*(-Math.pow(2,-10*a/g)+1)+f},easeInOutExpo:function(c,a,f,e,g){if(a==0)return f;if(a==g)return f+e;if((a/=g/2)<1)return e/2*Math.pow(2,10*(a-1))+f;return e/2*(-Math.pow(2,-10*--a)+2)+f}, 14 | easeInCirc:function(c,a,f,e,g){return-e*(Math.sqrt(1-(a/=g)*a)-1)+f},easeOutCirc:function(c,a,f,e,g){return e*Math.sqrt(1-(a=a/g-1)*a)+f},easeInOutCirc:function(c,a,f,e,g){if((a/=g/2)<1)return-e/2*(Math.sqrt(1-a*a)-1)+f;return e/2*(Math.sqrt(1-(a-=2)*a)+1)+f},easeInElastic:function(c,a,f,e,g){c=1.70158;var b=0,d=e;if(a==0)return f;if((a/=g)==1)return f+e;b||(b=g*0.3);if(d0&&a(function(){z(L)},Q);C=function(){H&&clearTimeout(H);o[v]=o[k]=o[s]=o[m]=null;x[u](o);B&&x[u](B)};window[O]=e;o=c(r)[0];o.id=q+U++;if(P)o[j]=P;var R=function(I){(o[k]|| 20 | f)();I=F;F=undefined;I?y(I[0]):z(h)};if(S.msie){o.event=k;o.htmlFor=o.id;o[v]=function(){o.readyState=="loaded"&&R()}}else{o[m]=o[s]=R;S.opera?(B=c(r)[0]).text="jQuery('#"+o.id+"')[0]."+m+"()":o[d]=d}o.src=t;x.insertBefore(o,x.firstChild);B&&x.insertBefore(B,x.firstChild)}},0);return l}var d="async",j="charset",p="",h="error",q="_jqjsp",k="onclick",m="on"+h,s="onload",v="onreadystatechange",u="removeChild",r="