├── src ├── optional.js ├── _includes │ ├── scripts.html │ ├── html-footer.html │ ├── footer.html │ ├── side-nav.html │ ├── side-nav-group.html │ ├── header.html │ ├── html-head.html │ └── meta-data.html ├── examples │ ├── froala │ │ ├── froala.css │ │ ├── README.md │ │ ├── index.html │ │ └── froala.js │ ├── easymde │ │ ├── easymde.css │ │ ├── README.md │ │ ├── default_editor_contents.js │ │ ├── easymde.js │ │ └── index.html │ ├── tui-editor │ │ ├── tui-editor.css │ │ ├── README.md │ │ ├── default_editor_contents.js │ │ ├── tui-editor.js │ │ └── index.html │ ├── pointer │ │ ├── assets │ │ │ └── cursor.png │ │ ├── README.md │ │ ├── pointer.css │ │ ├── index.html │ │ └── pointer.js │ ├── mxgraph │ │ ├── mxgraph.css │ │ ├── mxgraph.js │ │ ├── index.html │ │ └── default-graph.js │ ├── content-editable │ │ ├── content-editable.css │ │ ├── content-editable.js │ │ └── index.html │ ├── codemirror │ │ ├── codemirror.css │ │ ├── README.md │ │ ├── codemirror.js │ │ ├── index.html │ │ ├── default_editor_contents.js │ │ └── codemirror-adapter.js │ ├── input-elements │ │ ├── example.css │ │ ├── README.md │ │ ├── example.js │ │ └── index.html │ ├── monaco │ │ ├── monaco.css │ │ ├── README.md │ │ ├── monaco.js │ │ ├── index.html │ │ ├── default_editor_contents.js │ │ └── monaco-adapter.js │ ├── collaborative-textarea │ │ ├── example.css │ │ ├── README.md │ │ ├── index.html │ │ ├── example.js │ │ └── data.js │ ├── buddy-list │ │ ├── users.js │ │ ├── README.md │ │ ├── buddy-list.css │ │ ├── index.html │ │ └── buddy-list.js │ ├── ace │ │ ├── ace.css │ │ ├── README.md │ │ ├── index.html │ │ ├── default_editor_contents.js │ │ └── ace.js │ ├── chat-room │ │ ├── README.md │ │ ├── chat-room.css │ │ ├── index.html │ │ ├── chat-room.js │ │ └── chat-components.js │ ├── chartjs │ │ ├── index.html │ │ ├── chart.css │ │ ├── README.md │ │ └── chart.js │ └── jointjs │ │ ├── index.html │ │ ├── jointjs.css │ │ ├── jointjs.js │ │ └── default-graph-data.js ├── assets │ ├── img │ │ ├── logo.png │ │ ├── GitHub-Mark-32px.png │ │ └── GitHub-Mark-Light-32px.png │ ├── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon-96x96.png │ ├── fonts │ │ └── CenturyGothic.ttf │ ├── js │ │ ├── exampleId.js │ │ ├── examples.js │ │ └── example-helpers.js │ └── css │ │ ├── examples-common.css │ │ └── style.css ├── config.example.js ├── _data │ ├── categories.yaml │ └── examples.yaml ├── _layouts │ ├── default.html │ └── example.html └── index.html ├── docker ├── confd │ ├── templates │ │ └── config.js │ └── conf.d │ │ └── config.js.toml ├── Dockerfile ├── nginx │ └── default.conf └── bin │ └── boot.sh ├── .gitignore ├── scripts ├── clean.js ├── docker-build.sh ├── docker-run.sh ├── docker-prepare.sh ├── build-materialize.js └── copy-libs.js ├── .travis.yml ├── Gemfile ├── LICENSE ├── _config.yml ├── Gemfile.lock ├── package.json └── README.md /src/optional.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_includes/scripts.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/examples/froala/froala.css: -------------------------------------------------------------------------------- 1 | .fr-box { 2 | border: 1px solid lightgray; 3 | } -------------------------------------------------------------------------------- /src/examples/easymde/easymde.css: -------------------------------------------------------------------------------- 1 | .wrapped-editor { 2 | background: white; 3 | } 4 | -------------------------------------------------------------------------------- /src/examples/tui-editor/tui-editor.css: -------------------------------------------------------------------------------- 1 | #tui-editor { 2 | background: white; 3 | } 4 | -------------------------------------------------------------------------------- /docker/confd/templates/config.js: -------------------------------------------------------------------------------- 1 | const CONVERGENCE_URL = '{{ getenv "CONVERGENCE_URL" }}'; 2 | -------------------------------------------------------------------------------- /src/_includes/html-footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | config.js 4 | docker-build 5 | src/libs 6 | .sass-cache 7 | _site 8 | -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /src/assets/fonts/CenturyGothic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/fonts/CenturyGothic.ttf -------------------------------------------------------------------------------- /src/assets/img/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/img/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /src/examples/pointer/assets/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/examples/pointer/assets/cursor.png -------------------------------------------------------------------------------- /src/assets/img/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/convergencelabs/javascript-examples/HEAD/src/assets/img/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /src/examples/mxgraph/mxgraph.css: -------------------------------------------------------------------------------- 1 | #mxgraph { 2 | height: 400px; 3 | width: 600px; 4 | border: 1px solid darkgray; 5 | background: white; 6 | } 7 | -------------------------------------------------------------------------------- /src/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /docker/confd/conf.d/config.js.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | src = "config.js" 3 | dest = "/usr/share/nginx/html/config.js" 4 | owner = "root" 5 | group = "root" 6 | mode = "0644" 7 | -------------------------------------------------------------------------------- /scripts/clean.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | 3 | fs.removeSync("docker-build"); 4 | fs.removeSync("_site"); 5 | fs.removeSync("src/libs"); 6 | fs.removeSync(".sass-cache"); 7 | -------------------------------------------------------------------------------- /scripts/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | # Prepare the files. 6 | $(dirname "$0")/docker-prepare.sh 7 | 8 | docker build -t convergencelabs/javasript-examples docker-build 9 | -------------------------------------------------------------------------------- /scripts/docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | docker run --rm \ 6 | -e CONVERGENCE_URL="ws:localhost:8000/convergence/default" \ 7 | -p 4000:80 \ 8 | convergencelabs/javasript-examples -------------------------------------------------------------------------------- /src/examples/content-editable/content-editable.css: -------------------------------------------------------------------------------- 1 | .edit { 2 | border: 1px solid darkgrey; 3 | border-radius: 2px; 4 | height: 400px; 5 | width: 100%; 6 | margin-top: 15px; 7 | background: white; 8 | padding: 4px; 9 | } -------------------------------------------------------------------------------- /scripts/docker-prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | # Make sure there are no stale files 6 | rm -rf docker-build 7 | 8 | # Put all relevant files in the docker-build directory 9 | cp -a docker docker-build 10 | cp -a _site docker-build/www 11 | -------------------------------------------------------------------------------- /src/examples/froala/README.md: -------------------------------------------------------------------------------- 1 | # Froala Collaborative Rich Text Editor 2 | 3 | This example demonstrates integration between Convergence and the [Froala WYSIWYG HTML Editor](https://www.froala.com/wysiwyg-editor/). Froala is a popular Rich Text editor that uses HTML as it's data model. -------------------------------------------------------------------------------- /src/examples/tui-editor/README.md: -------------------------------------------------------------------------------- 1 | # Froala Collaborative Rich Text Editor 2 | 3 | This example demonstrates integration between Convergence and the [Froala WYSIWYG HTML Editor](https://www.froala.com/wysiwyg-editor/). Froala is a popular Rich Text editor that uses HTML as it's data model. -------------------------------------------------------------------------------- /src/examples/codemirror/codemirror.css: -------------------------------------------------------------------------------- 1 | .wrapped-editor { 2 | flex: 1; 3 | display: flex; 4 | border: 1px solid black; 5 | height: 550px; 6 | } 7 | 8 | .editor { 9 | display: flex; 10 | flex-direction: column; 11 | flex: 1; 12 | } 13 | 14 | .CodeMirror { 15 | flex: 1; 16 | } -------------------------------------------------------------------------------- /src/examples/input-elements/example.css: -------------------------------------------------------------------------------- 1 | #input-elements { 2 | font-size: 13px; 3 | } 4 | 5 | label { 6 | display: block; 7 | font-weight: bold; 8 | margin: 20px 0 10px 0; 9 | font-family: monospace; 10 | border-bottom: 1px solid darkblue; 11 | background: #EEEEEE; 12 | } 13 | -------------------------------------------------------------------------------- /src/_includes/side-nav.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/examples/input-elements/README.md: -------------------------------------------------------------------------------- 1 | # Input Element Bindings 2 | 3 | This example demonstrated binding to common HTML input elements. The examples makes us of the [Convergence Input Element Bindings](https://www.npmjs.com/package/@convergence/input-element-bindings) helper library, to greatly reduce the amount of effort it takes to get basic collaboration working in simple HTML Forms. 4 | -------------------------------------------------------------------------------- /src/examples/monaco/monaco.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | display: flex; 8 | flex-direction: column; 9 | padding: 5px; 10 | } 11 | 12 | .editor { 13 | display: inline-block; 14 | flex: 1; 15 | } 16 | 17 | .wrapped-editor { 18 | flex: 1; 19 | display: flex; 20 | border: 1px solid black; 21 | height: 550px; 22 | } -------------------------------------------------------------------------------- /src/examples/collaborative-textarea/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, sans-serif; 3 | } 4 | 5 | textarea { 6 | width: 100%; 7 | box-sizing: border-box; 8 | height: 300px; 9 | line-height: 1.2; 10 | resize: none; 11 | margin-top: 5px; 12 | padding: 4px; 13 | } 14 | 15 | textarea:focus { 16 | outline: none; 17 | } 18 | 19 | span { 20 | font-size: 13px; 21 | } 22 | 23 | span.username-label { 24 | font-weight: bold; 25 | } -------------------------------------------------------------------------------- /src/examples/buddy-list/users.js: -------------------------------------------------------------------------------- 1 | // These are the credentials that users of the example can connect with, and will 2 | // also be the users that show upu in the buddy list. These users must exist in 3 | // the Convergence domain. 4 | const USERS = [ 5 | {username: "michael", password: "Password1!" }, 6 | {username: "alec", password: "Password1!" }, 7 | {username: "cameron", password: "Password1!" }, 8 | {username: "john", password: "Password1!" } 9 | ]; 10 | -------------------------------------------------------------------------------- /src/_includes/side-nav-group.html: -------------------------------------------------------------------------------- 1 |
2 |
{{category.title}}
3 | 15 |
-------------------------------------------------------------------------------- /src/config.example.js: -------------------------------------------------------------------------------- 1 | // 2 | // This file configures the domain url that is used to connect to the Convergence service. 3 | // 4 | // By default, it is configured to use the default options in the Convergence Omnibus Container 5 | // (https://hub.docker.com/r/convergencelabs/convergence-omnibus) 6 | // 7 | // These are the relevant parts of the URL: http://:/api/realtime// 8 | const CONVERGENCE_URL = "http://localhost:8000/api/realtime/convergence/default"; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | 6 | env: 7 | - DOCKER_REPO=convergencelabs/javascript-examples 8 | 9 | services: 10 | - docker 11 | 12 | script: 13 | - bundle install 14 | - npm run build 15 | - ./scripts/docker-prepare.sh 16 | - docker build -t $DOCKER_REPO docker-build 17 | 18 | after_success: 19 | - if [ "$TRAVIS_BRANCH" == "master" ]; then 20 | docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"; 21 | docker push $DOCKER_REPO; 22 | fi -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:stable-alpine 2 | 3 | # confd 4 | RUN wget https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 && \ 5 | mv confd-0.16.0-linux-amd64 /usr/local/bin/confd && \ 6 | chmod +x /usr/local/bin/confd 7 | ADD confd /etc/confd/ 8 | 9 | # boot script 10 | ADD bin/boot.sh /boot.sh 11 | RUN chmod +x /boot.sh 12 | 13 | # nginx 14 | COPY nginx/default.conf /etc/nginx/conf.d/default.conf 15 | COPY www /usr/share/nginx/html 16 | 17 | CMD ["/boot.sh"] -------------------------------------------------------------------------------- /src/examples/ace/ace.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | display: flex; 8 | flex-direction: column; 9 | padding: 5px; 10 | } 11 | 12 | .editor { 13 | display: inline-block; 14 | flex: 1; 15 | } 16 | 17 | .wrapped-editor { 18 | flex: 1; 19 | display: flex; 20 | border: 1px solid black; 21 | height: 550px; 22 | } 23 | 24 | #radar-view { 25 | display: inline-block; 26 | min-width: 20px; 27 | background: #2F3129; 28 | } 29 | -------------------------------------------------------------------------------- /src/examples/pointer/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This example demonstrates how the Activity API can be used to implement collaboration awareness of mouse pointers. The example allows users to join the activity which immediately shares their mouse pointer. The mouse pointer is transmitted as a simple (x, y) coordinate. In addition the example shows where and when users click by visualizing the clicks through animations. 4 | 5 | The activity API takes care tracking users as they join and leave and cleaning up the shared state of users as they leave the activity. 6 | -------------------------------------------------------------------------------- /scripts/build-materialize.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const sass = require("sass"); 3 | 4 | // This builds a customized version of materialize that doesn't override the parent 5 | // site's styling. To use, comment out 6 | // 7 | // @import "components/toast" 8 | // @import "components/navbar"; 9 | // 10 | // in the source file (src/libs/...) and run this script. 11 | var result = sass.renderSync({ 12 | file: `src/libs/materialize-css/sass/materialize.scss`, 13 | outputStyle: 'compressed' 14 | }); 15 | 16 | fs.writeFile(`src/assets/css/materialize.min.css`, result.css); -------------------------------------------------------------------------------- /src/examples/chat-room/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This example demonstrates a simple chat room use case. Users can join any chat room they like and send messages to other users in the chat room. 4 | 5 | > Note: This example assumes that anonymous authentication is enabled in your domain. You can turn it on in the "Authentication" section of your domain in the Administration Web Console. 6 | 7 | # Persistence 8 | 9 | At this time, chat messages are transient, so you won't see any message history, just incoming messages when you are actually in the chat "room". We will be adding persistence soon, don't worry ;) -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | root /usr/share/nginx/html; 5 | 6 | location ~ ^.*[^/]$ { 7 | try_files $uri @rewrite; 8 | } 9 | 10 | location @rewrite { 11 | return 302 $scheme://$http_host$uri/; 12 | } 13 | 14 | location ~ connection.js { 15 | alias /usr/share/nginx/html/connection.js; 16 | } 17 | 18 | # redirect server error pages to the static page /50x.html 19 | error_page 500 502 503 504 /50x.html; 20 | location = /50x.html { 21 | root /usr/share/nginx/html; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/examples/easymde/README.md: -------------------------------------------------------------------------------- 1 | # Collaborative Easy Markdown Editor 2 | 3 | This example demonstrates a robust Markdown editor using the [Easy Markdown Editor](https://easymde.tk/). EasyMDE is a simple, embeddable, and rich JS markdown editor that styles Markdown text as you write. EasyMDE uses [CodeMirror](https://codemirror.net) under the hood. As such, we were able to leverage our [CodeMirror Collaborative Extensions](https://github.com/convergencelabs/codemirror-collab-extt) to provide a collaborative markdown editor in a relatively small amount of code. 4 | 5 | Features include: 6 | - Collaborative Markdown Editing 7 | - Shared cursors 8 | - Shared selections 9 | -------------------------------------------------------------------------------- /docker/bin/boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | require_env_var() { 4 | if [ "$1" == "" ]; then 5 | echo "Error: '$2' was not set." 6 | echo "Aborting." 7 | exit 1 8 | else 9 | echo " $2: $1" 10 | fi 11 | } 12 | 13 | echo "Convergence JavaScript Examples" 14 | echo "Checking required environment variables..." 15 | echo "" 16 | 17 | require_env_var "$CONVERGENCE_URL" "CONVERGENCE_URL" 18 | 19 | echo "" 20 | echo "All required environment variables are set. Processing config file." 21 | echo "" 22 | 23 | /usr/local/bin/confd -backend env --onetime 24 | 25 | echo "" 26 | echo "Starting the Convergence JavaScript Examples" 27 | 28 | exec nginx -g "daemon off;" -------------------------------------------------------------------------------- /src/examples/collaborative-textarea/README.md: -------------------------------------------------------------------------------- 1 | # Collaborative TextArea 2 | 3 | This is essentially a live example of the [HTML Text Collaborative Extensions](https://github.com/convergencelabs/html-text-collab-ext). The extensions are themselves backend-independent, but this example uses Convergence to synchronize state. 4 | 5 | Due to the simple data model (a string!), collaboration within a textarea is relatively simple. However, rendering remote selections and cursors on top of a textarea is a bit tricky, which is where the collaborative extensions come in. The text collaborative extensions combined with Convergence gives you a collaborative ` 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/examples/ace/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/ace/ 4 | exampleId: ace 5 | stylesheets: 6 | - /libs/@convergence/ace-collab-ext/css/ace-collab-ext.css 7 | - /examples/ace/ace.css 8 | overview: |- 9 | This example demonstrates building a realtime source code editor with the 10 | Ace Editor. This example uses the 11 | Ace Collaborative Extensions to 12 | add in remote cursors, selections, and a Radar View that shows where other users are editing. 13 | --- 14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/css/examples-common.css: -------------------------------------------------------------------------------- 1 | #loading { 2 | text-align: center; 3 | margin: 50px; 4 | } 5 | 6 | #example { 7 | padding: 10px 20px; 8 | max-width: 800px; 9 | min-width: 500px; 10 | margin: 0; 11 | position: relative; 12 | } 13 | 14 | #example div.example-header { 15 | border: 1px solid darkgrey; 16 | background: white; 17 | margin-bottom: 20px; 18 | border-radius: 2px; 19 | } 20 | 21 | #example div.example-title-bar { 22 | padding: 5px; 23 | display: flex; 24 | flex-direction: row; 25 | align-items: center; 26 | border-bottom: 1px solid darkgrey; 27 | } 28 | 29 | #example div.example-title { 30 | text-align: left; 31 | font-size: 24px; 32 | font-family: "Helvetica Neue", sans-serif; 33 | margin: 0 5px; 34 | } 35 | 36 | #example div.example-icons { 37 | justify-content: space-between; 38 | flex: 1; 39 | text-align: right; 40 | font-size: 0.9rem; 41 | } 42 | 43 | #example div.example-icons a { 44 | text-decoration: none; 45 | margin-left: 10px; 46 | } 47 | 48 | #example div.example-overview { 49 | font-family: "Helvetica Neue", sans-serif; 50 | background: #ffffff; 51 | border-radius: 2px; 52 | padding: 10px; 53 | text-align: justify; 54 | font-size: 15px; 55 | line-height: 1.4; 56 | } 57 | 58 | a { 59 | color: #039be5; 60 | text-decoration: none; 61 | } 62 | 63 | #example div.example-content { 64 | position: relative; 65 | } 66 | 67 | @media screen and (max-width: 600px) { 68 | #example { 69 | min-width: 0; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/examples/mxgraph/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/mxgraph/ 4 | exampleId: mxgraph 5 | stylesheets: 6 | - /examples/mxgraph/mxgraph.css 7 | overview: |- 8 | This example demonstrates shared editing of a graph visualized with the 9 | mxGraph library. The example uses the 10 | Convergence mxGraph Adapter 11 | to simplify the binding between mxGraph and Convergence. The example demonstrates 12 | shared pointers and selection, as well as real time editing. 13 | --- 14 |
15 |
16 |
17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/examples/chartjs/chart.css: -------------------------------------------------------------------------------- 1 | #chartjs { 2 | border: 1px solid darkgrey; 3 | border-radius: 2px; 4 | background: white; 5 | } 6 | 7 | #pieChart { 8 | margin: 0 auto 20px; 9 | } 10 | 11 | div.titles { 12 | font-family: Helvetica, sans-serif; 13 | font-size: 12px; 14 | margin-bottom: 5px; 15 | font-weight: bold; 16 | } 17 | 18 | div.titles div.enabled { 19 | width: 50px; 20 | display: inline-block; 21 | text-align: center; 22 | } 23 | 24 | div.titles div.slider { 25 | margin-top: 5px; 26 | width: 200px; 27 | display: inline-block; 28 | } 29 | 30 | div.titles div.value { 31 | width: 50px; 32 | display: inline-block; 33 | } 34 | 35 | div.controls-container { 36 | text-align: center; 37 | } 38 | 39 | div.controls { 40 | width: 300px; 41 | margin: auto; 42 | } 43 | 44 | div.control { 45 | margin-bottom: 15px; 46 | display: flex; 47 | flex-direction: row; 48 | } 49 | 50 | div.control div.value { 51 | text-align: center; 52 | display: inline-block; 53 | } 54 | 55 | div.control div.value input{ 56 | width: 30px; 57 | margin-left: 10px; 58 | font-size: 14px; 59 | } 60 | 61 | div.control div.slider { 62 | flex: 1; 63 | display: inline-block; 64 | position: relative; 65 | top: 5px; 66 | } 67 | 68 | div.control div.slider.noUi-horizontal .noUi-handle { 69 | top: -3px; 70 | height: 22px; 71 | } 72 | 73 | div.control div.slider .noUi-handle:after, .noUi-handle:before { 74 | top: 4px; 75 | height: 12px; 76 | } 77 | 78 | div.control div.slider div.noUi-connect { 79 | background: #f5f5f5; 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/examples/easymde/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/easymde/ 4 | exampleId: easymde 5 | stylesheets: 6 | - /libs/easymde/dist/easymde.min.css 7 | - /libs/@convergencelabs/codemirror-collab-ext/css/codemirror-collab-ext.css 8 | - /examples/easymde/easymde.css 9 | overview: |- 10 | This example demonstrates editing of markdown with styling applied in line using the 11 | Easy Markdown Editor. The Easy Markdown Editor 12 | uses CodeMirror under the hood, so we used several 13 | of our CodeMirror bindings to implement this integration with very little code. 14 | --- 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/assets/js/example-helpers.js: -------------------------------------------------------------------------------- 1 | const copyUrlToClipboard = () => { 2 | const el = document.createElement('textarea'); 3 | el.value = window.location.href; 4 | el.setAttribute('readonly', ''); 5 | el.style.position = 'absolute'; 6 | el.style.left = '-9999px'; 7 | document.body.appendChild(el); 8 | const selected = 9 | document.getSelection().rangeCount > 0 10 | ? document.getSelection().getRangeAt(0) 11 | : false; 12 | el.select(); 13 | document.execCommand('copy'); 14 | document.body.removeChild(el); 15 | if (selected) { 16 | document.getSelection().removeAllRanges(); 17 | document.getSelection().addRange(selected); 18 | } 19 | 20 | toastr["success"]("URL Copied"); 21 | }; 22 | 23 | toastr.options = { 24 | "closeButton": false, 25 | "debug": false, 26 | "newestOnTop": false, 27 | "progressBar": false, 28 | "positionClass": "toast-top-right", 29 | "preventDuplicates": false, 30 | "showDuration": "300", 31 | "hideDuration": "500", 32 | "timeOut": "2000", 33 | "extendedTimeOut": "1000", 34 | "showEasing": "swing", 35 | "hideEasing": "linear", 36 | "showMethod": "fadeIn", 37 | "hideMethod": "fadeOut" 38 | }; 39 | 40 | function randomDisplayName() { 41 | return "User-" + Math.round(Math.random() * 10000); 42 | } 43 | 44 | function exampleLoaded() { 45 | const loading = document.getElementById("loading"); 46 | if (loading.parentNode) { 47 | loading.parentNode.removeChild(loading); 48 | } 49 | 50 | const content = document.getElementById("example-content"); 51 | content.style.visibility = "visible"; 52 | } 53 | -------------------------------------------------------------------------------- /src/_includes/html-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Convergence Examples{% if page.title %}: {{page.title}}{% endif %} 5 | 6 | 7 | 8 | 9 | {% include meta-data.html%} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for stylesheet in page.stylesheets %} 24 | 25 | {% endfor %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/examples/buddy-list/buddy-list.css: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none; 3 | } 4 | 5 | .navbar { 6 | background: white; 7 | margin-bottom: 10px; 8 | border: 1px solid darkgray; 9 | display: block; 10 | } 11 | 12 | .navbar button { 13 | margin-left: 5px; 14 | } 15 | 16 | .navbar form { 17 | display: block; 18 | } 19 | 20 | .form-row { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex { 26 | flex: 1; 27 | } 28 | 29 | .buddy-list-header { 30 | border: 1px solid darkgray; 31 | padding: 4px; 32 | text-align: center; 33 | background: #e3f2fd; 34 | color: black; 35 | font-size: 14px; 36 | } 37 | 38 | .buddies { 39 | padding: 0; 40 | background: white; 41 | } 42 | 43 | .buddies, .not-connected { 44 | border: 1px solid darkgray; 45 | } 46 | 47 | .not-connected { 48 | text-align: center; 49 | padding: 4px; 50 | } 51 | 52 | .buddies li { 53 | list-style: none; 54 | } 55 | 56 | .buddies li.offline { 57 | color: #6d6c6c; 58 | font-style: italic; 59 | } 60 | 61 | .indicator { 62 | margin-left: 5px; 63 | height: 12px; 64 | width: 12px; 65 | background-color: #bbb; 66 | border-radius: 50%; 67 | display: inline-block; 68 | border: 1px solid darkgrey; 69 | } 70 | 71 | .buddies li.online .indicator.available { 72 | background-color: #4dc465; 73 | } 74 | 75 | .buddies li.online .indicator.away { 76 | background-color: #ffcf46; 77 | } 78 | 79 | .buddies li.online .indicator.dnd { 80 | background-color: #C70F16; 81 | } 82 | 83 | .username { 84 | margin-left: 5px; 85 | } 86 | 87 | .status { 88 | display: inline-block; 89 | margin-left: 10px; 90 | } 91 | 92 | .status label { 93 | display: inline; 94 | } -------------------------------------------------------------------------------- /src/examples/jointjs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/jointjs/ 4 | exampleId: jointjs 5 | stylesheets: 6 | - /libs/jointjs/dist/joint.css 7 | - /examples/jointjs/jointjs.css 8 | - /libs/@convergence/jointjs-utils/dist/css/convergence-jointjs-utils.css 9 | overview: |- 10 | This example demonstrates shared editing of a graph visualized with the 11 | Joint JS library. The example uses the 12 | Convergence Joint JS Utils 13 | to simplify the binding between Joint JS and Convergence. The example demonstrates 14 | shared pointers and selection, as well as real time editing. 15 | --- 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: The Realtime Collaboration Engine 3 | layout: default 4 | permalink: / 5 | --- 6 |
7 |
8 |
9 |

