├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
4 | <% childTypes.each(function (position, type) { %>
5 | <% if (_.isArray(position)) { %>
6 | -
7 | <%= getTypeName(type) %>
8 |
20 |
21 | <% } else { %>
22 | -
23 |
27 | <%= getTypeName(type) %>
28 |
29 |
30 | <% } %>
31 | <% }); %>
32 |
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 | 
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 | 
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 | 
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 | 
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 "" if a matching start tag is found. It will correctly ignore signal
7 | * characters for empty tags, comments, CDATA, etc.
8 | *
9 | * The function depends on internal parser state to identify tags. It is compatible with the
10 | * following CodeMirror modes and will ignore all others:
11 | * - htmlmixed
12 | * - xml
13 | * - xmlpure
14 | *
15 | * See demos/closetag.html for a usage example.
16 | *
17 | * @author Nathan Williams
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 '' edge case
100 | if (tagName.length > 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 | // tagName=foo, string=<
129 | if (tok.string == '<') {
130 | completeEndTag(cm, pos, tagName);
131 | return;
132 | }
133 | }
134 | }
135 |
136 | throw CodeMirror.Pass; // Bubble if not handled
137 | });
138 |
139 | function insertEndTag(cm, indent, pos, tagName) {
140 | if (shouldIndent(cm, indent, tagName)) {
141 | cm.replaceSelection('\n\n' + tagName + '>', '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('' + tagName + '>');
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("" + tag + ">", 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 = "" + tag + ">";
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("]*>|$)", "i"),
198 | modeExt: CodeMirror.modeExtensions["css"],
199 | modeName: "css"
200 | };
201 | modeMatchers[1] =
202 | {
203 | regex: new RegExp("]*>|$)", "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="",n="success",L="timeout",S=c.browser,x=c("head")[0]||document.documentElement,
21 | G={},U=0,F,M={callback:q,url:location.href};b.setup=function(l){c.extend(M,l)};c.jsonp=b})(jQuery,setTimeout);if(!this.JSON)this.JSON={};
22 | (function(){function c(h){return h<10?"0"+h:h}function a(h){g.lastIndex=0;return g.test(h)?'"'+h.replace(g,function(q){var k=j[q];return typeof k==="string"?k:"\\u"+("0000"+q.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+h+'"'}function f(h,q){var k,m,s,v,u=b,r,n=q[h];if(n&&typeof n==="object"&&typeof n.toJSON==="function")n=n.toJSON(h);if(typeof p==="function")n=p.call(q,h,n);switch(typeof n){case "string":return a(n);case "number":return isFinite(n)?String(n):"null";case "boolean":case "null":return String(n);
23 | case "object":if(!n)return"null";b+=d;r=[];if(Object.prototype.toString.apply(n)==="[object Array]"){v=n.length;for(k=0;k").attr("id",h.maskId);c("body").append(g)}var k=a();g.css({position:"absolute",top:0,left:0,width:k[0],height:k[1],display:"none",opacity:h.startOpacity,zIndex:h.zIndex});h.color&&g.css("backgroundColor",h.color);if(f(h.onBeforeLoad)===false)return this;h.closeOnEsc&&c(document).bind("keydown.mask",function(m){m.keyCode==
30 | 27&&c.mask.close(m)});h.closeOnClick&&g.bind("click.mask",function(m){c.mask.close(m)});c(window).bind("resize.mask",function(){c.mask.fit()});if(q&&q.length){p=q.eq(0).css("zIndex");c.each(q,function(){var m=c(this);/relative|absolute|fixed/i.test(m.css("position"))||m.css("position","relative")});b=q.css({zIndex:Math.max(h.zIndex+1,p=="auto"?0:p)})}g.css({display:"block"}).fadeTo(h.loadSpeed,h.opacity,function(){c.mask.fit();f(h.onLoad)});d=true;return this},close:function(){if(d){if(f(j.onBeforeClose)===
31 | false)return this;g.fadeOut(j.closeSpeed,function(){f(j.onClose);b&&b.css({zIndex:p})});c(document).unbind("keydown.mask");g.unbind("click.mask");c(window).unbind("resize.mask");d=false}return this},fit:function(){if(d){var h=a();g.css({width:h[0],height:h[1]})}},getMask:function(){return g},isLoaded:function(){return d},getConf:function(){return j},getExposed:function(){return b}};c.fn.mask=function(h){c.mask.load(h);return this};c.fn.expose=function(h){c.mask.load(h,this);return this}})(jQuery);
32 | (function(c){function a(){this.timer=this.documentTitle=this.instance=this.assemblyId=null;this._options={};this.uploads=[];this.results={};this.pollStarted=this.ended=null;this.seq=this.pollRetries=0;this.started=false;this.params=this.assembly=null;this.lastPoll=this.bytesReceivedBefore=0;this.$modal=this.$iframe=this.$fileClones=this.$files=this.$form=this.$params=null}var f=document.location.protocol=="https:"?"https://":"http://",e={service:f+"api2.transloadit.com/",assets:f+"assets.transloadit.com/",
33 | onStart:function(){},onProgress:function(){},onUpload:function(){},onResult:function(){},onCancel:function(){},onError:function(){},onSuccess:function(){},interval:2500,wait:false,autoSubmit:true,modal:true,exclude:"",fields:false,debug:true},g=false;c.fn.transloadit=function(){var b=Array.prototype.slice.call(arguments),d;if(b.length==1&&typeof b[0]=="object"||b[0]===undefined)b.unshift("init");d=b.shift();if(d=="init"){uploader=new a;b.unshift(this);this.data("transloadit.uploader",uploader)}else uploader=
34 | this.data("transloadit.uploader");if(!uploader)throw Error("Element is not initialized for transloadit!");b=uploader[d].apply(uploader,b);return b===undefined?this:b};a.prototype.init=function(b,d){this.$form=b;this.options(c.extend({},e,d||{}));var j=this;b.bind("submit.transloadit",function(){j.getBoredInstance();return false});this.includeCss()};a.prototype.getBoredInstance=function(){var b=this;this.instance=null;c.jsonp({url:this._options.service+"instances/bored",timeout:6E3,callbackParameter:"callback",
35 | success:function(d){if(d.error){b.ended=true;b.renderError(d);b._options.onError(d)}else{b.instance=d.api2_host;b.start()}},error:function(d,j){b.ended=true;var p={error:"CONNECTION_ERROR",message:"There was a problem connecting to the upload server",reason:"JSONP request status: "+j};b.renderError(p);b._options.onError(p)}});this._options.modal&&this.showModal()};a.prototype.start=function(){var b=this;this.ended=this.started=false;this.seq=this.pollRetries=this.bytesReceivedBefore=0;this.uploads=
36 | [];this.results={};this.assemblyId=this.uuid();var d=this.$form.find("input[name=params]");if(d.length){try{this.params=JSON.parse(d.val())}catch(j){alert("Error: input[name=params] seems to contain invalid JSON.");return}if(this.params.redirect_url)this.$form.attr("action",this.params.redirect_url);else if(this._options.autoSubmit&&this.$form.attr("action")==this._options.service+"assemblies"){alert("Error: input[name=params] does not include a redirect_url");return}this.$files=this.$form.find("input[type=file]").not(this._options.exclude);
37 | b.$fileClones=c().not(document);this.$files.each(function(){var h=c(this).clone();b.$fileClones=b.$fileClones.add(h);h.insertAfter(this)});this.$iframe=c('').appendTo("body").hide();this.$uploadForm=c('').attr("action",f+this.instance+"/assemblies/"+this.assemblyId+"?redirect=false").attr("target","transloadit-"+this.assemblyId).attr("method","POST").append(this.$files).appendTo("body").hide();
38 | var p="[name=params], [name=signature]";if(this._options.fields===true)p="*";else if(typeof this._options.fields=="string")p+=", "+this._options.fields;this.$form.find(":input[type!=file]").filter(p).clone().prependTo(this.$uploadForm);this.$params=d;this.$uploadForm.submit();this.lastPoll=+new Date;setTimeout(function(){b._poll()},300)}else alert("Could not find input[name=params] in your form.")};a.prototype._poll=function(b){var d=this;if(!this.ended){if(c.browser.mozilla&&!this.documentTitle){this.documentTitle=
39 | document.title;document.title="Loading..."}this.pollStarted=+new Date;c.jsonp({url:f+this.instance+"/assemblies/"+this.assemblyId+(b||"?seq="+this.seq),timeout:6E3,callbackParameter:"callback",success:function(j){if(!d.ended){d.assembly=j;if(j.error=="ASSEMBLY_NOT_FOUND"){d.pollRetries++;if(d.pollRetries>15){document.title=d.documentTitle;d.ended=true;d.renderError(j);d._options.onError(j)}else setTimeout(function(){d._poll()},400)}else if(j.error){d.ended=true;d.renderError(j);document.title=d.documentTitle;
40 | d._options.onError(j)}else{d.seq=j.last_seq;if(!d.started){d.started=true;d._options.onStart(j)}d.pollRetries=0;var p=j.ok=="ASSEMBLY_EXECUTING",h=j.ok=="ASSEMBLY_CANCELED",q=j.ok=="ASSEMBLY_COMPLETED";d._options.onProgress(j.bytes_received,j.bytes_expected,j);for(var k=0;k3){document.title=
42 | d.documentTitle;d.ended=true;var h={error:"CONNECTION_ERROR",message:"There was a problem connecting to the upload server",reason:"JSONP request status: "+p};d.renderError(h);d._options.onError(h)}else setTimeout(function(){d._poll()},350)}}})}};a.prototype.stop=function(){document.title=this.documentTitle;this.ended=true};a.prototype.cancel=function(){if(!this.ended){var b=this;this.$params.prependTo(this.$form);this.$fileClones.each(function(d){d=c(b.$files[d]);var j=c(this);d.insertAfter(j);j.remove()});
43 | clearTimeout(b.timer);this._poll("?method=delete");this.$iframe[0].contentWindow.stop();setTimeout(function(){b.$iframe.remove()},500)}if(this._options.modal){c.mask.close();this.$modal.remove()}};a.prototype.submitForm=function(){this.$fileClones.remove();c("").attr("name","transloadit").text(JSON.stringify(this.assembly)).prependTo(this.$form).hide();this.$form.unbind("submit.transloadit").submit()};a.prototype.showModal=function(){this.$modal=c('').appendTo("body");
44 | c.extend(this.$modal,{$status:this.$modal.find(".status"),$content:this.$modal.find(".content"),$close:this.$modal.find(".close"),$label:this.$modal.find("label"),$progress:this.$modal.find(".progress"),$progressSpan:this.$modal.find(".progress span"),$error:this.$modal.find(".error")});var b=this;this.$modal.$close.click(function(){b.cancel()});this.$modal.$error.hide();b=this;this.$modal.expose({api:true,maskId:"transloadit_expose",opacity:0.9,loadSpeed:250,closeOnEsc:false,closeOnClick:false}).load();
45 | this.$modal.$close.click(function(){b.cancel();return false})};a.prototype.renderError=function(b){if(this._options.modal){this.$modal.$content.addClass("content-error");this.$modal.$progress.hide();this.$modal.$error.html(this._options.debug?b.error+": "+b.message+"
"+(b.reason||""):"There was an internal error, please try your upload again.").show()}};a.prototype.renderProgress=function(b){if(this._options.modal){var d=b.bytes_received/b.bytes_expected,j=b.bytes_received-this.bytesReceivedBefore,
46 | p=+new Date-this.lastPoll,h=d==1?1E3:this._options.interval*2;j=d==1?"processing ...":(b.bytes_received/1024/1024).toFixed(2)+" MB / "+(b.bytes_expected/1024/1024).toFixed(2)+" MB ("+(j/1024/(p/1E3)).toFixed(1)+" kB / sec)";this.bytesReceivedBefore=b.bytes_received;this.$modal.$label.text(j);this.$modal.$progressSpan.stop().animate({width:d*100+"%"},h,"easeOutCubic")}};a.prototype.includeCss=function(){if(!(g||!this._options.modal)){g=true;c('').appendTo("head")}};a.prototype.uuid=function(){var b="";for(i=0;i<32;i++)b+=Math.floor(Math.random()*16).toString(16);return b};a.prototype.options=function(b){if(arguments.length==0)return this._options;c.extend(this._options,b)};a.prototype.option=function(b,d){if(arguments.length==1)return this._options[b];this._options[b]=d}})(jQuery);
--------------------------------------------------------------------------------