Here are several examples of different uses of Convergence. We have found that most developers can find something here related to their specific use case. Go here to suggest additional examples, or better yet, add your own to this repository!

10 |

Each example has a Share button, which will give you a URL to share with others to use the example together (or you can simply open the example in two windows yourself).

11 |
12 |
13 | {% for example in site.data.examples %} 14 |
15 |
16 |
17 | {{example[1].title}} 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
{{example[1].description}}
26 |
27 |
28 |
29 | {% endfor %} 30 |
31 |
32 |
-------------------------------------------------------------------------------- /src/examples/chartjs/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This example demonstrates collaborative visualization of a pie chart using [Chart.js](http://chartjs.org) and Convergence. The example also integrates the [noUiSlider](https://refreshless.com/nouislider/) component to allow easy manipulation of the chart data. The example primarily makes use of the Model API to syncrhonize data in real time between multiple users editing the chart data. 4 | 5 | ## Data Model 6 | The data model for this example follows the standard Chart.js data model for a pie chart. The data could be modeled in any way. This data schema does not necessarily follow the best practices for Data Modeling in Convergence. For example deleting a pie chart wedge would require deleting the same index in four different arrays. However, for this example, the easiest thing to do was to simply adopt the model used by Chart.js. This way users can view the [Chart.js Documentation](http://www.chartjs.org/docs/#doughnut-pie-chart-introduction) for Pie Charts to understand the data. 7 | 8 | The data model looks something like this: 9 | 10 | ```JavaScript 11 | { 12 | labels: ["Red", "Blue", "Yellow"], 13 | datasets: [{ 14 | data: [300, 50, 100], 15 | backgroundColor: ["#FF6384", "#36A2EB", "#FFCE56"], 16 | hoverBackgroundColor: ["#FF6384", "#36A2EB", "#FFCE56"] 17 | }] 18 | } 19 | ``` 20 | 21 | ## Advanced Editing 22 | 23 | You can edit a segment's label, background color and value in the Model Editor of the [Convergence Admin Console](https://admin.convergence.io). The chart will respond to changes in the Admin Console in real time, but be careful, you can easily corrupt the data if you don't maintain a valid Chart.js pie chart schema. -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | 11 | # Site settings 12 | # These are used to personalize your new site. If you look in the HTML files, 13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 14 | # You can create any custom variable you would like, and they will be accessible 15 | # in the templates via {{ site.myvariable }}. 16 | title: Convergence Examples 17 | email: webmaster@econvergence.io 18 | description: >- # this means to ignore newlines until "baseurl:" 19 | Convergence is a next generation API that allows developers to build rich, real-time collaboration into their applications. 20 | 21 | url: "https://examples.convergence.io/" 22 | host: 0.0.0.0 23 | 24 | # Build settings 25 | source: src 26 | markdown: kramdown 27 | theme: minima 28 | plugins: 29 | - jekyll-sitemap 30 | 31 | # Exclude from processing. 32 | # The following items will not be processed, by default. Create a custom list 33 | # to override the default setting. 34 | exclude: 35 | - README.md 36 | - Dockerfile 37 | - Jenkinsfile 38 | - Gemfile 39 | - Gemfile.lock 40 | - node_modules 41 | - vendor/bundle/ 42 | - vendor/cache/ 43 | - vendor/gems/ 44 | - vendor/ruby/ 45 | 46 | keep_files: 47 | - libs/ -------------------------------------------------------------------------------- /src/examples/codemirror/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/codemirror/ 4 | exampleId: codemirror 5 | stylesheets: 6 | - /libs/codemirror/lib/codemirror.css 7 | - /libs/@convergencelabs/codemirror-collab-ext/css/codemirror-collab-ext.css 8 | - /examples/codemirror/codemirror.css 9 | overview: |- 10 | This example demonstrates building a realtime source code editor with the 11 | CodeMirror Editor. This example uses the 12 | CodeMirror Collaborative Extensions to 13 | display remote cursors and selections, as well as to simplify the management of local and remote edits. 14 | --- 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/examples/pointer/pointer.css: -------------------------------------------------------------------------------- 1 | @keyframes ripple { 2 | 0% { width: 0; height: 0; margin: 0; opacity: 1; } 3 | 100%{ width: 36px; height: 36px; margin: -18px 0 0 -18px; opacity: 0.2; } 4 | } 5 | 6 | #pointer-content { 7 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 8 | font-size: 13px; 9 | display: flex; 10 | } 11 | 12 | .navbar { 13 | background: white; 14 | margin-bottom: 10px; 15 | border: 1px solid darkgray; 16 | border-radius: 2px; 17 | } 18 | 19 | .navbar button { 20 | margin-left: 5px; 21 | } 22 | 23 | #sessions { 24 | width: 300px; 25 | } 26 | 27 | #cursorBox { 28 | height: 300px; 29 | border: 1px solid darkgray; 30 | border-radius: 2px; 31 | position: relative; 32 | overflow: hidden; 33 | background: white; 34 | } 35 | 36 | #titleBar, #rightTitle { 37 | border: 1px solid darkgray; 38 | border-radius: 2px; 39 | padding: 4px; 40 | text-align: center; 41 | background: #e3f2fd; 42 | color: black; 43 | } 44 | 45 | #statusBar { 46 | border: 1px solid darkgray; 47 | padding: 2px; 48 | } 49 | 50 | #sessionId { 51 | margin-right: 20px; 52 | } 53 | 54 | #cursors { 55 | flex: 1; 56 | margin-right: 20px; 57 | background: white; 58 | } 59 | 60 | #mouseLocationsTable { 61 | width: 100%; 62 | border: 1px solid darkgray; 63 | border-radius: 2px; 64 | background: white; 65 | } 66 | 67 | #mouseLocationsTable td, #mouseLocationsTable th { 68 | text-align: center; 69 | border: 1px solid darkgray; 70 | } 71 | 72 | .remoteCursor { 73 | display: inline-block; 74 | position: absolute; 75 | width: 11px; 76 | height: 15px; 77 | } 78 | 79 | .clickSpot { 80 | position: absolute; 81 | animation: ripple 300ms forwards; 82 | border-radius: 18px; 83 | background-color: palegreen; 84 | } 85 | 86 | .label { 87 | font-weight: bold; 88 | } -------------------------------------------------------------------------------- /src/examples/froala/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/froala/ 4 | exampleId: froala 5 | stylesheets: 6 | - /examples/froala/froala.css 7 | - /libs/froala-editor/css/plugins/table.min.css 8 | - /libs/froala-editor/css/froala_editor.pkgd.min.css 9 | - /libs/froala-editor/css/froala_style.min.css 10 | - /libs/froala-editor/css/plugins/colors.min.css 11 | overview: |- 12 | This example demonstrates shared rich text editing the 13 | Froala Editor. The example uses the 14 | Convergence DOM Utils 15 | to create a binding between the Froala contents and a Convergence Realtime Model. 16 | --- 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/examples/tui-editor/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/tui-editor/ 4 | exampleId: tui-editor 5 | stylesheets: 6 | - /examples/tui-editor/tui-editor.css 7 | - /libs/tui-editor/dist/tui-editor.css 8 | - /libs/tui-editor/dist/tui-editor-contents.css 9 | - /libs/codemirror/lib/codemirror.css 10 | - /libs/highlight.js/styles/github.css 11 | - /libs/@convergencelabs/codemirror-collab-ext/css/codemirror-collab-ext.css 12 | overview: |- 13 | This example integrates the ToastUI Editor 14 | to implement a collaborative Markdown editor. The ToastUI Editor used 15 | CodeMirror when in Markdown mode, so we used several 16 | of our CodeMirror bindings to implement this integration with very little code. The WYSIWYG 17 | mode is not yet supported, but we are working on it. 18 | --- 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/examples/jointjs/jointjs.css: -------------------------------------------------------------------------------- 1 | #paper { 2 | margin-top: 15px; 3 | border: 1px solid black; 4 | } 5 | 6 | .joint-type-devs text { 7 | text-transform: uppercase; 8 | font-weight: 800; 9 | font-family: "Source Sans Pro", sans-serif; 10 | } 11 | 12 | .joint-type-devs .body { 13 | fill: #ffffff; 14 | stroke: #31d0c6; 15 | stroke-width: 6px; 16 | } 17 | 18 | .joint-type-devs .label { 19 | fill: #31d0c6; 20 | font-size: 16px; 21 | } 22 | 23 | .joint-type-devs .port-body { 24 | stroke: #ffffff; 25 | stroke-width: 3px; 26 | fill: #7c68fc; 27 | } 28 | 29 | .joint-type-devs .port-body:hover { 30 | opacity: 1; 31 | fill: #ff7e5d; 32 | } 33 | 34 | .joint-type-devs .port-label { 35 | fill: #7c68fc; 36 | text-decoration: none; 37 | } 38 | 39 | .joint-type-devs.joint-type-devs-atomic .body { 40 | stroke: #feb663; 41 | } 42 | 43 | .joint-type-devs.joint-type-devs-atomic .label { 44 | fill: #feb663; 45 | } 46 | 47 | /* links */ 48 | 49 | .joint-link .connection { 50 | stroke: #4B4F6A; 51 | stroke-width: 4px; 52 | } 53 | 54 | .joint-link .marker-arrowheads .marker-arrowhead, 55 | .joint-link .marker-vertex-group .marker-vertex, 56 | .joint-link .marker-vertex-group .marker-vertex:hover { 57 | fill: #31D0C6; 58 | } 59 | 60 | .joint-link .marker-arrowheads .marker-arrowhead:hover { 61 | fill: #F39C12; 62 | } 63 | 64 | .joint-link .link-tools .link-tool .tool-remove circle { 65 | fill: #fe854f; 66 | } 67 | 68 | /* highlighting */ 69 | 70 | .highlighted-parent .body { 71 | stroke: #fe854f; 72 | transition: stroke 1s; 73 | } 74 | 75 | .highlighted-parent .label { 76 | fill: #fe854f; 77 | transition: fill 1s; 78 | text-decoration: underline; 79 | } 80 | 81 | .joint-type-devs .joint-highlight-stroke { 82 | stroke: #333366; 83 | stroke-width: 2px; 84 | stroke-dasharray: 5; 85 | pointer-events: none; 86 | } 87 | -------------------------------------------------------------------------------- /src/examples/pointer/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/pointer/ 4 | exampleId: pointer 5 | stylesheets: 6 | - /examples/pointer/pointer.css 7 | overview: |- 8 | This example demonstrates using an Convergence Activity to share mouse pointer locations 9 | within an element. The pointer location is synchronized across all clients. Mouse clicks 10 | are also shared and rendered as a ripple animation. 11 | --- 12 | 19 | 20 |
21 |
22 |
Move Mouse In Box Below
23 |
24 |
25 | Local User: 26 |
27 | Local Mouse Location: (none) 28 |
29 |
30 |
31 |
Mouse Locations
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
UserMouse LocationFPS
41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/examples/monaco/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/monaco/ 4 | exampleId: monaco 5 | stylesheets: 6 | - /libs/monaco-editor/min/vs/editor/editor.main.css 7 | - /libs/@convergencelabs/monaco-collab-ext/css/monaco-collab-ext.css 8 | - /examples/monaco/monaco.css 9 | overview: |- 10 | This example demonstrates building a realtime source code editor with the 11 | Monaco Editor. This example uses the 12 | Monaco Collaborative Extensions to 13 | display remote cursors and selections, as well as to simplify the management of local and remote edits. 14 | --- 15 |
16 |
17 |
18 | 19 | 20 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.7.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.8) 8 | em-websocket (0.5.2) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0.6.0) 11 | eventmachine (1.2.7) 12 | ffi (1.15.1) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.6.0) 15 | i18n (0.9.5) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (3.9.1) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 0.7) 22 | jekyll-sass-converter (~> 1.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (>= 1.17, < 3) 25 | liquid (~> 4.0) 26 | mercenary (~> 0.3.3) 27 | pathutil (~> 0.9) 28 | rouge (>= 1.7, < 4) 29 | safe_yaml (~> 1.0) 30 | jekyll-feed (0.15.1) 31 | jekyll (>= 3.7, < 5.0) 32 | jekyll-sass-converter (1.5.2) 33 | sass (~> 3.4) 34 | jekyll-seo-tag (2.7.1) 35 | jekyll (>= 3.8, < 5.0) 36 | jekyll-sitemap (1.4.0) 37 | jekyll (>= 3.7, < 5.0) 38 | jekyll-watch (2.2.1) 39 | listen (~> 3.0) 40 | kramdown (2.3.1) 41 | rexml 42 | liquid (4.0.3) 43 | listen (3.5.1) 44 | rb-fsevent (~> 0.10, >= 0.10.3) 45 | rb-inotify (~> 0.9, >= 0.9.10) 46 | mercenary (0.3.6) 47 | minima (2.5.1) 48 | jekyll (>= 3.5, < 5.0) 49 | jekyll-feed (~> 0.9) 50 | jekyll-seo-tag (~> 2.1) 51 | pathutil (0.16.2) 52 | forwardable-extended (~> 2.6) 53 | public_suffix (4.0.6) 54 | rb-fsevent (0.11.0) 55 | rb-inotify (0.10.1) 56 | ffi (~> 1.0) 57 | rexml (3.2.5) 58 | rouge (3.26.0) 59 | safe_yaml (1.0.5) 60 | sass (3.7.4) 61 | sass-listen (~> 4.0.0) 62 | sass-listen (4.0.0) 63 | rb-fsevent (~> 0.9, >= 0.9.4) 64 | rb-inotify (~> 0.9, >= 0.9.7) 65 | webrick (1.7.0) 66 | 67 | PLATFORMS 68 | x86_64-darwin-20 69 | 70 | DEPENDENCIES 71 | jekyll (~> 3.9.0) 72 | jekyll-sitemap (~> 1.4.0) 73 | minima (~> 2.0) 74 | tzinfo-data 75 | webrick 76 | 77 | BUNDLED WITH 78 | 2.2.17 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@convergence/examples", 3 | "version": "0.2.1", 4 | "description": "Examples demonstrating various features of the Convergence platform.", 5 | "keywords": [ 6 | "convergence", 7 | "examples" 8 | ], 9 | "homepage": "http://convergencelabs.com", 10 | "author": { 11 | "name": "Convergence Labs", 12 | "email": "info@convergencelabs.com", 13 | "url": "http://convergencelabs.com" 14 | }, 15 | "contributors": [], 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/convergencelabs/javascript-examples.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/convergencelabs/javascript-examples/issues" 23 | }, 24 | "private": true, 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "@convergence/ace-collab-ext": "0.3.0", 28 | "@convergence/color-assigner": "0.3.0", 29 | "@convergence/convergence": "1.0.0-rc.7", 30 | "@convergence/dom-utils": "0.2.3", 31 | "@convergence/html-text-collab-ext": "0.2.0", 32 | "@convergence/input-element-bindings": "0.4.0", 33 | "@convergence/jointjs-utils": "0.6.0", 34 | "@convergence/mxgraph-adapter": "0.1.0", 35 | "@convergencelabs/codemirror-collab-ext": "0.1.2", 36 | "@convergencelabs/monaco-collab-ext": "0.2.0", 37 | "@fortawesome/fontawesome-free": "5.7.2", 38 | "ace-builds": "1.4.2", 39 | "bootstrap": "4.3.1", 40 | "chart.js": "^3.2.1", 41 | "director": "1.2.8", 42 | "easymde": "2.8.0", 43 | "froala-editor": "2.9.1", 44 | "fs-extra": "7.0.1", 45 | "handlebars": "^4.7.7", 46 | "jquery": "^3.6.0", 47 | "materialize-css": "1.0.0", 48 | "moment": "2.23.0", 49 | "mxgraph": "3.9.12", 50 | "nouislider": "12.1.0", 51 | "popper.js": "1.14.7", 52 | "rxjs": "^6.5.5", 53 | "sass": "1.23.0-module.beta.1", 54 | "toastr": "2.1.4", 55 | "tui-editor": "1.4.8", 56 | "vue": "2.6.10" 57 | }, 58 | "scripts": { 59 | "start": "node ./scripts/copy-libs.js && bundle exec jekyll serve", 60 | "build": "node ./scripts/copy-libs.js && bundle exec jekyll build", 61 | "clean": "node ./scripts/clean.js" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/examples/froala/froala.js: -------------------------------------------------------------------------------- 1 | // Connect to the domain. See ../config.js for the connection settings. 2 | 3 | Convergence.connectAnonymously(CONVERGENCE_URL) 4 | .then(domain => { 5 | const initialContents = "Here is some initial rich text with a bold section and some italics."; 6 | const initialData = ConvergenceDomUtils.DomConverter.htmlToJson(initialContents); 7 | // Now open the model, creating it using the initial data if it does not exist. 8 | return domain.models().openAutoCreate({ 9 | collection: "example-froala", 10 | id: convergenceExampleId, 11 | data: initialData, 12 | ephemeral: true 13 | }) 14 | }) 15 | .then(handleOpen) 16 | .catch(error => { 17 | console.error("Could not open model: " + error); 18 | }); 19 | 20 | function handleOpen(model) { 21 | const textArea = $('#froala'); 22 | textArea.froalaEditor(FROALA_OPTIONS); 23 | 24 | // This is a little trick to get a reference to the editor object. Perhaps there is an easier way? 25 | let froalaEditor; 26 | textArea.on('froalaEditor.html.set', function (e, editor, inputEvent) { 27 | froalaEditor = editor; 28 | }); 29 | textArea.froalaEditor('html.set', ConvergenceDomUtils.DomConverter.jsonToNode(model.root().value()).innerHTML); 30 | 31 | new ConvergenceDomUtils.DomBinder(froalaEditor.el, model.root()); 32 | 33 | exampleLoaded(); 34 | } 35 | 36 | const TOOLBAR_BUTTONS = [ 37 | 'fullscreen', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', '|', 38 | 'fontFamily', 'fontSize', 'color', 'inlineStyle', 'paragraphStyle', '|', 39 | 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', '-', 'insertLink', 'insertImage', 'insertVideo', 'embedly', 'insertFile', 'insertTable', '|', 40 | 'emoticons', 'specialCharacters', 'insertHR', 'selectAll', 'clearFormatting', 41 | '|', 'print', 'spellChecker', 'help', 'html', 42 | '|', 'undo', 'redo' 43 | ]; 44 | 45 | const FROALA_OPTIONS = { 46 | toolbarInline: false, 47 | toolbarButtons: TOOLBAR_BUTTONS, 48 | toolbarButtonsSM: TOOLBAR_BUTTONS, 49 | toolbarButtonsMD: TOOLBAR_BUTTONS, 50 | toolbarButtonsXS: TOOLBAR_BUTTONS, 51 | height: 300 52 | }; 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/examples/input-elements/example.js: -------------------------------------------------------------------------------- 1 | Convergence.connectAnonymously(CONVERGENCE_URL).then(domain => { 2 | return domain.models().openAutoCreate({ 3 | collection: "example-input-binder", 4 | id: convergenceExampleId, 5 | data: defaultData, 6 | ephemeral: true 7 | }); 8 | }).then(model => { 9 | const textInput = document.getElementById("textInput"); 10 | ConvergenceInputElementBinder.bindTextInput(textInput, model.elementAt("textInput")); 11 | 12 | const textArea = document.getElementById("textArea"); 13 | ConvergenceInputElementBinder.bindTextInput(textArea, model.elementAt("textArea")); 14 | 15 | const numberInput = document.getElementById("numberInput"); 16 | ConvergenceInputElementBinder.bindNumberInput(numberInput, model.elementAt("numberInput")); 17 | 18 | const checkboxInput = document.getElementById("checkboxInput"); 19 | ConvergenceInputElementBinder.bindCheckboxInput(checkboxInput, model.elementAt("checkboxInput")); 20 | 21 | const rangeInput = document.getElementById("rangeInput"); 22 | ConvergenceInputElementBinder.bindRangeInput(rangeInput, model.elementAt("rangeInput")); 23 | 24 | const colorInput = document.getElementById("colorInput"); 25 | ConvergenceInputElementBinder.bindColorInput(colorInput, model.elementAt("colorInput")); 26 | 27 | const singleSelect = document.getElementById("singleSelect"); 28 | ConvergenceInputElementBinder.bindSingleSelect(singleSelect, model.elementAt("singleSelect")); 29 | 30 | const multiSelect = document.getElementById("multiSelect"); 31 | ConvergenceInputElementBinder.bindMultiSelect(multiSelect, model.elementAt("multiSelect")); 32 | 33 | const radioInputs = []; 34 | radioInputs.push(document.getElementById("radio1")); 35 | radioInputs.push(document.getElementById("radio2")); 36 | radioInputs.push(document.getElementById("radio3")); 37 | ConvergenceInputElementBinder.bindRadioInputs(radioInputs, model.elementAt("radioInputs")); 38 | 39 | exampleLoaded(); 40 | }).catch(error => { 41 | console.error(error); 42 | }); 43 | 44 | const defaultData = { 45 | textInput: "textInput", 46 | textArea: "textArea", 47 | numberInput: 10, 48 | rangeInput: 5, 49 | checkboxInput: true, 50 | colorInput: "#0000FF", 51 | singleSelect: "one", 52 | multiSelect: ["one", "three"], 53 | radioInputs: "two" 54 | }; -------------------------------------------------------------------------------- /src/examples/input-elements/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/input-elements/ 4 | exampleId: input-elements 5 | stylesheets: 6 | - /examples/input-elements/example.css 7 | overview: |- 8 | All the major HTML form elements with values synced in real-time. 9 | --- 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 43 | 44 | 45 |
46 | One 47 | Two 48 | Three 49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Convergence Logo 2 | 3 | # Convergence Examples 4 | 5 | ![Build Status](https://travis-ci.org/convergencelabs/javascript-examples.svg?branch=master)](https://travis-ci.org/convergencelabs/javascript-examples) 6 | 7 | This repository contains several examples that demonstrate using various features of [Convergence](https://convergence.io). 8 | 9 | ## Dependencies 10 | 11 | * ruby, gem 12 | * jeykll >= 3.9.x 13 | * Convergence >= 1.0.0-rc.7 14 | 15 | ## Development Setup 16 | 17 | 1. Ensure development dependencies are installed for your platform. 18 | 1. `npm install` 19 | 1. `bundle install` 20 | 1. Copy the `src/config.example.js` to `src/config.js`. 21 | 1. Update `src/config.js` as appropriate for your domain. The default assumes that a [convergence server](https://convergence.io/quickstart/) is running on `localhost:8000`. 22 | 1. `npm start` 23 | 1. Open http://localhost:4000 24 | 25 | Alternatively, you can use Docker to build and run a container as shown below. 26 | 27 | 28 | ## Building and Running the Container 29 | To build the examples and associated docker container: 30 | 31 | ```shell script 32 | npm run build 33 | ./scripts/docker-build.sh 34 | ``` 35 | 36 | The container can then be run using: 37 | 38 | ```shell script 39 | ./scripts/docker-run.sh 40 | ``` 41 | 42 | If you need to modify how the container is run, simply copy the commands in the `docker-run.sh` file and modify as required. 43 | 44 | ## Convergence Server 45 | An easy way to get the convergence server up and running for the examples is launch the Convergence Omnibus Contianer: 46 | 47 | ``` 48 | docker run --name convergence -p "8000:80" convergencelabs/convergence-omnibus 49 | ``` 50 | 51 | ## Branches 52 | The `master` branch contains examples that work with the latest released version of Convergence. The `develop` branch contains new / updated examples that work with the current version of Convergence that is in development and may or may not worth with the latest release version. If you are not developing Convergence you should probably stick with `master`. 53 | 54 | ## Support 55 | [Convergence Labs](https://convergencelabs.com) provides several different channels for support: 56 | 57 | - Please use the [Convergence Community Forum](https://forum.convergence.io) for general and technical questions, so the whole community can benefit. 58 | - For paid dedicated support or custom development services, [contact us](https://convergence.io/contact-sales/) directly. 59 | - Chat with us on the [Convergence Public Slack](https://slack.convergence.io). 60 | - Email for all other inquiries. 61 | -------------------------------------------------------------------------------- /src/_data/examples.yaml: -------------------------------------------------------------------------------- 1 | # In each example's index.html, the front-matter has an index property which 2 | # refers to the example's index in this array. 3 | # So if you move these around, make sure to update the indices there appropriately. 4 | 5 | collaborative-textarea: 6 | title: Text Area 7 | description: A full fledged collaborative text area, with shared cursors and selections. 8 | 9 | chartjs: 10 | title: Chart.js 11 | description: |- 12 | Demonstrates the Model API by dynamically changing data visualized using Chart.js. 13 | The result is a highly dynamic visual experience. 14 | 15 | froala: 16 | title: Froala 17 | description: Demonstrates rich text editing in the Froala editor. 18 | 19 | content-editable: 20 | title: Content Editable 21 | description: Leverages dom synchronization to collaborate within a content editable element. 22 | 23 | input-elements: 24 | title: HTML Input Elements 25 | description: Show real time synchronization of HTML form input controls. 26 | 27 | jointjs: 28 | title: JointJS 29 | description: |- 30 | Demonstrates integration between JointJS and Convergence for collaborative 31 | diagramming. 32 | 33 | mxgraph: 34 | title: mxGraph 35 | description: |- 36 | Demonstrates integration between mxGraph and Convergence for 37 | collaborative diagramming. 38 | 39 | ace: 40 | title: Ace Editor 41 | description: Demonstrates collaborative code editing in the Ace Editor. 42 | 43 | monaco: 44 | title: Monaco Editor 45 | description: |- 46 | Demonstrates collaborative code editing in the Monaco 47 | Editor. 48 | 49 | codemirror: 50 | title: CodeMirror 51 | description: Demonstrates collaborative code editing in the CodeMirror Editor. 52 | 53 | pointer: 54 | title: Shared Pointer 55 | description: |- 56 | Shows how the Activity API can be used to share remote pointers. User can see where eah others pointers are and 57 | where they are clicking. 58 | 59 | chat-room: 60 | title: Chat Room 61 | description: |- 62 | A simple chat room example. Users can join any chat room by name and send messages to 63 | other users in the chat room. 64 | 65 | buddy-list: 66 | title: Buddy List 67 | description: |- 68 | A simple buddy list example written with VueJS. 69 | Users can see the presense state of other users in the system. 70 | 71 | easymde: 72 | title: Easy Markdown Editor 73 | description: |- 74 | A collaborative Markdown editor using the 75 | Easy Markdown Editr. 76 | 77 | tui-editor: 78 | title: ToastUI Editor 79 | description: |- 80 | A collaborative Markdown editor using the ToastUI Editor. -------------------------------------------------------------------------------- /src/examples/chat-room/chat-room.js: -------------------------------------------------------------------------------- 1 | 2 | Vue.component('chat-example', { 3 | data: function() { 4 | return { 5 | domain: null, 6 | room: null, 7 | messages: [] 8 | }; 9 | }, 10 | template: '' + 11 | '
' + 12 | ' ' + 13 | '
' + 14 | ' ' + 15 | ' ' + 16 | '
' + 17 | '
', 18 | methods: { 19 | handleConnect: function(username) { 20 | // Connect to the domain. See ../../config.js for the connection settings. 21 | Convergence.connectAnonymously(CONVERGENCE_URL, username) 22 | .then(d => { 23 | this.domain = d; 24 | // Blindly try to create the chat room, ignoring the error if it already exists. 25 | return this.domain.chat().create({ 26 | id: convergenceExampleId, 27 | type: "room", 28 | membership: "public", 29 | ignoreExistsError: true 30 | }); 31 | }) 32 | .then(channelId => this.domain.chat().join(channelId)) 33 | .then(this.handleJoin) 34 | .catch(error => { 35 | console.log("Could not join chat room: " + error); 36 | }); 37 | }, 38 | handleJoin: function(room) { 39 | this.room = room; 40 | 41 | // listen for a new message added to this room 42 | room.on("message", this.appendMessage); 43 | 44 | // fetch the 25 most recent messages 45 | room.getHistory({ 46 | limit: 25, 47 | // only return events of type "message" 48 | eventFilter: ["message"] 49 | }).then(response => { 50 | response.data.forEach(event => { 51 | this.appendMessage(event); 52 | }); 53 | }); 54 | }, 55 | appendMessage: function(event) { 56 | let messages = this.messages.slice(0); 57 | messages.push({ 58 | username: event.user.displayName, 59 | message: event.message, 60 | timestamp: event.timestamp 61 | }); 62 | // don't mutate the array, replace it 63 | this.messages = messages; 64 | }, 65 | handleMessageSubmission: function(messageText) { 66 | try { 67 | this.room.send(messageText); 68 | } catch (e) { 69 | // handle errors. say, the user isn't currently connected 70 | this.displayError(e); 71 | } 72 | }, 73 | handleLeave() { 74 | this.room.leave().then(() => { 75 | this.room = null; 76 | this.messages = []; 77 | return this.domain.dispose(); 78 | }); 79 | }, 80 | displayError(msg, detail) { 81 | // use the materialize toast 82 | if (detail) { 83 | M.toast({html: '

' + msg + '

' + detail + '

'}); 84 | } else { 85 | M.toast({html: msg}); 86 | } 87 | } 88 | } 89 | }); 90 | 91 | new Vue({ 92 | el: '#example' 93 | }); 94 | 95 | exampleLoaded(); -------------------------------------------------------------------------------- /src/_includes/meta-data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | {% if page.title %} 12 | 13 | {% else %} 14 | 15 | {% endif %} 16 | {% if page.description %} 17 | 18 | {% else %} 19 | 20 | {% endif %} 21 | {% if page.url %} 22 | 23 | {% endif %} 24 | {% if page.image %} 25 | 26 | {% else %} 27 | 28 | {% endif %} 29 | 30 | {% if page.categories %} 31 | {% for category in page.categories limit:1 %} 32 | 33 | {% endfor %} 34 | {% endif %} 35 | {% if page.tags %} 36 | {% for tag in page.tags %} 37 | 38 | {% endfor %} 39 | {% endif %} 40 | {% if page.date %} 41 | 42 | 43 | {% endif %} 44 | 45 | {% if page.title %} 46 | 47 | {% else %} 48 | 49 | {% endif %} 50 | {% if page.description %} 51 | 52 | {% else %} 53 | 54 | {% endif %} 55 | {% if page.image %} 56 | 57 | {% else %} 58 | 59 | {% endif %} 60 | 61 | {% if page.title %} 62 | 63 | {% else %} 64 | 65 | {% endif %} 66 | {% if page.url %} 67 | 68 | {% endif %} 69 | {% if page.description %} 70 | 71 | {% else %} 72 | 73 | {% endif %} 74 | {% if page.image %} 75 | 76 | {% else %} 77 | 78 | {% endif %} 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/examples/buddy-list/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: example 3 | permalink: /examples/buddy-list/ 4 | exampleId: buddy-list 5 | stylesheets: 6 | - /examples/buddy-list/buddy-list.css 7 | overview: |- 8 | This example demonstrates how to use the Presence API to create a simple buddy list. 9 | Users will show as online or offline depending on if there is at last on session connected 10 | for that users. This example is written in Vue.js. 11 | 12 | If you are running your own Convergence instance, this example requires a bit of configuration 13 | to work properly. Click the code link above for details. 14 | --- 15 | 16 | 17 | 18 | 19 | {% raw %} 20 |
21 | 56 |
57 |
Buddy List
58 |
    59 | 64 |
65 |
Connect to Load Buddies
66 |
67 |
68 | {% endraw %} 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/examples/buddy-list/buddy-list.js: -------------------------------------------------------------------------------- 1 | Vue.component('user-presence', { 2 | props: ['presenceSubscription'], 3 | template: ` 4 |
  • 5 | 8 | {{ displayName }} 9 |
  • 10 | `, 11 | computed: { 12 | statusTitle: function () { 13 | if (this.presenceSubscription.available) { 14 | switch (this.presenceSubscription.state.get("status")) { 15 | case "available": 16 | return "Available"; 17 | case "away": 18 | return "Away"; 19 | case "dnd": 20 | return "Do Not Disturb"; 21 | default: 22 | return "Unknown"; 23 | } 24 | } else { 25 | return "Offline"; 26 | } 27 | }, 28 | displayName: function() { 29 | return this.presenceSubscription.user.displayName; 30 | } 31 | }, 32 | created: function () { 33 | this.online = this.presenceSubscription.available; 34 | this.status = this.presenceSubscription.state.get("status"); 35 | 36 | this.presenceSubscription.on("availability_changed", (evt) => { 37 | this.online = evt.available; 38 | }); 39 | 40 | this.presenceSubscription.on("state_set", (evt) => { 41 | this.status = this.presenceSubscription.state.get("status"); 42 | }); 43 | }, 44 | destroyed: function() { 45 | this.presenceSubscription.unsubscribe(); 46 | } 47 | }); 48 | 49 | // TODO buddies is hard coded. Make dynamic 50 | const app = new Vue({ 51 | el: '#presence-app', 52 | data: { 53 | connected: false, 54 | status: null, 55 | username: USERS[0].username, 56 | users: USERS, 57 | buddies: null 58 | } 59 | }); 60 | 61 | let domain = null; 62 | function connect() { 63 | const user = USERS.find(user => user.username === app.username); 64 | Convergence.connectWithPassword(CONVERGENCE_URL, {username: user.username, password: user.password}) 65 | .then(d => { 66 | domain = d; 67 | app.connected = true; 68 | 69 | if (!domain.presence().state().has("status")) { 70 | domain.presence().setState("status", "available"); 71 | } 72 | 73 | app.status = domain.presence().state().get("status"); 74 | }) 75 | .then(() => { 76 | const usernames = USERS.map(user => user.username); 77 | return domain.presence().subscribe(usernames); 78 | }) 79 | .then(subscriptions => { 80 | app.buddies = subscriptions; 81 | domain.presence().on(Convergence.PresenceService.Events.STATE_SET, (evt) => { 82 | if (evt.state.has("status")) { 83 | app.status = evt.state.get("status"); 84 | } 85 | }); 86 | }) 87 | .catch(error => { 88 | console.log("Error connecting", error); 89 | }); 90 | } 91 | 92 | function disconnect() { 93 | app.connected = false; 94 | domain.dispose(); 95 | domain = null; 96 | app.buddies = null; 97 | } 98 | 99 | function changeStatus(target) { 100 | domain.presence().setState("status", target.value); 101 | app.status = target.value; 102 | } 103 | 104 | exampleLoaded(); 105 | -------------------------------------------------------------------------------- /src/examples/chat-room/chat-components.js: -------------------------------------------------------------------------------- 1 | var colorAssigner = new ConvergenceColorAssigner.ColorAssigner(); 2 | 3 | Vue.component('membership-actions', { 4 | props: { 5 | joined: Boolean 6 | }, 7 | data: function() { 8 | return { 9 | username: '' 10 | } 11 | }, 12 | template: '' + 13 | '
    ' + 14 | '
    ' + 15 | '
    ' + 16 | ' ' + 17 | ' ' + 18 | '
    ' + 19 | '
    ' + 20 | ' Join' + 21 | ' Leave' + 22 | '
    ' + 23 | '
    ', 24 | methods: { 25 | connectAndJoin: function() { 26 | this.$emit('connectAndJoin', this.username); 27 | }, 28 | handleLeave: function() { 29 | this.$emit('leave'); 30 | } 31 | } 32 | }); 33 | 34 | Vue.component('chat-messages', { 35 | props: { 36 | messages: Array 37 | }, 38 | template: '' + 39 | '
    ' + 40 | ' ' + 43 | '
    ' + 44 | '
    ' + 45 | '
      ' + 46 | ' ' + 47 | '
    ' + 48 | '
    ' + 49 | '
    ' + 50 | '
    ', 51 | watch: { 52 | messages: function() { 53 | this.$nextTick(() => { 54 | this.$refs.container.scrollTop = this.$refs.container.scrollHeight; 55 | }); 56 | } 57 | } 58 | }); 59 | 60 | Vue.component('chat-message', { 61 | props: { 62 | message: Object 63 | }, 64 | template: '' + 65 | '
  • ' + 66 | ' person' + 67 | ' {{ message.username }}' + 68 | '

    {{ message.message }}

    ' + 69 | ' {{ displayDate }}' + 70 | '
  • ', 71 | computed: { 72 | displayDate: function() { 73 | return moment(this.message.timestamp).format('h:mm a'); 74 | }, 75 | color: function() { 76 | return colorAssigner.getColorAsHex(this.message.username); 77 | } 78 | } 79 | }); 80 | 81 | Vue.component('chat-input', { 82 | props: { 83 | inputAllowed: Boolean 84 | }, 85 | data: function() { 86 | return { 87 | messageText: '' 88 | }; 89 | }, 90 | template: '' + 91 | '
    ' + 92 | ' ' + 93 | '
    ', 94 | methods: { 95 | handleSubmit() { 96 | this.$emit('submit', this.messageText); 97 | this.messageText = ''; 98 | } 99 | } 100 | }); -------------------------------------------------------------------------------- /src/examples/jointjs/jointjs.js: -------------------------------------------------------------------------------- 1 | const graph = new joint.dia.Graph; 2 | const paper = new joint.dia.Paper({ 3 | el: document.getElementById('paper'), 4 | width: 800, 5 | height: 400, 6 | gridSize: 1, 7 | model: graph, 8 | snapLinks: true, 9 | linkPinning: false, 10 | embeddingMode: true, 11 | highlighting: { 12 | 'default': {name: 'stroke', options: {padding: 6}}, 13 | 'embedding': {name: 'addClass', options: {className: 'highlighted-parent'}} 14 | }, 15 | validateEmbedding: function (childView, parentView) { 16 | return parentView.model instanceof joint.shapes.devs.Coupled; 17 | }, 18 | validateConnection: function (sourceView, sourceMagnet, targetView, targetMagnet) { 19 | return sourceMagnet != targetMagnet; 20 | } 21 | }); 22 | 23 | // Setup selection behavior 24 | let selectedCellView = null; 25 | let selectionManager = null; 26 | 27 | const highlightOptions = { 28 | highlighter: { 29 | name: 'stroke', 30 | options: { 31 | padding: 15, 32 | rx: 5, 33 | ry: 5 34 | } 35 | } 36 | }; 37 | 38 | paper.on('cell:pointerdown', function (cellView) { 39 | if (cellView === selectedCellView) { 40 | return; 41 | } 42 | 43 | if (selectedCellView !== null) { 44 | selectedCellView.unhighlight(null, highlightOptions); 45 | selectedCellView = null; 46 | } 47 | 48 | if (cellView.model.isElement()) { 49 | selectedCellView = cellView; 50 | selectedCellView.highlight(null, highlightOptions); 51 | selectionManager.setSelectedCells(selectedCellView.model); 52 | } else { 53 | selectionManager.setSelectedCells([]); 54 | } 55 | }); 56 | 57 | paper.on('blank:pointerdown', function (cellView) { 58 | if (selectedCellView !== null) { 59 | selectedCellView.unhighlight(); 60 | selectedCellView = null; 61 | } 62 | 63 | selectionManager.setSelectedCells([]); 64 | }); 65 | 66 | // Connect to the domain. See ../config.js for the connection settings. 67 | Convergence.connectAnonymously(CONVERGENCE_URL) 68 | .then(domain => { 69 | // Now open the model, creating it using the initial data if it does not exist. 70 | const modelPromise = domain.models().openAutoCreate({ 71 | collection: "example-jointjs", 72 | id: convergenceExampleId, 73 | data: () => { 74 | return ConvergenceJointUtils.DataConverter.graphJsonToModelData(DefaultGraphData); 75 | } 76 | }); 77 | 78 | const activityPromise = domain.activities().join("jointjs-" + convergenceExampleId); 79 | return Promise.all([modelPromise, activityPromise]) 80 | }) 81 | .then(results => { 82 | const model = results[0]; 83 | const activity = results[1]; 84 | 85 | const graphAdapter = new ConvergenceJointUtils.GraphAdapter(graph, model); 86 | graphAdapter.bind(); 87 | 88 | const colorManager = new ConvergenceJointUtils.ActivityColorManager(activity); 89 | const pointerManager = new ConvergenceJointUtils.PointerManager(paper, activity, colorManager, "/libs/@convergence/jointjs-utils/dist/img/cursor.svg"); 90 | selectionManager = new ConvergenceJointUtils.SelectionManager(paper, graphAdapter, colorManager); 91 | 92 | exampleLoaded(); 93 | }) 94 | .catch(function (error) { 95 | console.error("Could not open model", error); 96 | throw error; 97 | }); 98 | 99 | function reset() { 100 | graph.fromJSON(DefaultGraphData); 101 | } -------------------------------------------------------------------------------- /src/examples/collaborative-textarea/example.js: -------------------------------------------------------------------------------- 1 | const colorAssigner = new ConvergenceColorAssigner.ColorAssigner(); 2 | let textEditor; 3 | let localSelectionReference; 4 | 5 | const username = randomDisplayName(); 6 | document.getElementById("username").innerHTML = username; 7 | 8 | Convergence.connectAnonymously(CONVERGENCE_URL, username).then(domain => { 9 | return domain.models().openAutoCreate({ 10 | collection: "example-textarea", 11 | id: convergenceExampleId, 12 | ephemeral: true, 13 | data: defaultData 14 | }); 15 | }).then(model => { 16 | const textarea = document.getElementById("textarea"); 17 | const rts = model.elementAt(["text"]); 18 | 19 | // Set the initial data, and set the cursor to the beginning. 20 | textarea.value = rts.value(); 21 | textarea.selectionStart = 0; 22 | textarea.selectionEnd = 0; 23 | 24 | // Create the editor and set up two way data binding. 25 | textEditor = new HtmlTextCollabExt.CollaborativeTextArea({ 26 | control: textarea, 27 | onInsert: (index, value) => rts.insert(index, value), 28 | onDelete: (index, length) => rts.remove(index, length), 29 | onSelectionChanged: sendLocalSelection 30 | }); 31 | 32 | rts.on(Convergence.StringInsertEvent.NAME, (e) => textEditor.insertText(e.index, e.value)); 33 | rts.on(Convergence.StringRemoveEvent.NAME, (e) => textEditor.deleteText(e.index, e.value.length)); 34 | 35 | // handle reference events 36 | initSharedSelection(rts); 37 | 38 | exampleLoaded(); 39 | 40 | }).catch(error => { 41 | console.error(error); 42 | }); 43 | 44 | function sendLocalSelection() { 45 | const selection = textEditor.selectionManager().getSelection(); 46 | localSelectionReference.set({start: selection.anchor, end: selection.target}); 47 | } 48 | 49 | function initSharedSelection(rts) { 50 | localSelectionReference = rts.rangeReference("selection"); 51 | 52 | const references = rts.references({key: "selection"}); 53 | references.forEach((reference) => { 54 | if (!reference.isLocal()) { 55 | addSelection(reference); 56 | } 57 | }); 58 | 59 | sendLocalSelection(); 60 | localSelectionReference.share(); 61 | 62 | rts.on("reference", (e) => { 63 | if (e.reference.key() === "selection") { 64 | this.addSelection(e.reference); 65 | } 66 | }); 67 | 68 | textarea.addEventListener("blur", () => { 69 | localSelectionReference.clear(); 70 | }) 71 | } 72 | 73 | function addSelection(reference) { 74 | const color = colorAssigner.getColorAsHex(reference.sessionId()); 75 | const remoteRange = reference.value(); 76 | 77 | const selectionManager = textEditor.selectionManager(); 78 | 79 | selectionManager.addCollaborator( 80 | reference.sessionId(), 81 | reference.user().displayName, 82 | color, 83 | {anchor: remoteRange.start, target: remoteRange.end}); 84 | 85 | reference.on("cleared", () => { 86 | const collaborator = selectionManager.getCollaborator(reference.sessionId()); 87 | collaborator.clearSelection(); 88 | }); 89 | reference.on("disposed", () => selectionManager.removeCollaborator(reference.sessionId()) ); 90 | reference.on("set", (e) => { 91 | const selection = reference.value(); 92 | const collaborator = selectionManager.getCollaborator(reference.sessionId()); 93 | collaborator.setSelection({anchor: selection.start, target: selection.end}); 94 | if (!e.synthetic) { 95 | collaborator.flashCursorToolTip(2); 96 | } 97 | }); 98 | } 99 | 100 | const defaultData = { 101 | text: TEXT_DATA, 102 | }; 103 | -------------------------------------------------------------------------------- /src/examples/ace/default_editor_contents.js: -------------------------------------------------------------------------------- 1 | var defaultEditorContents = `var observableProto; 2 | 3 | /** 4 | * Represents a push-style collection. 5 | */ 6 | var Observable = Rx.Observable = (function () { 7 | 8 | function makeSubscribe(self, subscribe) { 9 | return function (o) { 10 | var oldOnError = o.onError; 11 | o.onError = function (e) { 12 | makeStackTraceLong(e, self); 13 | oldOnError.call(o, e); 14 | }; 15 | 16 | return subscribe.call(self, o); 17 | }; 18 | } 19 | 20 | function Observable() { 21 | if (Rx.config.longStackSupport && hasStacks) { 22 | var oldSubscribe = this._subscribe; 23 | var e = tryCatch(thrower)(new Error()).e; 24 | this.stack = e.stack.substring(e.stack.indexOf('\\n') + 1); 25 | this._subscribe = makeSubscribe(this, oldSubscribe); 26 | } 27 | } 28 | 29 | observableProto = Observable.prototype; 30 | 31 | /** 32 | * Determines whether the given object is an Observable 33 | * @param {Any} An object to determine whether it is an Observable 34 | * @returns {Boolean} true if an Observable, else false. 35 | */ 36 | Observable.isObservable = function (o) { 37 | return o && isFunction(o.subscribe); 38 | }; 39 | 40 | /** 41 | * Subscribes an o to the observable sequence. 42 | * @param {Mixed} [oOrOnNext] The object that is to receive notifications or an action to invoke for each element in the observable sequence. 43 | * @param {Function} [onError] Action to invoke upon exceptional termination of the observable sequence. 44 | * @param {Function} [onCompleted] Action to invoke upon graceful termination of the observable sequence. 45 | * @returns {Diposable} A disposable handling the subscriptions and unsubscriptions. 46 | */ 47 | observableProto.subscribe = observableProto.forEach = function (oOrOnNext, onError, onCompleted) { 48 | return this._subscribe(typeof oOrOnNext === 'object' ? 49 | oOrOnNext : 50 | observerCreate(oOrOnNext, onError, onCompleted)); 51 | }; 52 | 53 | /** 54 | * Subscribes to the next value in the sequence with an optional "this" argument. 55 | * @param {Function} onNext The function to invoke on each element in the observable sequence. 56 | * @param {Any} [thisArg] Object to use as this when executing callback. 57 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 58 | */ 59 | observableProto.subscribeOnNext = function (onNext, thisArg) { 60 | return this._subscribe(observerCreate(typeof thisArg !== 'undefined' ? function(x) { onNext.call(thisArg, x); } : onNext)); 61 | }; 62 | 63 | /** 64 | * Subscribes to an exceptional condition in the sequence with an optional "this" argument. 65 | * @param {Function} onError The function to invoke upon exceptional termination of the observable sequence. 66 | * @param {Any} [thisArg] Object to use as this when executing callback. 67 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 68 | */ 69 | observableProto.subscribeOnError = function (onError, thisArg) { 70 | return this._subscribe(observerCreate(null, typeof thisArg !== 'undefined' ? function(e) { onError.call(thisArg, e); } : onError)); 71 | }; 72 | 73 | /** 74 | * Subscribes to the next value in the sequence with an optional "this" argument. 75 | * @param {Function} onCompleted The function to invoke upon graceful termination of the observable sequence. 76 | * @param {Any} [thisArg] Object to use as this when executing callback. 77 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 78 | */ 79 | observableProto.subscribeOnCompleted = function (onCompleted, thisArg) { 80 | return this._subscribe(observerCreate(null, null, typeof thisArg !== 'undefined' ? function() { onCompleted.call(thisArg); } : onCompleted)); 81 | }; 82 | 83 | return Observable; 84 | })();`; -------------------------------------------------------------------------------- /src/examples/monaco/default_editor_contents.js: -------------------------------------------------------------------------------- 1 | var defaultEditorContents = `var observableProto; 2 | 3 | /** 4 | * Represents a push-style collection. 5 | */ 6 | var Observable = Rx.Observable = (function () { 7 | 8 | function makeSubscribe(self, subscribe) { 9 | return function (o) { 10 | var oldOnError = o.onError; 11 | o.onError = function (e) { 12 | makeStackTraceLong(e, self); 13 | oldOnError.call(o, e); 14 | }; 15 | 16 | return subscribe.call(self, o); 17 | }; 18 | } 19 | 20 | function Observable() { 21 | if (Rx.config.longStackSupport && hasStacks) { 22 | var oldSubscribe = this._subscribe; 23 | var e = tryCatch(thrower)(new Error()).e; 24 | this.stack = e.stack.substring(e.stack.indexOf('\\n') + 1); 25 | this._subscribe = makeSubscribe(this, oldSubscribe); 26 | } 27 | } 28 | 29 | observableProto = Observable.prototype; 30 | 31 | /** 32 | * Determines whether the given object is an Observable 33 | * @param {Any} An object to determine whether it is an Observable 34 | * @returns {Boolean} true if an Observable, else false. 35 | */ 36 | Observable.isObservable = function (o) { 37 | return o && isFunction(o.subscribe); 38 | }; 39 | 40 | /** 41 | * Subscribes an o to the observable sequence. 42 | * @param {Mixed} [oOrOnNext] The object that is to receive notifications or an action to invoke for each element in the observable sequence. 43 | * @param {Function} [onError] Action to invoke upon exceptional termination of the observable sequence. 44 | * @param {Function} [onCompleted] Action to invoke upon graceful termination of the observable sequence. 45 | * @returns {Diposable} A disposable handling the subscriptions and unsubscriptions. 46 | */ 47 | observableProto.subscribe = observableProto.forEach = function (oOrOnNext, onError, onCompleted) { 48 | return this._subscribe(typeof oOrOnNext === 'object' ? 49 | oOrOnNext : 50 | observerCreate(oOrOnNext, onError, onCompleted)); 51 | }; 52 | 53 | /** 54 | * Subscribes to the next value in the sequence with an optional "this" argument. 55 | * @param {Function} onNext The function to invoke on each element in the observable sequence. 56 | * @param {Any} [thisArg] Object to use as this when executing callback. 57 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 58 | */ 59 | observableProto.subscribeOnNext = function (onNext, thisArg) { 60 | return this._subscribe(observerCreate(typeof thisArg !== 'undefined' ? function(x) { onNext.call(thisArg, x); } : onNext)); 61 | }; 62 | 63 | /** 64 | * Subscribes to an exceptional condition in the sequence with an optional "this" argument. 65 | * @param {Function} onError The function to invoke upon exceptional termination of the observable sequence. 66 | * @param {Any} [thisArg] Object to use as this when executing callback. 67 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 68 | */ 69 | observableProto.subscribeOnError = function (onError, thisArg) { 70 | return this._subscribe(observerCreate(null, typeof thisArg !== 'undefined' ? function(e) { onError.call(thisArg, e); } : onError)); 71 | }; 72 | 73 | /** 74 | * Subscribes to the next value in the sequence with an optional "this" argument. 75 | * @param {Function} onCompleted The function to invoke upon graceful termination of the observable sequence. 76 | * @param {Any} [thisArg] Object to use as this when executing callback. 77 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 78 | */ 79 | observableProto.subscribeOnCompleted = function (onCompleted, thisArg) { 80 | return this._subscribe(observerCreate(null, null, typeof thisArg !== 'undefined' ? function() { onCompleted.call(thisArg); } : onCompleted)); 81 | }; 82 | 83 | return Observable; 84 | })();`; -------------------------------------------------------------------------------- /src/examples/codemirror/default_editor_contents.js: -------------------------------------------------------------------------------- 1 | const defaultEditorContents = `var observableProto; 2 | 3 | /** 4 | * Represents a push-style collection. 5 | */ 6 | var Observable = Rx.Observable = (function () { 7 | 8 | function makeSubscribe(self, subscribe) { 9 | return function (o) { 10 | var oldOnError = o.onError; 11 | o.onError = function (e) { 12 | makeStackTraceLong(e, self); 13 | oldOnError.call(o, e); 14 | }; 15 | 16 | return subscribe.call(self, o); 17 | }; 18 | } 19 | 20 | function Observable() { 21 | if (Rx.config.longStackSupport && hasStacks) { 22 | var oldSubscribe = this._subscribe; 23 | var e = tryCatch(thrower)(new Error()).e; 24 | this.stack = e.stack.substring(e.stack.indexOf('\\n') + 1); 25 | this._subscribe = makeSubscribe(this, oldSubscribe); 26 | } 27 | } 28 | 29 | observableProto = Observable.prototype; 30 | 31 | /** 32 | * Determines whether the given object is an Observable 33 | * @param {Any} An object to determine whether it is an Observable 34 | * @returns {Boolean} true if an Observable, else false. 35 | */ 36 | Observable.isObservable = function (o) { 37 | return o && isFunction(o.subscribe); 38 | }; 39 | 40 | /** 41 | * Subscribes an o to the observable sequence. 42 | * @param {Mixed} [oOrOnNext] The object that is to receive notifications or an action to invoke for each element in the observable sequence. 43 | * @param {Function} [onError] Action to invoke upon exceptional termination of the observable sequence. 44 | * @param {Function} [onCompleted] Action to invoke upon graceful termination of the observable sequence. 45 | * @returns {Diposable} A disposable handling the subscriptions and unsubscriptions. 46 | */ 47 | observableProto.subscribe = observableProto.forEach = function (oOrOnNext, onError, onCompleted) { 48 | return this._subscribe(typeof oOrOnNext === 'object' ? 49 | oOrOnNext : 50 | observerCreate(oOrOnNext, onError, onCompleted)); 51 | }; 52 | 53 | /** 54 | * Subscribes to the next value in the sequence with an optional "this" argument. 55 | * @param {Function} onNext The function to invoke on each element in the observable sequence. 56 | * @param {Any} [thisArg] Object to use as this when executing callback. 57 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 58 | */ 59 | observableProto.subscribeOnNext = function (onNext, thisArg) { 60 | return this._subscribe(observerCreate(typeof thisArg !== 'undefined' ? function(x) { onNext.call(thisArg, x); } : onNext)); 61 | }; 62 | 63 | /** 64 | * Subscribes to an exceptional condition in the sequence with an optional "this" argument. 65 | * @param {Function} onError The function to invoke upon exceptional termination of the observable sequence. 66 | * @param {Any} [thisArg] Object to use as this when executing callback. 67 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 68 | */ 69 | observableProto.subscribeOnError = function (onError, thisArg) { 70 | return this._subscribe(observerCreate(null, typeof thisArg !== 'undefined' ? function(e) { onError.call(thisArg, e); } : onError)); 71 | }; 72 | 73 | /** 74 | * Subscribes to the next value in the sequence with an optional "this" argument. 75 | * @param {Function} onCompleted The function to invoke upon graceful termination of the observable sequence. 76 | * @param {Any} [thisArg] Object to use as this when executing callback. 77 | * @returns {Disposable} A disposable handling the subscriptions and unsubscriptions. 78 | */ 79 | observableProto.subscribeOnCompleted = function (onCompleted, thisArg) { 80 | return this._subscribe(observerCreate(null, null, typeof thisArg !== 'undefined' ? function() { onCompleted.call(thisArg); } : onCompleted)); 81 | }; 82 | 83 | return Observable; 84 | })();`; -------------------------------------------------------------------------------- /src/examples/codemirror/codemirror-adapter.js: -------------------------------------------------------------------------------- 1 | class CodeMirrorConvergenceAdapter { 2 | constructor(editor, realtimeString) { 3 | this._editor = editor; 4 | this._model = realtimeString; 5 | this._colorAssigner = new ConvergenceColorAssigner.ColorAssigner(); 6 | } 7 | 8 | bind() { 9 | this._initSharedData(); 10 | this._initSharedCursors(); 11 | this._initSharedSelection(); 12 | } 13 | 14 | _initSharedData() { 15 | this._editor.setValue(this._model.value()); 16 | 17 | this._contentManager = new CodeMirrorCollabExt.EditorContentManager({ 18 | editor: this._editor, 19 | onInsert: (index, text) => { 20 | this._model.insert(index, text); 21 | }, 22 | onReplace: (index, length, text) => { 23 | this._model.model().startBatch(); 24 | this._model.remove(index, length); 25 | this._model.insert(index, text); 26 | this._model.model().completeBatch(); 27 | }, 28 | onDelete: (index, length) => { 29 | this._model.remove(index, length); 30 | }, 31 | remoteOrigin: "convergence" 32 | }); 33 | 34 | this._model.events().subscribe(e => { 35 | switch (e.name) { 36 | case "insert": 37 | this._contentManager.insert(e.index, e.value); 38 | break; 39 | case "remove": 40 | this._contentManager.delete(e.index, e.value.length); 41 | break; 42 | default: 43 | } 44 | }); 45 | } 46 | 47 | _initSharedCursors() { 48 | this._remoteCursorManager = new CodeMirrorCollabExt.RemoteCursorManager({ 49 | editor: this._editor, 50 | tooltips: true, 51 | tooltipDuration: 2 52 | }); 53 | this._cursorReference = this._model.indexReference("cursor"); 54 | 55 | const references = this._model.references({key: "cursor"}); 56 | references.forEach((reference) => { 57 | if (!reference.isLocal()) { 58 | this._addRemoteCursor(reference); 59 | } 60 | }); 61 | 62 | this._setLocalCursor(); 63 | this._cursorReference.share(); 64 | 65 | this._editor.on("cursorActivity", (e) => { 66 | this._setLocalCursor(); 67 | }); 68 | 69 | this._model.on("reference", (e) => { 70 | if (e.reference.key() === "cursor") { 71 | this._addRemoteCursor(e.reference); 72 | } 73 | }); 74 | } 75 | 76 | _setLocalCursor() { 77 | const position = this._editor.getCursor(); 78 | const index = this._editor.indexFromPos(position); 79 | this._cursorReference.set(index); 80 | } 81 | 82 | _addRemoteCursor(reference) { 83 | const color = this._colorAssigner.getColorAsHex(reference.sessionId()); 84 | const remoteCursor = this._remoteCursorManager.addCursor(reference.sessionId(), color, reference.user().displayName); 85 | 86 | reference.on("cleared", () => remoteCursor.hide()); 87 | reference.on("disposed", () => remoteCursor.dispose()); 88 | reference.on("set", () => { 89 | const cursorIndex = reference.value(); 90 | remoteCursor.setIndex(cursorIndex); 91 | }); 92 | } 93 | 94 | 95 | _initSharedSelection() { 96 | this._remoteSelectionManager = new CodeMirrorCollabExt.RemoteSelectionManager({editor: this._editor}); 97 | 98 | this._selectionReference = this._model.rangeReference("selection"); 99 | this._setLocalSelection(); 100 | this._selectionReference.share(); 101 | 102 | this._editor.on("cursorActivity", (e) => { 103 | this._setLocalSelection(); 104 | }); 105 | 106 | const references = this._model.references({key: "selection"}); 107 | references.forEach((reference) => { 108 | if (!reference.isLocal()) { 109 | this._addRemoteSelection(reference); 110 | } 111 | }); 112 | 113 | this._model.on("reference", (e) => { 114 | if (e.reference.key() === "selection") { 115 | this._addRemoteSelection(e.reference); 116 | } 117 | }); 118 | } 119 | 120 | _setLocalSelection() { 121 | const fromPosition = this._editor.getCursor("from"); 122 | const fromIndex = this._editor.indexFromPos(fromPosition); 123 | const toPosition = this._editor.getCursor("to"); 124 | const toIndex = this._editor.indexFromPos(toPosition); 125 | 126 | if (fromIndex !== toIndex) { 127 | this._selectionReference.set({start: fromIndex, end: toIndex}); 128 | } else if (this._selectionReference.isSet()) { 129 | this._selectionReference.clear(); 130 | } 131 | } 132 | 133 | _addRemoteSelection(reference) { 134 | const color = this._colorAssigner.getColorAsHex(reference.sessionId()) 135 | const remoteSelection = this._remoteSelectionManager.addSelection(reference.sessionId(), color); 136 | 137 | if (reference.isSet()) { 138 | const selection = reference.value(); 139 | remoteSelection.setIndices(selection.start, selection.end); 140 | } 141 | 142 | reference.on("cleared", () => remoteSelection.hide()); 143 | reference.on("disposed", () => remoteSelection.dispose()); 144 | reference.on("set", () => { 145 | const selection = reference.value(); 146 | remoteSelection.setIndices(selection.start, selection.end); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/examples/collaborative-textarea/data.js: -------------------------------------------------------------------------------- 1 | const TEXT_DATA = `It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way--in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only. 2 | 3 | There were a king with a large jaw and a queen with a plain face, on the throne of England; there were a king with a large jaw and a queen with a fair face, on the throne of France. In both countries it was clearer than crystal to the lords of the State preserves of loaves and fishes, that things in general were settled for ever. 4 | 5 | It was the year of Our Lord one thousand seven hundred and seventy-five. Spiritual revelations were conceded to England at that favoured period, as at this. Mrs. Southcott had recently attained her five-and-twentieth blessed birthday, of whom a prophetic private in the Life Guards had heralded the sublime appearance by announcing that arrangements were made for the swallowing up of London and Westminster. Even the Cock-lane ghost had been laid only a round dozen of years, after rapping out its messages, as the spirits of this very year last past (supernaturally deficient in originality) rapped out theirs. Mere messages in the earthly order of events had lately come to the English Crown and People, from a congress of British subjects in America: which, strange to relate, have proved more important to the human race than any communications yet received through any of the chickens of the Cock-lane brood. 6 | 7 | France, less favoured on the whole as to matters spiritual than her sister of the shield and trident, rolled with exceeding smoothness down hill, making paper money and spending it. Under the guidance of her Christian pastors, she entertained herself, besides, with such humane achievements as sentencing a youth to have his hands cut off, his tongue torn out with pincers, and his body burned alive, because he had not kneeled down in the rain to do honour to a dirty procession of monks which passed within his view, at a distance of some fifty or sixty yards. It is likely enough that, rooted in the woods of France and Norway, there were growing trees, when that sufferer was put to death, already marked by the Woodman, Fate, to come down and be sawn into boards, to make a certain movable framework with a sack and a knife in it, terrible in history. It is likely enough that in the rough outhouses of some tillers of the heavy lands adjacent to Paris, there were sheltered from the weather that very day, rude carts, bespattered with rustic mire, snuffed about by pigs, and roosted in by poultry, which the Farmer, Death, had already set apart to be his tumbrils of the Revolution. But that Woodman and that Farmer, though they work unceasingly, work silently, and no one heard them as they went about with muffled tread: the rather, forasmuch as to entertain any suspicion that they were awake, was to be atheistical and traitorous. 8 | 9 | In England, there was scarcely an amount of order and protection to justify much national boasting. Daring burglaries by armed men, and highway robberies, took place in the capital itself every night; families were publicly cautioned not to go out of town without removing their furniture to upholsterers' warehouses for security; the highwayman in the dark was a City tradesman in the light, and, being recognised and challenged by his fellow- tradesman whom he stopped in his character of "the Captain," gallantly shot him through the head and rode away; the mall was waylaid by seven robbers, and the guard shot three dead, and then got shot dead himself by the other four, "in consequence of the failure of his ammunition:" after which the mall was robbed in peace; that magnificent potentate, the Lord Mayor of London, was made to stand and deliver on Turnham Green, by one highwayman, who despoiled the illustrious creature in sight of all his retinue; prisoners in London gaols fought battles with their turnkeys, and the majesty of the law fired blunderbusses in among them, loaded with rounds of shot and ball; thieves snipped off diamond crosses from the necks of noble lords at Court drawing-rooms; musketeers went into St. Giles's, to search for contraband goods, and the mob fired on the musketeers, and the musketeers fired on the mob, and nobody thought any of these occurrences much out of the common way. In the midst of them, the hangman, ever busy and ever worse than useless, was in constant requisition; now, stringing up long rows of miscellaneous criminals; now, hanging a housebreaker on Saturday who had been taken on Tuesday; now, burning people in the hand at Newgate by the dozen, and now burning pamphlets at the door of Westminster Hall; to-day, taking the life of an atrocious murderer, and to-morrow of a wretched pilferer who had robbed a farmer's boy of sixpence. 10 | 11 | All these things, and a thousand like them, came to pass in and close upon the dear old year one thousand seven hundred and seventy-five. Environed by them, while the Woodman and the Farmer worked unheeded, those two of the large jaws, and those other two of the plain and the fair faces, trod with stir enough, and carried their divine rights with a high hand. Thus did the year one thousand seven hundred and seventy-five conduct their Greatnesses, and myriads of small creatures--the creatures of this chronicle among the rest--along the roads that lay before them.`; -------------------------------------------------------------------------------- /src/examples/monaco/monaco-adapter.js: -------------------------------------------------------------------------------- 1 | 2 | define("MonacoConvergenceAdapter", 3 | ['MonacoCollabExt', 'ConvergenceColorAssigner'], 4 | function (MonacoCollabExt, ConvergenceColorAssigner) { 5 | 6 | class MonacoConvergenceAdapter { 7 | constructor(monacoEditor, realtimeString) { 8 | this._monacoEditor = monacoEditor; 9 | this._model = realtimeString; 10 | this._colorAssigner = new ConvergenceColorAssigner.ColorAssigner(); 11 | } 12 | 13 | bind() { 14 | this._initSharedData(); 15 | this._initSharedCursors(); 16 | this._initSharedSelection(); 17 | } 18 | 19 | _initSharedData() { 20 | this._contentManager = new MonacoCollabExt.EditorContentManager({ 21 | editor: this._monacoEditor, 22 | onInsert: (index, text) => { 23 | this._model.insert(index, text); 24 | }, 25 | onReplace: (index, length, text) => { 26 | this._model.model().startBatch(); 27 | this._model.remove(index, length); 28 | this._model.insert(index, text); 29 | this._model.model().completeBatch(); 30 | }, 31 | onDelete: (index, length) => { 32 | this._model.remove(index, length); 33 | }, 34 | remoteSourceId: "convergence" 35 | 36 | }); 37 | 38 | this._model.events().subscribe(e => { 39 | switch (e.name) { 40 | case "insert": 41 | this._contentManager.insert(e.index, e.value); 42 | break; 43 | case "remove": 44 | this._contentManager.delete(e.index, e.value.length); 45 | break; 46 | default: 47 | } 48 | }); 49 | } 50 | 51 | _initSharedCursors() { 52 | this._remoteCursorManager = new MonacoCollabExt.RemoteCursorManager({ 53 | editor: this._monacoEditor, 54 | tooltips: true, 55 | tooltipDuration: 2 56 | }); 57 | this._cursorReference = this._model.indexReference("cursor"); 58 | 59 | const references = this._model.references({key: "cursor"}); 60 | references.forEach((reference) => { 61 | if (!reference.isLocal()) { 62 | this._addRemoteCursor(reference); 63 | } 64 | }); 65 | 66 | this._setLocalCursor(); 67 | this._cursorReference.share(); 68 | 69 | this._monacoEditor.onDidChangeCursorPosition(e => { 70 | this._setLocalCursor(); 71 | }); 72 | 73 | this._model.on("reference", (e) => { 74 | if (e.reference.key() === "cursor") { 75 | this._addRemoteCursor(e.reference); 76 | } 77 | }); 78 | } 79 | 80 | _setLocalCursor() { 81 | const position = this._monacoEditor.getPosition(); 82 | const offset = this._monacoEditor.getModel().getOffsetAt(position); 83 | this._cursorReference.set(offset); 84 | } 85 | 86 | _addRemoteCursor(reference) { 87 | const color = this._colorAssigner.getColorAsHex(reference.sessionId()); 88 | const remoteCursor = this._remoteCursorManager.addCursor(reference.sessionId(), color, reference.user().displayName); 89 | 90 | reference.on("cleared", () => remoteCursor.hide()); 91 | reference.on("disposed", () => remoteCursor.dispose()); 92 | reference.on("set", () => { 93 | const cursorIndex = reference.value(); 94 | remoteCursor.setOffset(cursorIndex); 95 | }); 96 | } 97 | 98 | 99 | _initSharedSelection() { 100 | this._remoteSelectionManager = new MonacoCollabExt.RemoteSelectionManager({editor: this._monacoEditor}); 101 | 102 | this._selectionReference = this._model.rangeReference("selection"); 103 | this._setLocalSelection(); 104 | this._selectionReference.share(); 105 | 106 | this._monacoEditor.onDidChangeCursorSelection(e => { 107 | this._setLocalSelection(); 108 | }); 109 | 110 | const references = this._model.references({key: "selection"}); 111 | references.forEach((reference) => { 112 | if (!reference.isLocal()) { 113 | this._addRemoteSelection(reference); 114 | } 115 | }); 116 | 117 | this._model.on("reference", (e) => { 118 | if (e.reference.key() === "selection") { 119 | this._addRemoteSelection(e.reference); 120 | } 121 | }); 122 | } 123 | 124 | _setLocalSelection() { 125 | const selection = this._monacoEditor.getSelection(); 126 | if (!selection.isEmpty()) { 127 | const start = this._monacoEditor.getModel().getOffsetAt(selection.getStartPosition()); 128 | const end = this._monacoEditor.getModel().getOffsetAt(selection.getEndPosition()); 129 | this._selectionReference.set({start, end}); 130 | } else if (this._selectionReference.isSet()) { 131 | this._selectionReference.clear(); 132 | } 133 | } 134 | 135 | _addRemoteSelection(reference) { 136 | const color = this._colorAssigner.getColorAsHex(reference.sessionId()) 137 | const remoteSelection = this._remoteSelectionManager.addSelection(reference.sessionId(), color); 138 | 139 | if (reference.isSet()) { 140 | const selection = reference.value(); 141 | remoteSelection.setOffsets(selection.start, selection.end); 142 | } 143 | 144 | reference.on("cleared", () => remoteSelection.hide()); 145 | reference.on("disposed", () => remoteSelection.dispose()); 146 | reference.on("set", () => { 147 | const selection = reference.value(); 148 | remoteSelection.setOffsets(selection.start, selection.end); 149 | }); 150 | } 151 | } 152 | 153 | return MonacoConvergenceAdapter; 154 | } 155 | ); 156 | -------------------------------------------------------------------------------- /src/assets/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Century Gothic"; 3 | src: url('../fonts/CenturyGothic.ttf'); 4 | } 5 | 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | right: 0; 13 | left: 0; 14 | display: flex; 15 | flex-direction: column; 16 | background: #f1f2f5; 17 | } 18 | 19 | nav#header { 20 | position: fixed; 21 | width: 100%; 22 | z-index: 10; 23 | background: white; 24 | display: flex; 25 | align-items: center; 26 | justify-content: space-between; 27 | padding: 5px 15px 5px 10px; 28 | box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.16); 29 | } 30 | nav#header img.logo { 31 | height: 40px; 32 | padding: 2px 0; 33 | } 34 | nav#header .brand-copy { 35 | font-family: "Century Gothic", sans-serif; 36 | -webkit-font-smoothing: antialiased; 37 | display: flex; 38 | } 39 | nav#header .brand-copy a { 40 | color: #11599c; 41 | margin-right: 10px; 42 | line-height: 1.5em; 43 | } 44 | nav#header span.title { 45 | font-size: 1.5em; 46 | } 47 | nav#header .links { 48 | display: flex; 49 | align-items: center; 50 | } 51 | nav#header .links .link { 52 | display: inline-block; 53 | padding: 0 10px; 54 | } 55 | nav#header .links a:hover { 56 | text-decoration: underline; 57 | } 58 | nav#header img.github-logo { 59 | height: 24px; 60 | } 61 | nav#header .menu { 62 | display: none; 63 | } 64 | nav#header .examples-title { 65 | font-size: 22px; 66 | } 67 | 68 | .wrapper { 69 | display: flex; 70 | width: 100%; 71 | flex: 1; 72 | align-items: stretch; 73 | margin-top: 50px; 74 | } 75 | 76 | #sidebar { 77 | height: 100%; 78 | display: flex; 79 | flex-direction: row; 80 | justify-content: left; 81 | } 82 | #sidebar .sidebar-content { 83 | opacity: 1; 84 | max-width: 100%; 85 | overflow: auto; 86 | border-right: 1px solid #a9a9a9; 87 | background-color: #1D2939; 88 | color: white; 89 | } 90 | #sidebar .sidebar-content.hidden { 91 | opacity: 0; 92 | max-width: 0; 93 | } 94 | #sidebar .all-examples { 95 | margin: 10px 0; 96 | padding: 4px 10px; 97 | } 98 | #sidebar .all-examples a { 99 | color: rgba(255, 255, 255, 0.65); 100 | } 101 | #sidebar .example-category { 102 | padding: 4px 10px; 103 | color: #039be5; 104 | border-bottom: 1px solid; 105 | margin-bottom: 5px; 106 | } 107 | #sidebar ul.example-group { 108 | margin: 0 0 15px; 109 | padding: 0 10px; 110 | } 111 | #sidebar ul.example-group li { 112 | list-style: none; 113 | margin-bottom: 5px; 114 | } 115 | #sidebar ul.example-group li a { 116 | color: rgba(255, 255, 255, 0.65); 117 | text-decoration: none; 118 | padding: 4px 8px; 119 | display: block; 120 | font-size: 14px; 121 | } 122 | #sidebar ul.example-group li:hover { 123 | color: white; 124 | background: #151b2a; 125 | } 126 | #sidebar ul.example-group li:hover a { 127 | color: white; 128 | } 129 | #sidebar ul.example-group li.active { 130 | border-radius: 2px; 131 | background-image: -webkit-gradient(linear, left top, right top, from(#19a6b3), to(#288eb9)); 132 | background-image: linear-gradient(to right, #19a6b3 0%, #288eb9 100%); 133 | } 134 | #sidebar ul.example-group li.active a { 135 | color: white; 136 | } 137 | 138 | #sidebar-toggle { 139 | padding-top: 48vh; 140 | z-index: 5; 141 | cursor: pointer; 142 | overflow: hidden; 143 | width: 15px; 144 | } 145 | #sidebar-toggle .sidebar-btn { 146 | transform: translateX(-50%); 147 | border: 1px solid #a9a9a9; 148 | border-radius: 100%; 149 | background-color: #1D2939; 150 | color: rgba(255, 255, 255, 0.65); 151 | width: 30px; 152 | height: 30px; 153 | } 154 | #sidebar-toggle i { 155 | padding-top: 6px; 156 | padding-left: 16px; 157 | display: none; 158 | } 159 | #sidebar-toggle i.fa-caret-right { 160 | padding-left: 18px; 161 | } 162 | #sidebar-toggle i.show { 163 | display: inline-block; 164 | } 165 | 166 | #content { 167 | flex: 1; 168 | } 169 | 170 | #examples { 171 | padding:10px; 172 | text-align: left; 173 | } 174 | #examples .header { 175 | margin: 10px 0 20px; 176 | padding: 10px; 177 | } 178 | #examples .header .larger { 179 | font-size: 1.1rem; 180 | } 181 | #examples .container { 182 | margin: 0; 183 | } 184 | #examples .card { 185 | margin-bottom: 30px; 186 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2); 187 | } 188 | #examples .card .card-header { 189 | display: flex; 190 | justify-content: space-between; 191 | } 192 | #examples .card .card-header a { 193 | color: #11599c; 194 | } 195 | #examples .card .code img { 196 | height: 20px; 197 | } 198 | 199 | div.row { 200 | display: flex; 201 | } 202 | div.row div.example-column { 203 | display: flex; 204 | } 205 | 206 | div.card { 207 | display: flex; 208 | flex-direction: column; 209 | width: 100%; 210 | } 211 | 212 | div.card-body { 213 | display: flex; 214 | flex-direction: column; 215 | flex: 1; 216 | } 217 | 218 | div.card-body div.description { 219 | flex: 1; 220 | } 221 | 222 | div.card-body div.actions { 223 | margin-top: 15px; 224 | } 225 | 226 | footer { 227 | margin-top: 0; 228 | text-align: center; 229 | font-size: 11px; 230 | background: #3c4044; 231 | padding: 10px; 232 | color: white; 233 | } 234 | 235 | 236 | @media screen and (min-width: 601px) { 237 | #sidebar .sidebar-content { 238 | transition: all .5s ease; 239 | } 240 | } 241 | 242 | @media screen and (max-width: 600px) { 243 | .wrapper { 244 | margin-top: 40px; 245 | } 246 | #content { 247 | margin-left: -15px; 248 | } 249 | 250 | nav#header img.logo { 251 | height: 25px; 252 | margin-right: 0; 253 | } 254 | nav#header span.title { 255 | font-size: 1.2em; 256 | line-height: 1.2em; 257 | } 258 | nav#header .menu { 259 | display: initial; 260 | align-self: flex-end; 261 | font-size: 1.2em; 262 | } 263 | nav#header .links { 264 | display: none; 265 | } 266 | nav#header .links.visible { 267 | position: absolute; 268 | top: 38px; 269 | left: 0; 270 | background: #fff; 271 | z-index: 10; 272 | width: 100%; 273 | display: flex; 274 | flex-direction: column; 275 | padding-bottom: 8px; 276 | border-top: 1px solid rgba(0, 0, 0, .07); 277 | border-bottom: 1px solid rgba(0, 0, 0, .3); 278 | } 279 | nav#header .links .link { 280 | padding: 5px 15px 0; 281 | align-self: flex-start; 282 | } 283 | 284 | #sidebar { 285 | font-size: 0.9rem; 286 | } 287 | } 288 | 289 | -------------------------------------------------------------------------------- /src/examples/pointer/pointer.js: -------------------------------------------------------------------------------- 1 | // References to the buttons that will be enabled / disabled 2 | const joinButton = document.getElementById("joinButton"); 3 | const leaveButton = document.getElementById("leaveButton"); 4 | 5 | // The elements that shows the local mouse location 6 | const localMouseSpan = document.getElementById("localMouse"); 7 | const localUserSpan = document.getElementById("localUser"); 8 | 9 | // The list where all the cursors by session are listed. 10 | const mouseLocations = document.getElementById("mouseLocations"); 11 | 12 | // The div where the mouse events are sourced / rendered 13 | const cursorBox = document.getElementById('cursorBox'); 14 | 15 | // The Convergence activity 16 | let activity; 17 | 18 | // A map of remote cursors by sessionId 19 | const sessions = new Map(); 20 | 21 | // The domain that this example connects too. 22 | let domain; 23 | 24 | let zOrder = 1; 25 | 26 | const username = "User-" + (Math.floor(Math.random() * 900000) + 100000); 27 | 28 | const POINTER_KEY = "pointer"; 29 | const CLICK_KEY = "click"; 30 | 31 | Convergence.connectAnonymously(CONVERGENCE_URL, username) 32 | .then((d) => { 33 | domain = d; 34 | joinButton.disabled = false; 35 | localUserSpan.innerHTML = domain.session().user().displayName; 36 | exampleLoaded(); 37 | }) 38 | .catch((error) => { 39 | console.log("Could not connect: " + error); 40 | }); 41 | 42 | // Handles clicking the open button 43 | function joinActivity() { 44 | domain.activities() 45 | .join(convergenceExampleId) 46 | .then((act) => { 47 | activity = act; 48 | const participants = activity.participants(); 49 | joinButton.disabled = true; 50 | leaveButton.disabled = false; 51 | 52 | participants.forEach((participant) => { 53 | const local = participant.sessionId === activity.session().sessionId(); 54 | handleSessionJoined(participant); 55 | const state = participant.state.get(POINTER_KEY); 56 | if (state) { 57 | updateMouseLocation(participant.sessionId, state.x, state.y, local); 58 | } 59 | }); 60 | 61 | activity.events().subscribe((event) => { 62 | switch (event.name) { 63 | case Convergence.Activity.Events.SESSION_JOINED: 64 | handleSessionJoined(event.participant); 65 | break; 66 | case Convergence.Activity.Events.SESSION_LEFT: 67 | handleSessionLeft(event.sessionId); 68 | break; 69 | case Convergence.Activity.Events.STATE_SET: 70 | if (event.key === POINTER_KEY) { 71 | const pointer = event.value; 72 | updateMouseLocation(event.sessionId, pointer.x, pointer.y, event.local); 73 | } 74 | 75 | if (event.key === CLICK_KEY && !event.local) { 76 | const click = event.value; 77 | remoteClicked(event.sessionId, click.x, click.y, event.local); 78 | } 79 | 80 | break; 81 | case Convergence.Activity.Events.STATE_REMOVED: 82 | if (event.key === POINTER_KEY) { 83 | hidePointer(event.sessionId, event.local); 84 | } 85 | break; 86 | } 87 | }); 88 | }); 89 | } 90 | 91 | // Handles clicking the leave button 92 | function leaveActivity() { 93 | activity.leave(); 94 | joinButton.disabled = false; 95 | leaveButton.disabled = true; 96 | 97 | sessions.forEach((remoteSession, sessionId) => { 98 | handleSessionLeft(sessionId); 99 | }); 100 | 101 | sessions.clear(); 102 | } 103 | 104 | // Handles a session joining (both remote and local) 105 | function handleSessionJoined(participant) { 106 | const sessionId = participant.sessionId; 107 | const username = participant.user.displayName; 108 | const sessionTr = document.createElement("tr"); 109 | const usernameCell = document.createElement("td"); 110 | usernameCell.innerHTML = participant.sessionId === activity.session().sessionId() ? 'local' : username; 111 | sessionTr.appendChild(usernameCell); 112 | 113 | const locationCell = document.createElement("td"); 114 | sessionTr.appendChild(locationCell); 115 | 116 | const fpsCell = document.createElement("td"); 117 | sessionTr.appendChild(fpsCell); 118 | 119 | mouseLocations.appendChild(sessionTr); 120 | let cursorDiv; 121 | 122 | if (!participant.local) { 123 | cursorDiv = document.createElement("img"); 124 | cursorDiv.src = "assets/cursor.png"; 125 | cursorDiv.className = "remoteCursor"; 126 | cursorDiv.style.zIndex = zOrder++; 127 | cursorBox.appendChild(cursorDiv); 128 | } 129 | 130 | const sessionRec = { 131 | sessionTr: sessionTr, 132 | locationCell: locationCell, 133 | fpsCell: fpsCell, 134 | cursorDiv: cursorDiv, 135 | updates: [] 136 | }; 137 | sessions.set(sessionId, sessionRec); 138 | 139 | if (participant.state.has(POINTER_KEY)) { 140 | const pointer = participant.state.get(POINTER_KEY); 141 | updateMouseLocation(sessionId, pointer.x, pointer.y, participant.local); 142 | } else { 143 | hidePointer(sessionId, participant.local); 144 | } 145 | 146 | if (!participant.local) { 147 | calculateFrameRate(sessionRec); 148 | } 149 | } 150 | 151 | // Handles a session leaving (both remote and local) 152 | function handleSessionLeft(sessionId) { 153 | const sessionRec = sessions.get(sessionId); 154 | sessionRec.sessionTr.parentNode.removeChild(sessionRec.sessionTr); 155 | if (sessionRec.cursorDiv) { 156 | cursorBox.removeChild(sessionRec.cursorDiv); 157 | } 158 | if (sessionRec.interval) { 159 | clearInterval(sessionRec.interval); 160 | } 161 | sessions.delete(sessionId); 162 | } 163 | 164 | function hidePointer(sessionId, local) { 165 | const sessionRec = sessions.get(sessionId); 166 | sessionRec.locationCell.innerHTML = "(none)"; 167 | sessionRec.fpsCell.innerHTML = local ? "-" : "0"; 168 | if (!local) { 169 | sessionRec.cursorDiv.style.visibility = "hidden"; 170 | } 171 | } 172 | 173 | function updateMouseLocation(sessionId, x, y, local) { 174 | const sessionRec = sessions.get(sessionId); 175 | sessionRec.locationCell.innerHTML = "(" + Math.round(x) + "," + Math.round(y) + ")"; 176 | if (!local) { 177 | sessionRec.cursorDiv.style.top = y + "px"; 178 | sessionRec.cursorDiv.style.left = x + "px"; 179 | sessionRec.cursorDiv.style.visibility = "visible"; 180 | } 181 | 182 | sessionRec.updates.push(performance.now()); 183 | } 184 | 185 | function calculateFrameRate(sessionRec) { 186 | sessionRec.interval = setInterval(function () { 187 | const times = sessionRec.updates; 188 | const now = performance.now(); 189 | while (times.length > 0 && times[0] <= now - 1000) { 190 | times.shift(); 191 | } 192 | sessionRec.fpsCell.innerHTML = times.length; 193 | }, 250); 194 | } 195 | 196 | function remoteClicked(sessionId, x, y) { 197 | const elem = createClickSpot(x, y); 198 | setTimeout(() => { 199 | elem.parentElement.removeChild(elem); 200 | }, 300); 201 | } 202 | 203 | function getMouseEventCoordinates(evt) { 204 | const cursorBoxOffset = $(cursorBox).offset(); 205 | return { 206 | x: evt.pageX - cursorBoxOffset.left, 207 | y: evt.pageY - cursorBoxOffset.top 208 | }; 209 | } 210 | 211 | // handles the local mouse movement and set events. 212 | function mouseMoved(evt) { 213 | const coordinates = getMouseEventCoordinates(evt); 214 | localMouseSpan.innerHTML = " (" + Math.round(coordinates.x) + "," + Math.round(coordinates.y) + ")"; 215 | 216 | if (activity) { 217 | activity.setState(POINTER_KEY, coordinates); 218 | } 219 | } 220 | 221 | function mouseOut(evt) { 222 | localMouseSpan.innerHTML = " (none)"; 223 | if (activity) { 224 | activity.removeState(POINTER_KEY); 225 | } 226 | } 227 | 228 | function mouseClicked(event) { 229 | if (activity) { 230 | const coordinates = getMouseEventCoordinates(event); 231 | activity.setState(CLICK_KEY, coordinates); 232 | } 233 | } 234 | 235 | function createClickSpot(posX, posY) { 236 | const spot = document.createElement("div"); 237 | spot.className = "clickSpot"; 238 | spot.style.left = posX + "px"; 239 | spot.style.top = posY + "px"; 240 | cursorBox.appendChild(spot); 241 | return spot; 242 | } 243 | -------------------------------------------------------------------------------- /src/examples/chartjs/chart.js: -------------------------------------------------------------------------------- 1 | const RealtimeChart = function () { 2 | this.controls = []; 3 | }; 4 | 5 | RealtimeChart.prototype = { 6 | 7 | /** 8 | * Initializes the demo by connecting to the domain and opening the model. 9 | */ 10 | init: function () { 11 | // Connect to the domain. See ../connection.js for the connection settings. 12 | Convergence.connectAnonymously(CONVERGENCE_URL).then(domain => { 13 | // Now open the model, creating it using the initial data if it does not exist. 14 | return domain.models().openAutoCreate({ 15 | collection: "example-chart", 16 | id: convergenceExampleId, 17 | data: initialData, 18 | ephemeral: true 19 | }); 20 | }).then(model => { 21 | // Initialize the chart with the model data and wire up the events. 22 | this.createChart(model.root().value()); 23 | this.realTimeModel = model; 24 | this.bindToSliders(); 25 | this.listenForRemoteChanges(); 26 | exampleLoaded(); 27 | }).catch(error => { 28 | console.log("Could not open model: " + error); 29 | }); 30 | }, 31 | 32 | /** 33 | * Creates the chart.js chart using the supplied JSON data and creates the 34 | * slider controls for each wedge of the pie chart. 35 | * 36 | * @param initialData The initial chart data. 37 | */ 38 | createChart: function (initialData) { 39 | const ctx = document.getElementById("pieChart"); 40 | this.pieChart = new Chart(ctx, { 41 | type: 'pie', 42 | data: initialData, 43 | options: { 44 | responsive: false, 45 | duration: 0, 46 | legend: { 47 | onClick: () => { 48 | } 49 | } 50 | } 51 | }); 52 | 53 | // Create slider controls for each segment, initialized with the current data 54 | const dataset = initialData.datasets[0]; 55 | initialData.labels.forEach((label, index) => { 56 | const attrs = { 57 | label: label, 58 | value: dataset.data[index], 59 | color: dataset.backgroundColor[index] 60 | }; 61 | const control = new ChartControl(attrs); 62 | control.addToDom(); 63 | this.controls.push(control); 64 | }); 65 | }, 66 | 67 | /** 68 | * Add an event handler to each control for when the user changes the value 69 | * by sliding the slider. 70 | */ 71 | bindToSliders: function () { 72 | this.controls.forEach((control, index) => { 73 | control.onSlide((value) => { 74 | const val = Math.round(parseInt(value[0], 10)); 75 | 76 | // update the RealTimeValue. This notifies any listeners 77 | this.realTimeModel.elementAt(['datasets', 0, 'data', index]).value(val); 78 | 79 | // Update the control widget and rerender the chart 80 | control.updateInputVal(val); 81 | this.updateSegmentValue(index, val); 82 | }); 83 | }); 84 | }, 85 | 86 | /** 87 | * Listen for remote data model changes and update the correct slider and data 88 | * in the chart. 89 | */ 90 | listenForRemoteChanges: function () { 91 | this.controls.forEach((control, index) => { 92 | // Update the UI when a segment's value is explicitly changed 93 | // This can be triggered either from the slider of another chart example, or 94 | // by explicitly typing in the value in the Model Editor of the Admin UI 95 | this.realTimeModel 96 | .elementAt('datasets', 0, 'data', index) 97 | .on(Convergence.RealTimeNumber.Events.VALUE, (e) => { 98 | control.updateSliderVal(e.element.value()); 99 | this.updateSegmentValue(index, e.element.value()); 100 | }); 101 | 102 | // Update the UI when a segment's value is incremented or decremented 103 | // This can be triggered with a value's spinner in the Model Editor of the Admin UI 104 | this.realTimeModel 105 | .elementAt('datasets', 0, 'data', index) 106 | .on(Convergence.RealTimeNumber.Events.DELTA, (e) => { 107 | const value = this.getSegmentValue(index) + e.value; 108 | control.updateSliderVal(value); 109 | this.updateSegmentdata(index, value); 110 | }); 111 | 112 | // Update the UI when a segment's color changes 113 | // This can be triggered by editing a "backgroundColor" value in the Admin UI 114 | const rtColorValue = this.realTimeModel.elementAt('datasets', 0, 'backgroundColor', index); 115 | rtColorValue.on(Convergence.RealTimeString.Events.MODEL_CHANGED, () => { 116 | const newColor = rtColorValue.value(); 117 | control.updateSliderColor(newColor); 118 | this.updateSegmentColor(index, newColor); 119 | }); 120 | 121 | // Update the UI when a segment's label changes 122 | // This can be triggered by editing a "labels" value in the Admin UI 123 | const rtLabelValue = this.realTimeModel.elementAt('labels', index); 124 | rtLabelValue.on(Convergence.RealTimeString.Events.MODEL_CHANGED, () => { 125 | const newLabel = rtLabelValue.value(); 126 | this.updateLabel(index, newLabel); 127 | }); 128 | }); 129 | }, 130 | 131 | /** 132 | * Sets the specified chart segment to the specified value. 133 | */ 134 | updateSegmentValue: function (index, value) { 135 | this.pieChart.data.datasets[0].data[index] = value; 136 | this.pieChart.update(); 137 | }, 138 | 139 | /** 140 | * Sets the specified chart segment to the specified color. 141 | */ 142 | updateSegmentColor: function (index, color) { 143 | this.pieChart.data.datasets[0].backgroundColor[index] = color; 144 | this.pieChart.update(); 145 | }, 146 | 147 | /** 148 | * Sets the specified slider number box to the specified value. 149 | */ 150 | updateLabel: function (index, value) { 151 | this.pieChart.data.labels[index] = value; 152 | this.pieChart.update(); 153 | }, 154 | 155 | /** 156 | * Gets the value of the specified chart segment. 157 | */ 158 | getSegmentValue: function (index) { 159 | return this.pieChart.data.datasets[0].data[index]; 160 | } 161 | }; 162 | 163 | function ChartControl(attrs) { 164 | this.id = attrs.label; 165 | this.value = attrs.value; 166 | this.color = attrs.color; 167 | } 168 | ChartControl.prototype = { 169 | addToDom: function () { 170 | const controlsDiv = document.getElementById("controls"); 171 | 172 | const controlDiv = document.createElement("div"); 173 | controlDiv.className = "control"; 174 | 175 | this.sliderEl = document.createElement("div"); 176 | this.sliderEl.className = "slider"; 177 | this.sliderEl.id = this.id; 178 | this.sliderEl.style.background = this.color; 179 | controlDiv.appendChild(this.sliderEl); 180 | 181 | const valueDiv = document.createElement("div"); 182 | valueDiv.className = "value"; 183 | 184 | this.valueInput = document.createElement("input"); 185 | this.valueInput.type = "text"; 186 | this.valueInput.disabled = true; 187 | this.valueInput.value = this.value; 188 | valueDiv.appendChild(this.valueInput); 189 | 190 | controlDiv.appendChild(valueDiv); 191 | 192 | this.createSlider(this.sliderEl); 193 | 194 | controlsDiv.appendChild(controlDiv); 195 | }, 196 | createSlider: function () { 197 | noUiSlider.create(this.sliderEl, { 198 | animate: false, 199 | start: 40, 200 | connect: [false, true], 201 | step: 1, 202 | range: { 203 | 'min': 0, 204 | 'max': 100 205 | } 206 | }); 207 | this.sliderEl.noUiSlider.set(this.value); 208 | }, 209 | onSlide: function (onSlideFn) { 210 | this.sliderEl.noUiSlider.on('slide', onSlideFn); 211 | }, 212 | updateInputVal: function (val) { 213 | this.valueInput.value = val; 214 | }, 215 | updateSliderVal: function (val) { 216 | this.updateInputVal(val); 217 | this.sliderEl.noUiSlider.set(val); 218 | }, 219 | updateSliderColor: function (color) { 220 | this.sliderEl.style.background = color; 221 | } 222 | }; 223 | 224 | // The default data that is provided if the model does not exist. 225 | const initialData = { 226 | labels: ["Red", "Green", "Yellow", "Blue"], 227 | datasets: [ 228 | { 229 | data: [80, 50, 30, 18], 230 | backgroundColor: ["#F7464A", "#46BFBD", "#FDB45C", "#62b4f7"] 231 | } 232 | ] 233 | }; 234 | 235 | const realTimeChart = new RealtimeChart(); 236 | realTimeChart.init(); 237 | -------------------------------------------------------------------------------- /src/examples/ace/ace.js: -------------------------------------------------------------------------------- 1 | // Connect to the domain. See ../config.js for the connection settings. 2 | 3 | const username = randomDisplayName(); 4 | 5 | Convergence.connectAnonymously(CONVERGENCE_URL, username) 6 | .then(d => { 7 | domain = d; 8 | // Now open the model, creating it using the initial data if it does not exist. 9 | return domain.models().openAutoCreate({ 10 | collection: "example-ace", 11 | id: convergenceExampleId, 12 | ephemeral: true, 13 | data: {text: defaultEditorContents} 14 | }) 15 | }) 16 | .then(handleOpen) 17 | .catch(error => { 18 | console.error("Could not open model ", error); 19 | }); 20 | 21 | const AceRange = ace.require("ace/range").Range; 22 | 23 | const colorAssigner = new ConvergenceColorAssigner.ColorAssigner(); 24 | 25 | let editor = null; 26 | let session = null; 27 | let doc = null; 28 | 29 | function handleOpen(model) { 30 | editor = ace.edit("ace-editor"); 31 | editor.setTheme('ace/theme/monokai'); 32 | 33 | session = editor.getSession(); 34 | session.setMode('ace/mode/javascript'); 35 | 36 | doc = session.getDocument(); 37 | 38 | const textModel = model.elementAt("text"); 39 | 40 | initModel(textModel); 41 | initSharedCursors(textModel); 42 | initSharedSelection(textModel); 43 | 44 | const radarViewElement = document.getElementById("radar-view"); 45 | initRadarView(textModel, radarViewElement); 46 | 47 | exampleLoaded(); 48 | } 49 | 50 | ///////////////////////////////////////////////////////////////////////////// 51 | // Text Binding 52 | ///////////////////////////////////////////////////////////////////////////// 53 | let suppressEvents = false; 54 | 55 | function initModel(textModel) { 56 | const session = editor.getSession(); 57 | session.setValue(textModel.value()); 58 | 59 | textModel.on("insert", (e) => { 60 | const pos = doc.indexToPosition(e.index); 61 | suppressEvents = true; 62 | doc.insert(pos, e.value); 63 | suppressEvents = false; 64 | }); 65 | 66 | textModel.on("remove", (e) => { 67 | const start = doc.indexToPosition(e.index); 68 | const end = doc.indexToPosition(e.index + e.value.length); 69 | suppressEvents = true; 70 | doc.remove(new AceRange(start.row, start.column, end.row, end.column)); 71 | suppressEvents = false; 72 | }); 73 | 74 | textModel.on("value", function (e) { 75 | suppressEvents = true; 76 | doc.setValue(e.value); 77 | suppressEvents = false; 78 | }); 79 | 80 | editor.on('change', (delta) => { 81 | if (suppressEvents) { 82 | return; 83 | } 84 | 85 | const pos = doc.positionToIndex(delta.start); 86 | switch (delta.action) { 87 | case "insert": 88 | textModel.insert(pos, delta.lines.join("\n")); 89 | break; 90 | case "remove": 91 | textModel.remove(pos, delta.lines.join("\n").length); 92 | break; 93 | default: 94 | throw new Error("unknown action: " + delta.action); 95 | } 96 | }); 97 | } 98 | 99 | ///////////////////////////////////////////////////////////////////////////// 100 | // Cursor Binding 101 | ///////////////////////////////////////////////////////////////////////////// 102 | const cursorKey = "cursor"; 103 | let cursorReference = null; 104 | let cursorManager = null; 105 | 106 | function initSharedCursors(textElement) { 107 | cursorManager = new AceCollabExt.AceMultiCursorManager(editor.getSession()); 108 | cursorReference = textElement.indexReference(cursorKey); 109 | 110 | const references = textElement.references({key: cursorKey}); 111 | references.forEach((reference) => { 112 | if (!reference.isLocal()) { 113 | addCursor(reference); 114 | } 115 | }); 116 | 117 | setLocalCursor(); 118 | cursorReference.share(); 119 | 120 | editor.getSession().selection.on('changeCursor', () => setLocalCursor()); 121 | 122 | textElement.on("reference", (e) => { 123 | if (e.reference.key() === cursorKey) { 124 | this.addCursor(e.reference); 125 | } 126 | }); 127 | } 128 | 129 | function setLocalCursor() { 130 | const position = editor.getCursorPosition(); 131 | const index = doc.positionToIndex(position); 132 | cursorReference.set(index); 133 | } 134 | 135 | function addCursor(reference) { 136 | const color = colorAssigner.getColorAsHex(reference.sessionId()); 137 | const remoteCursorIndex = reference.value(); 138 | cursorManager.addCursor(reference.sessionId(), reference.user().displayName, color, remoteCursorIndex); 139 | 140 | reference.on("cleared", () => cursorManager.clearCursor(reference.sessionId())); 141 | reference.on("disposed", () => cursorManager.removeCursor(reference.sessionId())); 142 | reference.on("set", () => { 143 | const cursorIndex = reference.value(); 144 | const cursorRow = doc.indexToPosition(cursorIndex).row; 145 | cursorManager.setCursor(reference.sessionId(), cursorIndex); 146 | 147 | if (radarView.hasView(reference.sessionId())) { 148 | radarView.setCursorRow(reference.sessionId(), cursorRow); 149 | } 150 | }); 151 | } 152 | 153 | ///////////////////////////////////////////////////////////////////////////// 154 | // Selection Binding 155 | ///////////////////////////////////////////////////////////////////////////// 156 | let selectionManager = null; 157 | let selectionReference = null; 158 | const selectionKey = "selection"; 159 | 160 | function initSharedSelection(textModel) { 161 | selectionManager = new AceCollabExt.AceMultiSelectionManager(editor.getSession()); 162 | 163 | selectionReference = textModel.rangeReference(selectionKey); 164 | setLocalSelection(); 165 | selectionReference.share(); 166 | 167 | session.selection.on('changeSelection', () => setLocalSelection()); 168 | 169 | const references = textModel.references({key: selectionKey}); 170 | references.forEach((reference) => { 171 | if (!reference.isLocal()) { 172 | addSelection(reference); 173 | } 174 | }); 175 | 176 | textModel.on("reference", (e) => { 177 | if (e.reference.key() === selectionKey) { 178 | addSelection(e.reference); 179 | } 180 | }); 181 | } 182 | 183 | function setLocalSelection() { 184 | if (!editor.selection.isEmpty()) { 185 | const aceRanges = editor.selection.getAllRanges(); 186 | const indexRanges = aceRanges.map((aceRagne) => { 187 | const start = doc.positionToIndex(aceRagne.start); 188 | const end = doc.positionToIndex(aceRagne.end); 189 | return {start: start, end: end}; 190 | }); 191 | 192 | selectionReference.set(indexRanges); 193 | } else if (selectionReference.isSet()) { 194 | selectionReference.clear(); 195 | } 196 | } 197 | 198 | function addSelection(reference) { 199 | const color = colorAssigner.getColorAsHex(reference.sessionId()); 200 | const remoteSelection = reference.values().map(range => toAceRange(range)); 201 | selectionManager.addSelection(reference.sessionId(), reference.user().username, color, remoteSelection); 202 | 203 | reference.on("cleared", () => selectionManager.clearSelection(reference.sessionId())); 204 | reference.on("disposed", () => selectionManager.removeSelection(reference.sessionId())); 205 | reference.on("set", () => { 206 | selectionManager.setSelection( 207 | reference.sessionId(), reference.values().map(range => toAceRange(range))); 208 | }); 209 | } 210 | 211 | function toAceRange(range) { 212 | if (typeof range !== 'object') { 213 | return null; 214 | } 215 | 216 | let start = range.start; 217 | let end = range.end; 218 | 219 | if (start > end) { 220 | const temp = start; 221 | start = end; 222 | end = temp; 223 | } 224 | 225 | const rangeAnchor = doc.indexToPosition(start); 226 | const rangeLead = doc.indexToPosition(end); 227 | return new AceRange(rangeAnchor.row, rangeAnchor.column, rangeLead.row, rangeLead.column); 228 | } 229 | 230 | ///////////////////////////////////////////////////////////////////////////// 231 | // Radar View Binding 232 | ///////////////////////////////////////////////////////////////////////////// 233 | let radarView = null; 234 | let viewReference = null; 235 | const viewKey = "view"; 236 | 237 | function initRadarView(textModel, radarViewElement) { 238 | radarView = new AceCollabExt.AceRadarView(radarViewElement, editor); 239 | viewReference = textModel.rangeReference(viewKey); 240 | 241 | const references = textModel.references({key: viewKey}); 242 | references.forEach((reference) => { 243 | if (!reference.isLocal()) { 244 | addView(reference); 245 | } 246 | }); 247 | 248 | session.on('changeScrollTop', () => { 249 | setTimeout(() => setLocalView(), 0); 250 | }); 251 | 252 | textModel.on("reference", (e) => { 253 | if (e.reference.key() === viewKey) { 254 | addView(e.reference); 255 | } 256 | }); 257 | 258 | setTimeout(() => { 259 | setLocalView(); 260 | viewReference.share(); 261 | }, 0); 262 | } 263 | 264 | function setLocalView() { 265 | const viewportIndices = AceCollabExt.AceViewportUtil.getVisibleIndexRange(editor); 266 | viewReference.set({start: viewportIndices.start, end: viewportIndices.end}); 267 | } 268 | 269 | function addView(reference) { 270 | const color = colorAssigner.getColorAsHex(reference.sessionId()); 271 | 272 | // fixme need the cursor 273 | let cursorRow = null; 274 | let viewRows = null; 275 | 276 | if (reference.isSet()) { 277 | const remoteViewIndices = reference.value(); 278 | viewRows = AceCollabExt.AceViewportUtil.indicesToRows(editor, remoteViewIndices.start, remoteViewIndices.end); 279 | } 280 | 281 | radarView.addView(reference.sessionId(), reference.user().username, color, viewRows, cursorRow); 282 | 283 | // fixme need to implement this on the ace collab side 284 | reference.on("cleared", () => radarView.clearView(reference.sessionId())); 285 | reference.on("disposed", () => radarView.removeView(reference.sessionId())); 286 | reference.on("set", () => { 287 | const v = reference.value(); 288 | const rows = AceCollabExt.AceViewportUtil.indicesToRows(editor, v.start, v.end); 289 | radarView.setViewRows(reference.sessionId(), rows); 290 | }); 291 | } 292 | -------------------------------------------------------------------------------- /src/examples/jointjs/default-graph-data.js: -------------------------------------------------------------------------------- 1 | const DefaultGraphData = { 2 | "cells": [ 3 | { 4 | "type": "devs.Coupled", 5 | "size": { "width": 300, "height": 300 }, 6 | "inPorts": ["in"], 7 | "outPorts": ["out 1", "out 2"], 8 | "ports": { 9 | "groups": { 10 | "in": { 11 | "position": { 12 | "name": "left" 13 | }, 14 | "attrs": { 15 | ".port-label": { 16 | "fill": "#000" 17 | }, 18 | ".port-body": { 19 | "fill": "#fff", 20 | "stroke": "#000", 21 | "r": 10, 22 | "magnet": true 23 | } 24 | }, 25 | "label": { 26 | "position": { 27 | "name": "left", 28 | "args": { 29 | "y": 10 30 | } 31 | } 32 | } 33 | }, 34 | "out": { 35 | "position": { 36 | "name": "right" 37 | }, 38 | "attrs": { 39 | ".port-label": { 40 | "fill": "#000" 41 | }, 42 | ".port-body": { 43 | "fill": "#fff", 44 | "stroke": "#000", 45 | "r": 10, 46 | "magnet": true 47 | } 48 | }, 49 | "label": { 50 | "position": { 51 | "name": "right", 52 | "args": { 53 | "y": 10 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "items": [ 60 | { 61 | "id": "in", 62 | "group": "in", 63 | "attrs": { 64 | ".port-label": { 65 | "text": "in" 66 | } 67 | } 68 | }, 69 | { 70 | "id": "out 1", 71 | "group": "out", 72 | "attrs": { 73 | ".port-label": { 74 | "text": "out 1" 75 | } 76 | } 77 | }, 78 | { 79 | "id": "out 2", 80 | "group": "out", 81 | "attrs": { 82 | ".port-label": { 83 | "text": "out 2" 84 | } 85 | } 86 | } 87 | ] 88 | }, 89 | "position": { 90 | "x": 230, 91 | "y": 50 92 | }, 93 | "angle": 0, 94 | "id": "021109e6-03fa-413c-adc6-46a1e87a02e4", 95 | "z": 1, 96 | "embeds": [ 97 | "bd2d3796-247b-4c61-a011-617a1080d747" 98 | ], 99 | "attrs": { 100 | ".body": { 101 | "rx": 6, 102 | "ry": 6 103 | } 104 | } 105 | }, 106 | { 107 | "type": "devs.Atomic", 108 | "size": { 109 | "width": 80, 110 | "height": 80 111 | }, 112 | "inPorts": [ 113 | "xy" 114 | ], 115 | "outPorts": [ 116 | "x", 117 | "y" 118 | ], 119 | "ports": { 120 | "groups": { 121 | "in": { 122 | "position": { 123 | "name": "left" 124 | }, 125 | "attrs": { 126 | ".port-label": { 127 | "fill": "#000" 128 | }, 129 | ".port-body": { 130 | "fill": "#fff", 131 | "stroke": "#000", 132 | "r": 10, 133 | "magnet": true 134 | } 135 | }, 136 | "label": { 137 | "position": { 138 | "name": "left", 139 | "args": { 140 | "y": 10 141 | } 142 | } 143 | } 144 | }, 145 | "out": { 146 | "position": { 147 | "name": "right" 148 | }, 149 | "attrs": { 150 | ".port-label": { 151 | "fill": "#000" 152 | }, 153 | ".port-body": { 154 | "fill": "#fff", 155 | "stroke": "#000", 156 | "r": 10, 157 | "magnet": true 158 | } 159 | }, 160 | "label": { 161 | "position": { 162 | "name": "right", 163 | "args": { 164 | "y": 10 165 | } 166 | } 167 | } 168 | } 169 | }, 170 | "items": [ 171 | { 172 | "id": "xy", 173 | "group": "in", 174 | "attrs": { 175 | ".port-label": { 176 | "text": "xy" 177 | } 178 | } 179 | }, 180 | { 181 | "id": "x", 182 | "group": "out", 183 | "attrs": { 184 | ".port-label": { 185 | "text": "x" 186 | } 187 | } 188 | }, 189 | { 190 | "id": "y", 191 | "group": "out", 192 | "attrs": { 193 | ".port-label": { 194 | "text": "y" 195 | } 196 | } 197 | } 198 | ] 199 | }, 200 | "position": { 201 | "x": 360, 202 | "y": 260 203 | }, 204 | "angle": 0, 205 | "id": "bd2d3796-247b-4c61-a011-617a1080d747", 206 | "z": 2, 207 | "parent": "021109e6-03fa-413c-adc6-46a1e87a02e4", 208 | "attrs": { 209 | ".body": { 210 | "rx": 6, 211 | "ry": 6 212 | } 213 | } 214 | }, 215 | { 216 | "type": "devs.Atomic", 217 | "size": { 218 | "width": 80, 219 | "height": 80 220 | }, 221 | "inPorts": [ 222 | 223 | ], 224 | "outPorts": [ 225 | "out" 226 | ], 227 | "ports": { 228 | "groups": { 229 | "in": { 230 | "position": { 231 | "name": "left" 232 | }, 233 | "attrs": { 234 | ".port-label": { 235 | "fill": "#000" 236 | }, 237 | ".port-body": { 238 | "fill": "#fff", 239 | "stroke": "#000", 240 | "r": 10, 241 | "magnet": true 242 | } 243 | }, 244 | "label": { 245 | "position": { 246 | "name": "left", 247 | "args": { 248 | "y": 10 249 | } 250 | } 251 | } 252 | }, 253 | "out": { 254 | "position": { 255 | "name": "right" 256 | }, 257 | "attrs": { 258 | ".port-label": { 259 | "fill": "#000" 260 | }, 261 | ".port-body": { 262 | "fill": "#fff", 263 | "stroke": "#000", 264 | "r": 10, 265 | "magnet": true 266 | } 267 | }, 268 | "label": { 269 | "position": { 270 | "name": "right", 271 | "args": { 272 | "y": 10 273 | } 274 | } 275 | } 276 | } 277 | }, 278 | "items": [ 279 | { 280 | "id": "out", 281 | "group": "out", 282 | "attrs": { 283 | ".port-label": { 284 | "text": "out" 285 | } 286 | } 287 | } 288 | ] 289 | }, 290 | "position": { 291 | "x": 50, 292 | "y": 160 293 | }, 294 | "angle": 0, 295 | "id": "16e8b085-d323-401c-b8da-bc991d5fda58", 296 | "z": 3, 297 | "attrs": { 298 | ".body": { 299 | "rx": 6, 300 | "ry": 6 301 | } 302 | } 303 | }, 304 | { 305 | "type": "devs.Atomic", 306 | "size": { 307 | "width": 100, 308 | "height": 300 309 | }, 310 | "inPorts": [ 311 | "a", 312 | "b" 313 | ], 314 | "outPorts": [ 315 | 316 | ], 317 | "ports": { 318 | "groups": { 319 | "in": { 320 | "position": { 321 | "name": "left" 322 | }, 323 | "attrs": { 324 | ".port-label": { 325 | "fill": "#000" 326 | }, 327 | ".port-body": { 328 | "fill": "#fff", 329 | "stroke": "#000", 330 | "r": 10, 331 | "magnet": true 332 | } 333 | }, 334 | "label": { 335 | "position": { 336 | "name": "left", 337 | "args": { 338 | "y": 10 339 | } 340 | } 341 | } 342 | }, 343 | "out": { 344 | "position": { 345 | "name": "right" 346 | }, 347 | "attrs": { 348 | ".port-label": { 349 | "fill": "#000" 350 | }, 351 | ".port-body": { 352 | "fill": "#fff", 353 | "stroke": "#000", 354 | "r": 10, 355 | "magnet": true 356 | } 357 | }, 358 | "label": { 359 | "position": { 360 | "name": "right", 361 | "args": { 362 | "y": 10 363 | } 364 | } 365 | } 366 | } 367 | }, 368 | "items": [ 369 | { 370 | "id": "a", 371 | "group": "in", 372 | "attrs": { 373 | ".port-label": { 374 | "text": "a" 375 | } 376 | } 377 | }, 378 | { 379 | "id": "b", 380 | "group": "in", 381 | "attrs": { 382 | ".port-label": { 383 | "text": "b" 384 | } 385 | } 386 | } 387 | ] 388 | }, 389 | "position": { 390 | "x": 650, 391 | "y": 50 392 | }, 393 | "angle": 0, 394 | "id": "8d5f0698-1e72-4b7a-90c5-95c54801cebf", 395 | "z": 4, 396 | "attrs": { 397 | ".body": { 398 | "rx": 6, 399 | "ry": 6 400 | } 401 | } 402 | }, 403 | { 404 | "type": "devs.Link", 405 | "source": { 406 | "id": "16e8b085-d323-401c-b8da-bc991d5fda58", 407 | "port": "out" 408 | }, 409 | "target": { 410 | "id": "021109e6-03fa-413c-adc6-46a1e87a02e4", 411 | "port": "in" 412 | }, 413 | "id": "413a2f55-b58b-481a-8326-0f7c7db14027", 414 | "z": 5, 415 | "attrs": { 416 | 417 | } 418 | }, 419 | { 420 | "type": "devs.Link", 421 | "source": { 422 | "id": "021109e6-03fa-413c-adc6-46a1e87a02e4", 423 | "port": "in" 424 | }, 425 | "target": { 426 | "id": "bd2d3796-247b-4c61-a011-617a1080d747", 427 | "port": "xy" 428 | }, 429 | "id": "9ff22465-54ec-44d9-9aba-0cadf5807af4", 430 | "z": 6, 431 | "attrs": { 432 | 433 | } 434 | }, 435 | { 436 | "type": "devs.Link", 437 | "source": { 438 | "id": "bd2d3796-247b-4c61-a011-617a1080d747", 439 | "port": "x" 440 | }, 441 | "target": { 442 | "id": "021109e6-03fa-413c-adc6-46a1e87a02e4", 443 | "port": "out 1" 444 | }, 445 | "id": "9d145a19-83f7-4dbd-8d34-1110f2051d7b", 446 | "z": 7, 447 | "attrs": { 448 | 449 | } 450 | }, 451 | { 452 | "type": "devs.Link", 453 | "source": { 454 | "id": "bd2d3796-247b-4c61-a011-617a1080d747", 455 | "port": "y" 456 | }, 457 | "target": { 458 | "id": "021109e6-03fa-413c-adc6-46a1e87a02e4", 459 | "port": "out 2" 460 | }, 461 | "id": "ff0a2d98-41c3-4c9b-9b4d-d643b25d1393", 462 | "z": 8, 463 | "attrs": { 464 | 465 | } 466 | }, 467 | { 468 | "type": "devs.Link", 469 | "source": { 470 | "id": "021109e6-03fa-413c-adc6-46a1e87a02e4", 471 | "port": "out 1" 472 | }, 473 | "target": { 474 | "id": "8d5f0698-1e72-4b7a-90c5-95c54801cebf", 475 | "port": "a" 476 | }, 477 | "id": "a8750174-3c3d-49a4-a79c-8e8e4b015637", 478 | "z": 9, 479 | "attrs": { 480 | 481 | } 482 | }, 483 | { 484 | "type": "devs.Link", 485 | "source": { 486 | "id": "021109e6-03fa-413c-adc6-46a1e87a02e4", 487 | "port": "out 2" 488 | }, 489 | "target": { 490 | "id": "8d5f0698-1e72-4b7a-90c5-95c54801cebf", 491 | "port": "b" 492 | }, 493 | "id": "85471e2f-3a96-473d-a57c-7bc6db55fcf9", 494 | "z": 10, 495 | "attrs": { 496 | 497 | } 498 | } 499 | ] 500 | }; -------------------------------------------------------------------------------- /src/examples/mxgraph/default-graph.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_GRAPH = { 2 | "root": "0", 3 | "cells": { 4 | "0": { 5 | "collapsed": false, 6 | "visible": true, 7 | "connectable": true, 8 | "id": "0" 9 | }, 10 | "1": { 11 | "parent": "0", 12 | "visible": true, 13 | "collapsed": false, 14 | "connectable": true, 15 | "id": "1" 16 | }, 17 | "LrfBnKz2Hvc4tyLOyrhw4clXmCd0hNMs": { 18 | "parent": "1", 19 | "source": "Dsr0WACg73GIQT7jGSnMd43cMPPChnLQ", 20 | "visible": true, 21 | "style": { 22 | "classes": [], 23 | "styles": { 24 | "startFill": "1", 25 | "entryPerimeter": "0", 26 | "entryY": "0.5", 27 | "endFill": "1", 28 | "exitY": "0.25", 29 | "entryX": "0.875", 30 | "startArrow": "block", 31 | "endArrow": "block", 32 | "html": "1", 33 | "exitX": "0", 34 | "strokeWidth": "2" 35 | } 36 | }, 37 | "collapsed": false, 38 | "connectable": true, 39 | "id": "LrfBnKz2Hvc4tyLOyrhw4clXmCd0hNMs", 40 | "target": "LMQq2xY8hVpDio4icCRjmyPngn5AOtMW", 41 | "edge": true, 42 | "value": "", 43 | "geometry": { 44 | "relative": true, 45 | "x": 0, 46 | "y": 0, 47 | "height": 50, 48 | "sourcePoint": { 49 | "x": 435, 50 | "y": 125 51 | }, 52 | "targetPoint": { 53 | "x": 435, 54 | "y": 95 55 | }, 56 | "width": 50 57 | } 58 | }, 59 | "edmptHmibXPB1OouanJBB6vrMrq8eGML": { 60 | "parent": "1", 61 | "source": "yqie69krxU6EWRo1pjurHpXyqtma5R54", 62 | "visible": true, 63 | "style": { 64 | "classes": [], 65 | "styles": { 66 | "startFill": "1", 67 | "entryY": "1", 68 | "endFill": "1", 69 | "entryX": "0.5", 70 | "startArrow": "block", 71 | "endArrow": "block", 72 | "html": "1", 73 | "strokeWidth": "2" 74 | } 75 | }, 76 | "collapsed": false, 77 | "connectable": true, 78 | "id": "edmptHmibXPB1OouanJBB6vrMrq8eGML", 79 | "target": "0XOfMOUKKnJ1TBKQd7tb7NaNJKWxJSpz", 80 | "edge": true, 81 | "value": "", 82 | "geometry": { 83 | "relative": true, 84 | "x": 0, 85 | "y": 0, 86 | "height": 50, 87 | "sourcePoint": { 88 | "x": 430, 89 | "y": 191 90 | }, 91 | "targetPoint": { 92 | "x": 435, 93 | "y": 95 94 | }, 95 | "width": 50 96 | } 97 | }, 98 | "MGNyh6ZvCvxrXh5tknPUUh3oIlHRPYlI": { 99 | "parent": "1", 100 | "source": "0XOfMOUKKnJ1TBKQd7tb7NaNJKWxJSpz", 101 | "visible": true, 102 | "style": { 103 | "classes": [], 104 | "styles": { 105 | "startFill": "1", 106 | "entryY": "1", 107 | "endFill": "1", 108 | "exitY": "0", 109 | "entryX": "0.5", 110 | "startArrow": "block", 111 | "endArrow": "block", 112 | "html": "1", 113 | "exitX": "0.5", 114 | "strokeWidth": "2" 115 | } 116 | }, 117 | "collapsed": false, 118 | "connectable": true, 119 | "id": "MGNyh6ZvCvxrXh5tknPUUh3oIlHRPYlI", 120 | "target": "Dsr0WACg73GIQT7jGSnMd43cMPPChnLQ", 121 | "edge": true, 122 | "value": "", 123 | "geometry": { 124 | "relative": true, 125 | "x": 0, 126 | "y": 0, 127 | "height": 50, 128 | "sourcePoint": { 129 | "x": 150, 130 | "y": 185 131 | }, 132 | "targetPoint": { 133 | "x": 150, 134 | "y": 155 135 | }, 136 | "width": 50 137 | } 138 | }, 139 | "kbLJtk6Z5iyQMfYQKpxth3XYXomSfkS5": { 140 | "parent": "1", 141 | "source": "eehKHQMjqAaJbrfc2iC7SSvfIF618ddY", 142 | "visible": true, 143 | "style": { 144 | "classes": [], 145 | "styles": { 146 | "startFill": "1", 147 | "entryY": "1", 148 | "endFill": "1", 149 | "exitY": "0.5", 150 | "entryX": "0.75", 151 | "startArrow": "block", 152 | "endArrow": "block", 153 | "html": "1", 154 | "exitX": "0", 155 | "strokeWidth": "2" 156 | } 157 | }, 158 | "collapsed": false, 159 | "connectable": true, 160 | "id": "kbLJtk6Z5iyQMfYQKpxth3XYXomSfkS5", 161 | "target": "PQtSpQkfuRW3TfV1lXNnawlePg9BdDzs", 162 | "edge": true, 163 | "value": "", 164 | "geometry": { 165 | "relative": true, 166 | "x": 0, 167 | "y": 0, 168 | "height": 50, 169 | "points": [ 170 | { 171 | "x": 170, 172 | "y": 275 173 | } 174 | ], 175 | "sourcePoint": { 176 | "x": 150, 177 | "y": 185 178 | }, 179 | "targetPoint": { 180 | "x": 160, 181 | "y": 245 182 | }, 183 | "width": 50 184 | } 185 | }, 186 | "IZTVQJiVJx9CN0NyJGyX9HYKPepOLnxR": { 187 | "parent": "1", 188 | "source": "PQtSpQkfuRW3TfV1lXNnawlePg9BdDzs", 189 | "visible": true, 190 | "style": { 191 | "classes": [], 192 | "styles": { 193 | "startFill": "1", 194 | "endFill": "1", 195 | "exitY": "0", 196 | "startArrow": "block", 197 | "endArrow": "block", 198 | "html": "1", 199 | "exitX": "0.5", 200 | "strokeWidth": "2" 201 | } 202 | }, 203 | "collapsed": false, 204 | "connectable": true, 205 | "id": "IZTVQJiVJx9CN0NyJGyX9HYKPepOLnxR", 206 | "target": "pnSDfZ2DJYfN6iribmU8Qx5hSEnEwdVO", 207 | "edge": true, 208 | "value": "", 209 | "geometry": { 210 | "relative": true, 211 | "x": 0, 212 | "y": 0, 213 | "height": 50, 214 | "sourcePoint": { 215 | "x": 150, 216 | "y": 125 217 | }, 218 | "targetPoint": { 219 | "x": 140, 220 | "y": 145 221 | }, 222 | "width": 50 223 | } 224 | }, 225 | "BCCGoW9eTsmy7e7QxeRLvidRKInmwThF": { 226 | "parent": "1", 227 | "vertex": true, 228 | "visible": true, 229 | "style": { 230 | "classes": [], 231 | "styles": { 232 | "boundedLbl": "1", 233 | "backgroundOutline": "1", 234 | "whiteSpace": "wrap", 235 | "shape": "cylinder", 236 | "html": "1" 237 | } 238 | }, 239 | "collapsed": false, 240 | "connectable": true, 241 | "id": "BCCGoW9eTsmy7e7QxeRLvidRKInmwThF", 242 | "value": "DB", 243 | "geometry": { 244 | "relative": false, 245 | "x": 10, 246 | "y": 235, 247 | "height": 80, 248 | "width": 60 249 | } 250 | }, 251 | "109yMp7FbTOijTz9trpu9I25VRrfGAtF": { 252 | "parent": "1", 253 | "source": "BCCGoW9eTsmy7e7QxeRLvidRKInmwThF", 254 | "visible": true, 255 | "style": { 256 | "classes": [], 257 | "styles": { 258 | "startFill": "1", 259 | "entryY": "1", 260 | "endFill": "1", 261 | "exitY": "0.5", 262 | "entryX": "0.25", 263 | "startArrow": "block", 264 | "endArrow": "block", 265 | "html": "1", 266 | "exitX": "1", 267 | "strokeWidth": "2" 268 | } 269 | }, 270 | "collapsed": false, 271 | "connectable": true, 272 | "id": "109yMp7FbTOijTz9trpu9I25VRrfGAtF", 273 | "target": "PQtSpQkfuRW3TfV1lXNnawlePg9BdDzs", 274 | "edge": true, 275 | "value": "", 276 | "geometry": { 277 | "relative": true, 278 | "x": 0, 279 | "y": 0, 280 | "height": 50, 281 | "points": [ 282 | { 283 | "x": 110, 284 | "y": 275 285 | } 286 | ], 287 | "sourcePoint": { 288 | "x": 120, 289 | "y": 325 290 | }, 291 | "targetPoint": { 292 | "x": 180, 293 | "y": 215 294 | }, 295 | "width": 50 296 | } 297 | }, 298 | "LMQq2xY8hVpDio4icCRjmyPngn5AOtMW": { 299 | "parent": "1", 300 | "vertex": true, 301 | "visible": true, 302 | "style": { 303 | "classes": [ 304 | "ellipse" 305 | ], 306 | "styles": { 307 | "shape": "cloud", 308 | "whiteSpace": "wrap", 309 | "html": "1" 310 | } 311 | }, 312 | "collapsed": false, 313 | "connectable": true, 314 | "id": "LMQq2xY8hVpDio4icCRjmyPngn5AOtMW", 315 | "value": "Internet", 316 | "geometry": { 317 | "relative": false, 318 | "x": 225, 319 | "y": 5, 320 | "height": 80, 321 | "width": 120 322 | } 323 | }, 324 | "yqie69krxU6EWRo1pjurHpXyqtma5R54": { 325 | "parent": "1", 326 | "vertex": true, 327 | "visible": true, 328 | "style": { 329 | "classes": [], 330 | "styles": { 331 | "outlineConnect": "0", 332 | "labelBackgroundColor": "#ffffff", 333 | "verticalAlign": "top", 334 | "verticalLabelPosition": "bottom", 335 | "shape": "umlActor", 336 | "html": "1" 337 | } 338 | }, 339 | "collapsed": false, 340 | "connectable": true, 341 | "id": "yqie69krxU6EWRo1pjurHpXyqtma5R54", 342 | "value": "User", 343 | "geometry": { 344 | "relative": false, 345 | "x": 410, 346 | "y": 185, 347 | "height": 60, 348 | "width": 30 349 | } 350 | }, 351 | "YJ61Sd1jQZPGp05W39IbZOGjSKV9V8ND": { 352 | "parent": "1", 353 | "source": null, 354 | "visible": true, 355 | "style": { 356 | "classes": [], 357 | "styles": { 358 | "edgeStyle": "orthogonalEdgeStyle", 359 | "rounded": "0", 360 | "html": "1" 361 | } 362 | }, 363 | "collapsed": false, 364 | "connectable": true, 365 | "id": "YJ61Sd1jQZPGp05W39IbZOGjSKV9V8ND", 366 | "target": null, 367 | "edge": true, 368 | "value": "", 369 | "geometry": { 370 | "relative": true, 371 | "x": 0, 372 | "y": 0, 373 | "height": 0, 374 | "width": 0 375 | } 376 | }, 377 | "5BL4lK7qHrzmOjQhp1tnVGWr2GDbfgI8": { 378 | "parent": null, 379 | "vertex": true, 380 | "visible": true, 381 | "style": { 382 | "classes": [], 383 | "styles": { 384 | "rounded": "0", 385 | "whiteSpace": "wrap", 386 | "html": "1" 387 | } 388 | }, 389 | "collapsed": false, 390 | "connectable": true, 391 | "id": "5BL4lK7qHrzmOjQhp1tnVGWr2GDbfgI8", 392 | "value": "Proxy", 393 | "geometry": { 394 | "relative": false, 395 | "x": 80, 396 | "y": 160, 397 | "height": 30, 398 | "width": 120 399 | } 400 | }, 401 | "PQtSpQkfuRW3TfV1lXNnawlePg9BdDzs": { 402 | "parent": "1", 403 | "vertex": true, 404 | "visible": true, 405 | "style": { 406 | "classes": [], 407 | "styles": { 408 | "rounded": "0", 409 | "whiteSpace": "wrap", 410 | "html": "1" 411 | } 412 | }, 413 | "collapsed": false, 414 | "connectable": true, 415 | "id": "PQtSpQkfuRW3TfV1lXNnawlePg9BdDzs", 416 | "value": "App Server", 417 | "geometry": { 418 | "relative": false, 419 | "x": 80, 420 | "y": 175, 421 | "height": 30, 422 | "width": 120 423 | } 424 | }, 425 | "0XOfMOUKKnJ1TBKQd7tb7NaNJKWxJSpz": { 426 | "parent": "1", 427 | "vertex": true, 428 | "visible": true, 429 | "style": { 430 | "classes": [], 431 | "styles": { 432 | "rounded": "0", 433 | "whiteSpace": "wrap", 434 | "html": "1" 435 | } 436 | }, 437 | "collapsed": false, 438 | "connectable": true, 439 | "id": "0XOfMOUKKnJ1TBKQd7tb7NaNJKWxJSpz", 440 | "value": "End User App", 441 | "geometry": { 442 | "relative": false, 443 | "x": 365, 444 | "y": 115, 445 | "height": 30, 446 | "width": 120 447 | } 448 | }, 449 | "eehKHQMjqAaJbrfc2iC7SSvfIF618ddY": { 450 | "parent": "1", 451 | "vertex": true, 452 | "visible": true, 453 | "style": { 454 | "classes": [], 455 | "styles": { 456 | "boundedLbl": "1", 457 | "backgroundOutline": "1", 458 | "whiteSpace": "wrap", 459 | "shape": "cylinder", 460 | "html": "1" 461 | } 462 | }, 463 | "collapsed": false, 464 | "connectable": true, 465 | "id": "eehKHQMjqAaJbrfc2iC7SSvfIF618ddY", 466 | "value": "LDAP", 467 | "geometry": { 468 | "relative": false, 469 | "x": 200, 470 | "y": 235, 471 | "height": 80, 472 | "width": 60 473 | } 474 | }, 475 | "pnSDfZ2DJYfN6iribmU8Qx5hSEnEwdVO": { 476 | "parent": "1", 477 | "vertex": true, 478 | "visible": true, 479 | "style": { 480 | "classes": [], 481 | "styles": { 482 | "rounded": "0", 483 | "whiteSpace": "wrap", 484 | "html": "1" 485 | } 486 | }, 487 | "collapsed": false, 488 | "connectable": true, 489 | "id": "pnSDfZ2DJYfN6iribmU8Qx5hSEnEwdVO", 490 | "value": "Web Server", 491 | "geometry": { 492 | "relative": false, 493 | "x": 80, 494 | "y": 115, 495 | "height": 30, 496 | "width": 120 497 | } 498 | }, 499 | "zNe35KzTqhoTPNt6KQuNFUZpFzsnaZW7": { 500 | "parent": "1", 501 | "source": null, 502 | "visible": true, 503 | "style": { 504 | "classes": [], 505 | "styles": { 506 | "edgeStyle": "orthogonalEdgeStyle", 507 | "rounded": "0", 508 | "html": "1" 509 | } 510 | }, 511 | "collapsed": false, 512 | "connectable": true, 513 | "id": "zNe35KzTqhoTPNt6KQuNFUZpFzsnaZW7", 514 | "target": null, 515 | "edge": true, 516 | "value": "", 517 | "geometry": { 518 | "relative": true, 519 | "x": 0, 520 | "y": 0, 521 | "height": 0, 522 | "width": 0 523 | } 524 | }, 525 | "ydMjRI2hYawPlxXeSCYE4thCl15rTg02": { 526 | "parent": "1", 527 | "visible": true, 528 | "style": { 529 | "classes": [], 530 | "styles": { 531 | "startFill": "1", 532 | "entryY": "1", 533 | "endFill": "1", 534 | "entryX": "0.5", 535 | "startArrow": "block", 536 | "endArrow": "block", 537 | "html": "1", 538 | "strokeWidth": "2" 539 | } 540 | }, 541 | "collapsed": false, 542 | "connectable": true, 543 | "id": "ydMjRI2hYawPlxXeSCYE4thCl15rTg02", 544 | "target": "VyHGzJJ5bCDntiskGjBMplUuCsRMKFgn", 545 | "edge": true, 546 | "value": "", 547 | "geometry": { 548 | "relative": true, 549 | "x": 0, 550 | "y": 0, 551 | "height": 50, 552 | "sourcePoint": { 553 | "x": 140, 554 | "y": 115 555 | }, 556 | "targetPoint": { 557 | "x": 170, 558 | "y": 205 559 | }, 560 | "width": 50 561 | } 562 | }, 563 | "VyHGzJJ5bCDntiskGjBMplUuCsRMKFgn": { 564 | "parent": "1", 565 | "vertex": true, 566 | "visible": true, 567 | "style": { 568 | "classes": [], 569 | "styles": { 570 | "rounded": "0", 571 | "whiteSpace": "wrap", 572 | "html": "1" 573 | } 574 | }, 575 | "collapsed": false, 576 | "connectable": true, 577 | "id": "VyHGzJJ5bCDntiskGjBMplUuCsRMKFgn", 578 | "value": "Proxy", 579 | "geometry": { 580 | "relative": false, 581 | "x": 80, 582 | "y": 55, 583 | "height": 30, 584 | "width": 120 585 | } 586 | }, 587 | "BSfjy1lgDAKlsooc6xDOSKmI3Wdw4UWW": { 588 | "parent": null, 589 | "vertex": true, 590 | "visible": true, 591 | "style": { 592 | "classes": [], 593 | "styles": { 594 | "rounded": "0", 595 | "whiteSpace": "wrap", 596 | "html": "1" 597 | } 598 | }, 599 | "collapsed": false, 600 | "connectable": true, 601 | "id": "BSfjy1lgDAKlsooc6xDOSKmI3Wdw4UWW", 602 | "value": "Proxy", 603 | "geometry": { 604 | "relative": false, 605 | "x": 80, 606 | "y": 160, 607 | "height": 30, 608 | "width": 120 609 | } 610 | }, 611 | "Dsr0WACg73GIQT7jGSnMd43cMPPChnLQ": { 612 | "parent": "1", 613 | "vertex": true, 614 | "visible": true, 615 | "style": { 616 | "classes": [], 617 | "styles": { 618 | "rounded": "0", 619 | "whiteSpace": "wrap", 620 | "html": "1" 621 | } 622 | }, 623 | "collapsed": false, 624 | "connectable": true, 625 | "id": "Dsr0WACg73GIQT7jGSnMd43cMPPChnLQ", 626 | "value": "Client API", 627 | "geometry": { 628 | "relative": false, 629 | "x": 365, 630 | "y": 55, 631 | "height": 30, 632 | "width": 120 633 | } 634 | }, 635 | "uXR5JZ17h1KAJvYSPZdq7JZ4kt81Hxbk": { 636 | "parent": "1", 637 | "source": "LMQq2xY8hVpDio4icCRjmyPngn5AOtMW", 638 | "visible": true, 639 | "style": { 640 | "classes": [], 641 | "styles": { 642 | "startFill": "1", 643 | "entryY": "0.5", 644 | "endFill": "1", 645 | "exitY": "0.55", 646 | "entryX": "1", 647 | "exitPerimeter": "0", 648 | "startArrow": "block", 649 | "endArrow": "block", 650 | "html": "1", 651 | "exitX": "0.16", 652 | "strokeWidth": "2" 653 | } 654 | }, 655 | "collapsed": false, 656 | "connectable": true, 657 | "id": "uXR5JZ17h1KAJvYSPZdq7JZ4kt81Hxbk", 658 | "target": "VyHGzJJ5bCDntiskGjBMplUuCsRMKFgn", 659 | "edge": true, 660 | "value": "", 661 | "geometry": { 662 | "relative": true, 663 | "x": 0, 664 | "y": 0, 665 | "height": 50, 666 | "sourcePoint": { 667 | "x": 250, 668 | "y": 70 669 | }, 670 | "targetPoint": { 671 | "x": 345, 672 | "y": 75 673 | }, 674 | "width": 50 675 | } 676 | } 677 | } 678 | }; 679 | --------------------------------------------------------------------------------