reset
122
src/static/css/admin.css
Normal file
|
@ -0,0 +1,122 @@
|
|||
body {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font: 14px helvetica, sans-serif;
|
||||
background: #ddd;
|
||||
background: -webkit-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
||||
background: -moz-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
||||
background: -ms-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
||||
background: -o-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
||||
border-top: 8px solid rgba(51,51,51,.8);
|
||||
}
|
||||
#wrapper {
|
||||
margin-top: 160px;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
opacity: .9;
|
||||
box-shadow: 0px 1px 8px rgba(0,0,0,0.3);
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
border-radius: 0 0 7px 7px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 29px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.separator {
|
||||
margin: 10px 0;
|
||||
height: 1px;
|
||||
background: #aaa;
|
||||
background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
}
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#inner {
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
input {
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
}
|
||||
input[type="button"] {
|
||||
padding: 4px 6px;
|
||||
margin: 0;
|
||||
}
|
||||
input[type="button"].do-install, input[type="button"].do-uninstall {
|
||||
float: right;
|
||||
width: 100px;
|
||||
}
|
||||
input[type="button"]#do-search {
|
||||
display: block;
|
||||
}
|
||||
input[type="text"] {
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
padding: 10px;
|
||||
*padding: 0; /* IE7 hack */
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border: 1px solid #ddd;
|
||||
margin: 0 0 5px 0;
|
||||
max-width: 500px;
|
||||
}
|
||||
table {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
table thead tr {
|
||||
background: #eee;
|
||||
}
|
||||
td, th {
|
||||
padding: 5px;
|
||||
}
|
||||
.template {
|
||||
display: none;
|
||||
}
|
||||
.dialog {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 700px;
|
||||
height: 500px;
|
||||
margin-left: -350px;
|
||||
margin-top: -250px;
|
||||
border: 3px solid #999;
|
||||
background: #eee;
|
||||
}
|
||||
.dialog .title {
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
border-bottom: 3px solid #999;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialog .title .close {
|
||||
float: right;
|
||||
padding: 1px 10px;
|
||||
}
|
||||
.dialog .history {
|
||||
background: #222;
|
||||
color: #eee;
|
||||
position: absolute;
|
||||
top: 41px;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
padding: 2px;
|
||||
overflow: auto;
|
||||
}
|
179
src/static/css/iframe_editor.css
Normal file
|
@ -0,0 +1,179 @@
|
|||
|
||||
/* These CSS rules are included in both the outer and inner ACE iframe.
|
||||
Also see inner.css, included only in the inner one.
|
||||
*/
|
||||
html { cursor: text; } /* in Safari, produces text cursor for whole doc (inc. below body) */
|
||||
span { cursor: auto; }
|
||||
|
||||
::selection {
|
||||
background: #acf;
|
||||
}
|
||||
::-moz-selection {
|
||||
background: #acf;
|
||||
}
|
||||
|
||||
a { cursor: pointer !important; }
|
||||
|
||||
ul, ol, li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
ul { margin-left: 1.5em; }
|
||||
ul ul { margin-left: 0 !important; }
|
||||
ul.list-bullet1 { margin-left: 1.5em; }
|
||||
ul.list-bullet2 { margin-left: 3em; }
|
||||
ul.list-bullet3 { margin-left: 4.5em; }
|
||||
ul.list-bullet4 { margin-left: 6em; }
|
||||
ul.list-bullet5 { margin-left: 7.5em; }
|
||||
ul.list-bullet6 { margin-left: 9em; }
|
||||
ul.list-bullet7 { margin-left: 10.5em; }
|
||||
ul.list-bullet8 { margin-left: 12em; }
|
||||
|
||||
ul { list-style-type: disc; }
|
||||
ul.list-bullet1 { list-style-type: disc; }
|
||||
ul.list-bullet2 { list-style-type: circle; }
|
||||
ul.list-bullet3 { list-style-type: square; }
|
||||
ul.list-bullet4 { list-style-type: disc; }
|
||||
ul.list-bullet5 { list-style-type: circle; }
|
||||
ul.list-bullet6 { list-style-type: square; }
|
||||
ul.list-bullet7 { list-style-type: disc; }
|
||||
ul.list-bullet8 { list-style-type: circle; }
|
||||
|
||||
ol.list-number1 { margin-left: 1.5em; }
|
||||
ol.list-number2 { margin-left: 3em; }
|
||||
ol.list-number3 { margin-left: 4.5em; }
|
||||
ol.list-number4 { margin-left: 6em; }
|
||||
ol.list-number5 { margin-left: 7.5em; }
|
||||
ol.list-number6 { margin-left: 9em; }
|
||||
ol.list-number7 { margin-left: 10.5em; }
|
||||
ol.list-number8 { margin-left: 12em; }
|
||||
|
||||
ol { list-style-type: decimal; }
|
||||
ol.list-number1 { list-style-type: decimal; }
|
||||
ol.list-number2 { list-style-type: lower-latin; }
|
||||
ol.list-number3 { list-style-type: lower-roman; }
|
||||
ol.list-number4 { list-style-type: decimal; }
|
||||
ol.list-number5 { list-style-type: lower-latin; }
|
||||
ol.list-number6 { list-style-type: lower-roman; }
|
||||
ol.list-number7 { list-style-type: decimal; }
|
||||
ol.list-number8 { list-style-type: lower-latin; }
|
||||
|
||||
ul.list-indent1 { margin-left: 1.5em; }
|
||||
ul.list-indent2 { margin-left: 3em; }
|
||||
ul.list-indent3 { margin-left: 4.5em; }
|
||||
ul.list-indent4 { margin-left: 6em; }
|
||||
ul.list-indent5 { margin-left: 7.5em; }
|
||||
ul.list-indent6 { margin-left: 9em; }
|
||||
ul.list-indent7 { margin-left: 10.5em; }
|
||||
ul.list-indent8 { margin-left: 12em; }
|
||||
|
||||
ul.list-indent1 { list-style-type: none; }
|
||||
ul.list-indent2 { list-style-type: none; }
|
||||
ul.list-indent3 { list-style-type: none; }
|
||||
ul.list-indent4 { list-style-type: none; }
|
||||
ul.list-indent5 { list-style-type: none; }
|
||||
ul.list-indent6 { list-style-type: none; }
|
||||
ul.list-indent7 { list-style-type: none; }
|
||||
ul.list-indent8 { list-style-type: none; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#outerdocbody {
|
||||
background-color: #fff;
|
||||
}
|
||||
body.grayedout { background-color: #eee !important }
|
||||
|
||||
#innerdocbody {
|
||||
font-size: 12px; /* overridden by body.style */
|
||||
font-family: monospace; /* overridden by body.style */
|
||||
line-height: 16px; /* overridden by body.style */
|
||||
}
|
||||
|
||||
body.doesWrap {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#innerdocbody {
|
||||
padding-top: 1px; /* important for some reason? */
|
||||
padding-right: 10px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 1px /* prevents characters from looking chopped off in FF3 -- Removed because it added too much whitespace */;
|
||||
overflow: hidden;
|
||||
/* blank 1x1 gif, so that IE8 doesn't consider the body transparent */
|
||||
background-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==);
|
||||
}
|
||||
|
||||
#sidediv {
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
line-height: 16px; /* overridden by sideDiv.style */
|
||||
padding-top: 8px; /* EDIT_BODY_PADDING_TOP */
|
||||
padding-right: 3px; /* LINE_NUMBER_PADDING_RIGHT - 1 */
|
||||
position: absolute;
|
||||
width: 20px; /* MIN_LINEDIV_WIDTH */
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#sidedivinner {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sidedivdelayed { /* class set after sizes are set */
|
||||
background-color: #eee;
|
||||
color: #888 !important;
|
||||
border-right: 1px solid #999;
|
||||
}
|
||||
.sidedivhidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#outerdocbody iframe {
|
||||
display: block; /* codemirror says it suppresses bugs */
|
||||
position: relative;
|
||||
left: 32px; /* MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT */
|
||||
top: 7px; /* EDIT_BODY_PADDING_TOP - 1*/
|
||||
border: 0;
|
||||
width: 1px; /* changed programmatically */
|
||||
height: 1px; /* changed programmatically */
|
||||
}
|
||||
|
||||
#outerdocbody .hotrect {
|
||||
border: 1px solid #999;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* cause "body" area (e.g. where clicks are heard) to grow horizontally with text */
|
||||
body.mozilla, body.safari {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
body.doesWrap {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.safari div {
|
||||
/* prevents the caret from disappearing on the longest line of the doc */
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#linemetricsdiv {
|
||||
position: absolute;
|
||||
left: -1000px;
|
||||
top: -1000px;
|
||||
color: white;
|
||||
z-index: -1;
|
||||
font-size: 12px; /* overridden by lineMetricsDiv.style */
|
||||
font-family: monospace; /* overridden by lineMetricsDiv.style */
|
||||
}
|
||||
|
||||
#overlaysdiv { position: absolute; left: -1000px; top: -1000px; }
|
854
src/static/css/pad.css
Normal file
|
@ -0,0 +1,854 @@
|
|||
*,
|
||||
html,
|
||||
body,
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.clear {
|
||||
clear: both
|
||||
}
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
width: 100%;
|
||||
}
|
||||
body,
|
||||
textarea {
|
||||
font-family: Helvetica, Arial, sans-serif
|
||||
}
|
||||
iframe {
|
||||
position: absolute
|
||||
}
|
||||
.readonly .acl-write {
|
||||
display: none;
|
||||
}
|
||||
#users {
|
||||
background: #f7f7f7;
|
||||
background: -webkit-linear-gradient( #F7F7F7,#EEE);
|
||||
background: -moz-linear-gradient( #F7F7F7,#EEE);
|
||||
background: -ms-linear-gradient( #F7F7F7,#EEE);
|
||||
background: -o-linear-gradient( #F7F7F7,#EEE);
|
||||
background: linear-gradient( #F7F7F7,#EEE);
|
||||
width: 160px;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
border-radius: 0 0 6px 6px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#otherusers {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
a img {
|
||||
border: 0
|
||||
}
|
||||
/* menu */
|
||||
.toolbar {
|
||||
background: #f7f7f7;
|
||||
background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
border-bottom: 1px solid #ccc;
|
||||
overflow: hidden;
|
||||
padding-top: 4px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
height: 32px;
|
||||
}
|
||||
.toolbar ul {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
padding-right: 3px;
|
||||
padding-left: 1px;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
float: left
|
||||
}
|
||||
.toolbar ul.menu_right {
|
||||
float: right
|
||||
}
|
||||
.toolbar ul li {
|
||||
float: left;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.toolbar ul li.separator {
|
||||
border: inherit;
|
||||
background: inherit;
|
||||
visibility: hidden;
|
||||
width: 0px;
|
||||
padding: 5px;
|
||||
}
|
||||
.toolbar ul li a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.toolbar ul li a:hover {
|
||||
background: #fff;
|
||||
background: -webkit-linear-gradient(#f4f4f4, #e4e4e4);
|
||||
background: -moz-linear-gradient(#f4f4f4, #e4e4e4);
|
||||
background: -o-linear-gradient(#f4f4f4, #e4e4e4);
|
||||
background: -ms-linear-gradient(#f4f4f4, #e4e4e4);
|
||||
background: linear-gradient(#f4f4f4, #e4e4e4);
|
||||
}
|
||||
.toolbar ul li a:active {
|
||||
background: #eee;
|
||||
background: -webkit-linear-gradient(#ddd, #fff);
|
||||
background: -moz-linear-gradient(#ddd, #fff);
|
||||
background: -o-linear-gradient(#ddd, #fff);
|
||||
background: -ms-linear-gradient(#ddd, #fff);
|
||||
background: linear-gradient(#ddd, #fff);
|
||||
-webkit-box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
|
||||
-moz-box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
|
||||
box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
|
||||
}
|
||||
.toolbar ul li a {
|
||||
background: #fff;
|
||||
background: -webkit-linear-gradient(#fff, #f0f0f0);
|
||||
background: -moz-linear-gradient(#fff, #f0f0f0);
|
||||
background: -o-linear-gradient(#fff, #f0f0f0);
|
||||
background: -ms-linear-gradient(#fff, #f0f0f0);
|
||||
background: linear-gradient(#fff, #f0f0f0);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
min-height: 18px;
|
||||
overflow: hidden;
|
||||
padding: 4px 5px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
min-width: 18px;
|
||||
}
|
||||
.toolbar ul li a .buttonicon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.toolbar ul li a.grouped-left {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
.toolbar ul li a.grouped-middle {
|
||||
border-radius: 0;
|
||||
margin-left: -2px;
|
||||
border-left: 0;
|
||||
}
|
||||
.toolbar ul li a.grouped-right {
|
||||
border-radius: 0 3px 3px 0;
|
||||
margin-left: -2px;
|
||||
border-left: 0;
|
||||
}
|
||||
.toolbar ul li a.selected {
|
||||
background: #eee !important;
|
||||
background: -webkit-linear-gradient(#EEE, #F0F0F0) !important;
|
||||
background: -moz-linear-gradient(#EEE, #F0F0F0) !important;
|
||||
background: -o-linear-gradient(#EEE, #F0F0F0) !important;
|
||||
background: -ms-linear-gradient(#EEE, #F0F0F0) !important;
|
||||
background: linear-gradient(#EEE, #F0F0F0) !important;
|
||||
}
|
||||
.toolbar ul li select {
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
line-height: 22px; /* fix for safari (win/mac) */
|
||||
height: 28px; /* fix for chrome (mac) */
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ccc;
|
||||
outline: none;
|
||||
}
|
||||
#usericon a {
|
||||
min-width: 30px;
|
||||
text-align: left;
|
||||
}
|
||||
#usericon a #online_count {
|
||||
color: #777;
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
#editorcontainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 37px; /* + 1px border */
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
z-index: 1;
|
||||
}
|
||||
#editorcontainer iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#editorloadingbox {
|
||||
padding-top: 100px;
|
||||
padding-bottom: 100px;
|
||||
font-size: 2.5em;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
z-index: 100;
|
||||
}
|
||||
#editorcontainerbox {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#padpage {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
#padmain {
|
||||
margin-top: 0px;
|
||||
position: absolute;
|
||||
top: 63px !important;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
zoom: 1;
|
||||
}
|
||||
#padeditor {
|
||||
bottom: 0px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0;
|
||||
zoom: 1;
|
||||
}
|
||||
#myswatchbox {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid #000;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
#myswatch {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent; /*...initially*/
|
||||
}
|
||||
#mycolorpicker {
|
||||
width: 232px;
|
||||
height: 265px;
|
||||
position: absolute;
|
||||
left: -250px;
|
||||
top: 0px;
|
||||
z-index: 101;
|
||||
display: none;
|
||||
border-radius: 0 0 6px 6px;
|
||||
background: #f7f7f7;
|
||||
border: 1px solid #ccc;
|
||||
border-top: 0;
|
||||
padding-left: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
#mycolorpickersave {
|
||||
left: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#mycolorpickercancel {
|
||||
left: 85px
|
||||
}
|
||||
#mycolorpickersave,
|
||||
#mycolorpickercancel {
|
||||
background: #fff;
|
||||
background: -webkit-linear-gradient(#fff, #ccc);
|
||||
background: -moz-linear-gradient(#fff, #ccc);
|
||||
background: -o-linear-gradient(#fff, #ccc);
|
||||
background: -ms-linear-gradient(#fff, #ccc);
|
||||
background: linear-gradient(#fff, #ccc);
|
||||
border: 1px solid #ccc;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
top: 240px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
}
|
||||
#mycolorpickerpreview {
|
||||
position: absolute;
|
||||
left: 207px;
|
||||
top: 240px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#myusernameform {
|
||||
margin-left: 35px
|
||||
}
|
||||
#myusernameedit {
|
||||
font-size: 1.3em;
|
||||
color: #fff;
|
||||
padding: 3px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
width: 117px;
|
||||
background: transparent;
|
||||
}
|
||||
#myusernameform input.editable {
|
||||
border: 1px solid #444
|
||||
}
|
||||
#myuser .myusernameedithoverable:hover {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
#mystatusform {
|
||||
margin-left: 35px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
#mystatusedit {
|
||||
font-size: 1.2em;
|
||||
color: #777;
|
||||
font-style: italic;
|
||||
display: none;
|
||||
padding: 2px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
border: 1px solid #bbb;
|
||||
width: 199px;
|
||||
background: transparent;
|
||||
}
|
||||
#myusernameform .editactive,
|
||||
#myusernameform .editempty {
|
||||
background: white;
|
||||
border-left: 1px solid #c3c3c3;
|
||||
border-top: 1px solid #c3c3c3;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
color: #000;
|
||||
}
|
||||
#myusernameform .editempty {
|
||||
color: #333
|
||||
}
|
||||
#myswatchbox, #myusernameedit, #otheruserstable .swatch {
|
||||
border: 1px solid #ccc !important;
|
||||
color: #333;
|
||||
}
|
||||
table#otheruserstable {
|
||||
display: none
|
||||
}
|
||||
#nootherusers {
|
||||
padding: 10px;
|
||||
font-size: 1.2em;
|
||||
color: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
#nootherusers a {
|
||||
color: #3C88FF
|
||||
}
|
||||
#otheruserstable td {
|
||||
height: 26px;
|
||||
vertical-align: middle;
|
||||
padding: 0 2px;
|
||||
color: #333;
|
||||
}
|
||||
#otheruserstable .swatch {
|
||||
border: 1px solid #000;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
overflow: hidden;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.usertdswatch {
|
||||
width: 1%
|
||||
}
|
||||
.usertdname {
|
||||
font-size: 1.3em;
|
||||
color: #444;
|
||||
}
|
||||
.usertdstatus {
|
||||
font-size: 1.1em;
|
||||
font-style: italic;
|
||||
color: #999;
|
||||
}
|
||||
.usertdactivity {
|
||||
font-size: 1.1em;
|
||||
color: #777;
|
||||
}
|
||||
.usertdname input {
|
||||
border: 1px solid #bbb;
|
||||
width: 80px;
|
||||
padding: 2px;
|
||||
}
|
||||
.usertdname input.editactive,
|
||||
.usertdname input.editempty {
|
||||
background: white;
|
||||
border-left: 1px solid #c3c3c3;
|
||||
border-top: 1px solid #c3c3c3;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
.usertdname input.editempty {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#connectivity {
|
||||
z-index: 600 !important;
|
||||
}
|
||||
|
||||
#connectivity * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#connectivity .visible,
|
||||
#connectivity .visible * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#reconnect_form button {
|
||||
font-size: 12pt;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.toolbar #overlay {
|
||||
z-index: 500;
|
||||
display: none;
|
||||
background-repeat: repeat-both;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
* html #overlay {
|
||||
/* for IE 6+ */
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
|
||||
filter: alpha(opacity=100);
|
||||
opacity: 1; /* in case this is looked at */
|
||||
background-image: none;
|
||||
background-repeat: no-repeat; /* scale the image */
|
||||
}
|
||||
|
||||
#chatbox {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 20px;
|
||||
width: 180px;
|
||||
height: 200px;
|
||||
z-index: 400;
|
||||
background-color: #f7f7f7;
|
||||
border-left: 1px solid #999;
|
||||
border-right: 1px solid #999;
|
||||
border-top: 1px solid #999;
|
||||
padding: 3px;
|
||||
padding-bottom: 10px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
display: none;
|
||||
}
|
||||
#chattext {
|
||||
background-color: white;
|
||||
border: 1px solid white;
|
||||
-ms-overflow-y: scroll;
|
||||
overflow-y: scroll;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
top: 25px;
|
||||
bottom: 25px;
|
||||
z-index: 1002;
|
||||
}
|
||||
#chattext p {
|
||||
padding: 3px;
|
||||
-ms-overflow-x: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#chatinputbox {
|
||||
padding: 3px 2px;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 3px;
|
||||
}
|
||||
#chatlabel {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
margin-right: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#chatinput {
|
||||
border: 1px solid #BBBBBB;
|
||||
width: 100%;
|
||||
float: right;
|
||||
}
|
||||
#chaticon {
|
||||
z-index: 400;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 20px;
|
||||
padding: 5px;
|
||||
border-left: 1px solid #999;
|
||||
border-right: 1px solid #999;
|
||||
border-top: 1px solid #999;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
#chaticon a {
|
||||
text-decoration: none
|
||||
}
|
||||
#chatcounter {
|
||||
color: #777;
|
||||
font-size: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#titlebar {
|
||||
line-height: 16px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
position: relative;
|
||||
bottom: 2px;
|
||||
}
|
||||
#titlelabel {
|
||||
font-size: 13px;
|
||||
margin: 4px 0 0 4px;
|
||||
position: absolute;
|
||||
}
|
||||
#titlecross {
|
||||
font-size: 25px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: #555;
|
||||
}
|
||||
.time {
|
||||
float: right;
|
||||
color: #333;
|
||||
font-style: italic;
|
||||
font-size: 10px;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.exporttype {
|
||||
margin-top: 4px;
|
||||
background-repeat: no-repeat;
|
||||
padding-left: 25px;
|
||||
background-image: url("../../static/img/etherpad_lite_icons.png");
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
#exporthtml {
|
||||
background-position: 0px -299px
|
||||
}
|
||||
#exportplain {
|
||||
background-position: 0px -395px
|
||||
}
|
||||
#exportword {
|
||||
background-position: 0px -275px
|
||||
}
|
||||
#exportpdf {
|
||||
background-position: 0px -371px
|
||||
}
|
||||
#exportopen {
|
||||
background-position: 0px -347px
|
||||
}
|
||||
#exportdokuwiki {
|
||||
background-position: 0px -459px
|
||||
}
|
||||
#importstatusball {
|
||||
display: none
|
||||
}
|
||||
#importarrow {
|
||||
display: none
|
||||
}
|
||||
#importmessagesuccess {
|
||||
display: none
|
||||
}
|
||||
#importsubmitinput {
|
||||
height: 25px;
|
||||
width: 85px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
#importstatusball {
|
||||
height: 50px
|
||||
}
|
||||
#chatthrob {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
font-size: 14px;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
right: 20px;
|
||||
z-index: 200;
|
||||
background-color: #000;
|
||||
color: white;
|
||||
background-color: rgb(0,0,0);
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
padding: 10px;
|
||||
-webkit-border-radius: 6px;
|
||||
-moz-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
|
||||
filter: alpha(opacity=80);
|
||||
opacity: .8;
|
||||
}
|
||||
.buttonicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url('../../static/img/etherpad_lite_icons.png');
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.buttonicon-bold {
|
||||
background-position: 0px -116px
|
||||
}
|
||||
.buttonicon-italic {
|
||||
background-position: 0px 0px
|
||||
}
|
||||
.buttonicon-underline {
|
||||
background-position: 0px -236px
|
||||
}
|
||||
.buttonicon-strikethrough {
|
||||
background-position: 0px -200px
|
||||
}
|
||||
.buttonicon-insertorderedlist {
|
||||
background-position: 0px -477px
|
||||
}
|
||||
.buttonicon-insertunorderedlist {
|
||||
background-position: 0px -34px
|
||||
}
|
||||
.buttonicon-indent {
|
||||
background-position: 0px -52px
|
||||
}
|
||||
.buttonicon-outdent {
|
||||
background-position: 0px -134px
|
||||
}
|
||||
.buttonicon-undo {
|
||||
background-position: 0px -255px
|
||||
}
|
||||
.buttonicon-redo {
|
||||
background-position: 0px -166px
|
||||
}
|
||||
.buttonicon-clearauthorship {
|
||||
background-position: 0px -86px
|
||||
}
|
||||
.buttonicon-settings {
|
||||
background-position: 0px -436px
|
||||
}
|
||||
.buttonicon-import_export {
|
||||
background-position: 0px -68px
|
||||
}
|
||||
.buttonicon-embed {
|
||||
background-position: 0px -18px
|
||||
}
|
||||
.buttonicon-history {
|
||||
background-position: 0px -218px
|
||||
}
|
||||
.buttonicon-chat {
|
||||
background-position: 0px -102px;
|
||||
}
|
||||
.buttonicon-showusers {
|
||||
background-position: 0px -183px;
|
||||
}
|
||||
.buttonicon-savedRevision {
|
||||
background-position: 0px -493px
|
||||
}
|
||||
#focusprotector {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
background-color: white;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=1)";
|
||||
filter: alpha(opacity=1);
|
||||
opacity: 0.01;
|
||||
display: none;
|
||||
}
|
||||
#online_count {
|
||||
color: #888;
|
||||
}
|
||||
.rtl {
|
||||
direction: RTL
|
||||
}
|
||||
#chattext p {
|
||||
word-wrap: break-word
|
||||
}
|
||||
/* fix for misaligned checkboxes */
|
||||
input[type=checkbox] {
|
||||
vertical-align: -1px
|
||||
}
|
||||
.right {
|
||||
float: right
|
||||
}
|
||||
.popup {
|
||||
font-size: 14px;
|
||||
width: 450px;
|
||||
padding: 10px;
|
||||
border-radius: 0 0 6px 6px;
|
||||
border: 1px solid #ccc;
|
||||
background: #f7f7f7;
|
||||
background: -webkit-linear-gradient(#F7F7F7, #EEE);
|
||||
background: -moz-linear-gradient(#F7F7F7, #EEE);
|
||||
background: -ms-linear-gradient(#F7F7F7, #EEE);
|
||||
background: -o-linear-gradient(#F7F7F7, #EEE);
|
||||
background: linear-gradient(#F7F7F7, #EEE);
|
||||
-webkit-box-shadow: 0 0 8px #888;
|
||||
-moz-box-shadow: 0 0 8px #888;
|
||||
box-shadow: 0 2px 4px #ddd;
|
||||
color: #222;
|
||||
}
|
||||
.popup input[type=text] {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.popup input[type=text], #users input[type=text] {
|
||||
outline: none;
|
||||
}
|
||||
.popup button {
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.popup a {
|
||||
text-decoration: none
|
||||
}
|
||||
.popup h1 {
|
||||
color: #555;
|
||||
font-size: 18px
|
||||
}
|
||||
.popup h2 {
|
||||
color: #777;
|
||||
font-size: 15px
|
||||
}
|
||||
.popup p {
|
||||
margin: 5px 0
|
||||
}
|
||||
.column {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
#settings,
|
||||
#importexport,
|
||||
#embed,
|
||||
#connectivity,
|
||||
#users {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
right: 20px;
|
||||
display: none;
|
||||
z-index: 500;
|
||||
}
|
||||
.stickyChat {
|
||||
background-color: #f1f1f1 !important;
|
||||
right: 0px !important;
|
||||
top: 37px;
|
||||
-webkit-border-radius: 0px !important;
|
||||
-moz-border-radius: 0px !important;
|
||||
border-radius: 0px !important;
|
||||
height: auto !important;
|
||||
border: none !important;
|
||||
border-left: 1px solid #ccc !important;
|
||||
width: 185px !important;
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.toolbar ul li.separator {
|
||||
display: none;
|
||||
}
|
||||
.toolbar ul li a {
|
||||
padding: 4px 1px
|
||||
}
|
||||
}
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 720px) {
|
||||
#users {
|
||||
top: 36px;
|
||||
bottom: 40px;
|
||||
border-radius: none;
|
||||
}
|
||||
#mycolorpicker {
|
||||
left: -73px;
|
||||
/* #mycolorpicker: width -#users: width */;
|
||||
}
|
||||
#editorcontainer {
|
||||
margin-bottom: 33px
|
||||
}
|
||||
.toolbar ul.menu_right {
|
||||
background: #f7f7f7;
|
||||
background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
.toolbar ul.menu_right > li:last-child {
|
||||
float: right;
|
||||
}
|
||||
.toolbar ul.menu_right > li a {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
.toolbar ul li a.selected {
|
||||
background: none !important
|
||||
}
|
||||
#chaticon, #timesliderlink {
|
||||
display: none !important
|
||||
}
|
||||
.popup {
|
||||
-webkit-border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
border-radius: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
#settings,
|
||||
#importexport,
|
||||
#connectivity,
|
||||
#embed {
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 33px;
|
||||
right: 0;
|
||||
}
|
||||
.toolbar ul li .separator {
|
||||
display: none
|
||||
}
|
||||
#online_count {
|
||||
line-height: 24px
|
||||
}
|
||||
}
|
295
src/static/css/timeslider.css
Normal file
|
@ -0,0 +1,295 @@
|
|||
#editorcontainerbox {
|
||||
overflow: auto;
|
||||
top: 40px;
|
||||
position: static;
|
||||
}
|
||||
#padcontent {
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
#timeslider-wrapper {
|
||||
left: 0;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
#timeslider-left {
|
||||
background-image: url(../../static/img/timeslider_left.png);
|
||||
height: 63px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 134px;
|
||||
}
|
||||
#timeslider-right {
|
||||
background-image: url(../../static/img/timeslider_right.png);
|
||||
height: 63px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 155px;
|
||||
}
|
||||
#timeslider {
|
||||
background-image: url(../../static/img/timeslider_background.png);
|
||||
height: 63px;
|
||||
margin: 0 9px;
|
||||
}
|
||||
#timeslider #timeslider-slider {
|
||||
height: 61px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
#ui-slider-handle {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
background-image: url(../../static/img/crushed_current_location.png);
|
||||
cursor: pointer;
|
||||
height: 61px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 13px;
|
||||
}
|
||||
#ui-slider-bar {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
height: 35px;
|
||||
margin-left: 5px;
|
||||
margin-right: 148px;
|
||||
position: relative;
|
||||
top: 20px;
|
||||
}
|
||||
#playpause_button,
|
||||
#playpause_button_icon {
|
||||
height: 47px;
|
||||
position: absolute;
|
||||
width: 47px;
|
||||
}
|
||||
#playpause_button {
|
||||
background-image: url(../../static/img/crushed_button_undepressed.png);
|
||||
right: 77px;
|
||||
top: 9px;
|
||||
}
|
||||
#playpause_button_icon {
|
||||
background-image: url(../../static/img/play.png);
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.pause#playpause_button_icon {
|
||||
background-image: url(../../static/img/pause.png)
|
||||
}
|
||||
#leftstar,
|
||||
#rightstar,
|
||||
#leftstep,
|
||||
#rightstep {
|
||||
background: url(../../static/img/stepper_buttons.png) 0 0 no-repeat;
|
||||
height: 21px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
#leftstar {
|
||||
background-position: 0 -44px;
|
||||
right: 34px;
|
||||
top: 8px;
|
||||
width: 30px;
|
||||
}
|
||||
#rightstar {
|
||||
background-position: -29px -44px;
|
||||
right: 5px;
|
||||
top: 8px;
|
||||
width: 29px;
|
||||
}
|
||||
#leftstep {
|
||||
background-position: 0 -22px;
|
||||
right: 34px;
|
||||
top: 20px;
|
||||
width: 30px;
|
||||
}
|
||||
#rightstep {
|
||||
background-position: -29px -22px;
|
||||
right: 5px;
|
||||
top: 20px;
|
||||
width: 29px;
|
||||
}
|
||||
#timeslider .star {
|
||||
background-image: url(../../static/img/star.png);
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
width: 15px;
|
||||
}
|
||||
#timeslider #timer {
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
left: 7px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 9px;
|
||||
width: 122px;
|
||||
}
|
||||
.topbarcenter,
|
||||
#docbar {
|
||||
display: none
|
||||
}
|
||||
#padmain {
|
||||
top: 0px !important
|
||||
}
|
||||
#editbarright {
|
||||
float: right
|
||||
}
|
||||
#returnbutton {
|
||||
color: #222;
|
||||
font-size: 16px;
|
||||
line-height: 29px;
|
||||
margin-top: 0;
|
||||
padding-right: 6px;
|
||||
}
|
||||
#settings,
|
||||
#importexport,
|
||||
#embed,
|
||||
#connectivity,
|
||||
#users {
|
||||
top: 62px;
|
||||
}
|
||||
#importexport .popup {
|
||||
width: 185px
|
||||
}
|
||||
#importexport {
|
||||
top: 118px;
|
||||
width: 185px;
|
||||
}
|
||||
.timeslider-bar {
|
||||
background: #f7f7f7;
|
||||
background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
background: linear-gradient(#f7f7f7, #f1f1f1 80%);
|
||||
overflow: hidden;
|
||||
padding-top: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
.timeslider-bar #editbar {
|
||||
border-bottom: none;
|
||||
float: right;
|
||||
width: 170px;
|
||||
width: initial;
|
||||
}
|
||||
.timeslider-bar h1 {
|
||||
margin: 5px
|
||||
}
|
||||
.timeslider-bar p {
|
||||
margin: 5px
|
||||
}
|
||||
#timeslider-top {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
}
|
||||
#authorsList .author {
|
||||
padding-left: 0.4em;
|
||||
padding-right: 0.4em;
|
||||
}
|
||||
#authorsList .author-anonymous {
|
||||
padding-left: 0.6em;
|
||||
padding-right: 0.6em;
|
||||
}
|
||||
#padeditor {
|
||||
position: static
|
||||
}
|
||||
/* lists */
|
||||
.list-bullet2,
|
||||
.list-indent2,
|
||||
.list-number2 {
|
||||
margin-left: 3em
|
||||
}
|
||||
.list-bullet3,
|
||||
.list-indent3,
|
||||
.list-number3 {
|
||||
margin-left: 4.5em
|
||||
}
|
||||
.list-bullet4,
|
||||
.list-indent4,
|
||||
.list-number4 {
|
||||
margin-left: 6em
|
||||
}
|
||||
.list-bullet5,
|
||||
.list-indent5,
|
||||
.list-number5 {
|
||||
margin-left: 7.5em
|
||||
}
|
||||
.list-bullet6,
|
||||
.list-indent6,
|
||||
.list-number6 {
|
||||
margin-left: 9em
|
||||
}
|
||||
.list-bullet7,
|
||||
.list-indent7,
|
||||
.list-number7 {
|
||||
margin-left: 10.5em
|
||||
}
|
||||
.list-bullet8,
|
||||
.list-indent8,
|
||||
.list-number8 {
|
||||
margin-left: 12em
|
||||
}
|
||||
/* unordered lists */
|
||||
UL {
|
||||
list-style-type: disc;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
UL UL {
|
||||
margin-left: 0 !important
|
||||
}
|
||||
.list-bullet2,
|
||||
.list-bullet5,
|
||||
.list-bullet8 {
|
||||
list-style-type: circle
|
||||
}
|
||||
.list-bullet3,
|
||||
.list-bullet6 {
|
||||
list-style-type: square
|
||||
}
|
||||
.list-indent1,
|
||||
.list-indent2,
|
||||
.list-indent3,
|
||||
.list-indent5,
|
||||
.list-indent5,
|
||||
.list-indent6,
|
||||
.list-indent7,
|
||||
.list-indent8 {
|
||||
list-style-type: none
|
||||
}
|
||||
/* ordered lists */
|
||||
OL {
|
||||
list-style-type: decimal;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
.list-number2,
|
||||
.list-number5,
|
||||
.list-number8 {
|
||||
list-style-type: lower-latin
|
||||
}
|
||||
.list-number3,
|
||||
.list-number6 {
|
||||
list-style-type: lower-roman
|
||||
}
|
||||
/* IE 6/7 fixes */
|
||||
* HTML #ui-slider-handle {
|
||||
background-image: url(../../static/img/current_location.gif)
|
||||
}
|
||||
* HTML #timeslider .star {
|
||||
background-image: url(../../static/img/star.gif)
|
||||
}
|
||||
* HTML #playpause_button_icon {
|
||||
background-image: url(../../static/img/play.gif)
|
||||
}
|
||||
* HTML .pause#playpause_button_icon {
|
||||
background-image: url(../../static/img/pause.gif)
|
||||
}
|
3
src/static/custom/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!.gitignore
|
||||
!*.template
|
8
src/static/custom/css.template
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
custom css files are loaded after core css files. Simply use the same selector to override a style.
|
||||
Example:
|
||||
#editbar LI {border:1px solid #000;}
|
||||
overrides
|
||||
#editbar LI {border:1px solid #d5d5d5;}
|
||||
from pad.css
|
||||
*/
|
6
src/static/custom/js.template
Normal file
|
@ -0,0 +1,6 @@
|
|||
function customStart()
|
||||
{
|
||||
//define your javascript here
|
||||
//jquery is available - except index.js
|
||||
//you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
|
||||
}
|
BIN
src/static/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/static/img/backgrad.gif
Normal file
After Width: | Height: | Size: 697 B |
BIN
src/static/img/connectingbar.gif
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/static/img/crushed_button_depressed.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/static/img/crushed_button_undepressed.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static/img/crushed_current_location.png
Normal file
After Width: | Height: | Size: 1,009 B |
BIN
src/static/img/etherpad_lite_icons.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
src/static/img/fileicons.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/static/img/leftarrow.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
src/static/img/loading.gif
Normal file
After Width: | Height: | Size: 658 B |
BIN
src/static/img/pause.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/static/img/play.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/static/img/roundcorner_left.gif
Normal file
After Width: | Height: | Size: 123 B |
BIN
src/static/img/roundcorner_right.gif
Normal file
After Width: | Height: | Size: 131 B |
BIN
src/static/img/star.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/static/img/stepper_buttons.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/static/img/timeslider_background.png
Normal file
After Width: | Height: | Size: 182 B |
BIN
src/static/img/timeslider_left.png
Normal file
After Width: | Height: | Size: 686 B |
BIN
src/static/img/timeslider_right.png
Normal file
After Width: | Height: | Size: 517 B |
164
src/static/js/AttributeManager.js
Normal file
|
@ -0,0 +1,164 @@
|
|||
var Changeset = require('./Changeset');
|
||||
var ChangesetUtils = require('./ChangesetUtils');
|
||||
var _ = require('./underscore');
|
||||
|
||||
var lineMarkerAttribute = 'lmkr';
|
||||
|
||||
// If one of these attributes are set to the first character of a
|
||||
// line it is considered as a line attribute marker i.e. attributes
|
||||
// set on this marker are applied to the whole line.
|
||||
// The list attribute is only maintained for compatibility reasons
|
||||
var lineAttributes = [lineMarkerAttribute,'list'];
|
||||
|
||||
/*
|
||||
The Attribute manager builds changesets based on a document
|
||||
representation for setting and removing range or line-based attributes.
|
||||
|
||||
@param rep the document representation to be used
|
||||
@param applyChangesetCallback this callback will be called
|
||||
once a changeset has been built.
|
||||
|
||||
|
||||
A document representation contains
|
||||
- an array `alines` containing 1 attributes string for each line
|
||||
- an Attribute pool `apool`
|
||||
- a SkipList `lines` containing the text lines of the document.
|
||||
*/
|
||||
|
||||
var AttributeManager = function(rep, applyChangesetCallback)
|
||||
{
|
||||
this.rep = rep;
|
||||
this.applyChangesetCallback = applyChangesetCallback;
|
||||
this.author = '';
|
||||
|
||||
// If the first char in a line has one of the following attributes
|
||||
// it will be considered as a line marker
|
||||
};
|
||||
|
||||
AttributeManager.lineAttributes = lineAttributes;
|
||||
|
||||
AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
||||
|
||||
applyChangeset: function(changeset){
|
||||
if(!this.applyChangesetCallback) return changeset;
|
||||
|
||||
var cs = changeset.toString();
|
||||
if (!Changeset.isIdentity(cs))
|
||||
{
|
||||
this.applyChangesetCallback(cs);
|
||||
}
|
||||
|
||||
return changeset;
|
||||
},
|
||||
|
||||
/*
|
||||
Sets attributes on a range
|
||||
@param start [row, col] tuple pointing to the start of the range
|
||||
@param end [row, col] tuple pointing to the end of the range
|
||||
@param attribute: an array of attributes
|
||||
*/
|
||||
setAttributesOnRange: function(start, end, attribs)
|
||||
{
|
||||
var builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, start);
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, start, end, attribs, this.rep.apool);
|
||||
return this.applyChangeset(builder);
|
||||
},
|
||||
|
||||
/*
|
||||
Returns if the line already has a line marker
|
||||
@param lineNum: the number of the line
|
||||
*/
|
||||
lineHasMarker: function(lineNum){
|
||||
var that = this;
|
||||
|
||||
return _.find(lineAttributes, function(attribute){
|
||||
return that.getAttributeOnLine(lineNum, attribute) != '';
|
||||
}) !== undefined;
|
||||
},
|
||||
|
||||
/*
|
||||
Gets a specified attribute on a line
|
||||
@param lineNum: the number of the line to set the attribute for
|
||||
@param attributeKey: the name of the attribute to get, e.g. list
|
||||
*/
|
||||
getAttributeOnLine: function(lineNum, attributeName){
|
||||
// get `attributeName` attribute of first char of line
|
||||
var aline = this.rep.alines[lineNum];
|
||||
if (aline)
|
||||
{
|
||||
var opIter = Changeset.opIterator(aline);
|
||||
if (opIter.hasNext())
|
||||
{
|
||||
return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/*
|
||||
Sets a specified attribute on a line
|
||||
@param lineNum: the number of the line to set the attribute for
|
||||
@param attributeKey: the name of the attribute to set, e.g. list
|
||||
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
|
||||
|
||||
*/
|
||||
setAttributeOnLine: function(lineNum, attributeName, attributeValue){
|
||||
var loc = [0,0];
|
||||
var builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
var hasMarker = this.lineHasMarker(lineNum);
|
||||
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||
|
||||
if(hasMarker){
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
||||
[attributeName, attributeValue]
|
||||
], this.rep.apool);
|
||||
}else{
|
||||
// add a line marker
|
||||
builder.insert('*', [
|
||||
['author', this.author],
|
||||
['insertorder', 'first'],
|
||||
[lineMarkerAttribute, '1'],
|
||||
[attributeName, attributeValue]
|
||||
], this.rep.apool);
|
||||
}
|
||||
|
||||
return this.applyChangeset(builder);
|
||||
},
|
||||
|
||||
/*
|
||||
Removes a specified attribute on a line
|
||||
@param lineNum: the number of the affected line
|
||||
@param attributeKey: the name of the attribute to remove, e.g. list
|
||||
|
||||
*/
|
||||
removeAttributeOnLine: function(lineNum, attributeName, attributeValue){
|
||||
|
||||
var loc = [0,0];
|
||||
var builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
var hasMarker = this.lineHasMarker(lineNum);
|
||||
|
||||
if(hasMarker){
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||
ChangesetUtils.buildRemoveRange(this.rep, builder, loc, (loc = [lineNum, 1]));
|
||||
}
|
||||
|
||||
return this.applyChangeset(builder);
|
||||
},
|
||||
|
||||
/*
|
||||
Sets a specified attribute on a line
|
||||
@param lineNum: the number of the line to set the attribute for
|
||||
@param attributeKey: the name of the attribute to set, e.g. list
|
||||
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
|
||||
*/
|
||||
toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) {
|
||||
return this.getAttributeOnLine(attributeName) ?
|
||||
this.removeAttributeOnLine(lineNum, attributeName) :
|
||||
this.setAttributeOnLine(lineNum, attributeName, attributeValue);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AttributeManager;
|
96
src/static/js/AttributePool.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* This code represents the Attribute Pool Object of the original Etherpad.
|
||||
* 90% of the code is still like in the original Etherpad
|
||||
* Look at https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
|
||||
* You can find a explanation what a attribute pool is here:
|
||||
* https://github.com/Pita/etherpad-lite/blob/master/doc/easysync/easysync-notes.txt
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
An AttributePool maintains a mapping from [key,value] Pairs called
|
||||
Attributes to Numbers (unsigened integers) and vice versa. These numbers are
|
||||
used to reference Attributes in Changesets.
|
||||
*/
|
||||
|
||||
var AttributePool = function () {
|
||||
this.numToAttrib = {}; // e.g. {0: ['foo','bar']}
|
||||
this.attribToNum = {}; // e.g. {'foo,bar': 0}
|
||||
this.nextNum = 0;
|
||||
};
|
||||
|
||||
AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) {
|
||||
var str = String(attrib);
|
||||
if (str in this.attribToNum) {
|
||||
return this.attribToNum[str];
|
||||
}
|
||||
if (dontAddIfAbsent) {
|
||||
return -1;
|
||||
}
|
||||
var num = this.nextNum++;
|
||||
this.attribToNum[str] = num;
|
||||
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
|
||||
return num;
|
||||
};
|
||||
|
||||
AttributePool.prototype.getAttrib = function (num) {
|
||||
var pair = this.numToAttrib[num];
|
||||
if (!pair) {
|
||||
return pair;
|
||||
}
|
||||
return [pair[0], pair[1]]; // return a mutable copy
|
||||
};
|
||||
|
||||
AttributePool.prototype.getAttribKey = function (num) {
|
||||
var pair = this.numToAttrib[num];
|
||||
if (!pair) return '';
|
||||
return pair[0];
|
||||
};
|
||||
|
||||
AttributePool.prototype.getAttribValue = function (num) {
|
||||
var pair = this.numToAttrib[num];
|
||||
if (!pair) return '';
|
||||
return pair[1];
|
||||
};
|
||||
|
||||
AttributePool.prototype.eachAttrib = function (func) {
|
||||
for (var n in this.numToAttrib) {
|
||||
var pair = this.numToAttrib[n];
|
||||
func(pair[0], pair[1]);
|
||||
}
|
||||
};
|
||||
|
||||
AttributePool.prototype.toJsonable = function () {
|
||||
return {
|
||||
numToAttrib: this.numToAttrib,
|
||||
nextNum: this.nextNum
|
||||
};
|
||||
};
|
||||
|
||||
AttributePool.prototype.fromJsonable = function (obj) {
|
||||
this.numToAttrib = obj.numToAttrib;
|
||||
this.nextNum = obj.nextNum;
|
||||
this.attribToNum = {};
|
||||
for (var n in this.numToAttrib) {
|
||||
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
module.exports = AttributePool;
|
2184
src/static/js/Changeset.js
Normal file
60
src/static/js/ChangesetUtils.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* This module contains several helper Functions to build Changesets
|
||||
* based on a SkipList
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
exports.buildRemoveRange = function(rep, builder, start, end)
|
||||
{
|
||||
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
var endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
if (end[0] > start[0])
|
||||
{
|
||||
builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]);
|
||||
builder.remove(end[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.remove(end[1] - start[1]);
|
||||
}
|
||||
}
|
||||
|
||||
exports.buildKeepRange = function(rep, builder, start, end, attribs, pool)
|
||||
{
|
||||
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
var endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
if (end[0] > start[0])
|
||||
{
|
||||
builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool);
|
||||
builder.keep(end[1], 0, attribs, pool);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.keep(end[1] - start[1], 0, attribs, pool);
|
||||
}
|
||||
}
|
||||
|
||||
exports.buildKeepToStartOfRange = function(rep, builder, start)
|
||||
{
|
||||
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
|
||||
builder.keep(startLineOffset, start[0]);
|
||||
builder.keep(start[1]);
|
||||
}
|
||||
|
323
src/static/js/ace.js
Normal file
|
@ -0,0 +1,323 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// requires: top
|
||||
// requires: plugins
|
||||
// requires: undefined
|
||||
|
||||
Ace2Editor.registry = {
|
||||
nextId: 1
|
||||
};
|
||||
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
var _ = require('./underscore');
|
||||
|
||||
function Ace2Editor()
|
||||
{
|
||||
var ace2 = Ace2Editor;
|
||||
|
||||
var editor = {};
|
||||
var info = {
|
||||
editor: editor,
|
||||
id: (ace2.registry.nextId++)
|
||||
};
|
||||
var loaded = false;
|
||||
|
||||
var actionsPendingInit = [];
|
||||
|
||||
function pendingInit(func, optDoNow)
|
||||
{
|
||||
return function()
|
||||
{
|
||||
var that = this;
|
||||
var args = arguments;
|
||||
var action = function()
|
||||
{
|
||||
func.apply(that, args);
|
||||
}
|
||||
if (optDoNow)
|
||||
{
|
||||
optDoNow.apply(that, args);
|
||||
}
|
||||
if (loaded)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
actionsPendingInit.push(action);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function doActionsPendingInit()
|
||||
{
|
||||
_.each(actionsPendingInit, function(fn,i){
|
||||
fn()
|
||||
});
|
||||
actionsPendingInit = [];
|
||||
}
|
||||
|
||||
ace2.registry[info.id] = info;
|
||||
|
||||
// The following functions (prefixed by 'ace_') are exposed by editor, but
|
||||
// execution is delayed until init is complete
|
||||
var aceFunctionsPendingInit = ['importText', 'importAText', 'focus',
|
||||
'setEditable', 'getFormattedCode', 'setOnKeyPress', 'setOnKeyDown',
|
||||
'setNotifyDirty', 'setProperty', 'setBaseText', 'setBaseAttributedText',
|
||||
'applyChangesToBase', 'applyPreparedChangesetToBase',
|
||||
'setUserChangeNotificationCallback', 'setAuthorInfo',
|
||||
'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange'];
|
||||
|
||||
_.each(aceFunctionsPendingInit, function(fnName,i){
|
||||
var prefix = 'ace_';
|
||||
var name = prefix + fnName;
|
||||
editor[fnName] = pendingInit(function(){
|
||||
info[prefix + fnName].apply(this, arguments);
|
||||
});
|
||||
});
|
||||
|
||||
editor.exportText = function()
|
||||
{
|
||||
if (!loaded) return "(awaiting init)\n";
|
||||
return info.ace_exportText();
|
||||
};
|
||||
|
||||
editor.getFrame = function()
|
||||
{
|
||||
return info.frame || null;
|
||||
};
|
||||
|
||||
editor.getDebugProperty = function(prop)
|
||||
{
|
||||
return info.ace_getDebugProperty(prop);
|
||||
};
|
||||
|
||||
// prepareUserChangeset:
|
||||
// Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes
|
||||
// to the latest base text into a Changeset, which is returned (as a string if encodeAsString).
|
||||
// If this method returns a truthy value, then applyPreparedChangesetToBase can be called
|
||||
// at some later point to consider these changes part of the base, after which prepareUserChangeset
|
||||
// must be called again before applyPreparedChangesetToBase. Multiple consecutive calls
|
||||
// to prepareUserChangeset will return an updated changeset that takes into account the
|
||||
// latest user changes, and modify the changeset to be applied by applyPreparedChangesetToBase
|
||||
// accordingly.
|
||||
editor.prepareUserChangeset = function()
|
||||
{
|
||||
if (!loaded) return null;
|
||||
return info.ace_prepareUserChangeset();
|
||||
};
|
||||
|
||||
editor.getUnhandledErrors = function()
|
||||
{
|
||||
if (!loaded) return [];
|
||||
// returns array of {error: <browser Error object>, time: +new Date()}
|
||||
return info.ace_getUnhandledErrors();
|
||||
};
|
||||
|
||||
|
||||
|
||||
function sortFilesByEmbeded(files) {
|
||||
var embededFiles = [];
|
||||
var remoteFiles = [];
|
||||
|
||||
if (Ace2Editor.EMBEDED) {
|
||||
for (var i = 0, ii = files.length; i < ii; i++) {
|
||||
var file = files[i];
|
||||
if (Object.prototype.hasOwnProperty.call(Ace2Editor.EMBEDED, file)) {
|
||||
embededFiles.push(file);
|
||||
} else {
|
||||
remoteFiles.push(file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remoteFiles = files;
|
||||
}
|
||||
|
||||
return {embeded: embededFiles, remote: remoteFiles};
|
||||
}
|
||||
function pushRequireScriptTo(buffer) {
|
||||
var KERNEL_SOURCE = '../static/js/require-kernel.js';
|
||||
var KERNEL_BOOT = '\
|
||||
require.setRootURI("../javascripts/src");\n\
|
||||
require.setLibraryURI("../javascripts/lib");\n\
|
||||
require.setGlobalKeyPath("require");\n\
|
||||
';
|
||||
if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) {
|
||||
buffer.push('<script type="text/javascript">');
|
||||
buffer.push(Ace2Editor.EMBEDED[KERNEL_SOURCE]);
|
||||
buffer.push(KERNEL_BOOT);
|
||||
buffer.push('<\/script>');
|
||||
} else {
|
||||
// Remotely src'd script tag will not work in IE; it must be embedded, so
|
||||
// throw an error if it is not.
|
||||
throw new Error("Require script could not be embedded.");
|
||||
}
|
||||
}
|
||||
function pushScriptsTo(buffer) {
|
||||
/* Folling is for packaging regular expression. */
|
||||
/* $$INCLUDE_JS("../javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define"); */
|
||||
/* $$INCLUDE_JS("../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define"); */
|
||||
var ACE_SOURCE = '../javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define';
|
||||
var ACE_COMMON = '../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define';
|
||||
if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[ACE_SOURCE]) {
|
||||
buffer.push('<script type="text/javascript">');
|
||||
buffer.push(Ace2Editor.EMBEDED[ACE_SOURCE]);
|
||||
buffer.push(Ace2Editor.EMBEDED[ACE_COMMON]);
|
||||
buffer.push('<\/script>');
|
||||
} else {
|
||||
buffer.push('<script type="application/javascript" src="' + ACE_SOURCE + '"><\/script>');
|
||||
buffer.push('<script type="application/javascript" src="' + ACE_COMMON + '"><\/script>');
|
||||
}
|
||||
}
|
||||
function pushStyleTagsFor(buffer, files) {
|
||||
var sorted = sortFilesByEmbeded(files);
|
||||
var embededFiles = sorted.embeded;
|
||||
var remoteFiles = sorted.remote;
|
||||
|
||||
if (embededFiles.length > 0) {
|
||||
buffer.push('<style type="text/css">');
|
||||
for (var i = 0, ii = embededFiles.length; i < ii; i++) {
|
||||
var file = embededFiles[i];
|
||||
buffer.push(Ace2Editor.EMBEDED[file].replace(/<\//g, '<\\/'));
|
||||
}
|
||||
buffer.push('<\/style>');
|
||||
}
|
||||
for (var i = 0, ii = remoteFiles.length; i < ii; i++) {
|
||||
var file = remoteFiles[i];
|
||||
buffer.push('<link rel="stylesheet" type="text/css" href="' + file + '"\/>');
|
||||
}
|
||||
}
|
||||
|
||||
editor.destroy = pendingInit(function()
|
||||
{
|
||||
info.ace_dispose();
|
||||
info.frame.parentNode.removeChild(info.frame);
|
||||
delete ace2.registry[info.id];
|
||||
info = null; // prevent IE 6 closure memory leaks
|
||||
});
|
||||
|
||||
editor.init = function(containerId, initialCode, doneFunc)
|
||||
{
|
||||
|
||||
editor.importText(initialCode);
|
||||
|
||||
info.onEditorReady = function()
|
||||
{
|
||||
loaded = true;
|
||||
doActionsPendingInit();
|
||||
doneFunc();
|
||||
};
|
||||
|
||||
(function()
|
||||
{
|
||||
var doctype = "<!doctype html>";
|
||||
|
||||
var iframeHTML = [];
|
||||
|
||||
iframeHTML.push(doctype);
|
||||
iframeHTML.push("<html><head>");
|
||||
|
||||
// calls to these functions ($$INCLUDE_...) are replaced when this file is processed
|
||||
// and compressed, putting the compressed code from the named file directly into the
|
||||
// source here.
|
||||
// these lines must conform to a specific format because they are passed by the build script:
|
||||
var includedCSS = [];
|
||||
var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)};
|
||||
$$INCLUDE_CSS("../static/css/iframe_editor.css");
|
||||
$$INCLUDE_CSS("../static/css/pad.css");
|
||||
$$INCLUDE_CSS("../static/custom/pad.css");
|
||||
|
||||
var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path });
|
||||
includedCSS = includedCSS.concat(additionalCSS);
|
||||
|
||||
pushStyleTagsFor(iframeHTML, includedCSS);
|
||||
|
||||
var includedJS = [];
|
||||
pushRequireScriptTo(iframeHTML);
|
||||
pushScriptsTo(iframeHTML);
|
||||
|
||||
// Inject my plugins into my child.
|
||||
iframeHTML.push('\
|
||||
<script type="text/javascript">\
|
||||
parent_req = require("ep_etherpad-lite/static/js/pluginfw/parent_require");\
|
||||
parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/hooks");\
|
||||
parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/plugins");\
|
||||
</script>\
|
||||
');
|
||||
|
||||
iframeHTML.push('<script type="text/javascript">');
|
||||
iframeHTML.push('$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK');
|
||||
iframeHTML.push('require("ep_etherpad-lite/static/js/ace2_inner");');
|
||||
iframeHTML.push('<\/script>');
|
||||
|
||||
iframeHTML.push('<style type="text/css" title="dynamicsyntax"></style>');
|
||||
|
||||
hooks.callAll("aceInitInnerdocbodyHead", {
|
||||
iframeHTML: iframeHTML
|
||||
});
|
||||
|
||||
iframeHTML.push('</head><body id="innerdocbody" class="syntax" spellcheck="false"> </body></html>');
|
||||
|
||||
// Expose myself to global for my child frame.
|
||||
var thisFunctionsName = "ChildAccessibleAce2Editor";
|
||||
(function () {return this}())[thisFunctionsName] = Ace2Editor;
|
||||
|
||||
var outerScript = 'editorId = "' + info.id + '"; editorInfo = parent.' + thisFunctionsName + '.registry[editorId]; ' + 'window.onload = function() ' + '{ window.onload = null; setTimeout' + '(function() ' + '{ var iframe = document.createElement("IFRAME"); iframe.name = "ace_inner";' + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); ' + 'iframe.frameBorder = 0; iframe.allowTransparency = true; ' + // for IE
|
||||
'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); ' + 'iframe.ace_outerWin = window; ' + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; ' + 'var doc = iframe.contentWindow.document; doc.open(); var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');doc.write(text); doc.close(); ' + '}, 0); }';
|
||||
|
||||
var outerHTML = [doctype, '<html><head>']
|
||||
|
||||
var includedCSS = [];
|
||||
var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)};
|
||||
$$INCLUDE_CSS("../static/css/iframe_editor.css");
|
||||
$$INCLUDE_CSS("../static/css/pad.css");
|
||||
$$INCLUDE_CSS("../static/custom/pad.css");
|
||||
|
||||
|
||||
var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path });
|
||||
includedCSS = includedCSS.concat(additionalCSS);
|
||||
|
||||
pushStyleTagsFor(outerHTML, includedCSS);
|
||||
|
||||
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
|
||||
// (throbs busy while typing)
|
||||
outerHTML.push('<link rel="stylesheet" type="text/css" href="data:text/css,"/>', '\x3cscript>\n', outerScript.replace(/<\//g, '<\\/'), '\n\x3c/script>', '</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>');
|
||||
|
||||
var outerFrame = document.createElement("IFRAME");
|
||||
outerFrame.name = "ace_outer";
|
||||
outerFrame.frameBorder = 0; // for IE
|
||||
info.frame = outerFrame;
|
||||
document.getElementById(containerId).appendChild(outerFrame);
|
||||
|
||||
var editorDocument = outerFrame.contentWindow.document;
|
||||
|
||||
editorDocument.open();
|
||||
editorDocument.write(outerHTML.join(''));
|
||||
editorDocument.close();
|
||||
})();
|
||||
};
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
exports.Ace2Editor = Ace2Editor;
|
106
src/static/js/ace2_common.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var Security = require('./security');
|
||||
|
||||
function isNodeText(node)
|
||||
{
|
||||
return (node.nodeType == 3);
|
||||
}
|
||||
|
||||
function object(o)
|
||||
{
|
||||
var f = function(){};
|
||||
f.prototype = o;
|
||||
return new f();
|
||||
}
|
||||
var userAgent = (((function () {return this;})().navigator || {}).userAgent || 'node-js').toLowerCase();
|
||||
|
||||
// Figure out what browser is being used (stolen from jquery 1.2.1)
|
||||
var browser = {
|
||||
version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1],
|
||||
safari: /webkit/.test(userAgent),
|
||||
opera: /opera/.test(userAgent),
|
||||
msie: /msie/.test(userAgent) && !/opera/.test(userAgent),
|
||||
mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent),
|
||||
windows: /windows/.test(userAgent),
|
||||
mobile: /mobile/.test(userAgent) || /android/.test(userAgent)
|
||||
};
|
||||
|
||||
|
||||
function getAssoc(obj, name)
|
||||
{
|
||||
return obj["_magicdom_" + name];
|
||||
}
|
||||
|
||||
function setAssoc(obj, name, value)
|
||||
{
|
||||
// note that in IE designMode, properties of a node can get
|
||||
// copied to new nodes that are spawned during editing; also,
|
||||
// properties representable in HTML text can survive copy-and-paste
|
||||
obj["_magicdom_" + name] = value;
|
||||
}
|
||||
|
||||
// "func" is a function over 0..(numItems-1) that is monotonically
|
||||
// "increasing" with index (false, then true). Finds the boundary
|
||||
// between false and true, a number between 0 and numItems inclusive.
|
||||
|
||||
|
||||
function binarySearch(numItems, func)
|
||||
{
|
||||
if (numItems < 1) return 0;
|
||||
if (func(0)) return 0;
|
||||
if (!func(numItems - 1)) return numItems;
|
||||
var low = 0; // func(low) is always false
|
||||
var high = numItems - 1; // func(high) is always true
|
||||
while ((high - low) > 1)
|
||||
{
|
||||
var x = Math.floor((low + high) / 2); // x != low, x != high
|
||||
if (func(x)) high = x;
|
||||
else low = x;
|
||||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
function binarySearchInfinite(expectedLength, func)
|
||||
{
|
||||
var i = 0;
|
||||
while (!func(i)) i += expectedLength;
|
||||
return binarySearch(i, func);
|
||||
}
|
||||
|
||||
function htmlPrettyEscape(str)
|
||||
{
|
||||
return Security.escapeHTML(str).replace(/\r?\n/g, '\\n');
|
||||
}
|
||||
|
||||
var noop = function(){};
|
||||
|
||||
exports.isNodeText = isNodeText;
|
||||
exports.object = object;
|
||||
exports.browser = browser;
|
||||
exports.getAssoc = getAssoc;
|
||||
exports.setAssoc = setAssoc;
|
||||
exports.binarySearch = binarySearch;
|
||||
exports.binarySearchInfinite = binarySearchInfinite;
|
||||
exports.htmlPrettyEscape = htmlPrettyEscape;
|
||||
exports.noop = noop;
|
5454
src/static/js/ace2_inner.js
Normal file
144
src/static/js/admin/plugins.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
$(document).ready(function () {
|
||||
|
||||
var socket,
|
||||
loc = document.location,
|
||||
port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port,
|
||||
url = loc.protocol + "//" + loc.hostname + ":" + port + "/",
|
||||
pathComponents = location.pathname.split('/'),
|
||||
// Strip admin/plugins
|
||||
baseURL = pathComponents.slice(0,pathComponents.length-2).join('/') + '/',
|
||||
resource = baseURL.substring(1) + "socket.io";
|
||||
|
||||
//connect
|
||||
socket = io.connect(url, {resource : resource}).of("/pluginfw/installer");
|
||||
|
||||
$('.search-results').data('query', {
|
||||
pattern: '',
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
});
|
||||
|
||||
var doUpdate = false;
|
||||
|
||||
var search = function () {
|
||||
socket.emit("search", $('.search-results').data('query'));
|
||||
}
|
||||
|
||||
function updateHandlers() {
|
||||
$("#progress.dialog .close").unbind('click').click(function () {
|
||||
$("#progress.dialog").hide();
|
||||
});
|
||||
|
||||
$("#do-search").unbind('click').click(function () {
|
||||
var query = $('.search-results').data('query');
|
||||
query.pattern = $("#search-query")[0].value;
|
||||
query.offset = 0;
|
||||
search();
|
||||
});
|
||||
|
||||
$(".do-install").unbind('click').click(function (e) {
|
||||
var row = $(e.target).closest("tr");
|
||||
doUpdate = true;
|
||||
socket.emit("install", row.find(".name").html());
|
||||
});
|
||||
|
||||
$(".do-uninstall").unbind('click').click(function (e) {
|
||||
var row = $(e.target).closest("tr");
|
||||
doUpdate = true;
|
||||
socket.emit("uninstall", row.find(".name").html());
|
||||
});
|
||||
|
||||
$(".do-prev-page").unbind('click').click(function (e) {
|
||||
var query = $('.search-results').data('query');
|
||||
query.offset -= query.limit;
|
||||
if (query.offset < 0) {
|
||||
query.offset = 0;
|
||||
}
|
||||
search();
|
||||
});
|
||||
$(".do-next-page").unbind('click').click(function (e) {
|
||||
var query = $('.search-results').data('query');
|
||||
var total = $('.search-results').data('total');
|
||||
if (query.offset + query.limit < total) {
|
||||
query.offset += query.limit;
|
||||
}
|
||||
search();
|
||||
});
|
||||
}
|
||||
|
||||
updateHandlers();
|
||||
|
||||
socket.on('progress', function (data) {
|
||||
if (data.progress > 0 && $('#progress.dialog').data('progress') > data.progress) return;
|
||||
|
||||
$("#progress.dialog .close").hide();
|
||||
$("#progress.dialog").show();
|
||||
|
||||
$('#progress.dialog').data('progress', data.progress);
|
||||
|
||||
var message = "Unknown status";
|
||||
if (data.message) {
|
||||
message = "<span class='status'>" + data.message.toString() + "</span>";
|
||||
}
|
||||
if (data.error) {
|
||||
message = "<span class='error'>" + data.error.toString() + "<span>";
|
||||
}
|
||||
$("#progress.dialog .message").html(message);
|
||||
$("#progress.dialog .history").append("<div>" + message + "</div>");
|
||||
|
||||
if (data.progress >= 1) {
|
||||
if (data.error) {
|
||||
$("#progress.dialog .close").show();
|
||||
} else {
|
||||
if (doUpdate) {
|
||||
doUpdate = false;
|
||||
socket.emit("load");
|
||||
}
|
||||
$("#progress.dialog").hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('search-result', function (data) {
|
||||
var widget=$(".search-results");
|
||||
|
||||
widget.data('query', data.query);
|
||||
widget.data('total', data.total);
|
||||
|
||||
widget.find('.offset').html(data.query.offset);
|
||||
widget.find('.limit').html(data.query.offset + data.query.limit);
|
||||
widget.find('.total').html(data.total);
|
||||
|
||||
widget.find(".results *").remove();
|
||||
for (plugin_name in data.results) {
|
||||
var plugin = data.results[plugin_name];
|
||||
var row = widget.find(".template tr").clone();
|
||||
|
||||
for (attr in plugin) {
|
||||
row.find("." + attr).html(plugin[attr]);
|
||||
}
|
||||
widget.find(".results").append(row);
|
||||
}
|
||||
|
||||
updateHandlers();
|
||||
});
|
||||
|
||||
socket.on('installed-results', function (data) {
|
||||
$("#installed-plugins *").remove();
|
||||
for (plugin_name in data.results) {
|
||||
if (plugin_name == "ep_etherpad-lite") continue; // Hack...
|
||||
var plugin = data.results[plugin_name];
|
||||
var row = $("#installed-plugin-template").clone();
|
||||
|
||||
for (attr in plugin.package) {
|
||||
row.find("." + attr).html(plugin.package[attr]);
|
||||
}
|
||||
$("#installed-plugins").append(row);
|
||||
}
|
||||
updateHandlers();
|
||||
});
|
||||
|
||||
socket.emit("load");
|
||||
search();
|
||||
|
||||
});
|
579
src/static/js/broadcast.js
Normal file
|
@ -0,0 +1,579 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||
var domline = require('./domline').domline;
|
||||
var AttribPool = require('./AttributePool');
|
||||
var Changeset = require('./Changeset');
|
||||
var linestylefilter = require('./linestylefilter').linestylefilter;
|
||||
var colorutils = require('./colorutils').colorutils;
|
||||
var _ = require('./underscore');
|
||||
|
||||
// These parameters were global, now they are injected. A reference to the
|
||||
// Timeslider controller would probably be more appropriate.
|
||||
function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider)
|
||||
{
|
||||
var changesetLoader = undefined;
|
||||
|
||||
// Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm
|
||||
if (!Array.prototype.indexOf)
|
||||
{
|
||||
Array.prototype.indexOf = function(elt /*, from*/ )
|
||||
{
|
||||
var len = this.length >>> 0;
|
||||
|
||||
var from = Number(arguments[1]) || 0;
|
||||
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
|
||||
if (from < 0) from += len;
|
||||
|
||||
for (; from < len; from++)
|
||||
{
|
||||
if (from in this && this[from] === elt) return from;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
function debugLog()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (window.console) console.log.apply(console, arguments);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
if (window.console) console.log("error printing: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
// for IE
|
||||
if ($.browser.msie)
|
||||
{
|
||||
try
|
||||
{
|
||||
document.execCommand("BackgroundImageCache", false, true);
|
||||
}
|
||||
catch (e)
|
||||
{}
|
||||
}
|
||||
|
||||
|
||||
var socketId;
|
||||
//var socket;
|
||||
var channelState = "DISCONNECTED";
|
||||
|
||||
var appLevelDisconnectReason = null;
|
||||
|
||||
var padContents = {
|
||||
currentRevision: clientVars.collab_client_vars.rev,
|
||||
currentTime: clientVars.collab_client_vars.time,
|
||||
currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
||||
currentDivs: null,
|
||||
// to be filled in once the dom loads
|
||||
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
|
||||
alines: Changeset.splitAttributionLines(
|
||||
clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text),
|
||||
|
||||
// generates a jquery element containing HTML for a line
|
||||
lineToElement: function(line, aline)
|
||||
{
|
||||
var element = document.createElement("div");
|
||||
var emptyLine = (line == '\n');
|
||||
var domInfo = domline.createDomLine(!emptyLine, true);
|
||||
linestylefilter.populateDomLine(line, aline, this.apool, domInfo);
|
||||
domInfo.prepareForAdd();
|
||||
element.className = domInfo.node.className;
|
||||
element.innerHTML = domInfo.node.innerHTML;
|
||||
element.id = Math.random();
|
||||
return $(element);
|
||||
},
|
||||
|
||||
applySpliceToDivs: function(start, numRemoved, newLines)
|
||||
{
|
||||
// remove spliced-out lines from DOM
|
||||
for (var i = start; i < start + numRemoved && i < this.currentDivs.length; i++)
|
||||
{
|
||||
debugLog("removing", this.currentDivs[i].attr('id'));
|
||||
this.currentDivs[i].remove();
|
||||
}
|
||||
|
||||
// remove spliced-out line divs from currentDivs array
|
||||
this.currentDivs.splice(start, numRemoved);
|
||||
|
||||
var newDivs = [];
|
||||
for (var i = 0; i < newLines.length; i++)
|
||||
{
|
||||
newDivs.push(this.lineToElement(newLines[i], this.alines[start + i]));
|
||||
}
|
||||
|
||||
// grab the div just before the first one
|
||||
var startDiv = this.currentDivs[start - 1] || null;
|
||||
|
||||
// insert the div elements into the correct place, in the correct order
|
||||
for (var i = 0; i < newDivs.length; i++)
|
||||
{
|
||||
if (startDiv)
|
||||
{
|
||||
startDiv.after(newDivs[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#padcontent").prepend(newDivs[i]);
|
||||
}
|
||||
startDiv = newDivs[i];
|
||||
}
|
||||
|
||||
// insert new divs into currentDivs array
|
||||
newDivs.unshift(0); // remove 0 elements
|
||||
newDivs.unshift(start);
|
||||
this.currentDivs.splice.apply(this.currentDivs, newDivs);
|
||||
return this;
|
||||
},
|
||||
|
||||
// splice the lines
|
||||
splice: function(start, numRemoved, newLinesVA)
|
||||
{
|
||||
var newLines = _.map(Array.prototype.slice.call(arguments, 2), function(s) {
|
||||
return s;
|
||||
});
|
||||
|
||||
// apply this splice to the divs
|
||||
this.applySpliceToDivs(start, numRemoved, newLines);
|
||||
|
||||
// call currentLines.splice, to keep the currentLines array up to date
|
||||
newLines.unshift(numRemoved);
|
||||
newLines.unshift(start);
|
||||
this.currentLines.splice.apply(this.currentLines, arguments);
|
||||
},
|
||||
// returns the contents of the specified line I
|
||||
get: function(i)
|
||||
{
|
||||
return this.currentLines[i];
|
||||
},
|
||||
// returns the number of lines in the document
|
||||
length: function()
|
||||
{
|
||||
return this.currentLines.length;
|
||||
},
|
||||
|
||||
getActiveAuthors: function()
|
||||
{
|
||||
var self = this;
|
||||
var authors = [];
|
||||
var seenNums = {};
|
||||
var alines = self.alines;
|
||||
for (var i = 0; i < alines.length; i++)
|
||||
{
|
||||
Changeset.eachAttribNumber(alines[i], function(n)
|
||||
{
|
||||
if (!seenNums[n])
|
||||
{
|
||||
seenNums[n] = true;
|
||||
if (self.apool.getAttribKey(n) == 'author')
|
||||
{
|
||||
var a = self.apool.getAttribValue(n);
|
||||
if (a)
|
||||
{
|
||||
authors.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
authors.sort();
|
||||
return authors;
|
||||
}
|
||||
};
|
||||
|
||||
function callCatchingErrors(catcher, func)
|
||||
{
|
||||
try
|
||||
{
|
||||
wrapRecordingErrors(catcher, func)();
|
||||
}
|
||||
catch (e)
|
||||
{ /*absorb*/
|
||||
}
|
||||
}
|
||||
|
||||
function wrapRecordingErrors(catcher, func)
|
||||
{
|
||||
return function()
|
||||
{
|
||||
try
|
||||
{
|
||||
return func.apply(this, Array.prototype.slice.call(arguments));
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// caughtErrors.push(e);
|
||||
// caughtErrorCatchers.push(catcher);
|
||||
// caughtErrorTimes.push(+new Date());
|
||||
// console.dir({catcher: catcher, e: e});
|
||||
debugLog(e); // TODO(kroo): added temporary, to catch errors
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta)
|
||||
{
|
||||
var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest);
|
||||
debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision);
|
||||
revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta);
|
||||
BroadcastSlider.setSliderLength(revisionInfo.latest);
|
||||
if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta);
|
||||
}
|
||||
|
||||
/*
|
||||
At this point, we must be certain that the changeset really does map from
|
||||
the current revision to the specified revision. Any mistakes here will
|
||||
cause the whole slider to get out of sync.
|
||||
*/
|
||||
|
||||
function applyChangeset(changeset, revision, preventSliderMovement, timeDelta)
|
||||
{
|
||||
// disable the next 'gotorevision' call handled by a timeslider update
|
||||
if (!preventSliderMovement)
|
||||
{
|
||||
goToRevisionIfEnabledCount++;
|
||||
BroadcastSlider.setSliderPosition(revision);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// must mutate attribution lines before text lines
|
||||
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
debugLog(e);
|
||||
}
|
||||
|
||||
Changeset.mutateTextLines(changeset, padContents);
|
||||
padContents.currentRevision = revision;
|
||||
padContents.currentTime += timeDelta;
|
||||
debugLog('Time Delta: ', timeDelta)
|
||||
updateTimer();
|
||||
|
||||
var authors = _.map(padContents.getActiveAuthors(), function(name)
|
||||
{
|
||||
return authorData[name];
|
||||
});
|
||||
|
||||
BroadcastSlider.setAuthors(authors);
|
||||
}
|
||||
|
||||
function updateTimer()
|
||||
{
|
||||
var zpad = function(str, length)
|
||||
{
|
||||
str = str + "";
|
||||
while (str.length < length)
|
||||
str = '0' + str;
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var date = new Date(padContents.currentTime);
|
||||
var dateFormat = function()
|
||||
{
|
||||
var month = zpad(date.getMonth() + 1, 2);
|
||||
var day = zpad(date.getDate(), 2);
|
||||
var year = (date.getFullYear());
|
||||
var hours = zpad(date.getHours(), 2);
|
||||
var minutes = zpad(date.getMinutes(), 2);
|
||||
var seconds = zpad(date.getSeconds(), 2);
|
||||
return ([month, '/', day, '/', year, ' ', hours, ':', minutes, ':', seconds].join(""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$('#timer').html(dateFormat());
|
||||
|
||||
var revisionDate = ["Saved", ["Jan", "Feb", "March", "April", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][date.getMonth()], date.getDate() + ",", date.getFullYear()].join(" ")
|
||||
$('#revision_date').html(revisionDate)
|
||||
|
||||
}
|
||||
|
||||
updateTimer();
|
||||
|
||||
function goToRevision(newRevision)
|
||||
{
|
||||
padContents.targetRevision = newRevision;
|
||||
var self = this;
|
||||
var path = revisionInfo.getPath(padContents.currentRevision, newRevision);
|
||||
debugLog('newRev: ', padContents.currentRevision, path);
|
||||
if (path.status == 'complete')
|
||||
{
|
||||
var cs = path.changesets;
|
||||
debugLog("status: complete, changesets: ", cs, "path:", path);
|
||||
var changeset = cs[0];
|
||||
var timeDelta = path.times[0];
|
||||
for (var i = 1; i < cs.length; i++)
|
||||
{
|
||||
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
|
||||
timeDelta += path.times[i];
|
||||
}
|
||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||
}
|
||||
else if (path.status == "partial")
|
||||
{
|
||||
debugLog('partial');
|
||||
var sliderLocation = padContents.currentRevision;
|
||||
// callback is called after changeset information is pulled from server
|
||||
// this may never get called, if the changeset has already been loaded
|
||||
var update = function(start, end)
|
||||
{
|
||||
// if we've called goToRevision in the time since, don't goToRevision
|
||||
goToRevision(padContents.targetRevision);
|
||||
};
|
||||
|
||||
// do our best with what we have...
|
||||
var cs = path.changesets;
|
||||
|
||||
var changeset = cs[0];
|
||||
var timeDelta = path.times[0];
|
||||
for (var i = 1; i < cs.length; i++)
|
||||
{
|
||||
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
|
||||
timeDelta += path.times[i];
|
||||
}
|
||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||
|
||||
|
||||
if (BroadcastSlider.getSliderLength() > 10000)
|
||||
{
|
||||
var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10
|
||||
changesetLoader.queueUp(start, 100);
|
||||
}
|
||||
|
||||
if (BroadcastSlider.getSliderLength() > 1000)
|
||||
{
|
||||
var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1
|
||||
changesetLoader.queueUp(start, 10);
|
||||
}
|
||||
|
||||
start = (Math.floor((newRevision) / 100) * 100);
|
||||
|
||||
changesetLoader.queueUp(start, 1, update);
|
||||
}
|
||||
|
||||
var authors = _.map(padContents.getActiveAuthors(), function(name){
|
||||
return authorData[name];
|
||||
});
|
||||
BroadcastSlider.setAuthors(authors);
|
||||
}
|
||||
|
||||
changesetLoader = {
|
||||
running: false,
|
||||
resolved: [],
|
||||
requestQueue1: [],
|
||||
requestQueue2: [],
|
||||
requestQueue3: [],
|
||||
reqCallbacks: [],
|
||||
queueUp: function(revision, width, callback)
|
||||
{
|
||||
if (revision < 0) revision = 0;
|
||||
// if(changesetLoader.requestQueue.indexOf(revision) != -1)
|
||||
// return; // already in the queue.
|
||||
if (changesetLoader.resolved.indexOf(revision + "_" + width) != -1) return; // already loaded from the server
|
||||
changesetLoader.resolved.push(revision + "_" + width);
|
||||
|
||||
var requestQueue = width == 1 ? changesetLoader.requestQueue3 : width == 10 ? changesetLoader.requestQueue2 : changesetLoader.requestQueue1;
|
||||
requestQueue.push(
|
||||
{
|
||||
'rev': revision,
|
||||
'res': width,
|
||||
'callback': callback
|
||||
});
|
||||
if (!changesetLoader.running)
|
||||
{
|
||||
changesetLoader.running = true;
|
||||
setTimeout(changesetLoader.loadFromQueue, 10);
|
||||
}
|
||||
},
|
||||
loadFromQueue: function()
|
||||
{
|
||||
var self = changesetLoader;
|
||||
var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : self.requestQueue2.length > 0 ? self.requestQueue2 : self.requestQueue3.length > 0 ? self.requestQueue3 : null;
|
||||
|
||||
if (!requestQueue)
|
||||
{
|
||||
self.running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var request = requestQueue.pop();
|
||||
var granularity = request.res;
|
||||
var callback = request.callback;
|
||||
var start = request.rev;
|
||||
var requestID = Math.floor(Math.random() * 100000);
|
||||
|
||||
sendSocketMsg("CHANGESET_REQ", {
|
||||
"start": start,
|
||||
"granularity": granularity,
|
||||
"requestID": requestID
|
||||
});
|
||||
|
||||
self.reqCallbacks[requestID] = callback;
|
||||
},
|
||||
handleSocketResponse: function(message)
|
||||
{
|
||||
var self = changesetLoader;
|
||||
|
||||
var start = message.data.start;
|
||||
var granularity = message.data.granularity;
|
||||
var callback = self.reqCallbacks[message.data.requestID];
|
||||
delete self.reqCallbacks[message.data.requestID];
|
||||
|
||||
self.handleResponse(message.data, start, granularity, callback);
|
||||
setTimeout(self.loadFromQueue, 10);
|
||||
},
|
||||
handleResponse: function(data, start, granularity, callback)
|
||||
{
|
||||
debugLog("response: ", data);
|
||||
var pool = (new AttribPool()).fromJsonable(data.apool);
|
||||
for (var i = 0; i < data.forwardsChangesets.length; i++)
|
||||
{
|
||||
var astart = start + i * granularity - 1; // rev -1 is a blank single line
|
||||
var aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
|
||||
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
|
||||
debugLog("adding changeset:", astart, aend);
|
||||
var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
||||
var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
|
||||
revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
|
||||
}
|
||||
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
|
||||
},
|
||||
handleMessageFromServer: function (obj)
|
||||
{
|
||||
debugLog("handleMessage:", arguments);
|
||||
|
||||
if (obj.type == "COLLABROOM")
|
||||
{
|
||||
obj = obj.data;
|
||||
|
||||
if (obj.type == "NEW_CHANGES")
|
||||
{
|
||||
debugLog(obj);
|
||||
var changeset = Changeset.moveOpsToNewPool(
|
||||
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
var changesetBack = Changeset.inverse(
|
||||
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
||||
|
||||
var changesetBack = Changeset.moveOpsToNewPool(
|
||||
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
||||
}
|
||||
else if (obj.type == "NEW_AUTHORDATA")
|
||||
{
|
||||
var authorMap = {};
|
||||
authorMap[obj.author] = obj.data;
|
||||
receiveAuthorData(authorMap);
|
||||
|
||||
var authors = _.map(padContents.getActiveAuthors(), function(name) {
|
||||
return authorData[name];
|
||||
});
|
||||
|
||||
BroadcastSlider.setAuthors(authors);
|
||||
}
|
||||
else if (obj.type == "NEW_SAVEDREV")
|
||||
{
|
||||
var savedRev = obj.savedRev;
|
||||
BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
|
||||
}
|
||||
}
|
||||
else if(obj.type == "CHANGESET_REQ")
|
||||
{
|
||||
changesetLoader.handleSocketResponse(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
debugLog("Unknown message type: " + obj.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// to start upon window load, just push a function onto this array
|
||||
//window['onloadFuncts'].push(setUpSocket);
|
||||
//window['onloadFuncts'].push(function ()
|
||||
fireWhenAllScriptsAreLoaded.push(function()
|
||||
{
|
||||
// set up the currentDivs and DOM
|
||||
padContents.currentDivs = [];
|
||||
$("#padcontent").html("");
|
||||
for (var i = 0; i < padContents.currentLines.length; i++)
|
||||
{
|
||||
var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]);
|
||||
padContents.currentDivs.push(div);
|
||||
$("#padcontent").append(div);
|
||||
}
|
||||
debugLog(padContents.currentDivs);
|
||||
});
|
||||
|
||||
// this is necessary to keep infinite loops of events firing,
|
||||
// since goToRevision changes the slider position
|
||||
var goToRevisionIfEnabledCount = 0;
|
||||
var goToRevisionIfEnabled = function() {
|
||||
if (goToRevisionIfEnabledCount > 0)
|
||||
{
|
||||
goToRevisionIfEnabledCount--;
|
||||
}
|
||||
else
|
||||
{
|
||||
goToRevision.apply(goToRevision, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
BroadcastSlider.onSlider(goToRevisionIfEnabled);
|
||||
|
||||
var dynamicCSS = makeCSSManager('dynamicsyntax');
|
||||
var authorData = {};
|
||||
|
||||
function receiveAuthorData(newAuthorData)
|
||||
{
|
||||
for (var author in newAuthorData)
|
||||
{
|
||||
var data = newAuthorData[author];
|
||||
var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId;
|
||||
if (bgcolor && dynamicCSS)
|
||||
{
|
||||
var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author));
|
||||
selector.backgroundColor = bgcolor
|
||||
selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000'; //see ace2_inner.js for the other part
|
||||
}
|
||||
authorData[author] = data;
|
||||
}
|
||||
}
|
||||
|
||||
receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);
|
||||
|
||||
return changesetLoader;
|
||||
}
|
||||
|
||||
exports.loadBroadcastJS = loadBroadcastJS;
|
128
src/static/js/broadcast_revisions.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
// revision info is a skip list whos entries represent a particular revision
|
||||
// of the document. These revisions are connected together by various
|
||||
// changesets, or deltas, between any two revisions.
|
||||
|
||||
function loadBroadcastRevisionsJS()
|
||||
{
|
||||
function Revision(revNum)
|
||||
{
|
||||
this.rev = revNum;
|
||||
this.changesets = [];
|
||||
}
|
||||
|
||||
Revision.prototype.addChangeset = function(destIndex, changeset, timeDelta)
|
||||
{
|
||||
var changesetWrapper = {
|
||||
deltaRev: destIndex - this.rev,
|
||||
deltaTime: timeDelta,
|
||||
getValue: function()
|
||||
{
|
||||
return changeset;
|
||||
}
|
||||
};
|
||||
this.changesets.push(changesetWrapper);
|
||||
this.changesets.sort(function(a, b)
|
||||
{
|
||||
return (b.deltaRev - a.deltaRev)
|
||||
});
|
||||
}
|
||||
|
||||
revisionInfo = {};
|
||||
revisionInfo.addChangeset = function(fromIndex, toIndex, changeset, backChangeset, timeDelta)
|
||||
{
|
||||
var startRevision = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex);
|
||||
var endRevision = revisionInfo[toIndex] || revisionInfo.createNew(toIndex);
|
||||
startRevision.addChangeset(toIndex, changeset, timeDelta);
|
||||
endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta);
|
||||
}
|
||||
|
||||
revisionInfo.latest = clientVars.collab_client_vars.rev || -1;
|
||||
|
||||
revisionInfo.createNew = function(index)
|
||||
{
|
||||
revisionInfo[index] = new Revision(index);
|
||||
if (index > revisionInfo.latest)
|
||||
{
|
||||
revisionInfo.latest = index;
|
||||
}
|
||||
|
||||
return revisionInfo[index];
|
||||
}
|
||||
|
||||
// assuming that there is a path from fromIndex to toIndex, and that the links
|
||||
// are laid out in a skip-list format
|
||||
revisionInfo.getPath = function(fromIndex, toIndex)
|
||||
{
|
||||
var changesets = [];
|
||||
var spans = [];
|
||||
var times = [];
|
||||
var elem = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex);
|
||||
if (elem.changesets.length != 0 && fromIndex != toIndex)
|
||||
{
|
||||
var reverse = !(fromIndex < toIndex)
|
||||
while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse))
|
||||
{
|
||||
var couldNotContinue = false;
|
||||
var oldRev = elem.rev;
|
||||
|
||||
for (var i = reverse ? elem.changesets.length - 1 : 0;
|
||||
reverse ? i >= 0 : i < elem.changesets.length;
|
||||
i += reverse ? -1 : 1)
|
||||
{
|
||||
if (((elem.changesets[i].deltaRev < 0) && !reverse) || ((elem.changesets[i].deltaRev > 0) && reverse))
|
||||
{
|
||||
couldNotContinue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) || ((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse))
|
||||
{
|
||||
var topush = elem.changesets[i];
|
||||
changesets.push(topush.getValue());
|
||||
spans.push(elem.changesets[i].deltaRev);
|
||||
times.push(topush.deltaTime);
|
||||
elem = revisionInfo[elem.rev + elem.changesets[i].deltaRev];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (couldNotContinue || oldRev == elem.rev) break;
|
||||
}
|
||||
}
|
||||
|
||||
var status = 'partial';
|
||||
if (elem.rev == toIndex) status = 'complete';
|
||||
|
||||
return {
|
||||
'fromRev': fromIndex,
|
||||
'rev': elem.rev,
|
||||
'status': status,
|
||||
'changesets': changesets,
|
||||
'spans': spans,
|
||||
'times': times
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;
|
495
src/static/js/broadcast_slider.js
Normal file
|
@ -0,0 +1,495 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// These parameters were global, now they are injected. A reference to the
|
||||
// Timeslider controller would probably be more appropriate.
|
||||
var _ = require('./underscore');
|
||||
var padmodals = require('./pad_modals').padmodals;
|
||||
|
||||
function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
|
||||
{
|
||||
var BroadcastSlider;
|
||||
|
||||
(function()
|
||||
{ // wrap this code in its own namespace
|
||||
var sliderLength = 1000;
|
||||
var sliderPos = 0;
|
||||
var sliderActive = false;
|
||||
var slidercallbacks = [];
|
||||
var savedRevisions = [];
|
||||
var sliderPlaying = false;
|
||||
|
||||
function disableSelection(element)
|
||||
{
|
||||
element.onselectstart = function()
|
||||
{
|
||||
return false;
|
||||
};
|
||||
element.unselectable = "on";
|
||||
element.style.MozUserSelect = "none";
|
||||
element.style.cursor = "default";
|
||||
}
|
||||
var _callSliderCallbacks = function(newval)
|
||||
{
|
||||
sliderPos = newval;
|
||||
for (var i = 0; i < slidercallbacks.length; i++)
|
||||
{
|
||||
slidercallbacks[i](newval);
|
||||
}
|
||||
}
|
||||
|
||||
var updateSliderElements = function()
|
||||
{
|
||||
for (var i = 0; i < savedRevisions.length; i++)
|
||||
{
|
||||
var position = parseInt(savedRevisions[i].attr('pos'));
|
||||
savedRevisions[i].css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1);
|
||||
}
|
||||
$("#ui-slider-handle").css('left', sliderPos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0));
|
||||
}
|
||||
|
||||
var addSavedRevision = function(position, info)
|
||||
{
|
||||
var newSavedRevision = $('<div></div>');
|
||||
newSavedRevision.addClass("star");
|
||||
|
||||
newSavedRevision.attr('pos', position);
|
||||
newSavedRevision.css('position', 'absolute');
|
||||
newSavedRevision.css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1);
|
||||
$("#timeslider-slider").append(newSavedRevision);
|
||||
newSavedRevision.mouseup(function(evt)
|
||||
{
|
||||
BroadcastSlider.setSliderPosition(position);
|
||||
});
|
||||
savedRevisions.push(newSavedRevision);
|
||||
};
|
||||
|
||||
var removeSavedRevision = function(position)
|
||||
{
|
||||
var element = $("div.star [pos=" + position + "]");
|
||||
savedRevisions.remove(element);
|
||||
element.remove();
|
||||
return element;
|
||||
};
|
||||
|
||||
/* Begin small 'API' */
|
||||
|
||||
function onSlider(callback)
|
||||
{
|
||||
slidercallbacks.push(callback);
|
||||
}
|
||||
|
||||
function getSliderPosition()
|
||||
{
|
||||
return sliderPos;
|
||||
}
|
||||
|
||||
function setSliderPosition(newpos)
|
||||
{
|
||||
newpos = Number(newpos);
|
||||
if (newpos < 0 || newpos > sliderLength) return;
|
||||
$("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0));
|
||||
$("a.tlink").map(function()
|
||||
{
|
||||
$(this).attr('href', $(this).attr('thref').replace("%revision%", newpos));
|
||||
});
|
||||
$("#revision_label").html("Version " + newpos);
|
||||
|
||||
if (newpos == 0)
|
||||
{
|
||||
$("#leftstar").css('opacity', .5);
|
||||
$("#leftstep").css('opacity', .5);
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#leftstar").css('opacity', 1);
|
||||
$("#leftstep").css('opacity', 1);
|
||||
}
|
||||
|
||||
if (newpos == sliderLength)
|
||||
{
|
||||
$("#rightstar").css('opacity', .5);
|
||||
$("#rightstep").css('opacity', .5);
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#rightstar").css('opacity', 1);
|
||||
$("#rightstep").css('opacity', 1);
|
||||
}
|
||||
|
||||
sliderPos = newpos;
|
||||
_callSliderCallbacks(newpos);
|
||||
}
|
||||
|
||||
function getSliderLength()
|
||||
{
|
||||
return sliderLength;
|
||||
}
|
||||
|
||||
function setSliderLength(newlength)
|
||||
{
|
||||
sliderLength = newlength;
|
||||
updateSliderElements();
|
||||
}
|
||||
|
||||
// just take over the whole slider screen with a reconnect message
|
||||
|
||||
function showReconnectUI()
|
||||
{
|
||||
padmodals.showModal("disconnected");
|
||||
}
|
||||
|
||||
var fixPadHeight = _.throttle(function(){
|
||||
var height = $('#timeslider-top').height();
|
||||
$('#editorcontainerbox').css({marginTop: height});
|
||||
}, 600);
|
||||
|
||||
function setAuthors(authors)
|
||||
{
|
||||
var authorsList = $("#authorsList");
|
||||
authorsList.empty();
|
||||
var numAnonymous = 0;
|
||||
var numNamed = 0;
|
||||
var colorsAnonymous = [];
|
||||
_.each(authors, function(author)
|
||||
{
|
||||
var authorColor = clientVars.colorPalette[author.colorId] || author.colorId;
|
||||
if (author.name)
|
||||
{
|
||||
if (numNamed !== 0) authorsList.append(', ');
|
||||
|
||||
$('<span />')
|
||||
.text(author.name || "unnamed")
|
||||
.css('background-color', authorColor)
|
||||
.addClass('author')
|
||||
.appendTo(authorsList);
|
||||
|
||||
numNamed++;
|
||||
}
|
||||
else
|
||||
{
|
||||
numAnonymous++;
|
||||
if(authorColor) colorsAnonymous.push(authorColor);
|
||||
}
|
||||
});
|
||||
if (numAnonymous > 0)
|
||||
{
|
||||
var anonymousAuthorString = numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "")
|
||||
if (numNamed !== 0){
|
||||
authorsList.append(' + ' + anonymousAuthorString);
|
||||
} else {
|
||||
authorsList.append(anonymousAuthorString);
|
||||
}
|
||||
|
||||
if(colorsAnonymous.length > 0){
|
||||
authorsList.append(' (');
|
||||
_.each(colorsAnonymous, function(color, i){
|
||||
if( i > 0 ) authorsList.append(' ');
|
||||
$('<span> </span>')
|
||||
.css('background-color', color)
|
||||
.addClass('author author-anonymous')
|
||||
.appendTo(authorsList);
|
||||
});
|
||||
authorsList.append(')');
|
||||
}
|
||||
|
||||
}
|
||||
if (authors.length == 0)
|
||||
{
|
||||
authorsList.append("No Authors");
|
||||
}
|
||||
|
||||
fixPadHeight();
|
||||
}
|
||||
|
||||
BroadcastSlider = {
|
||||
onSlider: onSlider,
|
||||
getSliderPosition: getSliderPosition,
|
||||
setSliderPosition: setSliderPosition,
|
||||
getSliderLength: getSliderLength,
|
||||
setSliderLength: setSliderLength,
|
||||
isSliderActive: function()
|
||||
{
|
||||
return sliderActive;
|
||||
},
|
||||
playpause: playpause,
|
||||
addSavedRevision: addSavedRevision,
|
||||
showReconnectUI: showReconnectUI,
|
||||
setAuthors: setAuthors
|
||||
}
|
||||
|
||||
function playButtonUpdater()
|
||||
{
|
||||
if (sliderPlaying)
|
||||
{
|
||||
if (getSliderPosition() + 1 > sliderLength)
|
||||
{
|
||||
$("#playpause_button_icon").toggleClass('pause');
|
||||
sliderPlaying = false;
|
||||
return;
|
||||
}
|
||||
setSliderPosition(getSliderPosition() + 1);
|
||||
|
||||
setTimeout(playButtonUpdater, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function playpause()
|
||||
{
|
||||
$("#playpause_button_icon").toggleClass('pause');
|
||||
|
||||
if (!sliderPlaying)
|
||||
{
|
||||
if (getSliderPosition() == sliderLength) setSliderPosition(0);
|
||||
sliderPlaying = true;
|
||||
playButtonUpdater();
|
||||
}
|
||||
else
|
||||
{
|
||||
sliderPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
// assign event handlers to html UI elements after page load
|
||||
//$(window).load(function ()
|
||||
fireWhenAllScriptsAreLoaded.push(function()
|
||||
{
|
||||
disableSelection($("#playpause_button")[0]);
|
||||
disableSelection($("#timeslider")[0]);
|
||||
|
||||
$(document).keyup(function(e)
|
||||
{
|
||||
var code = -1;
|
||||
if (!e) var e = window.event;
|
||||
if (e.keyCode) code = e.keyCode;
|
||||
else if (e.which) code = e.which;
|
||||
|
||||
if (code == 37)
|
||||
{ // left
|
||||
if (!e.shiftKey)
|
||||
{
|
||||
setSliderPosition(getSliderPosition() - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
var nextStar = 0; // default to first revision in document
|
||||
for (var i = 0; i < savedRevisions.length; i++)
|
||||
{
|
||||
var pos = parseInt(savedRevisions[i].attr('pos'));
|
||||
if (pos < getSliderPosition() && nextStar < pos) nextStar = pos;
|
||||
}
|
||||
setSliderPosition(nextStar);
|
||||
}
|
||||
}
|
||||
else if (code == 39)
|
||||
{
|
||||
if (!e.shiftKey)
|
||||
{
|
||||
setSliderPosition(getSliderPosition() + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
var nextStar = sliderLength; // default to last revision in document
|
||||
for (var i = 0; i < savedRevisions.length; i++)
|
||||
{
|
||||
var pos = parseInt(savedRevisions[i].attr('pos'));
|
||||
if (pos > getSliderPosition() && nextStar > pos) nextStar = pos;
|
||||
}
|
||||
setSliderPosition(nextStar);
|
||||
}
|
||||
}
|
||||
else if (code == 32) playpause();
|
||||
|
||||
});
|
||||
|
||||
$(window).resize(function()
|
||||
{
|
||||
updateSliderElements();
|
||||
});
|
||||
|
||||
$("#ui-slider-bar").mousedown(function(evt)
|
||||
{
|
||||
setSliderPosition(Math.floor((evt.clientX - $("#ui-slider-bar").offset().left) * sliderLength / 742));
|
||||
$("#ui-slider-handle").css('left', (evt.clientX - $("#ui-slider-bar").offset().left));
|
||||
$("#ui-slider-handle").trigger(evt);
|
||||
});
|
||||
|
||||
// Slider dragging
|
||||
$("#ui-slider-handle").mousedown(function(evt)
|
||||
{
|
||||
this.startLoc = evt.clientX;
|
||||
this.currentLoc = parseInt($(this).css('left'));
|
||||
var self = this;
|
||||
sliderActive = true;
|
||||
$(document).mousemove(function(evt2)
|
||||
{
|
||||
$(self).css('pointer', 'move')
|
||||
var newloc = self.currentLoc + (evt2.clientX - self.startLoc);
|
||||
if (newloc < 0) newloc = 0;
|
||||
if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2);
|
||||
$("#revision_label").html("Version " + Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2)));
|
||||
$(self).css('left', newloc);
|
||||
if (getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) _callSliderCallbacks(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2)))
|
||||
});
|
||||
$(document).mouseup(function(evt2)
|
||||
{
|
||||
$(document).unbind('mousemove');
|
||||
$(document).unbind('mouseup');
|
||||
sliderActive = false;
|
||||
var newloc = self.currentLoc + (evt2.clientX - self.startLoc);
|
||||
if (newloc < 0) newloc = 0;
|
||||
if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2);
|
||||
$(self).css('left', newloc);
|
||||
// if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2)))
|
||||
setSliderPosition(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2)))
|
||||
self.currentLoc = parseInt($(self).css('left'));
|
||||
});
|
||||
})
|
||||
|
||||
// play/pause toggling
|
||||
$("#playpause_button").mousedown(function(evt)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
$(self).css('background-image', 'url(/static/img/crushed_button_depressed.png)');
|
||||
$(self).mouseup(function(evt2)
|
||||
{
|
||||
$(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)');
|
||||
$(self).unbind('mouseup');
|
||||
BroadcastSlider.playpause();
|
||||
});
|
||||
$(document).mouseup(function(evt2)
|
||||
{
|
||||
$(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)');
|
||||
$(document).unbind('mouseup');
|
||||
});
|
||||
});
|
||||
|
||||
// next/prev saved revision and changeset
|
||||
$('.stepper').mousedown(function(evt)
|
||||
{
|
||||
var self = this;
|
||||
var origcss = $(self).css('background-position');
|
||||
if (!origcss)
|
||||
{
|
||||
origcss = $(self).css('background-position-x') + " " + $(self).css('background-position-y');
|
||||
}
|
||||
var origpos = parseInt(origcss.split(" ")[1]);
|
||||
var newpos = (origpos - 43);
|
||||
if (newpos < 0) newpos += 87;
|
||||
|
||||
var newcss = (origcss.split(" ")[0] + " " + newpos + "px");
|
||||
if ($(self).css('opacity') != 1.0) newcss = origcss;
|
||||
|
||||
$(self).css('background-position', newcss)
|
||||
|
||||
$(self).mouseup(function(evt2)
|
||||
{
|
||||
$(self).css('background-position', origcss);
|
||||
$(self).unbind('mouseup');
|
||||
$(document).unbind('mouseup');
|
||||
if ($(self).attr("id") == ("leftstep"))
|
||||
{
|
||||
setSliderPosition(getSliderPosition() - 1);
|
||||
}
|
||||
else if ($(self).attr("id") == ("rightstep"))
|
||||
{
|
||||
setSliderPosition(getSliderPosition() + 1);
|
||||
}
|
||||
else if ($(self).attr("id") == ("leftstar"))
|
||||
{
|
||||
var nextStar = 0; // default to first revision in document
|
||||
for (var i = 0; i < savedRevisions.length; i++)
|
||||
{
|
||||
var pos = parseInt(savedRevisions[i].attr('pos'));
|
||||
if (pos < getSliderPosition() && nextStar < pos) nextStar = pos;
|
||||
}
|
||||
setSliderPosition(nextStar);
|
||||
}
|
||||
else if ($(self).attr("id") == ("rightstar"))
|
||||
{
|
||||
var nextStar = sliderLength; // default to last revision in document
|
||||
for (var i = 0; i < savedRevisions.length; i++)
|
||||
{
|
||||
var pos = parseInt(savedRevisions[i].attr('pos'));
|
||||
if (pos > getSliderPosition() && nextStar > pos) nextStar = pos;
|
||||
}
|
||||
setSliderPosition(nextStar);
|
||||
}
|
||||
});
|
||||
$(document).mouseup(function(evt2)
|
||||
{
|
||||
$(self).css('background-position', origcss);
|
||||
$(self).unbind('mouseup');
|
||||
$(document).unbind('mouseup');
|
||||
});
|
||||
})
|
||||
|
||||
if (clientVars)
|
||||
{
|
||||
if (clientVars.fullWidth)
|
||||
{
|
||||
$("#padpage").css('width', '100%');
|
||||
$("#revision").css('position', "absolute")
|
||||
$("#revision").css('right', "20px")
|
||||
$("#revision").css('top', "20px")
|
||||
$("#padmain").css('left', '0px');
|
||||
$("#padmain").css('right', '197px');
|
||||
$("#padmain").css('width', 'auto');
|
||||
$("#rightbars").css('right', '7px');
|
||||
$("#rightbars").css('margin-right', '0px');
|
||||
$("#timeslider").css('width', 'auto');
|
||||
}
|
||||
|
||||
if (clientVars.disableRightBar)
|
||||
{
|
||||
$("#rightbars").css('display', 'none');
|
||||
$('#padmain').css('width', 'auto');
|
||||
if (clientVars.fullWidth) $("#padmain").css('right', '7px');
|
||||
else $("#padmain").css('width', '860px');
|
||||
$("#revision").css('position', "absolute");
|
||||
$("#revision").css('right', "20px");
|
||||
$("#revision").css('top', "20px");
|
||||
}
|
||||
|
||||
$("#timeslider").show();
|
||||
setSliderLength(clientVars.collab_client_vars.rev);
|
||||
setSliderPosition(clientVars.collab_client_vars.rev);
|
||||
|
||||
_.each(clientVars.savedRevisions, function(revision)
|
||||
{
|
||||
addSavedRevision(revision.revNum, revision);
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
BroadcastSlider.onSlider(function(loc)
|
||||
{
|
||||
$("#viewlatest").html(loc == BroadcastSlider.getSliderLength() ? "Viewing latest content" : "View latest content");
|
||||
})
|
||||
|
||||
return BroadcastSlider;
|
||||
}
|
||||
|
||||
exports.loadBroadcastSliderJS = loadBroadcastSliderJS;
|
213
src/static/js/changesettracker.js
Normal file
|
@ -0,0 +1,213 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var AttributePool = require('./AttributePool');
|
||||
var Changeset = require('./Changeset');
|
||||
|
||||
function makeChangesetTracker(scheduler, apool, aceCallbacksProvider)
|
||||
{
|
||||
|
||||
// latest official text from server
|
||||
var baseAText = Changeset.makeAText("\n");
|
||||
// changes applied to baseText that have been submitted
|
||||
var submittedChangeset = null;
|
||||
// changes applied to submittedChangeset since it was prepared
|
||||
var userChangeset = Changeset.identity(1);
|
||||
// is the changesetTracker enabled
|
||||
var tracking = false;
|
||||
// stack state flag so that when we change the rep we don't
|
||||
// handle the notification recursively. When setting, always
|
||||
// unset in a "finally" block. When set to true, the setter
|
||||
// takes change of userChangeset.
|
||||
var applyingNonUserChanges = false;
|
||||
|
||||
var changeCallback = null;
|
||||
|
||||
var changeCallbackTimeout = null;
|
||||
|
||||
function setChangeCallbackTimeout()
|
||||
{
|
||||
// can call this multiple times per call-stack, because
|
||||
// we only schedule a call to changeCallback if it exists
|
||||
// and if there isn't a timeout already scheduled.
|
||||
if (changeCallback && changeCallbackTimeout === null)
|
||||
{
|
||||
changeCallbackTimeout = scheduler.setTimeout(function()
|
||||
{
|
||||
try
|
||||
{
|
||||
changeCallback();
|
||||
}
|
||||
finally
|
||||
{
|
||||
changeCallbackTimeout = null;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
var self;
|
||||
return self = {
|
||||
isTracking: function()
|
||||
{
|
||||
return tracking;
|
||||
},
|
||||
setBaseText: function(text)
|
||||
{
|
||||
self.setBaseAttributedText(Changeset.makeAText(text), null);
|
||||
},
|
||||
setBaseAttributedText: function(atext, apoolJsonObj)
|
||||
{
|
||||
aceCallbacksProvider.withCallbacks("setBaseText", function(callbacks)
|
||||
{
|
||||
tracking = true;
|
||||
baseAText = Changeset.cloneAText(atext);
|
||||
if (apoolJsonObj)
|
||||
{
|
||||
var wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
||||
}
|
||||
submittedChangeset = null;
|
||||
userChangeset = Changeset.identity(atext.text.length);
|
||||
applyingNonUserChanges = true;
|
||||
try
|
||||
{
|
||||
callbacks.setDocumentAttributedText(atext);
|
||||
}
|
||||
finally
|
||||
{
|
||||
applyingNonUserChanges = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
composeUserChangeset: function(c)
|
||||
{
|
||||
if (!tracking) return;
|
||||
if (applyingNonUserChanges) return;
|
||||
if (Changeset.isIdentity(c)) return;
|
||||
userChangeset = Changeset.compose(userChangeset, c, apool);
|
||||
|
||||
setChangeCallbackTimeout();
|
||||
},
|
||||
applyChangesToBase: function(c, optAuthor, apoolJsonObj)
|
||||
{
|
||||
if (!tracking) return;
|
||||
|
||||
aceCallbacksProvider.withCallbacks("applyChangesToBase", function(callbacks)
|
||||
{
|
||||
|
||||
if (apoolJsonObj)
|
||||
{
|
||||
var wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
|
||||
}
|
||||
|
||||
baseAText = Changeset.applyToAText(c, baseAText, apool);
|
||||
|
||||
var c2 = c;
|
||||
if (submittedChangeset)
|
||||
{
|
||||
var oldSubmittedChangeset = submittedChangeset;
|
||||
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
|
||||
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
|
||||
}
|
||||
|
||||
var preferInsertingAfterUserChanges = true;
|
||||
var oldUserChangeset = userChangeset;
|
||||
userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
|
||||
var postChange = Changeset.follow(oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
|
||||
|
||||
var preferInsertionAfterCaret = true; //(optAuthor && optAuthor > thisAuthor);
|
||||
applyingNonUserChanges = true;
|
||||
try
|
||||
{
|
||||
callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret);
|
||||
}
|
||||
finally
|
||||
{
|
||||
applyingNonUserChanges = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
prepareUserChangeset: function()
|
||||
{
|
||||
// If there are user changes to submit, 'changeset' will be the
|
||||
// changeset, else it will be null.
|
||||
var toSubmit;
|
||||
if (submittedChangeset)
|
||||
{
|
||||
// submission must have been canceled, prepare new changeset
|
||||
// that includes old submittedChangeset
|
||||
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
|
||||
else toSubmit = userChangeset;
|
||||
}
|
||||
|
||||
var cs = null;
|
||||
if (toSubmit)
|
||||
{
|
||||
submittedChangeset = toSubmit;
|
||||
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
|
||||
|
||||
cs = toSubmit;
|
||||
}
|
||||
var wireApool = null;
|
||||
if (cs)
|
||||
{
|
||||
var forWire = Changeset.prepareForWire(cs, apool);
|
||||
wireApool = forWire.pool.toJsonable();
|
||||
cs = forWire.translated;
|
||||
}
|
||||
|
||||
var data = {
|
||||
changeset: cs,
|
||||
apool: wireApool
|
||||
};
|
||||
return data;
|
||||
},
|
||||
applyPreparedChangesetToBase: function()
|
||||
{
|
||||
if (!submittedChangeset)
|
||||
{
|
||||
// violation of protocol; use prepareUserChangeset first
|
||||
throw new Error("applySubmittedChangesToBase: no submitted changes to apply");
|
||||
}
|
||||
//bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
|
||||
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
|
||||
submittedChangeset = null;
|
||||
},
|
||||
setUserChangeNotificationCallback: function(callback)
|
||||
{
|
||||
changeCallback = callback;
|
||||
},
|
||||
hasUncommittedChanges: function()
|
||||
{
|
||||
return !!(submittedChangeset || (!Changeset.isIdentity(userChangeset)));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
exports.makeChangesetTracker = makeChangesetTracker;
|
173
src/static/js/chat.js
Normal file
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
var padcookie = require('./pad_cookie').padcookie;
|
||||
|
||||
require('./tinycon');
|
||||
|
||||
var chat = (function()
|
||||
{
|
||||
var isStuck = false;
|
||||
var chatMentions = 0;
|
||||
var self = {
|
||||
show: function ()
|
||||
{
|
||||
$("#chaticon").hide();
|
||||
$("#chatbox").show();
|
||||
self.scrollDown();
|
||||
chatMentions = 0;
|
||||
Tinycon.setBubble(0);
|
||||
},
|
||||
stickToScreen: function(fromInitialCall) // Make chat stick to right hand side of screen
|
||||
{
|
||||
chat.show();
|
||||
if(!isStuck || fromInitialCall) { // Stick it to
|
||||
padcookie.setPref("chatAlwaysVisible", true);
|
||||
$('#chatbox').addClass("stickyChat");
|
||||
$('#chattext').css({"top":"0px"});
|
||||
$('#editorcontainer').css({"right":"192px", "width":"auto"});
|
||||
isStuck = true;
|
||||
} else { // Unstick it
|
||||
padcookie.setPref("chatAlwaysVisible", false);
|
||||
$('#chatbox').removeClass("stickyChat");
|
||||
$('#chattext').css({"top":"25px"});
|
||||
$('#editorcontainer').css({"right":"0px", "width":"100%"});
|
||||
isStuck = false;
|
||||
}
|
||||
},
|
||||
hide: function ()
|
||||
{
|
||||
$("#chatcounter").text("0");
|
||||
$("#chaticon").show();
|
||||
$("#chatbox").hide();
|
||||
},
|
||||
scrollDown: function()
|
||||
{
|
||||
if($('#chatbox').css("display") != "none"){
|
||||
if(!self.lastMessage || !self.lastMessage.position() || self.lastMessage.position().top < $('#chattext').height()) {
|
||||
$('#chattext').animate({scrollTop: $('#chattext')[0].scrollHeight}, "slow");
|
||||
self.lastMessage = $('#chattext > p').eq(-1);
|
||||
}
|
||||
}
|
||||
},
|
||||
send: function()
|
||||
{
|
||||
var text = $("#chatinput").val();
|
||||
this._pad.collabClient.sendMessage({"type": "CHAT_MESSAGE", "text": text});
|
||||
$("#chatinput").val("");
|
||||
},
|
||||
addMessage: function(msg, increment)
|
||||
{
|
||||
//correct the time
|
||||
msg.time += this._pad.clientTimeOffset;
|
||||
|
||||
//create the time string
|
||||
var minutes = "" + new Date(msg.time).getMinutes();
|
||||
var hours = "" + new Date(msg.time).getHours();
|
||||
if(minutes.length == 1)
|
||||
minutes = "0" + minutes ;
|
||||
if(hours.length == 1)
|
||||
hours = "0" + hours ;
|
||||
var timeStr = hours + ":" + minutes;
|
||||
|
||||
//create the authorclass
|
||||
var authorClass = "author-" + msg.userId.replace(/[^a-y0-9]/g, function(c)
|
||||
{
|
||||
if (c == ".") return "-";
|
||||
return 'z' + c.charCodeAt(0) + 'z';
|
||||
});
|
||||
|
||||
var text = padutils.escapeHtmlWithClickableLinks(msg.text, "_blank");
|
||||
|
||||
/* Performs an action if your name is mentioned */
|
||||
var myName = $('#myusernameedit').val();
|
||||
myName = myName.toLowerCase();
|
||||
var chatText = text.toLowerCase();
|
||||
var wasMentioned = false;
|
||||
if (chatText.indexOf(myName) !== -1 && myName != "undefined"){
|
||||
wasMentioned = true;
|
||||
}
|
||||
/* End of new action */
|
||||
|
||||
var authorName = msg.userName == null ? "unnamed" : padutils.escapeHtml(msg.userName);
|
||||
|
||||
var html = "<p class='" + authorClass + "'><b>" + authorName + ":</b><span class='time " + authorClass + "'>" + timeStr + "</span> " + text + "</p>";
|
||||
$("#chattext").append(html);
|
||||
|
||||
//should we increment the counter??
|
||||
if(increment)
|
||||
{
|
||||
var count = Number($("#chatcounter").text());
|
||||
count++;
|
||||
|
||||
// is the users focus already in the chatbox?
|
||||
var alreadyFocused = $("#chatinput").is(":focus");
|
||||
|
||||
$("#chatcounter").text(count);
|
||||
// chat throb stuff -- Just make it throw for twice as long
|
||||
if(wasMentioned && !alreadyFocused)
|
||||
{ // If the user was mentioned show for twice as long and flash the browser window
|
||||
$('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text).show().delay(4000).hide(400);
|
||||
chatMentions++;
|
||||
Tinycon.setBubble(chatMentions);
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text).show().delay(2000).hide(400);
|
||||
}
|
||||
}
|
||||
// Clear the chat mentions when the user clicks on the chat input box
|
||||
$('#chatinput').click(function(){
|
||||
chatMentions = 0;
|
||||
Tinycon.setBubble(0);
|
||||
});
|
||||
self.scrollDown();
|
||||
|
||||
},
|
||||
init: function(pad)
|
||||
{
|
||||
this._pad = pad;
|
||||
$("#chatinput").keypress(function(evt)
|
||||
{
|
||||
//if the user typed enter, fire the send
|
||||
if(evt.which == 13)
|
||||
{
|
||||
evt.preventDefault();
|
||||
self.send();
|
||||
}
|
||||
});
|
||||
|
||||
var that = this;
|
||||
$.each(clientVars.chatHistory, function(i, o){
|
||||
that.addMessage(o, false);
|
||||
})
|
||||
|
||||
$("#chatcounter").text(clientVars.chatHistory.length);
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.chat = chat;
|
||||
|
595
src/static/js/collab_client.js
Normal file
|
@ -0,0 +1,595 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var chat = require('./chat').chat;
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
|
||||
// Dependency fill on init. This exists for `pad.socket` only.
|
||||
// TODO: bind directly to the socket.
|
||||
var pad = undefined;
|
||||
function getSocket() {
|
||||
return pad && pad.socket;
|
||||
}
|
||||
|
||||
/** Call this when the document is ready, and a new Ace2Editor() has been created and inited.
|
||||
ACE's ready callback does not need to have fired yet.
|
||||
"serverVars" are from calling doc.getCollabClientVars() on the server. */
|
||||
function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
||||
{
|
||||
var editor = ace2editor;
|
||||
pad = _pad; // Inject pad to avoid a circular dependency.
|
||||
|
||||
var rev = serverVars.rev;
|
||||
var padId = serverVars.padId;
|
||||
var globalPadId = serverVars.globalPadId;
|
||||
|
||||
var state = "IDLE";
|
||||
var stateMessage;
|
||||
var stateMessageSocketId;
|
||||
var channelState = "CONNECTING";
|
||||
var appLevelDisconnectReason = null;
|
||||
|
||||
var lastCommitTime = 0;
|
||||
var initialStartConnectTime = 0;
|
||||
|
||||
var userId = initialUserInfo.userId;
|
||||
var socketId;
|
||||
//var socket;
|
||||
var userSet = {}; // userId -> userInfo
|
||||
userSet[userId] = initialUserInfo;
|
||||
|
||||
var reconnectTimes = [];
|
||||
var caughtErrors = [];
|
||||
var caughtErrorCatchers = [];
|
||||
var caughtErrorTimes = [];
|
||||
var debugMessages = [];
|
||||
|
||||
tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
|
||||
tellAceActiveAuthorInfo(initialUserInfo);
|
||||
|
||||
var callbacks = {
|
||||
onUserJoin: function()
|
||||
{},
|
||||
onUserLeave: function()
|
||||
{},
|
||||
onUpdateUserInfo: function()
|
||||
{},
|
||||
onChannelStateChange: function()
|
||||
{},
|
||||
onClientMessage: function()
|
||||
{},
|
||||
onInternalAction: function()
|
||||
{},
|
||||
onConnectionTrouble: function()
|
||||
{},
|
||||
onServerMessage: function()
|
||||
{}
|
||||
};
|
||||
|
||||
if ($.browser.mozilla)
|
||||
{
|
||||
// Prevent "escape" from taking effect and canceling a comet connection;
|
||||
// doesn't work if focus is on an iframe.
|
||||
$(window).bind("keydown", function(evt)
|
||||
{
|
||||
if (evt.which == 27)
|
||||
{
|
||||
evt.preventDefault()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editor.setProperty("userAuthor", userId);
|
||||
editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool);
|
||||
editor.setUserChangeNotificationCallback(wrapRecordingErrors("handleUserChanges", handleUserChanges));
|
||||
|
||||
function dmesg(str)
|
||||
{
|
||||
if (typeof window.ajlog == "string") window.ajlog += str + '\n';
|
||||
debugMessages.push(str);
|
||||
}
|
||||
|
||||
function handleUserChanges()
|
||||
{
|
||||
if ((!getSocket()) || channelState == "CONNECTING")
|
||||
{
|
||||
if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000))
|
||||
{
|
||||
setChannelState("DISCONNECTED", "initsocketfail");
|
||||
}
|
||||
else
|
||||
{
|
||||
// check again in a bit
|
||||
setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), 1000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var t = (+new Date());
|
||||
|
||||
if (state != "IDLE")
|
||||
{
|
||||
if (state == "COMMITTING" && (t - lastCommitTime) > 20000)
|
||||
{
|
||||
// a commit is taking too long
|
||||
setChannelState("DISCONNECTED", "slowcommit");
|
||||
}
|
||||
else if (state == "COMMITTING" && (t - lastCommitTime) > 5000)
|
||||
{
|
||||
callbacks.onConnectionTrouble("SLOW");
|
||||
}
|
||||
else
|
||||
{
|
||||
// run again in a few seconds, to detect a disconnect
|
||||
setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), 3000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var earliestCommit = lastCommitTime + 500;
|
||||
if (t < earliestCommit)
|
||||
{
|
||||
setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), earliestCommit - t);
|
||||
return;
|
||||
}
|
||||
|
||||
var sentMessage = false;
|
||||
var userChangesData = editor.prepareUserChangeset();
|
||||
if (userChangesData.changeset)
|
||||
{
|
||||
lastCommitTime = t;
|
||||
state = "COMMITTING";
|
||||
stateMessage = {
|
||||
type: "USER_CHANGES",
|
||||
baseRev: rev,
|
||||
changeset: userChangesData.changeset,
|
||||
apool: userChangesData.apool
|
||||
};
|
||||
stateMessageSocketId = socketId;
|
||||
sendMessage(stateMessage);
|
||||
sentMessage = true;
|
||||
callbacks.onInternalAction("commitPerformed");
|
||||
}
|
||||
|
||||
if (sentMessage)
|
||||
{
|
||||
// run again in a few seconds, to detect a disconnect
|
||||
setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function getStats()
|
||||
{
|
||||
var stats = {};
|
||||
|
||||
stats.screen = [$(window).width(), $(window).height(), window.screen.availWidth, window.screen.availHeight, window.screen.width, window.screen.height].join(',');
|
||||
stats.ip = serverVars.clientIp;
|
||||
stats.useragent = serverVars.clientAgent;
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
function setUpSocket()
|
||||
{
|
||||
hiccupCount = 0;
|
||||
setChannelState("CONNECTED");
|
||||
doDeferredActions();
|
||||
|
||||
initialStartConnectTime = +new Date();
|
||||
}
|
||||
|
||||
var hiccupCount = 0;
|
||||
|
||||
function sendMessage(msg)
|
||||
{
|
||||
getSocket().json.send(
|
||||
{
|
||||
type: "COLLABROOM",
|
||||
component: "pad",
|
||||
data: msg
|
||||
});
|
||||
}
|
||||
|
||||
function wrapRecordingErrors(catcher, func)
|
||||
{
|
||||
return function()
|
||||
{
|
||||
try
|
||||
{
|
||||
return func.apply(this, Array.prototype.slice.call(arguments));
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
caughtErrors.push(e);
|
||||
caughtErrorCatchers.push(catcher);
|
||||
caughtErrorTimes.push(+new Date());
|
||||
//console.dir({catcher: catcher, e: e});
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function callCatchingErrors(catcher, func)
|
||||
{
|
||||
try
|
||||
{
|
||||
wrapRecordingErrors(catcher, func)();
|
||||
}
|
||||
catch (e)
|
||||
{ /*absorb*/
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessageFromServer(evt)
|
||||
{
|
||||
if (window.console) console.log(evt);
|
||||
|
||||
if (!getSocket()) return;
|
||||
if (!evt.data) return;
|
||||
var wrapper = evt;
|
||||
if (wrapper.type != "COLLABROOM") return;
|
||||
var msg = wrapper.data;
|
||||
if (msg.type == "NEW_CHANGES")
|
||||
{
|
||||
var newRev = msg.newRev;
|
||||
var changeset = msg.changeset;
|
||||
var author = (msg.author || '');
|
||||
var apool = msg.apool;
|
||||
if (newRev != (rev + 1))
|
||||
{
|
||||
dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (rev + 1));
|
||||
setChannelState("DISCONNECTED", "badmessage_newchanges");
|
||||
return;
|
||||
}
|
||||
rev = newRev;
|
||||
editor.applyChangesToBase(changeset, author, apool);
|
||||
}
|
||||
else if (msg.type == "ACCEPT_COMMIT")
|
||||
{
|
||||
var newRev = msg.newRev;
|
||||
if (newRev != (rev + 1))
|
||||
{
|
||||
dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (rev + 1));
|
||||
setChannelState("DISCONNECTED", "badmessage_acceptcommit");
|
||||
return;
|
||||
}
|
||||
rev = newRev;
|
||||
editor.applyPreparedChangesetToBase();
|
||||
setStateIdle();
|
||||
callCatchingErrors("onInternalAction", function()
|
||||
{
|
||||
callbacks.onInternalAction("commitAcceptedByServer");
|
||||
});
|
||||
callCatchingErrors("onConnectionTrouble", function()
|
||||
{
|
||||
callbacks.onConnectionTrouble("OK");
|
||||
});
|
||||
handleUserChanges();
|
||||
}
|
||||
else if (msg.type == "NO_COMMIT_PENDING")
|
||||
{
|
||||
if (state == "COMMITTING")
|
||||
{
|
||||
// server missed our commit message; abort that commit
|
||||
setStateIdle();
|
||||
handleUserChanges();
|
||||
}
|
||||
}
|
||||
else if (msg.type == "USER_NEWINFO")
|
||||
{
|
||||
var userInfo = msg.userInfo;
|
||||
var id = userInfo.userId;
|
||||
|
||||
if (userSet[id])
|
||||
{
|
||||
userSet[id] = userInfo;
|
||||
callbacks.onUpdateUserInfo(userInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
userSet[id] = userInfo;
|
||||
callbacks.onUserJoin(userInfo);
|
||||
}
|
||||
tellAceActiveAuthorInfo(userInfo);
|
||||
}
|
||||
else if (msg.type == "USER_LEAVE")
|
||||
{
|
||||
var userInfo = msg.userInfo;
|
||||
var id = userInfo.userId;
|
||||
if (userSet[id])
|
||||
{
|
||||
delete userSet[userInfo.userId];
|
||||
fadeAceAuthorInfo(userInfo);
|
||||
callbacks.onUserLeave(userInfo);
|
||||
}
|
||||
}
|
||||
else if (msg.type == "DISCONNECT_REASON")
|
||||
{
|
||||
appLevelDisconnectReason = msg.reason;
|
||||
}
|
||||
else if (msg.type == "CLIENT_MESSAGE")
|
||||
{
|
||||
callbacks.onClientMessage(msg.payload);
|
||||
}
|
||||
else if (msg.type == "CHAT_MESSAGE")
|
||||
{
|
||||
chat.addMessage(msg, true);
|
||||
}
|
||||
else if (msg.type == "SERVER_MESSAGE")
|
||||
{
|
||||
callbacks.onServerMessage(msg.payload);
|
||||
}
|
||||
hooks.callAll('handleClientMessage_' + msg.type, {payload: msg.payload});
|
||||
}
|
||||
|
||||
function updateUserInfo(userInfo)
|
||||
{
|
||||
userInfo.userId = userId;
|
||||
userSet[userId] = userInfo;
|
||||
tellAceActiveAuthorInfo(userInfo);
|
||||
if (!getSocket()) return;
|
||||
sendMessage(
|
||||
{
|
||||
type: "USERINFO_UPDATE",
|
||||
userInfo: userInfo
|
||||
});
|
||||
}
|
||||
|
||||
function tellAceActiveAuthorInfo(userInfo)
|
||||
{
|
||||
tellAceAuthorInfo(userInfo.userId, userInfo.colorId);
|
||||
}
|
||||
|
||||
function tellAceAuthorInfo(userId, colorId, inactive)
|
||||
{
|
||||
if(typeof colorId == "number")
|
||||
{
|
||||
colorId = clientVars.colorPalette[colorId];
|
||||
}
|
||||
|
||||
var cssColor = colorId;
|
||||
if (inactive)
|
||||
{
|
||||
editor.setAuthorInfo(userId, {
|
||||
bgcolor: cssColor,
|
||||
fade: 0.5
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
editor.setAuthorInfo(userId, {
|
||||
bgcolor: cssColor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fadeAceAuthorInfo(userInfo)
|
||||
{
|
||||
tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true);
|
||||
}
|
||||
|
||||
function getConnectedUsers()
|
||||
{
|
||||
return valuesArray(userSet);
|
||||
}
|
||||
|
||||
function tellAceAboutHistoricalAuthors(hadata)
|
||||
{
|
||||
for (var author in hadata)
|
||||
{
|
||||
var data = hadata[author];
|
||||
if (!userSet[author])
|
||||
{
|
||||
tellAceAuthorInfo(author, data.colorId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setChannelState(newChannelState, moreInfo)
|
||||
{
|
||||
if (newChannelState != channelState)
|
||||
{
|
||||
channelState = newChannelState;
|
||||
callbacks.onChannelStateChange(channelState, moreInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function keys(obj)
|
||||
{
|
||||
var array = [];
|
||||
$.each(obj, function(k, v)
|
||||
{
|
||||
array.push(k);
|
||||
});
|
||||
return array;
|
||||
}
|
||||
|
||||
function valuesArray(obj)
|
||||
{
|
||||
var array = [];
|
||||
$.each(obj, function(k, v)
|
||||
{
|
||||
array.push(v);
|
||||
});
|
||||
return array;
|
||||
}
|
||||
|
||||
// We need to present a working interface even before the socket
|
||||
// is connected for the first time.
|
||||
var deferredActions = [];
|
||||
|
||||
function defer(func, tag)
|
||||
{
|
||||
return function()
|
||||
{
|
||||
var that = this;
|
||||
var args = arguments;
|
||||
|
||||
function action()
|
||||
{
|
||||
func.apply(that, args);
|
||||
}
|
||||
action.tag = tag;
|
||||
if (channelState == "CONNECTING")
|
||||
{
|
||||
deferredActions.push(action);
|
||||
}
|
||||
else
|
||||
{
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doDeferredActions(tag)
|
||||
{
|
||||
var newArray = [];
|
||||
for (var i = 0; i < deferredActions.length; i++)
|
||||
{
|
||||
var a = deferredActions[i];
|
||||
if ((!tag) || (tag == a.tag))
|
||||
{
|
||||
a();
|
||||
}
|
||||
else
|
||||
{
|
||||
newArray.push(a);
|
||||
}
|
||||
}
|
||||
deferredActions = newArray;
|
||||
}
|
||||
|
||||
function sendClientMessage(msg)
|
||||
{
|
||||
sendMessage(
|
||||
{
|
||||
type: "CLIENT_MESSAGE",
|
||||
payload: msg
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentRevisionNumber()
|
||||
{
|
||||
return rev;
|
||||
}
|
||||
|
||||
function getMissedChanges()
|
||||
{
|
||||
var obj = {};
|
||||
obj.userInfo = userSet[userId];
|
||||
obj.baseRev = rev;
|
||||
if (state == "COMMITTING" && stateMessage)
|
||||
{
|
||||
obj.committedChangeset = stateMessage.changeset;
|
||||
obj.committedChangesetAPool = stateMessage.apool;
|
||||
obj.committedChangesetSocketId = stateMessageSocketId;
|
||||
editor.applyPreparedChangesetToBase();
|
||||
}
|
||||
var userChangesData = editor.prepareUserChangeset();
|
||||
if (userChangesData.changeset)
|
||||
{
|
||||
obj.furtherChangeset = userChangesData.changeset;
|
||||
obj.furtherChangesetAPool = userChangesData.apool;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function setStateIdle()
|
||||
{
|
||||
state = "IDLE";
|
||||
callbacks.onInternalAction("newlyIdle");
|
||||
schedulePerhapsCallIdleFuncs();
|
||||
}
|
||||
|
||||
function callWhenNotCommitting(func)
|
||||
{
|
||||
idleFuncs.push(func);
|
||||
schedulePerhapsCallIdleFuncs();
|
||||
}
|
||||
|
||||
var idleFuncs = [];
|
||||
|
||||
function schedulePerhapsCallIdleFuncs()
|
||||
{
|
||||
setTimeout(function()
|
||||
{
|
||||
if (state == "IDLE")
|
||||
{
|
||||
while (idleFuncs.length > 0)
|
||||
{
|
||||
var f = idleFuncs.shift();
|
||||
f();
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
var self = {
|
||||
setOnUserJoin: function(cb)
|
||||
{
|
||||
callbacks.onUserJoin = cb;
|
||||
},
|
||||
setOnUserLeave: function(cb)
|
||||
{
|
||||
callbacks.onUserLeave = cb;
|
||||
},
|
||||
setOnUpdateUserInfo: function(cb)
|
||||
{
|
||||
callbacks.onUpdateUserInfo = cb;
|
||||
},
|
||||
setOnChannelStateChange: function(cb)
|
||||
{
|
||||
callbacks.onChannelStateChange = cb;
|
||||
},
|
||||
setOnClientMessage: function(cb)
|
||||
{
|
||||
callbacks.onClientMessage = cb;
|
||||
},
|
||||
setOnInternalAction: function(cb)
|
||||
{
|
||||
callbacks.onInternalAction = cb;
|
||||
},
|
||||
setOnConnectionTrouble: function(cb)
|
||||
{
|
||||
callbacks.onConnectionTrouble = cb;
|
||||
},
|
||||
setOnServerMessage: function(cb)
|
||||
{
|
||||
callbacks.onServerMessage = cb;
|
||||
},
|
||||
updateUserInfo: defer(updateUserInfo),
|
||||
handleMessageFromServer: handleMessageFromServer,
|
||||
getConnectedUsers: getConnectedUsers,
|
||||
sendClientMessage: sendClientMessage,
|
||||
sendMessage: sendMessage,
|
||||
getCurrentRevisionNumber: getCurrentRevisionNumber,
|
||||
getMissedChanges: getMissedChanges,
|
||||
callWhenNotCommitting: callWhenNotCommitting,
|
||||
addHistoricalAuthors: tellAceAboutHistoricalAuthors,
|
||||
setChannelState: setChannelState
|
||||
};
|
||||
|
||||
$(document).ready(setUpSocket);
|
||||
return self;
|
||||
}
|
||||
|
||||
exports.getCollabClient = getCollabClient;
|
138
src/static/js/colorutils.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js
|
||||
// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var colorutils = {};
|
||||
|
||||
// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0]
|
||||
colorutils.css2triple = function(cssColor)
|
||||
{
|
||||
var sixHex = colorutils.css2sixhex(cssColor);
|
||||
|
||||
function hexToFloat(hh)
|
||||
{
|
||||
return Number("0x" + hh) / 255;
|
||||
}
|
||||
return [hexToFloat(sixHex.substr(0, 2)), hexToFloat(sixHex.substr(2, 2)), hexToFloat(sixHex.substr(4, 2))];
|
||||
}
|
||||
|
||||
// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff"
|
||||
colorutils.css2sixhex = function(cssColor)
|
||||
{
|
||||
var h = /[0-9a-fA-F]+/.exec(cssColor)[0];
|
||||
if (h.length != 6)
|
||||
{
|
||||
var a = h.charAt(0);
|
||||
var b = h.charAt(1);
|
||||
var c = h.charAt(2);
|
||||
h = a + a + b + b + c + c;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// [1.0, 1.0, 1.0] -> "#ffffff"
|
||||
colorutils.triple2css = function(triple)
|
||||
{
|
||||
function floatToHex(n)
|
||||
{
|
||||
var n2 = colorutils.clamp(Math.round(n * 255), 0, 255);
|
||||
return ("0" + n2.toString(16)).slice(-2);
|
||||
}
|
||||
return "#" + floatToHex(triple[0]) + floatToHex(triple[1]) + floatToHex(triple[2]);
|
||||
}
|
||||
|
||||
|
||||
colorutils.clamp = function(v, bot, top)
|
||||
{
|
||||
return v < bot ? bot : (v > top ? top : v);
|
||||
};
|
||||
colorutils.min3 = function(a, b, c)
|
||||
{
|
||||
return (a < b) ? (a < c ? a : c) : (b < c ? b : c);
|
||||
};
|
||||
colorutils.max3 = function(a, b, c)
|
||||
{
|
||||
return (a > b) ? (a > c ? a : c) : (b > c ? b : c);
|
||||
};
|
||||
colorutils.colorMin = function(c)
|
||||
{
|
||||
return colorutils.min3(c[0], c[1], c[2]);
|
||||
};
|
||||
colorutils.colorMax = function(c)
|
||||
{
|
||||
return colorutils.max3(c[0], c[1], c[2]);
|
||||
};
|
||||
colorutils.scale = function(v, bot, top)
|
||||
{
|
||||
return colorutils.clamp(bot + v * (top - bot), 0, 1);
|
||||
};
|
||||
colorutils.unscale = function(v, bot, top)
|
||||
{
|
||||
return colorutils.clamp((v - bot) / (top - bot), 0, 1);
|
||||
};
|
||||
|
||||
colorutils.scaleColor = function(c, bot, top)
|
||||
{
|
||||
return [colorutils.scale(c[0], bot, top), colorutils.scale(c[1], bot, top), colorutils.scale(c[2], bot, top)];
|
||||
}
|
||||
|
||||
colorutils.unscaleColor = function(c, bot, top)
|
||||
{
|
||||
return [colorutils.unscale(c[0], bot, top), colorutils.unscale(c[1], bot, top), colorutils.unscale(c[2], bot, top)];
|
||||
}
|
||||
|
||||
colorutils.luminosity = function(c)
|
||||
{
|
||||
// rule of thumb for RGB brightness; 1.0 is white
|
||||
return c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11;
|
||||
}
|
||||
|
||||
colorutils.saturate = function(c)
|
||||
{
|
||||
var min = colorutils.colorMin(c);
|
||||
var max = colorutils.colorMax(c);
|
||||
if (max - min <= 0) return [1.0, 1.0, 1.0];
|
||||
return colorutils.unscaleColor(c, min, max);
|
||||
}
|
||||
|
||||
colorutils.blend = function(c1, c2, t)
|
||||
{
|
||||
return [colorutils.scale(t, c1[0], c2[0]), colorutils.scale(t, c1[1], c2[1]), colorutils.scale(t, c1[2], c2[2])];
|
||||
}
|
||||
|
||||
colorutils.invert = function(c)
|
||||
{
|
||||
return [1 - c[0], 1 - c[1], 1- c[2]];
|
||||
}
|
||||
|
||||
colorutils.complementary = function(c)
|
||||
{
|
||||
var inv = colorutils.invert(c);
|
||||
return [
|
||||
(inv[0] >= c[0]) ? Math.min(inv[0] * 1.30, 1) : (c[0] * 0.30),
|
||||
(inv[1] >= c[1]) ? Math.min(inv[1] * 1.59, 1) : (c[1] * 0.59),
|
||||
(inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11)
|
||||
];
|
||||
}
|
||||
|
||||
exports.colorutils = colorutils;
|
707
src/static/js/contentcollector.js
Normal file
|
@ -0,0 +1,707 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
|
||||
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
||||
// %APPJET%: import("etherpad.admin.plugins");
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var _MAX_LIST_LEVEL = 8;
|
||||
|
||||
var UNorm = require('./unorm');
|
||||
var Changeset = require('./Changeset');
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
var _ = require('./underscore');
|
||||
|
||||
function sanitizeUnicode(s)
|
||||
{
|
||||
return UNorm.nfc(s).replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?');
|
||||
}
|
||||
|
||||
function makeContentCollector(collectStyles, browser, apool, domInterface, className2Author)
|
||||
{
|
||||
browser = browser || {};
|
||||
|
||||
var dom = domInterface || {
|
||||
isNodeText: function(n)
|
||||
{
|
||||
return (n.nodeType == 3);
|
||||
},
|
||||
nodeTagName: function(n)
|
||||
{
|
||||
return n.tagName;
|
||||
},
|
||||
nodeValue: function(n)
|
||||
{
|
||||
return n.nodeValue;
|
||||
},
|
||||
nodeNumChildren: function(n)
|
||||
{
|
||||
return n.childNodes.length;
|
||||
},
|
||||
nodeChild: function(n, i)
|
||||
{
|
||||
return n.childNodes.item(i);
|
||||
},
|
||||
nodeProp: function(n, p)
|
||||
{
|
||||
return n[p];
|
||||
},
|
||||
nodeAttr: function(n, a)
|
||||
{
|
||||
return n.getAttribute(a);
|
||||
},
|
||||
optNodeInnerHTML: function(n)
|
||||
{
|
||||
return n.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
var _blockElems = {
|
||||
"div": 1,
|
||||
"p": 1,
|
||||
"pre": 1,
|
||||
"li": 1
|
||||
};
|
||||
|
||||
function isBlockElement(n)
|
||||
{
|
||||
return !!_blockElems[(dom.nodeTagName(n) || "").toLowerCase()];
|
||||
}
|
||||
|
||||
function textify(str)
|
||||
{
|
||||
return sanitizeUnicode(
|
||||
str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '));
|
||||
}
|
||||
|
||||
function getAssoc(node, name)
|
||||
{
|
||||
return dom.nodeProp(node, "_magicdom_" + name);
|
||||
}
|
||||
|
||||
var lines = (function()
|
||||
{
|
||||
var textArray = [];
|
||||
var attribsArray = [];
|
||||
var attribsBuilder = null;
|
||||
var op = Changeset.newOp('+');
|
||||
var self = {
|
||||
length: function()
|
||||
{
|
||||
return textArray.length;
|
||||
},
|
||||
atColumnZero: function()
|
||||
{
|
||||
return textArray[textArray.length - 1] === "";
|
||||
},
|
||||
startNew: function()
|
||||
{
|
||||
textArray.push("");
|
||||
self.flush(true);
|
||||
attribsBuilder = Changeset.smartOpAssembler();
|
||||
},
|
||||
textOfLine: function(i)
|
||||
{
|
||||
return textArray[i];
|
||||
},
|
||||
appendText: function(txt, attrString)
|
||||
{
|
||||
textArray[textArray.length - 1] += txt;
|
||||
//dmesg(txt+" / "+attrString);
|
||||
op.attribs = attrString;
|
||||
op.chars = txt.length;
|
||||
attribsBuilder.append(op);
|
||||
},
|
||||
textLines: function()
|
||||
{
|
||||
return textArray.slice();
|
||||
},
|
||||
attribLines: function()
|
||||
{
|
||||
return attribsArray;
|
||||
},
|
||||
// call flush only when you're done
|
||||
flush: function(withNewline)
|
||||
{
|
||||
if (attribsBuilder)
|
||||
{
|
||||
attribsArray.push(attribsBuilder.toString());
|
||||
attribsBuilder = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
self.startNew();
|
||||
return self;
|
||||
}());
|
||||
var cc = {};
|
||||
|
||||
function _ensureColumnZero(state)
|
||||
{
|
||||
if (!lines.atColumnZero())
|
||||
{
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
}
|
||||
var selection, startPoint, endPoint;
|
||||
var selStart = [-1, -1],
|
||||
selEnd = [-1, -1];
|
||||
function _isEmpty(node, state)
|
||||
{
|
||||
// consider clean blank lines pasted in IE to be empty
|
||||
if (dom.nodeNumChildren(node) == 0) return true;
|
||||
if (dom.nodeNumChildren(node) == 1 && getAssoc(node, "shouldBeEmpty") && dom.optNodeInnerHTML(node) == " " && !getAssoc(node, "unpasted"))
|
||||
{
|
||||
if (state)
|
||||
{
|
||||
var child = dom.nodeChild(node, 0);
|
||||
_reachPoint(child, 0, state);
|
||||
_reachPoint(child, 1, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function _pointHere(charsAfter, state)
|
||||
{
|
||||
var ln = lines.length() - 1;
|
||||
var chr = lines.textOfLine(ln).length;
|
||||
if (chr == 0 && !_.isEmpty(state.lineAttributes))
|
||||
{
|
||||
chr += 1; // listMarker
|
||||
}
|
||||
chr += charsAfter;
|
||||
return [ln, chr];
|
||||
}
|
||||
|
||||
function _reachBlockPoint(nd, idx, state)
|
||||
{
|
||||
if (!dom.isNodeText(nd)) _reachPoint(nd, idx, state);
|
||||
}
|
||||
|
||||
function _reachPoint(nd, idx, state)
|
||||
{
|
||||
if (startPoint && nd == startPoint.node && startPoint.index == idx)
|
||||
{
|
||||
selStart = _pointHere(0, state);
|
||||
}
|
||||
if (endPoint && nd == endPoint.node && endPoint.index == idx)
|
||||
{
|
||||
selEnd = _pointHere(0, state);
|
||||
}
|
||||
}
|
||||
cc.incrementFlag = function(state, flagName)
|
||||
{
|
||||
state.flags[flagName] = (state.flags[flagName] || 0) + 1;
|
||||
}
|
||||
cc.decrementFlag = function(state, flagName)
|
||||
{
|
||||
state.flags[flagName]--;
|
||||
}
|
||||
cc.incrementAttrib = function(state, attribName)
|
||||
{
|
||||
if (!state.attribs[attribName])
|
||||
{
|
||||
state.attribs[attribName] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.attribs[attribName]++;
|
||||
}
|
||||
_recalcAttribString(state);
|
||||
}
|
||||
cc.decrementAttrib = function(state, attribName)
|
||||
{
|
||||
state.attribs[attribName]--;
|
||||
_recalcAttribString(state);
|
||||
}
|
||||
|
||||
function _enterList(state, listType)
|
||||
{
|
||||
var oldListType = state.lineAttributes['list'];
|
||||
if (listType != 'none')
|
||||
{
|
||||
state.listNesting = (state.listNesting || 0) + 1;
|
||||
}
|
||||
|
||||
if(listType === 'none' || !listType ){
|
||||
delete state.lineAttributes['list'];
|
||||
}
|
||||
else{
|
||||
state.lineAttributes['list'] = listType;
|
||||
}
|
||||
|
||||
_recalcAttribString(state);
|
||||
return oldListType;
|
||||
}
|
||||
|
||||
function _exitList(state, oldListType)
|
||||
{
|
||||
if (state.lineAttributes['list'])
|
||||
{
|
||||
state.listNesting--;
|
||||
}
|
||||
if (oldListType && oldListType != 'none') { state.lineAttributes['list'] = oldListType; }
|
||||
else { delete state.lineAttributes['list']; }
|
||||
_recalcAttribString(state);
|
||||
}
|
||||
|
||||
function _enterAuthor(state, author)
|
||||
{
|
||||
var oldAuthor = state.author;
|
||||
state.authorLevel = (state.authorLevel || 0) + 1;
|
||||
state.author = author;
|
||||
_recalcAttribString(state);
|
||||
return oldAuthor;
|
||||
}
|
||||
|
||||
function _exitAuthor(state, oldAuthor)
|
||||
{
|
||||
state.authorLevel--;
|
||||
state.author = oldAuthor;
|
||||
_recalcAttribString(state);
|
||||
}
|
||||
|
||||
function _recalcAttribString(state)
|
||||
{
|
||||
var lst = [];
|
||||
for (var a in state.attribs)
|
||||
{
|
||||
if (state.attribs[a])
|
||||
{
|
||||
lst.push([a, 'true']);
|
||||
}
|
||||
}
|
||||
if (state.authorLevel > 0)
|
||||
{
|
||||
var authorAttrib = ['author', state.author];
|
||||
if (apool.putAttrib(authorAttrib, true) >= 0)
|
||||
{
|
||||
// require that author already be in pool
|
||||
// (don't add authors from other documents, etc.)
|
||||
lst.push(authorAttrib);
|
||||
}
|
||||
}
|
||||
state.attribString = Changeset.makeAttribsString('+', lst, apool);
|
||||
}
|
||||
|
||||
function _produceLineAttributesMarker(state)
|
||||
{
|
||||
// TODO: This has to go to AttributeManager.
|
||||
var attributes = [
|
||||
['lmkr', '1'],
|
||||
['insertorder', 'first']
|
||||
].concat(
|
||||
_.map(state.lineAttributes,function(value,key){
|
||||
if (window.console) console.log([key, value])
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
|
||||
lines.appendText('*', Changeset.makeAttribsString('+', attributes , apool));
|
||||
}
|
||||
cc.startNewLine = function(state)
|
||||
{
|
||||
if (state)
|
||||
{
|
||||
var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0;
|
||||
if (atBeginningOfLine && !_.isEmpty(state.lineAttributes))
|
||||
{
|
||||
_produceLineAttributesMarker(state);
|
||||
}
|
||||
}
|
||||
lines.startNew();
|
||||
}
|
||||
cc.notifySelection = function(sel)
|
||||
{
|
||||
if (sel)
|
||||
{
|
||||
selection = sel;
|
||||
startPoint = selection.startPoint;
|
||||
endPoint = selection.endPoint;
|
||||
}
|
||||
};
|
||||
cc.doAttrib = function(state, na)
|
||||
{
|
||||
state.localAttribs = (state.localAttribs || []);
|
||||
state.localAttribs.push(na);
|
||||
cc.incrementAttrib(state, na);
|
||||
};
|
||||
cc.collectContent = function(node, state)
|
||||
{
|
||||
if (!state)
|
||||
{
|
||||
state = {
|
||||
flags: { /*name -> nesting counter*/
|
||||
},
|
||||
localAttribs: null,
|
||||
attribs: { /*name -> nesting counter*/
|
||||
},
|
||||
attribString: '',
|
||||
// lineAttributes maintain a map from attributes to attribute values set on a line
|
||||
lineAttributes: {
|
||||
/*
|
||||
example:
|
||||
'list': 'bullet1',
|
||||
*/
|
||||
}
|
||||
};
|
||||
}
|
||||
var localAttribs = state.localAttribs;
|
||||
state.localAttribs = null;
|
||||
var isBlock = isBlockElement(node);
|
||||
var isEmpty = _isEmpty(node, state);
|
||||
if (isBlock) _ensureColumnZero(state);
|
||||
var startLine = lines.length() - 1;
|
||||
_reachBlockPoint(node, 0, state);
|
||||
if (dom.isNodeText(node))
|
||||
{
|
||||
var txt = dom.nodeValue(node);
|
||||
var rest = '';
|
||||
var x = 0; // offset into original text
|
||||
if (txt.length == 0)
|
||||
{
|
||||
if (startPoint && node == startPoint.node)
|
||||
{
|
||||
selStart = _pointHere(0, state);
|
||||
}
|
||||
if (endPoint && node == endPoint.node)
|
||||
{
|
||||
selEnd = _pointHere(0, state);
|
||||
}
|
||||
}
|
||||
while (txt.length > 0)
|
||||
{
|
||||
var consumed = 0;
|
||||
if (state.flags.preMode)
|
||||
{
|
||||
var firstLine = txt.split('\n', 1)[0];
|
||||
consumed = firstLine.length + 1;
|
||||
rest = txt.substring(consumed);
|
||||
txt = firstLine;
|
||||
}
|
||||
else
|
||||
{ /* will only run this loop body once */
|
||||
}
|
||||
if (startPoint && node == startPoint.node && startPoint.index - x <= txt.length)
|
||||
{
|
||||
selStart = _pointHere(startPoint.index - x, state);
|
||||
}
|
||||
if (endPoint && node == endPoint.node && endPoint.index - x <= txt.length)
|
||||
{
|
||||
selEnd = _pointHere(endPoint.index - x, state);
|
||||
}
|
||||
var txt2 = txt;
|
||||
if ((!state.flags.preMode) && /^[\r\n]*$/.exec(txt))
|
||||
{
|
||||
// prevents textnodes containing just "\n" from being significant
|
||||
// in safari when pasting text, now that we convert them to
|
||||
// spaces instead of removing them, because in other cases
|
||||
// removing "\n" from pasted HTML will collapse words together.
|
||||
txt2 = "";
|
||||
}
|
||||
var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0;
|
||||
if (atBeginningOfLine)
|
||||
{
|
||||
// newlines in the source mustn't become spaces at beginning of line box
|
||||
txt2 = txt2.replace(/^\n*/, '');
|
||||
}
|
||||
if (atBeginningOfLine && !_.isEmpty(state.lineAttributes))
|
||||
{
|
||||
_produceLineAttributesMarker(state);
|
||||
}
|
||||
lines.appendText(textify(txt2), state.attribString);
|
||||
x += consumed;
|
||||
txt = rest;
|
||||
if (txt.length > 0)
|
||||
{
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var tname = (dom.nodeTagName(node) || "").toLowerCase();
|
||||
if (tname == "br")
|
||||
{
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
else if (tname == "script" || tname == "style")
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
else if (!isEmpty)
|
||||
{
|
||||
var styl = dom.nodeAttr(node, "style");
|
||||
var cls = dom.nodeProp(node, "className");
|
||||
|
||||
var isPre = (tname == "pre");
|
||||
if ((!isPre) && browser.safari)
|
||||
{
|
||||
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
|
||||
}
|
||||
if (isPre) cc.incrementFlag(state, 'preMode');
|
||||
var oldListTypeOrNull = null;
|
||||
var oldAuthorOrNull = null;
|
||||
if (collectStyles)
|
||||
{
|
||||
hooks.callAll('collectContentPre', {
|
||||
cc: cc,
|
||||
state: state,
|
||||
tname: tname,
|
||||
styl: styl,
|
||||
cls: cls
|
||||
});
|
||||
if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || tname == "strong")
|
||||
{
|
||||
cc.doAttrib(state, "bold");
|
||||
}
|
||||
if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || tname == "em")
|
||||
{
|
||||
cc.doAttrib(state, "italic");
|
||||
}
|
||||
if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || tname == "ins")
|
||||
{
|
||||
cc.doAttrib(state, "underline");
|
||||
}
|
||||
if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || tname == "del")
|
||||
{
|
||||
cc.doAttrib(state, "strikethrough");
|
||||
}
|
||||
if (tname == "ul" || tname == "ol")
|
||||
{
|
||||
var type;
|
||||
var rr = cls && /(?:^| )list-([a-z]+[12345678])\b/.exec(cls);
|
||||
type = rr && rr[1] || "bullet" + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
|
||||
oldListTypeOrNull = (_enterList(state, type) || 'none');
|
||||
}
|
||||
else if ((tname == "div" || tname == "p") && cls && cls.match(/(?:^| )ace-line\b/))
|
||||
{
|
||||
oldListTypeOrNull = (_enterList(state, type) || 'none');
|
||||
}
|
||||
if (className2Author && cls)
|
||||
{
|
||||
var classes = cls.match(/\S+/g);
|
||||
if (classes && classes.length > 0)
|
||||
{
|
||||
for (var i = 0; i < classes.length; i++)
|
||||
{
|
||||
var c = classes[i];
|
||||
var a = className2Author(c);
|
||||
if (a)
|
||||
{
|
||||
oldAuthorOrNull = (_enterAuthor(state, a) || 'none');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var nc = dom.nodeNumChildren(node);
|
||||
for (var i = 0; i < nc; i++)
|
||||
{
|
||||
var c = dom.nodeChild(node, i);
|
||||
cc.collectContent(c, state);
|
||||
}
|
||||
|
||||
if (collectStyles)
|
||||
{
|
||||
hooks.callAll('collectContentPost', {
|
||||
cc: cc,
|
||||
state: state,
|
||||
tname: tname,
|
||||
styl: styl,
|
||||
cls: cls
|
||||
});
|
||||
}
|
||||
|
||||
if (isPre) cc.decrementFlag(state, 'preMode');
|
||||
if (state.localAttribs)
|
||||
{
|
||||
for (var i = 0; i < state.localAttribs.length; i++)
|
||||
{
|
||||
cc.decrementAttrib(state, state.localAttribs[i]);
|
||||
}
|
||||
}
|
||||
if (oldListTypeOrNull)
|
||||
{
|
||||
_exitList(state, oldListTypeOrNull);
|
||||
}
|
||||
if (oldAuthorOrNull)
|
||||
{
|
||||
_exitAuthor(state, oldAuthorOrNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!browser.msie)
|
||||
{
|
||||
_reachBlockPoint(node, 1, state);
|
||||
}
|
||||
if (isBlock)
|
||||
{
|
||||
if (lines.length() - 1 == startLine)
|
||||
{
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ensureColumnZero(state);
|
||||
}
|
||||
}
|
||||
|
||||
if (browser.msie)
|
||||
{
|
||||
// in IE, a point immediately after a DIV appears on the next line
|
||||
_reachBlockPoint(node, 1, state);
|
||||
}
|
||||
|
||||
state.localAttribs = localAttribs;
|
||||
};
|
||||
// can pass a falsy value for end of doc
|
||||
cc.notifyNextNode = function(node)
|
||||
{
|
||||
// an "empty block" won't end a line; this addresses an issue in IE with
|
||||
// typing into a blank line at the end of the document. typed text
|
||||
// goes into the body, and the empty line div still looks clean.
|
||||
// it is incorporated as dirty by the rule that a dirty region has
|
||||
// to end a line.
|
||||
if ((!node) || (isBlockElement(node) && !_isEmpty(node)))
|
||||
{
|
||||
_ensureColumnZero(null);
|
||||
}
|
||||
};
|
||||
// each returns [line, char] or [-1,-1]
|
||||
var getSelectionStart = function()
|
||||
{
|
||||
return selStart;
|
||||
};
|
||||
var getSelectionEnd = function()
|
||||
{
|
||||
return selEnd;
|
||||
};
|
||||
|
||||
// returns array of strings for lines found, last entry will be "" if
|
||||
// last line is complete (i.e. if a following span should be on a new line).
|
||||
// can be called at any point
|
||||
cc.getLines = function()
|
||||
{
|
||||
return lines.textLines();
|
||||
};
|
||||
|
||||
cc.finish = function()
|
||||
{
|
||||
lines.flush();
|
||||
var lineAttribs = lines.attribLines();
|
||||
var lineStrings = cc.getLines();
|
||||
|
||||
lineStrings.length--;
|
||||
lineAttribs.length--;
|
||||
|
||||
var ss = getSelectionStart();
|
||||
var se = getSelectionEnd();
|
||||
|
||||
function fixLongLines()
|
||||
{
|
||||
// design mode does not deal with with really long lines!
|
||||
var lineLimit = 2000; // chars
|
||||
var buffer = 10; // chars allowed over before wrapping
|
||||
var linesWrapped = 0;
|
||||
var numLinesAfter = 0;
|
||||
for (var i = lineStrings.length - 1; i >= 0; i--)
|
||||
{
|
||||
var oldString = lineStrings[i];
|
||||
var oldAttribString = lineAttribs[i];
|
||||
if (oldString.length > lineLimit + buffer)
|
||||
{
|
||||
var newStrings = [];
|
||||
var newAttribStrings = [];
|
||||
while (oldString.length > lineLimit)
|
||||
{
|
||||
//var semiloc = oldString.lastIndexOf(';', lineLimit-1);
|
||||
//var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit);
|
||||
lengthToTake = lineLimit;
|
||||
newStrings.push(oldString.substring(0, lengthToTake));
|
||||
oldString = oldString.substring(lengthToTake);
|
||||
newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake));
|
||||
oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake);
|
||||
}
|
||||
if (oldString.length > 0)
|
||||
{
|
||||
newStrings.push(oldString);
|
||||
newAttribStrings.push(oldAttribString);
|
||||
}
|
||||
|
||||
function fixLineNumber(lineChar)
|
||||
{
|
||||
if (lineChar[0] < 0) return;
|
||||
var n = lineChar[0];
|
||||
var c = lineChar[1];
|
||||
if (n > i)
|
||||
{
|
||||
n += (newStrings.length - 1);
|
||||
}
|
||||
else if (n == i)
|
||||
{
|
||||
var a = 0;
|
||||
while (c > newStrings[a].length)
|
||||
{
|
||||
c -= newStrings[a].length;
|
||||
a++;
|
||||
}
|
||||
n += a;
|
||||
}
|
||||
lineChar[0] = n;
|
||||
lineChar[1] = c;
|
||||
}
|
||||
fixLineNumber(ss);
|
||||
fixLineNumber(se);
|
||||
linesWrapped++;
|
||||
numLinesAfter += newStrings.length;
|
||||
|
||||
newStrings.unshift(i, 1);
|
||||
lineStrings.splice.apply(lineStrings, newStrings);
|
||||
newAttribStrings.unshift(i, 1);
|
||||
lineAttribs.splice.apply(lineAttribs, newAttribStrings);
|
||||
}
|
||||
}
|
||||
return {
|
||||
linesWrapped: linesWrapped,
|
||||
numLinesAfter: numLinesAfter
|
||||
};
|
||||
}
|
||||
var wrapData = fixLongLines();
|
||||
|
||||
return {
|
||||
selStart: ss,
|
||||
selEnd: se,
|
||||
linesWrapped: wrapData.linesWrapped,
|
||||
numLinesAfter: wrapData.numLinesAfter,
|
||||
lines: lineStrings,
|
||||
lineAttribs: lineAttribs
|
||||
};
|
||||
}
|
||||
|
||||
return cc;
|
||||
}
|
||||
|
||||
exports.sanitizeUnicode = sanitizeUnicode;
|
||||
exports.makeContentCollector = makeContentCollector;
|
106
src/static/js/cssmanager.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function makeCSSManager(emptyStylesheetTitle)
|
||||
{
|
||||
|
||||
function getSheetByTitle(title)
|
||||
{
|
||||
var allSheets = document.styleSheets;
|
||||
|
||||
for (var i = 0; i < allSheets.length; i++)
|
||||
{
|
||||
var s = allSheets[i];
|
||||
if (s.title == title)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var browserSheet = getSheetByTitle(emptyStylesheetTitle);
|
||||
|
||||
function browserRules()
|
||||
{
|
||||
return (browserSheet.cssRules || browserSheet.rules);
|
||||
}
|
||||
|
||||
function browserDeleteRule(i)
|
||||
{
|
||||
if (browserSheet.deleteRule) browserSheet.deleteRule(i);
|
||||
else browserSheet.removeRule(i);
|
||||
}
|
||||
|
||||
function browserInsertRule(i, selector)
|
||||
{
|
||||
if (browserSheet.insertRule) browserSheet.insertRule(selector + ' {}', i);
|
||||
else browserSheet.addRule(selector, null, i);
|
||||
}
|
||||
var selectorList = [];
|
||||
|
||||
function indexOfSelector(selector)
|
||||
{
|
||||
for (var i = 0; i < selectorList.length; i++)
|
||||
{
|
||||
if (selectorList[i] == selector)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function selectorStyle(selector)
|
||||
{
|
||||
var i = indexOfSelector(selector);
|
||||
if (i < 0)
|
||||
{
|
||||
// add selector
|
||||
browserInsertRule(0, selector);
|
||||
selectorList.splice(0, 0, selector);
|
||||
i = 0;
|
||||
}
|
||||
return browserRules().item(i).style;
|
||||
}
|
||||
|
||||
function removeSelectorStyle(selector)
|
||||
{
|
||||
var i = indexOfSelector(selector);
|
||||
if (i >= 0)
|
||||
{
|
||||
browserDeleteRule(i);
|
||||
selectorList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectorStyle: selectorStyle,
|
||||
removeSelectorStyle: removeSelectorStyle,
|
||||
info: function()
|
||||
{
|
||||
return selectorList.length + ":" + browserRules().length;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.makeCSSManager = makeCSSManager;
|
309
src/static/js/domline.js
Normal file
|
@ -0,0 +1,309 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
|
||||
// %APPJET%: import("etherpad.admin.plugins");
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// requires: top
|
||||
// requires: plugins
|
||||
// requires: undefined
|
||||
|
||||
var Security = require('./security');
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
var _ = require('./underscore');
|
||||
var lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
|
||||
var Ace2Common = require('./ace2_common');
|
||||
var noop = Ace2Common.noop;
|
||||
|
||||
|
||||
var domline = {};
|
||||
|
||||
domline.addToLineClass = function(lineClass, cls)
|
||||
{
|
||||
// an "empty span" at any point can be used to add classes to
|
||||
// the line, using line:className. otherwise, we ignore
|
||||
// the span.
|
||||
cls.replace(/\S+/g, function(c)
|
||||
{
|
||||
if (c.indexOf("line:") == 0)
|
||||
{
|
||||
// add class to line
|
||||
lineClass = (lineClass ? lineClass + ' ' : '') + c.substring(5);
|
||||
}
|
||||
});
|
||||
return lineClass;
|
||||
}
|
||||
|
||||
// if "document" is falsy we don't create a DOM node, just
|
||||
// an object with innerHTML and className
|
||||
domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument)
|
||||
{
|
||||
var result = {
|
||||
node: null,
|
||||
appendSpan: noop,
|
||||
prepareForAdd: noop,
|
||||
notifyAdded: noop,
|
||||
clearSpans: noop,
|
||||
finishUpdate: noop,
|
||||
lineMarker: 0
|
||||
};
|
||||
|
||||
var browser = (optBrowser || {});
|
||||
var document = optDocument;
|
||||
|
||||
if (document)
|
||||
{
|
||||
result.node = document.createElement("div");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.node = {
|
||||
innerHTML: '',
|
||||
className: ''
|
||||
};
|
||||
}
|
||||
|
||||
var html = [];
|
||||
var preHtml = '',
|
||||
postHtml = '';
|
||||
var curHTML = null;
|
||||
|
||||
function processSpaces(s)
|
||||
{
|
||||
return domline.processSpaces(s, doesWrap);
|
||||
}
|
||||
|
||||
var perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
|
||||
var perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
|
||||
var lineClass = 'ace-line';
|
||||
result.appendSpan = function(txt, cls)
|
||||
{
|
||||
var processedMarker = false;
|
||||
// Handle lineAttributeMarker, if present
|
||||
if (cls.indexOf(lineAttributeMarker) >= 0)
|
||||
{
|
||||
var listType = /(?:^| )list:(\S+)/.exec(cls);
|
||||
var start = /(?:^| )start:(\S+)/.exec(cls);
|
||||
if (listType)
|
||||
{
|
||||
listType = listType[1];
|
||||
start = start?'start="'+Security.escapeHTMLAttribute(start[1])+'"':'';
|
||||
if (listType)
|
||||
{
|
||||
if(listType.indexOf("number") < 0)
|
||||
{
|
||||
preHtml = '<ul class="list-' + Security.escapeHTMLAttribute(listType) + '"><li>';
|
||||
postHtml = '</li></ul>';
|
||||
}
|
||||
else
|
||||
{
|
||||
preHtml = '<ol '+start+' class="list-' + Security.escapeHTMLAttribute(listType) + '"><li>';
|
||||
postHtml = '</li></ol>';
|
||||
}
|
||||
}
|
||||
processedMarker = true;
|
||||
}
|
||||
|
||||
_.map(hooks.callAll("aceDomLineProcessLineAttributes", {
|
||||
domline: domline,
|
||||
cls: cls
|
||||
}), function(modifier)
|
||||
{
|
||||
preHtml += modifier.preHtml;
|
||||
postHtml += modifier.postHtml;
|
||||
processedMarker |= modifier.processedMarker;
|
||||
});
|
||||
|
||||
if( processedMarker ){
|
||||
result.lineMarker += txt.length;
|
||||
return; // don't append any text
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
var href = null;
|
||||
var simpleTags = null;
|
||||
if (cls.indexOf('url') >= 0)
|
||||
{
|
||||
cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url)
|
||||
{
|
||||
href = url;
|
||||
return space + "url";
|
||||
});
|
||||
}
|
||||
if (cls.indexOf('tag') >= 0)
|
||||
{
|
||||
cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag)
|
||||
{
|
||||
if (!simpleTags) simpleTags = [];
|
||||
simpleTags.push(tag.toLowerCase());
|
||||
return space + tag;
|
||||
});
|
||||
}
|
||||
|
||||
var extraOpenTags = "";
|
||||
var extraCloseTags = "";
|
||||
|
||||
_.map(hooks.callAll("aceCreateDomLine", {
|
||||
domline: domline,
|
||||
cls: cls
|
||||
}), function(modifier)
|
||||
{
|
||||
cls = modifier.cls;
|
||||
extraOpenTags = extraOpenTags + modifier.extraOpenTags;
|
||||
extraCloseTags = modifier.extraCloseTags + extraCloseTags;
|
||||
});
|
||||
|
||||
if ((!txt) && cls)
|
||||
{
|
||||
lineClass = domline.addToLineClass(lineClass, cls);
|
||||
}
|
||||
else if (txt)
|
||||
{
|
||||
if (href)
|
||||
{
|
||||
if(!~href.indexOf("http")) // if the url doesn't include http or https etc prefix it.
|
||||
{
|
||||
href = "http://"+href;
|
||||
}
|
||||
extraOpenTags = extraOpenTags + '<a href="' + Security.escapeHTMLAttribute(href) + '">';
|
||||
extraCloseTags = '</a>' + extraCloseTags;
|
||||
}
|
||||
if (simpleTags)
|
||||
{
|
||||
simpleTags.sort();
|
||||
extraOpenTags = extraOpenTags + '<' + simpleTags.join('><') + '>';
|
||||
simpleTags.reverse();
|
||||
extraCloseTags = '</' + simpleTags.join('></') + '>' + extraCloseTags;
|
||||
}
|
||||
html.push('<span class="', Security.escapeHTMLAttribute(cls || ''), '">', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, '</span>');
|
||||
}
|
||||
};
|
||||
result.clearSpans = function()
|
||||
{
|
||||
html = [];
|
||||
lineClass = ''; // non-null to cause update
|
||||
result.lineMarker = 0;
|
||||
};
|
||||
|
||||
function writeHTML()
|
||||
{
|
||||
var newHTML = perHtmlLineProcess(html.join(''));
|
||||
if (!newHTML)
|
||||
{
|
||||
if ((!document) || (!optBrowser))
|
||||
{
|
||||
newHTML += ' ';
|
||||
}
|
||||
else if (!browser.msie)
|
||||
{
|
||||
newHTML += '<br/>';
|
||||
}
|
||||
}
|
||||
if (nonEmpty)
|
||||
{
|
||||
newHTML = (preHtml || '') + newHTML + (postHtml || '');
|
||||
}
|
||||
html = preHtml = postHtml = ''; // free memory
|
||||
if (newHTML !== curHTML)
|
||||
{
|
||||
curHTML = newHTML;
|
||||
result.node.innerHTML = curHTML;
|
||||
}
|
||||
if (lineClass !== null) result.node.className = lineClass;
|
||||
|
||||
hooks.callAll("acePostWriteDomLineHTML", {
|
||||
node: result.node
|
||||
});
|
||||
}
|
||||
result.prepareForAdd = writeHTML;
|
||||
result.finishUpdate = writeHTML;
|
||||
result.getInnerHTML = function()
|
||||
{
|
||||
return curHTML || '';
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
domline.processSpaces = function(s, doesWrap)
|
||||
{
|
||||
if (s.indexOf("<") < 0 && !doesWrap)
|
||||
{
|
||||
// short-cut
|
||||
return s.replace(/ /g, ' ');
|
||||
}
|
||||
var parts = [];
|
||||
s.replace(/<[^>]*>?| |[^ <]+/g, function(m)
|
||||
{
|
||||
parts.push(m);
|
||||
});
|
||||
if (doesWrap)
|
||||
{
|
||||
var endOfLine = true;
|
||||
var beforeSpace = false;
|
||||
// last space in a run is normal, others are nbsp,
|
||||
// end of line is nbsp
|
||||
for (var i = parts.length - 1; i >= 0; i--)
|
||||
{
|
||||
var p = parts[i];
|
||||
if (p == " ")
|
||||
{
|
||||
if (endOfLine || beforeSpace) parts[i] = ' ';
|
||||
endOfLine = false;
|
||||
beforeSpace = true;
|
||||
}
|
||||
else if (p.charAt(0) != "<")
|
||||
{
|
||||
endOfLine = false;
|
||||
beforeSpace = false;
|
||||
}
|
||||
}
|
||||
// beginning of line is nbsp
|
||||
for (var i = 0; i < parts.length; i++)
|
||||
{
|
||||
var p = parts[i];
|
||||
if (p == " ")
|
||||
{
|
||||
parts[i] = ' ';
|
||||
break;
|
||||
}
|
||||
else if (p.charAt(0) != "<")
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < parts.length; i++)
|
||||
{
|
||||
var p = parts[i];
|
||||
if (p == " ")
|
||||
{
|
||||
parts[i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join('');
|
||||
};
|
||||
|
||||
exports.domline = domline;
|
197
src/static/js/draggable.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function makeDraggable(jqueryNodes, eventHandler)
|
||||
{
|
||||
jqueryNodes.each(function()
|
||||
{
|
||||
var node = $(this);
|
||||
var state = {};
|
||||
var inDrag = false;
|
||||
|
||||
function dragStart(evt)
|
||||
{
|
||||
if (inDrag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
inDrag = true;
|
||||
if (eventHandler('dragstart', evt, state) !== false)
|
||||
{
|
||||
$(document).bind('mousemove', dragUpdate);
|
||||
$(document).bind('mouseup', dragEnd);
|
||||
}
|
||||
evt.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
function dragUpdate(evt)
|
||||
{
|
||||
if (!inDrag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
eventHandler('dragupdate', evt, state);
|
||||
evt.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
function dragEnd(evt)
|
||||
{
|
||||
if (!inDrag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
inDrag = false;
|
||||
try
|
||||
{
|
||||
eventHandler('dragend', evt, state);
|
||||
}
|
||||
finally
|
||||
{
|
||||
$(document).unbind('mousemove', dragUpdate);
|
||||
$(document).unbind('mouseup', dragEnd);
|
||||
evt.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
node.bind('mousedown', dragStart);
|
||||
});
|
||||
}
|
||||
|
||||
function makeResizableVPane(top, sep, bottom, minTop, minBottom, callback)
|
||||
{
|
||||
if (minTop === undefined) minTop = 0;
|
||||
if (minBottom === undefined) minBottom = 0;
|
||||
|
||||
makeDraggable($(sep), function(eType, evt, state)
|
||||
{
|
||||
if (eType == 'dragstart')
|
||||
{
|
||||
state.startY = evt.pageY;
|
||||
state.topHeight = $(top).height();
|
||||
state.bottomHeight = $(bottom).height();
|
||||
state.minTop = minTop;
|
||||
state.maxTop = (state.topHeight + state.bottomHeight) - minBottom;
|
||||
}
|
||||
else if (eType == 'dragupdate')
|
||||
{
|
||||
var change = evt.pageY - state.startY;
|
||||
|
||||
var topHeight = state.topHeight + change;
|
||||
if (topHeight < state.minTop)
|
||||
{
|
||||
topHeight = state.minTop;
|
||||
}
|
||||
if (topHeight > state.maxTop)
|
||||
{
|
||||
topHeight = state.maxTop;
|
||||
}
|
||||
change = topHeight - state.topHeight;
|
||||
|
||||
var bottomHeight = state.bottomHeight - change;
|
||||
var sepHeight = $(sep).height();
|
||||
|
||||
var totalHeight = topHeight + sepHeight + bottomHeight;
|
||||
topHeight = 100.0 * topHeight / totalHeight;
|
||||
sepHeight = 100.0 * sepHeight / totalHeight;
|
||||
bottomHeight = 100.0 * bottomHeight / totalHeight;
|
||||
|
||||
$(top).css('bottom', 'auto');
|
||||
$(top).css('height', topHeight + "%");
|
||||
$(sep).css('top', topHeight + "%");
|
||||
$(bottom).css('top', (topHeight + sepHeight) + '%');
|
||||
$(bottom).css('height', 'auto');
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeResizableHPane(left, sep, right, minLeft, minRight, sepWidth, sepOffset, callback)
|
||||
{
|
||||
if (minLeft === undefined) minLeft = 0;
|
||||
if (minRight === undefined) minRight = 0;
|
||||
|
||||
makeDraggable($(sep), function(eType, evt, state)
|
||||
{
|
||||
if (eType == 'dragstart')
|
||||
{
|
||||
state.startX = evt.pageX;
|
||||
state.leftWidth = $(left).width();
|
||||
state.rightWidth = $(right).width();
|
||||
state.minLeft = minLeft;
|
||||
state.maxLeft = (state.leftWidth + state.rightWidth) - minRight;
|
||||
}
|
||||
else if (eType == 'dragend' || eType == 'dragupdate')
|
||||
{
|
||||
var change = evt.pageX - state.startX;
|
||||
|
||||
var leftWidth = state.leftWidth + change;
|
||||
if (leftWidth < state.minLeft)
|
||||
{
|
||||
leftWidth = state.minLeft;
|
||||
}
|
||||
if (leftWidth > state.maxLeft)
|
||||
{
|
||||
leftWidth = state.maxLeft;
|
||||
}
|
||||
change = leftWidth - state.leftWidth;
|
||||
|
||||
var rightWidth = state.rightWidth - change;
|
||||
newSepWidth = sepWidth;
|
||||
if (newSepWidth == undefined) newSepWidth = $(sep).width();
|
||||
newSepOffset = sepOffset;
|
||||
if (newSepOffset == undefined) newSepOffset = 0;
|
||||
|
||||
if (change == 0)
|
||||
{
|
||||
if (rightWidth != minRight || state.lastRightWidth == undefined)
|
||||
{
|
||||
state.lastRightWidth = rightWidth;
|
||||
rightWidth = minRight;
|
||||
}
|
||||
else
|
||||
{
|
||||
rightWidth = state.lastRightWidth;
|
||||
state.lastRightWidth = minRight;
|
||||
}
|
||||
change = state.rightWidth - rightWidth;
|
||||
leftWidth = change + state.leftWidth;
|
||||
}
|
||||
|
||||
var totalWidth = leftWidth + newSepWidth + rightWidth;
|
||||
leftWidth = 100.0 * leftWidth / totalWidth;
|
||||
newSepWidth = 100.0 * newSepWidth / totalWidth;
|
||||
newSepOffset = 100.0 * newSepOffset / totalWidth;
|
||||
rightWidth = 100.0 * rightWidth / totalWidth;
|
||||
|
||||
$(left).css('right', 'auto');
|
||||
$(left).css('width', leftWidth + "%");
|
||||
$(sep).css('left', (leftWidth + newSepOffset) + "%");
|
||||
$(right).css('left', (leftWidth + newSepWidth) + '%');
|
||||
$(right).css('width', 'auto');
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.makeDraggable = makeDraggable;
|
35
src/static/js/excanvas.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2006 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
document.createElement("canvas").getContext||(function(){var s=Math,j=s.round,F=s.sin,G=s.cos,V=s.abs,W=s.sqrt,k=10,v=k/2;function X(){return this.context_||(this.context_=new H(this))}var L=Array.prototype.slice;function Y(b,a){var c=L.call(arguments,2);return function(){return b.apply(a,c.concat(L.call(arguments)))}}var M={init:function(b){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var a=b||document;a.createElement("canvas");a.attachEvent("onreadystatechange",Y(this.init_,this,a))}},init_:function(b){b.namespaces.g_vml_||
|
||||
b.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML");b.namespaces.g_o_||b.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML");if(!b.styleSheets.ex_canvas_){var a=b.createStyleSheet();a.owningElement.id="ex_canvas_";a.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}g_o_\\:*{behavior:url(#default#VML)}"}var c=b.getElementsByTagName("canvas"),d=0;for(;d<c.length;d++)this.initElement(c[d])},
|
||||
initElement:function(b){if(!b.getContext){b.getContext=X;b.innerHTML="";b.attachEvent("onpropertychange",Z);b.attachEvent("onresize",$);var a=b.attributes;if(a.width&&a.width.specified)b.style.width=a.width.nodeValue+"px";else b.width=b.clientWidth;if(a.height&&a.height.specified)b.style.height=a.height.nodeValue+"px";else b.height=b.clientHeight}return b}};function Z(b){var a=b.srcElement;switch(b.propertyName){case "width":a.style.width=a.attributes.width.nodeValue+"px";a.getContext().clearRect();
|
||||
break;case "height":a.style.height=a.attributes.height.nodeValue+"px";a.getContext().clearRect();break}}function $(b){var a=b.srcElement;if(a.firstChild){a.firstChild.style.width=a.clientWidth+"px";a.firstChild.style.height=a.clientHeight+"px"}}M.init();var N=[],B=0;for(;B<16;B++){var C=0;for(;C<16;C++)N[B*16+C]=B.toString(16)+C.toString(16)}function I(){return[[1,0,0],[0,1,0],[0,0,1]]}function y(b,a){var c=I(),d=0;for(;d<3;d++){var f=0;for(;f<3;f++){var h=0,g=0;for(;g<3;g++)h+=b[d][g]*a[g][f];c[d][f]=
|
||||
h}}return c}function O(b,a){a.fillStyle=b.fillStyle;a.lineCap=b.lineCap;a.lineJoin=b.lineJoin;a.lineWidth=b.lineWidth;a.miterLimit=b.miterLimit;a.shadowBlur=b.shadowBlur;a.shadowColor=b.shadowColor;a.shadowOffsetX=b.shadowOffsetX;a.shadowOffsetY=b.shadowOffsetY;a.strokeStyle=b.strokeStyle;a.globalAlpha=b.globalAlpha;a.arcScaleX_=b.arcScaleX_;a.arcScaleY_=b.arcScaleY_;a.lineScale_=b.lineScale_}function P(b){var a,c=1;b=String(b);if(b.substring(0,3)=="rgb"){var d=b.indexOf("(",3),f=b.indexOf(")",d+
|
||||
1),h=b.substring(d+1,f).split(",");a="#";var g=0;for(;g<3;g++)a+=N[Number(h[g])];if(h.length==4&&b.substr(3,1)=="a")c=h[3]}else a=b;return{color:a,alpha:c}}function aa(b){switch(b){case "butt":return"flat";case "round":return"round";case "square":default:return"square"}}function H(b){this.m_=I();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.fillStyle=this.strokeStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=k*1;this.globalAlpha=1;this.canvas=b;
|
||||
var a=b.ownerDocument.createElement("div");a.style.width=b.clientWidth+"px";a.style.height=b.clientHeight+"px";a.style.overflow="hidden";a.style.position="absolute";b.appendChild(a);this.element_=a;this.lineScale_=this.arcScaleY_=this.arcScaleX_=1}var i=H.prototype;i.clearRect=function(){this.element_.innerHTML=""};i.beginPath=function(){this.currentPath_=[]};i.moveTo=function(b,a){var c=this.getCoords_(b,a);this.currentPath_.push({type:"moveTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y};
|
||||
i.lineTo=function(b,a){var c=this.getCoords_(b,a);this.currentPath_.push({type:"lineTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y};i.bezierCurveTo=function(b,a,c,d,f,h){var g=this.getCoords_(f,h),l=this.getCoords_(b,a),e=this.getCoords_(c,d);Q(this,l,e,g)};function Q(b,a,c,d){b.currentPath_.push({type:"bezierCurveTo",cp1x:a.x,cp1y:a.y,cp2x:c.x,cp2y:c.y,x:d.x,y:d.y});b.currentX_=d.x;b.currentY_=d.y}i.quadraticCurveTo=function(b,a,c,d){var f=this.getCoords_(b,a),h=this.getCoords_(c,d),g={x:this.currentX_+
|
||||
0.6666666666666666*(f.x-this.currentX_),y:this.currentY_+0.6666666666666666*(f.y-this.currentY_)};Q(this,g,{x:g.x+(h.x-this.currentX_)/3,y:g.y+(h.y-this.currentY_)/3},h)};i.arc=function(b,a,c,d,f,h){c*=k;var g=h?"at":"wa",l=b+G(d)*c-v,e=a+F(d)*c-v,m=b+G(f)*c-v,r=a+F(f)*c-v;if(l==m&&!h)l+=0.125;var n=this.getCoords_(b,a),o=this.getCoords_(l,e),q=this.getCoords_(m,r);this.currentPath_.push({type:g,x:n.x,y:n.y,radius:c,xStart:o.x,yStart:o.y,xEnd:q.x,yEnd:q.y})};i.rect=function(b,a,c,d){this.moveTo(b,
|
||||
a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath()};i.strokeRect=function(b,a,c,d){var f=this.currentPath_;this.beginPath();this.moveTo(b,a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath();this.stroke();this.currentPath_=f};i.fillRect=function(b,a,c,d){var f=this.currentPath_;this.beginPath();this.moveTo(b,a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath();this.fill();this.currentPath_=f};i.createLinearGradient=function(b,
|
||||
a,c,d){var f=new D("gradient");f.x0_=b;f.y0_=a;f.x1_=c;f.y1_=d;return f};i.createRadialGradient=function(b,a,c,d,f,h){var g=new D("gradientradial");g.x0_=b;g.y0_=a;g.r0_=c;g.x1_=d;g.y1_=f;g.r1_=h;return g};i.drawImage=function(b){var a,c,d,f,h,g,l,e,m=b.runtimeStyle.width,r=b.runtimeStyle.height;b.runtimeStyle.width="auto";b.runtimeStyle.height="auto";var n=b.width,o=b.height;b.runtimeStyle.width=m;b.runtimeStyle.height=r;if(arguments.length==3){a=arguments[1];c=arguments[2];h=g=0;l=d=n;e=f=o}else if(arguments.length==
|
||||
5){a=arguments[1];c=arguments[2];d=arguments[3];f=arguments[4];h=g=0;l=n;e=o}else if(arguments.length==9){h=arguments[1];g=arguments[2];l=arguments[3];e=arguments[4];a=arguments[5];c=arguments[6];d=arguments[7];f=arguments[8]}else throw Error("Invalid number of arguments");var q=this.getCoords_(a,c),t=[];t.push(" <g_vml_:group",' coordsize="',k*10,",",k*10,'"',' coordorigin="0,0"',' style="width:',10,"px;height:",10,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]){var E=[];E.push("M11=",
|
||||
this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",j(q.x/k),",","Dy=",j(q.y/k),"");var p=q,z=this.getCoords_(a+d,c),w=this.getCoords_(a,c+f),x=this.getCoords_(a+d,c+f);p.x=s.max(p.x,z.x,w.x,x.x);p.y=s.max(p.y,z.y,w.y,x.y);t.push("padding:0 ",j(p.x/k),"px ",j(p.y/k),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",E.join(""),", sizingmethod='clip');")}else t.push("top:",j(q.y/k),"px;left:",j(q.x/k),"px;");t.push(' ">','<g_vml_:image src="',b.src,
|
||||
'"',' style="width:',k*d,"px;"," height:",k*f,'px;"',' cropleft="',h/n,'"',' croptop="',g/o,'"',' cropright="',(n-h-l)/n,'"',' cropbottom="',(o-g-e)/o,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",t.join(""))};i.stroke=function(b){var a=[],c=P(b?this.fillStyle:this.strokeStyle),d=c.color,f=c.alpha*this.globalAlpha;a.push("<g_vml_:shape",' filled="',!!b,'"',' style="position:absolute;width:',10,"px;height:",10,'px;"',' coordorigin="0 0" coordsize="',k*10," ",k*10,'"',' stroked="',
|
||||
!b,'"',' path="');var h={x:null,y:null},g={x:null,y:null},l=0;for(;l<this.currentPath_.length;l++){var e=this.currentPath_[l];switch(e.type){case "moveTo":a.push(" m ",j(e.x),",",j(e.y));break;case "lineTo":a.push(" l ",j(e.x),",",j(e.y));break;case "close":a.push(" x ");e=null;break;case "bezierCurveTo":a.push(" c ",j(e.cp1x),",",j(e.cp1y),",",j(e.cp2x),",",j(e.cp2y),",",j(e.x),",",j(e.y));break;case "at":case "wa":a.push(" ",e.type," ",j(e.x-this.arcScaleX_*e.radius),",",j(e.y-this.arcScaleY_*e.radius),
|
||||
" ",j(e.x+this.arcScaleX_*e.radius),",",j(e.y+this.arcScaleY_*e.radius)," ",j(e.xStart),",",j(e.yStart)," ",j(e.xEnd),",",j(e.yEnd));break}if(e){if(h.x==null||e.x<h.x)h.x=e.x;if(g.x==null||e.x>g.x)g.x=e.x;if(h.y==null||e.y<h.y)h.y=e.y;if(g.y==null||e.y>g.y)g.y=e.y}}a.push(' ">');if(b)if(typeof this.fillStyle=="object"){var m=this.fillStyle,r=0,n={x:0,y:0},o=0,q=1;if(m.type_=="gradient"){var t=m.x1_/this.arcScaleX_,E=m.y1_/this.arcScaleY_,p=this.getCoords_(m.x0_/this.arcScaleX_,m.y0_/this.arcScaleY_),
|
||||
z=this.getCoords_(t,E);r=Math.atan2(z.x-p.x,z.y-p.y)*180/Math.PI;if(r<0)r+=360;if(r<1.0E-6)r=0}else{var p=this.getCoords_(m.x0_,m.y0_),w=g.x-h.x,x=g.y-h.y;n={x:(p.x-h.x)/w,y:(p.y-h.y)/x};w/=this.arcScaleX_*k;x/=this.arcScaleY_*k;var R=s.max(w,x);o=2*m.r0_/R;q=2*m.r1_/R-o}var u=m.colors_;u.sort(function(ba,ca){return ba.offset-ca.offset});var J=u.length,da=u[0].color,ea=u[J-1].color,fa=u[0].alpha*this.globalAlpha,ga=u[J-1].alpha*this.globalAlpha,S=[],l=0;for(;l<J;l++){var T=u[l];S.push(T.offset*q+
|
||||
o+" "+T.color)}a.push('<g_vml_:fill type="',m.type_,'"',' method="none" focus="100%"',' color="',da,'"',' color2="',ea,'"',' colors="',S.join(","),'"',' opacity="',ga,'"',' g_o_:opacity2="',fa,'"',' angle="',r,'"',' focusposition="',n.x,",",n.y,'" />')}else a.push('<g_vml_:fill color="',d,'" opacity="',f,'" />');else{var K=this.lineScale_*this.lineWidth;if(K<1)f*=K;a.push("<g_vml_:stroke",' opacity="',f,'"',' joinstyle="',this.lineJoin,'"',' miterlimit="',this.miterLimit,'"',' endcap="',aa(this.lineCap),
|
||||
'"',' weight="',K,'px"',' color="',d,'" />')}a.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",a.join(""))};i.fill=function(){this.stroke(true)};i.closePath=function(){this.currentPath_.push({type:"close"})};i.getCoords_=function(b,a){var c=this.m_;return{x:k*(b*c[0][0]+a*c[1][0]+c[2][0])-v,y:k*(b*c[0][1]+a*c[1][1]+c[2][1])-v}};i.save=function(){var b={};O(this,b);this.aStack_.push(b);this.mStack_.push(this.m_);this.m_=y(I(),this.m_)};i.restore=function(){O(this.aStack_.pop(),
|
||||
this);this.m_=this.mStack_.pop()};function ha(b){var a=0;for(;a<3;a++){var c=0;for(;c<2;c++)if(!isFinite(b[a][c])||isNaN(b[a][c]))return false}return true}function A(b,a,c){if(!!ha(a)){b.m_=a;if(c)b.lineScale_=W(V(a[0][0]*a[1][1]-a[0][1]*a[1][0]))}}i.translate=function(b,a){A(this,y([[1,0,0],[0,1,0],[b,a,1]],this.m_),false)};i.rotate=function(b){var a=G(b),c=F(b);A(this,y([[a,c,0],[-c,a,0],[0,0,1]],this.m_),false)};i.scale=function(b,a){this.arcScaleX_*=b;this.arcScaleY_*=a;A(this,y([[b,0,0],[0,a,
|
||||
0],[0,0,1]],this.m_),true)};i.transform=function(b,a,c,d,f,h){A(this,y([[b,a,0],[c,d,0],[f,h,1]],this.m_),true)};i.setTransform=function(b,a,c,d,f,h){A(this,[[b,a,0],[c,d,0],[f,h,1]],true)};i.clip=function(){};i.arcTo=function(){};i.createPattern=function(){return new U};function D(b){this.type_=b;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}D.prototype.addColorStop=function(b,a){a=P(a);this.colors_.push({offset:b,color:a.color,alpha:a.alpha})};function U(){}G_vmlCanvasManager=
|
||||
M;CanvasRenderingContext2D=H;CanvasGradient=D;CanvasPattern=U})();
|
524
src/static/js/farbtastic.js
Normal file
|
@ -0,0 +1,524 @@
|
|||
// Farbtastic 2.0 alpha
|
||||
(function ($) {
|
||||
|
||||
var __debug = false;
|
||||
var __factor = 0.5;
|
||||
|
||||
$.fn.farbtastic = function (options) {
|
||||
$.farbtastic(this, options);
|
||||
return this;
|
||||
};
|
||||
|
||||
$.farbtastic = function (container, options) {
|
||||
var container = $(container)[0];
|
||||
return container.farbtastic || (container.farbtastic = new $._farbtastic(container, options));
|
||||
}
|
||||
|
||||
$._farbtastic = function (container, options) {
|
||||
var fb = this;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Link to the given element(s) or callback.
|
||||
*/
|
||||
fb.linkTo = function (callback) {
|
||||
// Unbind previous nodes
|
||||
if (typeof fb.callback == 'object') {
|
||||
$(fb.callback).unbind('keyup', fb.updateValue);
|
||||
}
|
||||
|
||||
// Reset color
|
||||
fb.color = null;
|
||||
|
||||
// Bind callback or elements
|
||||
if (typeof callback == 'function') {
|
||||
fb.callback = callback;
|
||||
}
|
||||
else if (typeof callback == 'object' || typeof callback == 'string') {
|
||||
fb.callback = $(callback);
|
||||
fb.callback.bind('keyup', fb.updateValue);
|
||||
if (fb.callback[0].value) {
|
||||
fb.setColor(fb.callback[0].value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
fb.updateValue = function (event) {
|
||||
if (this.value && this.value != fb.color) {
|
||||
fb.setColor(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change color with HTML syntax #123456
|
||||
*/
|
||||
fb.setColor = function (color) {
|
||||
var unpack = fb.unpack(color);
|
||||
if (fb.color != color && unpack) {
|
||||
fb.color = color;
|
||||
fb.rgb = unpack;
|
||||
fb.hsl = fb.RGBToHSL(fb.rgb);
|
||||
fb.updateDisplay();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change color with HSL triplet [0..1, 0..1, 0..1]
|
||||
*/
|
||||
fb.setHSL = function (hsl) {
|
||||
fb.hsl = hsl;
|
||||
|
||||
var convertedHSL = [hsl[0]]
|
||||
convertedHSL[1] = hsl[1]*__factor+((1-__factor)/2);
|
||||
convertedHSL[2] = hsl[2]*__factor+((1-__factor)/2);
|
||||
|
||||
fb.rgb = fb.HSLToRGB(convertedHSL);
|
||||
fb.color = fb.pack(fb.rgb);
|
||||
fb.updateDisplay();
|
||||
return this;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//excanvas-compatible building of canvases
|
||||
fb._makeCanvas = function(className){
|
||||
var c = document.createElement('canvas');
|
||||
if (!c.getContext) { // excanvas hack
|
||||
c = window.G_vmlCanvasManager.initElement(c);
|
||||
c.getContext(); //this creates the excanvas children
|
||||
}
|
||||
$(c).addClass(className);
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the color picker widget.
|
||||
*/
|
||||
fb.initWidget = function () {
|
||||
|
||||
// Insert markup and size accordingly.
|
||||
var dim = {
|
||||
width: options.width,
|
||||
height: options.width
|
||||
};
|
||||
$(container)
|
||||
.html(
|
||||
'<div class="farbtastic" style="position: relative">' +
|
||||
'<div class="farbtastic-solid"></div>' +
|
||||
'</div>'
|
||||
)
|
||||
.children('.farbtastic')
|
||||
.append(fb._makeCanvas('farbtastic-mask'))
|
||||
.append(fb._makeCanvas('farbtastic-overlay'))
|
||||
.end()
|
||||
.find('*').attr(dim).css(dim).end()
|
||||
.find('div>*').css('position', 'absolute');
|
||||
|
||||
// Determine layout
|
||||
fb.radius = (options.width - options.wheelWidth) / 2 - 1;
|
||||
fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1;
|
||||
fb.mid = Math.floor(options.width / 2);
|
||||
fb.markerSize = options.wheelWidth * 0.3;
|
||||
fb.solidFill = $('.farbtastic-solid', container).css({
|
||||
width: fb.square * 2 - 1,
|
||||
height: fb.square * 2 - 1,
|
||||
left: fb.mid - fb.square,
|
||||
top: fb.mid - fb.square
|
||||
});
|
||||
|
||||
// Set up drawing context.
|
||||
fb.cnvMask = $('.farbtastic-mask', container);
|
||||
fb.ctxMask = fb.cnvMask[0].getContext('2d');
|
||||
fb.cnvOverlay = $('.farbtastic-overlay', container);
|
||||
fb.ctxOverlay = fb.cnvOverlay[0].getContext('2d');
|
||||
fb.ctxMask.translate(fb.mid, fb.mid);
|
||||
fb.ctxOverlay.translate(fb.mid, fb.mid);
|
||||
|
||||
// Draw widget base layers.
|
||||
fb.drawCircle();
|
||||
fb.drawMask();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the color wheel.
|
||||
*/
|
||||
fb.drawCircle = function () {
|
||||
var tm = +(new Date());
|
||||
// Draw a hue circle with a bunch of gradient-stroked beziers.
|
||||
// Have to use beziers, as gradient-stroked arcs don't work.
|
||||
var n = 24,
|
||||
r = fb.radius,
|
||||
w = options.wheelWidth,
|
||||
nudge = 8 / r / n * Math.PI, // Fudge factor for seams.
|
||||
m = fb.ctxMask,
|
||||
angle1 = 0, color1, d1;
|
||||
m.save();
|
||||
m.lineWidth = w / r;
|
||||
m.scale(r, r);
|
||||
// Each segment goes from angle1 to angle2.
|
||||
for (var i = 0; i <= n; ++i) {
|
||||
var d2 = i / n,
|
||||
angle2 = d2 * Math.PI * 2,
|
||||
// Endpoints
|
||||
x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
|
||||
x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
|
||||
// Midpoint chosen so that the endpoints are tangent to the circle.
|
||||
am = (angle1 + angle2) / 2,
|
||||
tan = 1 / Math.cos((angle2 - angle1) / 2),
|
||||
xm = Math.sin(am) * tan, ym = -Math.cos(am) * tan,
|
||||
// New color
|
||||
color2 = fb.pack(fb.HSLToRGB([d2, 1, 0.5]));
|
||||
if (i > 0) {
|
||||
if ($.browser.msie) {
|
||||
// IE's gradient calculations mess up the colors. Correct along the diagonals.
|
||||
var corr = (1 + Math.min(Math.abs(Math.tan(angle1)), Math.abs(Math.tan(Math.PI / 2 - angle1)))) / n;
|
||||
color1 = fb.pack(fb.HSLToRGB([d1 - 0.15 * corr, 1, 0.5]));
|
||||
color2 = fb.pack(fb.HSLToRGB([d2 + 0.15 * corr, 1, 0.5]));
|
||||
// Create gradient fill between the endpoints.
|
||||
var grad = m.createLinearGradient(x1, y1, x2, y2);
|
||||
grad.addColorStop(0, color1);
|
||||
grad.addColorStop(1, color2);
|
||||
m.fillStyle = grad;
|
||||
// Draw quadratic curve segment as a fill.
|
||||
var r1 = (r + w / 2) / r, r2 = (r - w / 2) / r; // inner/outer radius.
|
||||
m.beginPath();
|
||||
m.moveTo(x1 * r1, y1 * r1);
|
||||
m.quadraticCurveTo(xm * r1, ym * r1, x2 * r1, y2 * r1);
|
||||
m.lineTo(x2 * r2, y2 * r2);
|
||||
m.quadraticCurveTo(xm * r2, ym * r2, x1 * r2, y1 * r2);
|
||||
m.fill();
|
||||
}
|
||||
else {
|
||||
// Create gradient fill between the endpoints.
|
||||
var grad = m.createLinearGradient(x1, y1, x2, y2);
|
||||
grad.addColorStop(0, color1);
|
||||
grad.addColorStop(1, color2);
|
||||
m.strokeStyle = grad;
|
||||
// Draw quadratic curve segment.
|
||||
m.beginPath();
|
||||
m.moveTo(x1, y1);
|
||||
m.quadraticCurveTo(xm, ym, x2, y2);
|
||||
m.stroke();
|
||||
}
|
||||
}
|
||||
// Prevent seams where curves join.
|
||||
angle1 = angle2 - nudge; color1 = color2; d1 = d2;
|
||||
}
|
||||
m.restore();
|
||||
__debug && $('body').append('<div>drawCircle '+ (+(new Date()) - tm) +'ms');
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the saturation/luminance mask.
|
||||
*/
|
||||
fb.drawMask = function () {
|
||||
var tm = +(new Date());
|
||||
|
||||
// Iterate over sat/lum space and calculate appropriate mask pixel values.
|
||||
var size = fb.square * 2, sq = fb.square;
|
||||
function calculateMask(sizex, sizey, outputPixel) {
|
||||
var isx = 1 / sizex, isy = 1 / sizey;
|
||||
for (var y = 0; y <= sizey; ++y) {
|
||||
var l = 1 - y * isy;
|
||||
for (var x = 0; x <= sizex; ++x) {
|
||||
var s = 1 - x * isx;
|
||||
// From sat/lum to alpha and color (grayscale)
|
||||
var a = 1 - 2 * Math.min(l * s, (1 - l) * s);
|
||||
var c = (a > 0) ? ((2 * l - 1 + a) * .5 / a) : 0;
|
||||
|
||||
a = a*__factor+(1-__factor)/2;
|
||||
c = c*__factor+(1-__factor)/2;
|
||||
|
||||
outputPixel(x, y, c, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method #1: direct pixel access (new Canvas).
|
||||
if (fb.ctxMask.getImageData) {
|
||||
// Create half-resolution buffer.
|
||||
var sz = Math.floor(size / 2);
|
||||
var buffer = document.createElement('canvas');
|
||||
buffer.width = buffer.height = sz + 1;
|
||||
var ctx = buffer.getContext('2d');
|
||||
var frame = ctx.getImageData(0, 0, sz + 1, sz + 1);
|
||||
|
||||
var i = 0;
|
||||
calculateMask(sz, sz, function (x, y, c, a) {
|
||||
frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255;
|
||||
frame.data[i++] = a * 255;
|
||||
});
|
||||
|
||||
ctx.putImageData(frame, 0, 0);
|
||||
fb.ctxMask.drawImage(buffer, 0, 0, sz + 1, sz + 1, -sq, -sq, sq * 2, sq * 2);
|
||||
}
|
||||
// Method #2: drawing commands (old Canvas).
|
||||
else if (!$.browser.msie) {
|
||||
// Render directly at half-resolution
|
||||
var sz = Math.floor(size / 2);
|
||||
calculateMask(sz, sz, function (x, y, c, a) {
|
||||
c = Math.round(c * 255);
|
||||
fb.ctxMask.fillStyle = 'rgba(' + c + ', ' + c + ', ' + c + ', ' + a +')';
|
||||
fb.ctxMask.fillRect(x * 2 - sq - 1, y * 2 - sq - 1, 2, 2);
|
||||
});
|
||||
}
|
||||
// Method #3: vertical DXImageTransform gradient strips (IE).
|
||||
else {
|
||||
var cache_last, cache, w = 6; // Each strip is 6 pixels wide.
|
||||
var sizex = Math.floor(size / w);
|
||||
// 6 vertical pieces of gradient per strip.
|
||||
calculateMask(sizex, 6, function (x, y, c, a) {
|
||||
if (x == 0) {
|
||||
cache_last = cache;
|
||||
cache = [];
|
||||
}
|
||||
c = Math.round(c * 255);
|
||||
a = Math.round(a * 255);
|
||||
// We can only start outputting gradients once we have two rows of pixels.
|
||||
if (y > 0) {
|
||||
var c_last = cache_last[x][0],
|
||||
a_last = cache_last[x][1],
|
||||
color1 = fb.packDX(c_last, a_last),
|
||||
color2 = fb.packDX(c, a),
|
||||
y1 = Math.round(fb.mid + ((y - 1) * .333 - 1) * sq),
|
||||
y2 = Math.round(fb.mid + (y * .333 - 1) * sq);
|
||||
$('<div>').css({
|
||||
position: 'absolute',
|
||||
filter: "progid:DXImageTransform.Microsoft.Gradient(StartColorStr="+ color1 +", EndColorStr="+ color2 +", GradientType=0)",
|
||||
top: y1,
|
||||
height: y2 - y1,
|
||||
// Avoid right-edge sticking out.
|
||||
left: fb.mid + (x * w - sq - 1),
|
||||
width: w - (x == sizex ? Math.round(w / 2) : 0)
|
||||
}).appendTo(fb.cnvMask);
|
||||
}
|
||||
cache.push([c, a]);
|
||||
});
|
||||
}
|
||||
__debug && $('body').append('<div>drawMask '+ (+(new Date()) - tm) +'ms');
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the selection markers.
|
||||
*/
|
||||
fb.drawMarkers = function () {
|
||||
// Determine marker dimensions
|
||||
var sz = options.width, lw = Math.ceil(fb.markerSize / 4), r = fb.markerSize - lw + 1;
|
||||
var angle = fb.hsl[0] * 6.28,
|
||||
x1 = Math.sin(angle) * fb.radius,
|
||||
y1 = -Math.cos(angle) * fb.radius,
|
||||
x2 = 2 * fb.square * (.5 - fb.hsl[1]),
|
||||
y2 = 2 * fb.square * (.5 - fb.hsl[2]),
|
||||
c1 = fb.invert ? '#fff' : '#000',
|
||||
c2 = fb.invert ? '#000' : '#fff';
|
||||
var circles = [
|
||||
{ x: x1, y: y1, r: r, c: '#000', lw: lw + 1 },
|
||||
{ x: x1, y: y1, r: fb.markerSize, c: '#fff', lw: lw },
|
||||
{ x: x2, y: y2, r: r, c: c2, lw: lw + 1 },
|
||||
{ x: x2, y: y2, r: fb.markerSize, c: c1, lw: lw },
|
||||
];
|
||||
|
||||
// Update the overlay canvas.
|
||||
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
|
||||
for (i in circles) {
|
||||
var c = circles[i];
|
||||
fb.ctxOverlay.lineWidth = c.lw;
|
||||
fb.ctxOverlay.strokeStyle = c.c;
|
||||
fb.ctxOverlay.beginPath();
|
||||
fb.ctxOverlay.arc(c.x, c.y, c.r, 0, Math.PI * 2, true);
|
||||
fb.ctxOverlay.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the markers and styles
|
||||
*/
|
||||
fb.updateDisplay = function () {
|
||||
// Determine whether labels/markers should invert.
|
||||
fb.invert = (fb.rgb[0] * 0.3 + fb.rgb[1] * .59 + fb.rgb[2] * .11) <= 0.6;
|
||||
|
||||
// Update the solid background fill.
|
||||
fb.solidFill.css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
|
||||
|
||||
// Draw markers
|
||||
fb.drawMarkers();
|
||||
|
||||
// Linked elements or callback
|
||||
if (typeof fb.callback == 'object') {
|
||||
// Set background/foreground color
|
||||
$(fb.callback).css({
|
||||
backgroundColor: fb.color,
|
||||
color: fb.invert ? '#fff' : '#000'
|
||||
});
|
||||
|
||||
// Change linked value
|
||||
$(fb.callback).each(function() {
|
||||
if ((typeof this.value == 'string') && this.value != fb.color) {
|
||||
this.value = fb.color;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (typeof fb.callback == 'function') {
|
||||
fb.callback.call(fb, fb.color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for returning coordinates relative to the center.
|
||||
*/
|
||||
fb.widgetCoords = function (event) {
|
||||
return {
|
||||
x: event.pageX - fb.offset.left - fb.mid,
|
||||
y: event.pageY - fb.offset.top - fb.mid
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mousedown handler
|
||||
*/
|
||||
fb.mousedown = function (event) {
|
||||
// Capture mouse
|
||||
if (!$._farbtastic.dragging) {
|
||||
$(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
|
||||
$._farbtastic.dragging = true;
|
||||
}
|
||||
|
||||
// Update the stored offset for the widget.
|
||||
fb.offset = $(container).offset();
|
||||
|
||||
// Check which area is being dragged
|
||||
var pos = fb.widgetCoords(event);
|
||||
fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) > (fb.square + 2);
|
||||
|
||||
// Process
|
||||
fb.mousemove(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mousemove handler
|
||||
*/
|
||||
fb.mousemove = function (event) {
|
||||
// Get coordinates relative to color picker center
|
||||
var pos = fb.widgetCoords(event);
|
||||
|
||||
// Set new HSL parameters
|
||||
if (fb.circleDrag) {
|
||||
var hue = Math.atan2(pos.x, -pos.y) / 6.28;
|
||||
fb.setHSL([(hue + 1) % 1, fb.hsl[1], fb.hsl[2]]);
|
||||
}
|
||||
else {
|
||||
var sat = Math.max(0, Math.min(1, -(pos.x / fb.square / 2) + .5));
|
||||
var lum = Math.max(0, Math.min(1, -(pos.y / fb.square / 2) + .5));
|
||||
fb.setHSL([fb.hsl[0], sat, lum]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouseup handler
|
||||
*/
|
||||
fb.mouseup = function () {
|
||||
// Uncapture mouse
|
||||
$(document).unbind('mousemove', fb.mousemove);
|
||||
$(document).unbind('mouseup', fb.mouseup);
|
||||
$._farbtastic.dragging = false;
|
||||
}
|
||||
|
||||
/* Various color utility functions */
|
||||
fb.dec2hex = function (x) {
|
||||
return (x < 16 ? '0' : '') + x.toString(16);
|
||||
}
|
||||
|
||||
fb.packDX = function (c, a) {
|
||||
return '#' + fb.dec2hex(a) + fb.dec2hex(c) + fb.dec2hex(c) + fb.dec2hex(c);
|
||||
};
|
||||
|
||||
fb.pack = function (rgb) {
|
||||
var r = Math.round(rgb[0] * 255);
|
||||
var g = Math.round(rgb[1] * 255);
|
||||
var b = Math.round(rgb[2] * 255);
|
||||
return '#' + fb.dec2hex(r) + fb.dec2hex(g) + fb.dec2hex(b);
|
||||
};
|
||||
|
||||
fb.unpack = function (color) {
|
||||
if (color.length == 7) {
|
||||
function x(i) {
|
||||
return parseInt(color.substring(i, i + 2), 16) / 255;
|
||||
}
|
||||
return [ x(1), x(3), x(5) ];
|
||||
}
|
||||
else if (color.length == 4) {
|
||||
function x(i) {
|
||||
return parseInt(color.substring(i, i + 1), 16) / 15;
|
||||
}
|
||||
return [ x(1), x(2), x(3) ];
|
||||
}
|
||||
};
|
||||
|
||||
fb.HSLToRGB = function (hsl) {
|
||||
var m1, m2, r, g, b;
|
||||
var h = hsl[0], s = hsl[1], l = hsl[2];
|
||||
m2 = (l <= 0.5) ? l * (s + 1) : l + s - l * s;
|
||||
m1 = l * 2 - m2;
|
||||
return [
|
||||
this.hueToRGB(m1, m2, h + 0.33333),
|
||||
this.hueToRGB(m1, m2, h),
|
||||
this.hueToRGB(m1, m2, h - 0.33333)
|
||||
];
|
||||
};
|
||||
|
||||
fb.hueToRGB = function (m1, m2, h) {
|
||||
h = (h + 1) % 1;
|
||||
if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
|
||||
if (h * 2 < 1) return m2;
|
||||
if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
|
||||
return m1;
|
||||
};
|
||||
|
||||
fb.RGBToHSL = function (rgb) {
|
||||
var r = rgb[0], g = rgb[1], b = rgb[2],
|
||||
min = Math.min(r, g, b),
|
||||
max = Math.max(r, g, b),
|
||||
delta = max - min,
|
||||
h = 0,
|
||||
s = 0,
|
||||
l = (min + max) / 2;
|
||||
if (l > 0 && l < 1) {
|
||||
s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
|
||||
}
|
||||
if (delta > 0) {
|
||||
if (max == r && max != g) h += (g - b) / delta;
|
||||
if (max == g && max != b) h += (2 + (b - r) / delta);
|
||||
if (max == b && max != r) h += (4 + (r - g) / delta);
|
||||
h /= 6;
|
||||
}
|
||||
return [h, s, l];
|
||||
};
|
||||
|
||||
// Parse options.
|
||||
if (!options.callback) {
|
||||
options = { callback: options };
|
||||
}
|
||||
options = $.extend({
|
||||
width: 300,
|
||||
wheelWidth: (options.width || 300) / 10,
|
||||
callback: null
|
||||
}, options);
|
||||
|
||||
// Initialize.
|
||||
fb.initWidget();
|
||||
|
||||
// Install mousedown handler (the others are set on the document on-demand)
|
||||
$('canvas.farbtastic-overlay', container).mousedown(fb.mousedown);
|
||||
|
||||
// Set linked elements/callback
|
||||
if (options.callback) {
|
||||
fb.linkTo(options.callback);
|
||||
}
|
||||
// Set to gray.
|
||||
fb.setColor('#808080');
|
||||
}
|
||||
|
||||
})(jQuery);
|
473
src/static/js/json2.js
Normal file
|
@ -0,0 +1,473 @@
|
|||
/*
|
||||
http://www.JSON.org/json2.js
|
||||
2011-02-23
|
||||
|
||||
Public Domain.
|
||||
|
||||
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||
|
||||
See http://www.JSON.org/js.html
|
||||
|
||||
|
||||
This code should be minified before deployment.
|
||||
See http://javascript.crockford.com/jsmin.html
|
||||
|
||||
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||
NOT CONTROL.
|
||||
|
||||
|
||||
This file creates a global JSON object containing two methods: stringify
|
||||
and parse.
|
||||
|
||||
JSON.stringify(value, replacer, space)
|
||||
value any JavaScript value, usually an object or array.
|
||||
|
||||
replacer an optional parameter that determines how object
|
||||
values are stringified for objects. It can be a
|
||||
function or an array of strings.
|
||||
|
||||
space an optional parameter that specifies the indentation
|
||||
of nested structures. If it is omitted, the text will
|
||||
be packed without extra whitespace. If it is a number,
|
||||
it will specify the number of spaces to indent at each
|
||||
level. If it is a string (such as '\t' or ' '),
|
||||
it contains the characters used to indent at each level.
|
||||
|
||||
This method produces a JSON text from a JavaScript value.
|
||||
|
||||
When an object value is found, if the object contains a toJSON
|
||||
method, its toJSON method will be called and the result will be
|
||||
stringified. A toJSON method does not serialize: it returns the
|
||||
value represented by the name/value pair that should be serialized,
|
||||
or undefined if nothing should be serialized. The toJSON method
|
||||
will be passed the key associated with the value, and this will be
|
||||
bound to the value
|
||||
|
||||
For example, this would serialize Dates as ISO strings.
|
||||
|
||||
Date.prototype.toJSON = function (key) {
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
return this.getUTCFullYear() + '-' +
|
||||
f(this.getUTCMonth() + 1) + '-' +
|
||||
f(this.getUTCDate()) + 'T' +
|
||||
f(this.getUTCHours()) + ':' +
|
||||
f(this.getUTCMinutes()) + ':' +
|
||||
f(this.getUTCSeconds()) + 'Z';
|
||||
};
|
||||
|
||||
You can provide an optional replacer method. It will be passed the
|
||||
key and value of each member, with this bound to the containing
|
||||
object. The value that is returned from your method will be
|
||||
serialized. If your method returns undefined, then the member will
|
||||
be excluded from the serialization.
|
||||
|
||||
If the replacer parameter is an array of strings, then it will be
|
||||
used to select the members to be serialized. It filters the results
|
||||
such that only members with keys listed in the replacer array are
|
||||
stringified.
|
||||
|
||||
Values that do not have JSON representations, such as undefined or
|
||||
functions, will not be serialized. Such values in objects will be
|
||||
dropped; in arrays they will be replaced with null. You can use
|
||||
a replacer function to replace those with JSON values.
|
||||
JSON.stringify(undefined) returns undefined.
|
||||
|
||||
The optional space parameter produces a stringification of the
|
||||
value that is filled with line breaks and indentation to make it
|
||||
easier to read.
|
||||
|
||||
If the space parameter is a non-empty string, then that string will
|
||||
be used for indentation. If the space parameter is a number, then
|
||||
the indentation will be that many spaces.
|
||||
|
||||
Example:
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}]);
|
||||
// text is '["e",{"pluribus":"unum"}]'
|
||||
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
|
||||
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||
|
||||
text = JSON.stringify([new Date()], function (key, value) {
|
||||
return this[key] instanceof Date ?
|
||||
'Date(' + this[key] + ')' : value;
|
||||
});
|
||||
// text is '["Date(---current time---)"]'
|
||||
|
||||
|
||||
JSON.parse(text, reviver)
|
||||
This method parses a JSON text to produce an object or array.
|
||||
It can throw a SyntaxError exception.
|
||||
|
||||
The optional reviver parameter is a function that can filter and
|
||||
transform the results. It receives each of the keys and values,
|
||||
and its return value is used instead of the original value.
|
||||
If it returns what it received, then the structure is not modified.
|
||||
If it returns undefined then the member is deleted.
|
||||
|
||||
Example:
|
||||
|
||||
// Parse the text. Values that look like ISO date strings will
|
||||
// be converted to Date objects.
|
||||
|
||||
myData = JSON.parse(text, function (key, value) {
|
||||
var a;
|
||||
if (typeof value === 'string') {
|
||||
a =
|
||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||
if (a) {
|
||||
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
|
||||
+a[5], +a[6]));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
|
||||
var d;
|
||||
if (typeof value === 'string' &&
|
||||
value.slice(0, 5) === 'Date(' &&
|
||||
value.slice(-1) === ')') {
|
||||
d = new Date(value.slice(5, -1));
|
||||
if (d) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
|
||||
This is a reference implementation. You are free to copy, modify, or
|
||||
redistribute.
|
||||
*/
|
||||
|
||||
/*jslint evil: true, strict: false, regexp: false */
|
||||
|
||||
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
|
||||
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||
test, toJSON, toString, valueOf
|
||||
*/
|
||||
|
||||
|
||||
// Create a JSON object only if one does not already exist. We create the
|
||||
// methods in a closure to avoid creating global variables.
|
||||
var JSON;
|
||||
if (!JSON)
|
||||
{
|
||||
JSON = {};
|
||||
}
|
||||
|
||||
(function()
|
||||
{
|
||||
"use strict";
|
||||
|
||||
function f(n)
|
||||
{
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
if (typeof Date.prototype.toJSON !== 'function')
|
||||
{
|
||||
|
||||
Date.prototype.toJSON = function(key)
|
||||
{
|
||||
|
||||
return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null;
|
||||
};
|
||||
|
||||
String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function(key)
|
||||
{
|
||||
return this.valueOf();
|
||||
};
|
||||
}
|
||||
|
||||
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
gap, indent, meta = { // table of character substitutions
|
||||
'\b': '\\b',
|
||||
'\t': '\\t',
|
||||
'\n': '\\n',
|
||||
'\f': '\\f',
|
||||
'\r': '\\r',
|
||||
'"': '\\"',
|
||||
'\\': '\\\\'
|
||||
},
|
||||
rep;
|
||||
|
||||
|
||||
function quote(string)
|
||||
{
|
||||
|
||||
// If the string contains no control characters, no quote characters, and no
|
||||
// backslash characters, then we can safely slap some quotes around it.
|
||||
// Otherwise we must also replace the offending characters with safe escape
|
||||
// sequences.
|
||||
escapable.lastIndex = 0;
|
||||
return escapable.test(string) ? '"' + string.replace(escapable, function(a)
|
||||
{
|
||||
var c = meta[a];
|
||||
return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
}) + '"' : '"' + string + '"';
|
||||
}
|
||||
|
||||
|
||||
function str(key, holder)
|
||||
{
|
||||
|
||||
// Produce a string from holder[key].
|
||||
var i, // The loop counter.
|
||||
k, // The member key.
|
||||
v, // The member value.
|
||||
length, mind = gap,
|
||||
partial, value = holder[key];
|
||||
|
||||
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||
if (value && typeof value === 'object' && typeof value.toJSON === 'function')
|
||||
{
|
||||
value = value.toJSON(key);
|
||||
}
|
||||
|
||||
// If we were called with a replacer function, then call the replacer to
|
||||
// obtain a replacement value.
|
||||
if (typeof rep === 'function')
|
||||
{
|
||||
value = rep.call(holder, key, value);
|
||||
}
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
switch (typeof value)
|
||||
{
|
||||
case 'string':
|
||||
return quote(value);
|
||||
|
||||
case 'number':
|
||||
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
return isFinite(value) ? String(value) : 'null';
|
||||
|
||||
case 'boolean':
|
||||
case 'null':
|
||||
|
||||
// If the value is a boolean or null, convert it to a string. Note:
|
||||
// typeof null does not produce 'null'. The case is included here in
|
||||
// the remote chance that this gets fixed someday.
|
||||
return String(value);
|
||||
|
||||
// If the type is 'object', we might be dealing with an object or an array or
|
||||
// null.
|
||||
case 'object':
|
||||
|
||||
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||
// so watch out for that case.
|
||||
if (!value)
|
||||
{
|
||||
return 'null';
|
||||
}
|
||||
|
||||
// Make an array to hold the partial results of stringifying this object value.
|
||||
gap += indent;
|
||||
partial = [];
|
||||
|
||||
// Is the value an array?
|
||||
if (Object.prototype.toString.apply(value) === '[object Array]')
|
||||
{
|
||||
|
||||
// The value is an array. Stringify every element. Use null as a placeholder
|
||||
// for non-JSON values.
|
||||
length = value.length;
|
||||
for (i = 0; i < length; i += 1)
|
||||
{
|
||||
partial[i] = str(i, value) || 'null';
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap them in
|
||||
// brackets.
|
||||
v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
|
||||
// If the replacer is an array, use it to select the members to be stringified.
|
||||
if (rep && typeof rep === 'object')
|
||||
{
|
||||
length = rep.length;
|
||||
for (i = 0; i < length; i += 1)
|
||||
{
|
||||
if (typeof rep[i] === 'string')
|
||||
{
|
||||
k = rep[i];
|
||||
v = str(k, value);
|
||||
if (v)
|
||||
{
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
// Otherwise, iterate through all of the keys in the object.
|
||||
for (k in value)
|
||||
{
|
||||
if (Object.prototype.hasOwnProperty.call(value, k))
|
||||
{
|
||||
v = str(k, value);
|
||||
if (v)
|
||||
{
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// If the JSON object does not yet have a stringify method, give it one.
|
||||
if (typeof JSON.stringify !== 'function')
|
||||
{
|
||||
JSON.stringify = function(value, replacer, space)
|
||||
{
|
||||
|
||||
// The stringify method takes a value and an optional replacer, and an optional
|
||||
// space parameter, and returns a JSON text. The replacer can be a function
|
||||
// that can replace values, or an array of strings that will select the keys.
|
||||
// A default replacer method can be provided. Use of the space parameter can
|
||||
// produce text that is more easily readable.
|
||||
var i;
|
||||
gap = '';
|
||||
indent = '';
|
||||
|
||||
// If the space parameter is a number, make an indent string containing that
|
||||
// many spaces.
|
||||
if (typeof space === 'number')
|
||||
{
|
||||
for (i = 0; i < space; i += 1)
|
||||
{
|
||||
indent += ' ';
|
||||
}
|
||||
|
||||
// If the space parameter is a string, it will be used as the indent string.
|
||||
}
|
||||
else if (typeof space === 'string')
|
||||
{
|
||||
indent = space;
|
||||
}
|
||||
|
||||
// If there is a replacer, it must be a function or an array.
|
||||
// Otherwise, throw an error.
|
||||
rep = replacer;
|
||||
if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number'))
|
||||
{
|
||||
throw new Error('JSON.stringify');
|
||||
}
|
||||
|
||||
// Make a fake root object containing our value under the key of ''.
|
||||
// Return the result of stringifying the value.
|
||||
return str('', {
|
||||
'': value
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If the JSON object does not yet have a parse method, give it one.
|
||||
if (typeof JSON.parse !== 'function')
|
||||
{
|
||||
JSON.parse = function(text, reviver)
|
||||
{
|
||||
|
||||
// The parse method takes a text and an optional reviver function, and returns
|
||||
// a JavaScript value if the text is a valid JSON text.
|
||||
var j;
|
||||
|
||||
function walk(holder, key)
|
||||
{
|
||||
|
||||
// The walk method is used to recursively walk the resulting structure so
|
||||
// that modifications can be made.
|
||||
var k, v, value = holder[key];
|
||||
if (value && typeof value === 'object')
|
||||
{
|
||||
for (k in value)
|
||||
{
|
||||
if (Object.prototype.hasOwnProperty.call(value, k))
|
||||
{
|
||||
v = walk(value, k);
|
||||
if (v !== undefined)
|
||||
{
|
||||
value[k] = v;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete value[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviver.call(holder, key, value);
|
||||
}
|
||||
|
||||
|
||||
// Parsing happens in four stages. In the first stage, we replace certain
|
||||
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||
text = String(text);
|
||||
cx.lastIndex = 0;
|
||||
if (cx.test(text))
|
||||
{
|
||||
text = text.replace(cx, function(a)
|
||||
{
|
||||
return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
});
|
||||
}
|
||||
|
||||
// In the second stage, we run the text against regular expressions that look
|
||||
// for non-JSON patterns. We are especially concerned with '()' and 'new'
|
||||
// because they can cause invocation, and '=' because it can cause mutation.
|
||||
// But just to be safe, we want to reject all unexpected forms.
|
||||
// We split the second stage into 4 regexp operations in order to work around
|
||||
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
|
||||
// replace all simple value tokens with ']' characters. Third, we delete all
|
||||
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||
// we look to see that the remaining characters are only whitespace or ']' or
|
||||
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
|
||||
if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, '')))
|
||||
{
|
||||
|
||||
// In the third stage we use the eval function to compile the text into a
|
||||
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
|
||||
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||
// in parens to eliminate the ambiguity.
|
||||
j = eval('(' + text + ')');
|
||||
|
||||
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||
// each name/value pair to a reviver function for possible transformation.
|
||||
return typeof reviver === 'function' ? walk(
|
||||
{
|
||||
'': j
|
||||
}, '') : j;
|
||||
}
|
||||
|
||||
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||
throw new SyntaxError('JSON.parse: ' + text);
|
||||
};
|
||||
}
|
||||
}());
|
||||
|
||||
module.exports = JSON;
|
352
src/static/js/linestylefilter.js
Normal file
|
@ -0,0 +1,352 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.linestylefilter
|
||||
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
||||
// %APPJET%: import("etherpad.admin.plugins");
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// requires: easysync2.Changeset
|
||||
// requires: top
|
||||
// requires: plugins
|
||||
// requires: undefined
|
||||
|
||||
var Changeset = require('./Changeset');
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
var linestylefilter = {};
|
||||
var _ = require('./underscore');
|
||||
var AttributeManager = require('./AttributeManager');
|
||||
|
||||
|
||||
linestylefilter.ATTRIB_CLASSES = {
|
||||
'bold': 'tag:b',
|
||||
'italic': 'tag:i',
|
||||
'underline': 'tag:u',
|
||||
'strikethrough': 'tag:s'
|
||||
};
|
||||
|
||||
var lineAttributeMarker = 'lineAttribMarker';
|
||||
exports.lineAttributeMarker = lineAttributeMarker;
|
||||
|
||||
linestylefilter.getAuthorClassName = function(author)
|
||||
{
|
||||
return "author-" + author.replace(/[^a-y0-9]/g, function(c)
|
||||
{
|
||||
if (c == ".") return "-";
|
||||
return 'z' + c.charCodeAt(0) + 'z';
|
||||
});
|
||||
};
|
||||
|
||||
// lineLength is without newline; aline includes newline,
|
||||
// but may be falsy if lineLength == 0
|
||||
linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFunc, apool)
|
||||
{
|
||||
|
||||
if (lineLength == 0) return textAndClassFunc;
|
||||
|
||||
var nextAfterAuthorColors = textAndClassFunc;
|
||||
|
||||
var authorColorFunc = (function()
|
||||
{
|
||||
var lineEnd = lineLength;
|
||||
var curIndex = 0;
|
||||
var extraClasses;
|
||||
var leftInAuthor;
|
||||
|
||||
function attribsToClasses(attribs)
|
||||
{
|
||||
var classes = '';
|
||||
var isLineAttribMarker = false;
|
||||
|
||||
Changeset.eachAttribNumber(attribs, function(n)
|
||||
{
|
||||
var key = apool.getAttribKey(n);
|
||||
if (key)
|
||||
{
|
||||
var value = apool.getAttribValue(n);
|
||||
if (value)
|
||||
{
|
||||
if (!isLineAttribMarker && _.indexOf(AttributeManager.lineAttributes, key) >= 0){
|
||||
isLineAttribMarker = true;
|
||||
}
|
||||
if (key == 'author')
|
||||
{
|
||||
classes += ' ' + linestylefilter.getAuthorClassName(value);
|
||||
}
|
||||
else if (key == 'list')
|
||||
{
|
||||
classes += ' list:' + value;
|
||||
}
|
||||
else if (key == 'start')
|
||||
{
|
||||
classes += ' start:' + value;
|
||||
}
|
||||
else if (linestylefilter.ATTRIB_CLASSES[key])
|
||||
{
|
||||
classes += ' ' + linestylefilter.ATTRIB_CLASSES[key];
|
||||
}
|
||||
else
|
||||
{
|
||||
classes += hooks.callAllStr("aceAttribsToClasses", {
|
||||
linestylefilter: linestylefilter,
|
||||
key: key,
|
||||
value: value
|
||||
}, " ", " ", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(isLineAttribMarker) classes += ' ' + lineAttributeMarker;
|
||||
return classes.substring(1);
|
||||
}
|
||||
|
||||
var attributionIter = Changeset.opIterator(aline);
|
||||
var nextOp, nextOpClasses;
|
||||
|
||||
function goNextOp()
|
||||
{
|
||||
nextOp = attributionIter.next();
|
||||
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
|
||||
}
|
||||
goNextOp();
|
||||
|
||||
function nextClasses()
|
||||
{
|
||||
if (curIndex < lineEnd)
|
||||
{
|
||||
extraClasses = nextOpClasses;
|
||||
leftInAuthor = nextOp.chars;
|
||||
goNextOp();
|
||||
while (nextOp.opcode && nextOpClasses == extraClasses)
|
||||
{
|
||||
leftInAuthor += nextOp.chars;
|
||||
goNextOp();
|
||||
}
|
||||
}
|
||||
}
|
||||
nextClasses();
|
||||
|
||||
return function(txt, cls)
|
||||
{
|
||||
while (txt.length > 0)
|
||||
{
|
||||
if (leftInAuthor <= 0)
|
||||
{
|
||||
// prevent infinite loop if something funny's going on
|
||||
return nextAfterAuthorColors(txt, cls);
|
||||
}
|
||||
var spanSize = txt.length;
|
||||
if (spanSize > leftInAuthor)
|
||||
{
|
||||
spanSize = leftInAuthor;
|
||||
}
|
||||
var curTxt = txt.substring(0, spanSize);
|
||||
txt = txt.substring(spanSize);
|
||||
nextAfterAuthorColors(curTxt, (cls && cls + " ") + extraClasses);
|
||||
curIndex += spanSize;
|
||||
leftInAuthor -= spanSize;
|
||||
if (leftInAuthor == 0)
|
||||
{
|
||||
nextClasses();
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
return authorColorFunc;
|
||||
};
|
||||
|
||||
linestylefilter.getAtSignSplitterFilter = function(lineText, textAndClassFunc)
|
||||
{
|
||||
var at = /@/g;
|
||||
at.lastIndex = 0;
|
||||
var splitPoints = null;
|
||||
var execResult;
|
||||
while ((execResult = at.exec(lineText)))
|
||||
{
|
||||
if (!splitPoints)
|
||||
{
|
||||
splitPoints = [];
|
||||
}
|
||||
splitPoints.push(execResult.index);
|
||||
}
|
||||
|
||||
if (!splitPoints) return textAndClassFunc;
|
||||
|
||||
return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints);
|
||||
};
|
||||
|
||||
linestylefilter.getRegexpFilter = function(regExp, tag)
|
||||
{
|
||||
return function(lineText, textAndClassFunc)
|
||||
{
|
||||
regExp.lastIndex = 0;
|
||||
var regExpMatchs = null;
|
||||
var splitPoints = null;
|
||||
var execResult;
|
||||
while ((execResult = regExp.exec(lineText)))
|
||||
{
|
||||
if (!regExpMatchs)
|
||||
{
|
||||
regExpMatchs = [];
|
||||
splitPoints = [];
|
||||
}
|
||||
var startIndex = execResult.index;
|
||||
var regExpMatch = execResult[0];
|
||||
regExpMatchs.push([startIndex, regExpMatch]);
|
||||
splitPoints.push(startIndex, startIndex + regExpMatch.length);
|
||||
}
|
||||
|
||||
if (!regExpMatchs) return textAndClassFunc;
|
||||
|
||||
function regExpMatchForIndex(idx)
|
||||
{
|
||||
for (var k = 0; k < regExpMatchs.length; k++)
|
||||
{
|
||||
var u = regExpMatchs[k];
|
||||
if (idx >= u[0] && idx < u[0] + u[1].length)
|
||||
{
|
||||
return u[1];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var handleRegExpMatchsAfterSplit = (function()
|
||||
{
|
||||
var curIndex = 0;
|
||||
return function(txt, cls)
|
||||
{
|
||||
var txtlen = txt.length;
|
||||
var newCls = cls;
|
||||
var regExpMatch = regExpMatchForIndex(curIndex);
|
||||
if (regExpMatch)
|
||||
{
|
||||
newCls += " " + tag + ":" + regExpMatch;
|
||||
}
|
||||
textAndClassFunc(txt, newCls);
|
||||
curIndex += txtlen;
|
||||
};
|
||||
})();
|
||||
|
||||
return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
linestylefilter.REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
|
||||
linestylefilter.REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + linestylefilter.REGEX_WORDCHAR.source + ')');
|
||||
linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:|www\.)/.source + linestylefilter.REGEX_URLCHAR.source + '*(?![:.,;])' + linestylefilter.REGEX_URLCHAR.source, 'g');
|
||||
linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(
|
||||
linestylefilter.REGEX_URL, 'url');
|
||||
|
||||
linestylefilter.textAndClassFuncSplitter = function(func, splitPointsOpt)
|
||||
{
|
||||
var nextPointIndex = 0;
|
||||
var idx = 0;
|
||||
|
||||
// don't split at 0
|
||||
while (splitPointsOpt && nextPointIndex < splitPointsOpt.length && splitPointsOpt[nextPointIndex] == 0)
|
||||
{
|
||||
nextPointIndex++;
|
||||
}
|
||||
|
||||
function spanHandler(txt, cls)
|
||||
{
|
||||
if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length)
|
||||
{
|
||||
func(txt, cls);
|
||||
idx += txt.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
var splitPoints = splitPointsOpt;
|
||||
var pointLocInSpan = splitPoints[nextPointIndex] - idx;
|
||||
var txtlen = txt.length;
|
||||
if (pointLocInSpan >= txtlen)
|
||||
{
|
||||
func(txt, cls);
|
||||
idx += txt.length;
|
||||
if (pointLocInSpan == txtlen)
|
||||
{
|
||||
nextPointIndex++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pointLocInSpan > 0)
|
||||
{
|
||||
func(txt.substring(0, pointLocInSpan), cls);
|
||||
idx += pointLocInSpan;
|
||||
}
|
||||
nextPointIndex++;
|
||||
// recurse
|
||||
spanHandler(txt.substring(pointLocInSpan), cls);
|
||||
}
|
||||
}
|
||||
}
|
||||
return spanHandler;
|
||||
};
|
||||
|
||||
linestylefilter.getFilterStack = function(lineText, textAndClassFunc, browser)
|
||||
{
|
||||
var func = linestylefilter.getURLFilter(lineText, textAndClassFunc);
|
||||
|
||||
var hookFilters = hooks.callAll("aceGetFilterStack", {
|
||||
linestylefilter: linestylefilter,
|
||||
browser: browser
|
||||
});
|
||||
_.map(hookFilters ,function(hookFilter)
|
||||
{
|
||||
func = hookFilter(lineText, func);
|
||||
});
|
||||
|
||||
if (browser !== undefined && browser.msie)
|
||||
{
|
||||
// IE7+ will take an e-mail address like <foo@bar.com> and linkify it to foo@bar.com.
|
||||
// We then normalize it back to text with no angle brackets. It's weird. So always
|
||||
// break spans at an "at" sign.
|
||||
func = linestylefilter.getAtSignSplitterFilter(
|
||||
lineText, func);
|
||||
}
|
||||
return func;
|
||||
};
|
||||
|
||||
// domLineObj is like that returned by domline.createDomLine
|
||||
linestylefilter.populateDomLine = function(textLine, aline, apool, domLineObj)
|
||||
{
|
||||
// remove final newline from text if any
|
||||
var text = textLine;
|
||||
if (text.slice(-1) == '\n')
|
||||
{
|
||||
text = text.substring(0, text.length - 1);
|
||||
}
|
||||
|
||||
function textAndClassFunc(tokenText, tokenClass)
|
||||
{
|
||||
domLineObj.appendSpan(tokenText, tokenClass);
|
||||
}
|
||||
|
||||
var func = linestylefilter.getFilterStack(text, textAndClassFunc);
|
||||
func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool);
|
||||
func(text, '');
|
||||
};
|
||||
|
||||
exports.linestylefilter = linestylefilter;
|
1028
src/static/js/pad.js
Normal file
90
src/static/js/pad_connectionstatus.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var padmodals = require('./pad_modals').padmodals;
|
||||
var padeditbar = require('./pad_editbar').padeditbar;
|
||||
|
||||
var padconnectionstatus = (function()
|
||||
{
|
||||
|
||||
var status = {
|
||||
what: 'connecting'
|
||||
};
|
||||
|
||||
var self = {
|
||||
init: function()
|
||||
{
|
||||
$('button#forcereconnect').click(function()
|
||||
{
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
connected: function()
|
||||
{
|
||||
status = {
|
||||
what: 'connected'
|
||||
};
|
||||
|
||||
padmodals.showModal('connected');
|
||||
padmodals.hideOverlay(500);
|
||||
},
|
||||
reconnecting: function()
|
||||
{
|
||||
status = {
|
||||
what: 'reconnecting'
|
||||
};
|
||||
|
||||
padmodals.showModal('reconnecting');
|
||||
padmodals.showOverlay(500);
|
||||
},
|
||||
disconnected: function(msg)
|
||||
{
|
||||
if(status.what == "disconnected")
|
||||
return;
|
||||
|
||||
status = {
|
||||
what: 'disconnected',
|
||||
why: msg
|
||||
};
|
||||
|
||||
var k = String(msg).toLowerCase(); // known reason why
|
||||
if (!(k == 'userdup' || k == 'deleted' || k == 'looping' || k == 'slowcommit' || k == 'initsocketfail' || k == 'unauth'))
|
||||
{
|
||||
k = 'disconnected';
|
||||
}
|
||||
|
||||
padmodals.showModal(k);
|
||||
padmodals.showOverlay(500);
|
||||
},
|
||||
isFullyConnected: function()
|
||||
{
|
||||
return status.what == 'connected';
|
||||
},
|
||||
getStatus: function()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.padconnectionstatus = padconnectionstatus;
|
133
src/static/js/pad_cookie.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
var padcookie = (function()
|
||||
{
|
||||
function getRawCookie()
|
||||
{
|
||||
// returns null if can't get cookie text
|
||||
if (!document.cookie)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// look for (start of string OR semicolon) followed by whitespace followed by prefs=(something);
|
||||
var regexResult = document.cookie.match(/(?:^|;)\s*prefs=([^;]*)(?:;|$)/);
|
||||
if ((!regexResult) || (!regexResult[1]))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return regexResult[1];
|
||||
}
|
||||
|
||||
function setRawCookie(safeText)
|
||||
{
|
||||
var expiresDate = new Date();
|
||||
expiresDate.setFullYear(3000);
|
||||
document.cookie = ('prefs=' + safeText + ';expires=' + expiresDate.toGMTString());
|
||||
}
|
||||
|
||||
function parseCookie(text)
|
||||
{
|
||||
// returns null if can't parse cookie.
|
||||
try
|
||||
{
|
||||
var cookieData = JSON.parse(unescape(text));
|
||||
return cookieData;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyCookie(data)
|
||||
{
|
||||
return escape(JSON.stringify(data));
|
||||
}
|
||||
|
||||
function saveCookie()
|
||||
{
|
||||
if (!inited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
setRawCookie(stringifyCookie(cookieData));
|
||||
|
||||
if (pad.getIsProPad() && (!getRawCookie()) && (!alreadyWarnedAboutNoCookies))
|
||||
{
|
||||
alert("Warning: it appears that your browser does not have cookies enabled." + " EtherPad uses cookies to keep track of unique users for the purpose" + " of putting a quota on the number of active users. Using EtherPad without " + " cookies may fill up your server's user quota faster than expected.");
|
||||
alreadyWarnedAboutNoCookies = true;
|
||||
}
|
||||
}
|
||||
|
||||
var wasNoCookie = true;
|
||||
var cookieData = {};
|
||||
var alreadyWarnedAboutNoCookies = false;
|
||||
var inited = false;
|
||||
|
||||
var pad = undefined;
|
||||
var self = {
|
||||
init: function(prefsToSet, _pad)
|
||||
{
|
||||
pad = _pad;
|
||||
|
||||
var rawCookie = getRawCookie();
|
||||
if (rawCookie)
|
||||
{
|
||||
var cookieObj = parseCookie(rawCookie);
|
||||
if (cookieObj)
|
||||
{
|
||||
wasNoCookie = false; // there was a cookie
|
||||
delete cookieObj.userId;
|
||||
delete cookieObj.name;
|
||||
delete cookieObj.colorId;
|
||||
cookieData = cookieObj;
|
||||
}
|
||||
}
|
||||
|
||||
for (var k in prefsToSet)
|
||||
{
|
||||
cookieData[k] = prefsToSet[k];
|
||||
}
|
||||
|
||||
inited = true;
|
||||
saveCookie();
|
||||
},
|
||||
wasNoCookie: function()
|
||||
{
|
||||
return wasNoCookie;
|
||||
},
|
||||
getPref: function(prefName)
|
||||
{
|
||||
return cookieData[prefName];
|
||||
},
|
||||
setPref: function(prefName, value)
|
||||
{
|
||||
cookieData[prefName] = value;
|
||||
saveCookie();
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.padcookie = padcookie;
|
466
src/static/js/pad_docbar.js
Normal file
|
@ -0,0 +1,466 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
|
||||
var paddocbar = (function()
|
||||
{
|
||||
var isTitleEditable = false;
|
||||
var isEditingTitle = false;
|
||||
var isEditingPassword = false;
|
||||
var enabled = false;
|
||||
|
||||
function getPanelOpenCloseAnimator(panelName, panelHeight)
|
||||
{
|
||||
var wrapper = $("#" + panelName + "-wrapper");
|
||||
var openingClass = "docbar" + panelName + "-opening";
|
||||
var openClass = "docbar" + panelName + "-open";
|
||||
var closingClass = "docbar" + panelName + "-closing";
|
||||
|
||||
function setPanelState(action)
|
||||
{
|
||||
$("#docbar").removeClass(openingClass).removeClass(openClass).
|
||||
removeClass(closingClass);
|
||||
if (action != "closed")
|
||||
{
|
||||
$("#docbar").addClass("docbar" + panelName + "-" + action);
|
||||
}
|
||||
}
|
||||
|
||||
function openCloseAnimate(state)
|
||||
{
|
||||
function pow(x)
|
||||
{
|
||||
x = 1 - x;
|
||||
x *= x * x;
|
||||
return 1 - x;
|
||||
}
|
||||
|
||||
if (state == -1)
|
||||
{
|
||||
// startng to open
|
||||
setPanelState("opening");
|
||||
wrapper.css('height', '0');
|
||||
}
|
||||
else if (state < 0)
|
||||
{
|
||||
// opening
|
||||
var height = Math.round(pow(state + 1) * (panelHeight - 1)) + 'px';
|
||||
wrapper.css('height', height);
|
||||
}
|
||||
else if (state == 0)
|
||||
{
|
||||
// open
|
||||
setPanelState("open");
|
||||
wrapper.css('height', panelHeight - 1);
|
||||
}
|
||||
else if (state < 1)
|
||||
{
|
||||
// closing
|
||||
setPanelState("closing");
|
||||
var height = Math.round((1 - pow(state)) * (panelHeight - 1)) + 'px';
|
||||
wrapper.css('height', height);
|
||||
}
|
||||
else if (state == 1)
|
||||
{
|
||||
// closed
|
||||
setPanelState("closed");
|
||||
wrapper.css('height', '0');
|
||||
}
|
||||
}
|
||||
|
||||
return padutils.makeShowHideAnimator(openCloseAnimate, false, 25, 500);
|
||||
}
|
||||
|
||||
|
||||
var currentPanel = null;
|
||||
|
||||
function setCurrentPanel(newCurrentPanel)
|
||||
{
|
||||
if (currentPanel != newCurrentPanel)
|
||||
{
|
||||
currentPanel = newCurrentPanel;
|
||||
padutils.cancelActions("hide-docbar-panel");
|
||||
}
|
||||
}
|
||||
var panels;
|
||||
|
||||
function changePassword(newPass)
|
||||
{
|
||||
if ((newPass || null) != (self.password || null))
|
||||
{
|
||||
self.password = (newPass || null);
|
||||
pad.notifyChangePassword(newPass);
|
||||
}
|
||||
self.renderPassword();
|
||||
}
|
||||
|
||||
var pad = undefined;
|
||||
var self = {
|
||||
title: null,
|
||||
password: null,
|
||||
init: function(opts, _pad)
|
||||
{
|
||||
pad = _pad;
|
||||
|
||||
panels = {
|
||||
impexp: {
|
||||
animator: getPanelOpenCloseAnimator("impexp", 160)
|
||||
},
|
||||
savedrevs: {
|
||||
animator: getPanelOpenCloseAnimator("savedrevs", 79)
|
||||
},
|
||||
options: {
|
||||
animator: getPanelOpenCloseAnimator("options", 114)
|
||||
},
|
||||
security: {
|
||||
animator: getPanelOpenCloseAnimator("security", 130)
|
||||
}
|
||||
};
|
||||
|
||||
isTitleEditable = opts.isTitleEditable;
|
||||
self.title = opts.initialTitle;
|
||||
self.password = opts.initialPassword;
|
||||
|
||||
$("#docbarimpexp").click(function()
|
||||
{
|
||||
self.togglePanel("impexp");
|
||||
});
|
||||
$("#docbarsavedrevs").click(function()
|
||||
{
|
||||
self.togglePanel("savedrevs");
|
||||
});
|
||||
$("#docbaroptions").click(function()
|
||||
{
|
||||
self.togglePanel("options");
|
||||
});
|
||||
$("#docbarsecurity").click(function()
|
||||
{
|
||||
self.togglePanel("security");
|
||||
});
|
||||
|
||||
$("#docbarrenamelink").click(self.editTitle);
|
||||
$("#padtitlesave").click(function()
|
||||
{
|
||||
self.closeTitleEdit(true);
|
||||
});
|
||||
$("#padtitlecancel").click(function()
|
||||
{
|
||||
self.closeTitleEdit(false);
|
||||
});
|
||||
padutils.bindEnterAndEscape($("#padtitleedit"), function()
|
||||
{
|
||||
$("#padtitlesave").trigger('click');
|
||||
}, function()
|
||||
{
|
||||
$("#padtitlecancel").trigger('click');
|
||||
});
|
||||
|
||||
$("#options-close").click(function()
|
||||
{
|
||||
self.setShownPanel(null);
|
||||
});
|
||||
$("#security-close").click(function()
|
||||
{
|
||||
self.setShownPanel(null);
|
||||
});
|
||||
|
||||
if (pad.getIsProPad())
|
||||
{
|
||||
self.initPassword();
|
||||
}
|
||||
|
||||
enabled = true;
|
||||
self.render();
|
||||
|
||||
// public/private
|
||||
$("#security-access input").bind("change click", function(evt)
|
||||
{
|
||||
pad.changePadOption('guestPolicy', $("#security-access input[name='padaccess']:checked").val());
|
||||
});
|
||||
self.setGuestPolicy(opts.guestPolicy);
|
||||
},
|
||||
setGuestPolicy: function(newPolicy)
|
||||
{
|
||||
$("#security-access input[value='" + newPolicy + "']").attr("checked", "checked");
|
||||
self.render();
|
||||
},
|
||||
initPassword: function()
|
||||
{
|
||||
self.renderPassword();
|
||||
$("#password-clearlink").click(function()
|
||||
{
|
||||
changePassword(null);
|
||||
});
|
||||
$("#password-setlink, #password-display").click(function()
|
||||
{
|
||||
self.enterPassword();
|
||||
});
|
||||
$("#password-cancellink").click(function()
|
||||
{
|
||||
self.exitPassword(false);
|
||||
});
|
||||
$("#password-savelink").click(function()
|
||||
{
|
||||
self.exitPassword(true);
|
||||
});
|
||||
padutils.bindEnterAndEscape($("#security-passwordedit"), function()
|
||||
{
|
||||
self.exitPassword(true);
|
||||
}, function()
|
||||
{
|
||||
self.exitPassword(false);
|
||||
});
|
||||
},
|
||||
enterPassword: function()
|
||||
{
|
||||
isEditingPassword = true;
|
||||
$("#security-passwordedit").val(self.password || '');
|
||||
self.renderPassword();
|
||||
$("#security-passwordedit").focus().select();
|
||||
},
|
||||
exitPassword: function(accept)
|
||||
{
|
||||
isEditingPassword = false;
|
||||
if (accept)
|
||||
{
|
||||
changePassword($("#security-passwordedit").val());
|
||||
}
|
||||
else
|
||||
{
|
||||
self.renderPassword();
|
||||
}
|
||||
},
|
||||
renderPassword: function()
|
||||
{
|
||||
if (isEditingPassword)
|
||||
{
|
||||
$("#password-nonedit").hide();
|
||||
$("#password-inedit").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#password-nonedit").toggleClass('nopassword', !self.password);
|
||||
$("#password-setlink").html(self.password ? "Change..." : "Set...");
|
||||
if (self.password)
|
||||
{
|
||||
$("#password-display").html(self.password.replace(/./g, '•'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#password-display").html("None");
|
||||
}
|
||||
$("#password-inedit").hide();
|
||||
$("#password-nonedit").show();
|
||||
}
|
||||
},
|
||||
togglePanel: function(panelName)
|
||||
{
|
||||
if (panelName in panels)
|
||||
{
|
||||
if (currentPanel == panelName)
|
||||
{
|
||||
self.setShownPanel(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.setShownPanel(panelName);
|
||||
}
|
||||
}
|
||||
},
|
||||
setShownPanel: function(panelName)
|
||||
{
|
||||
function animateHidePanel(panelName, next)
|
||||
{
|
||||
var delay = 0;
|
||||
if (panelName == 'options' && isEditingPassword)
|
||||
{
|
||||
// give user feedback that the password they've
|
||||
// typed in won't actually take effect
|
||||
self.exitPassword(false);
|
||||
delay = 500;
|
||||
}
|
||||
|
||||
window.setTimeout(function()
|
||||
{
|
||||
panels[panelName].animator.hide();
|
||||
if (next)
|
||||
{
|
||||
next();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
if (!panelName)
|
||||
{
|
||||
if (currentPanel)
|
||||
{
|
||||
animateHidePanel(currentPanel);
|
||||
setCurrentPanel(null);
|
||||
}
|
||||
}
|
||||
else if (panelName in panels)
|
||||
{
|
||||
if (currentPanel != panelName)
|
||||
{
|
||||
if (currentPanel)
|
||||
{
|
||||
animateHidePanel(currentPanel, function()
|
||||
{
|
||||
panels[panelName].animator.show();
|
||||
setCurrentPanel(panelName);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
panels[panelName].animator.show();
|
||||
setCurrentPanel(panelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isPanelShown: function(panelName)
|
||||
{
|
||||
if (!panelName)
|
||||
{
|
||||
return !currentPanel;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (panelName == currentPanel);
|
||||
}
|
||||
},
|
||||
changeTitle: function(newTitle)
|
||||
{
|
||||
self.title = newTitle;
|
||||
self.render();
|
||||
},
|
||||
editTitle: function()
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
$("#padtitleedit").val(self.title);
|
||||
isEditingTitle = true;
|
||||
self.render();
|
||||
$("#padtitleedit").focus().select();
|
||||
},
|
||||
closeTitleEdit: function(accept)
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (accept)
|
||||
{
|
||||
var newTitle = $("#padtitleedit").val();
|
||||
if (newTitle)
|
||||
{
|
||||
newTitle = newTitle.substring(0, 80);
|
||||
self.title = newTitle;
|
||||
|
||||
pad.notifyChangeTitle(newTitle);
|
||||
}
|
||||
}
|
||||
|
||||
isEditingTitle = false;
|
||||
self.render();
|
||||
},
|
||||
changePassword: function(newPass)
|
||||
{
|
||||
if (newPass)
|
||||
{
|
||||
self.password = newPass;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.password = null;
|
||||
}
|
||||
self.renderPassword();
|
||||
},
|
||||
render: function()
|
||||
{
|
||||
if (isEditingTitle)
|
||||
{
|
||||
$("#docbarpadtitle").hide();
|
||||
$("#docbarrenamelink").hide();
|
||||
$("#padtitleedit").show();
|
||||
$("#padtitlebuttons").show();
|
||||
if (!enabled)
|
||||
{
|
||||
$("#padtitleedit").attr('disabled', 'disabled');
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#padtitleedit").removeAttr('disabled');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#padtitleedit").hide();
|
||||
$("#padtitlebuttons").hide();
|
||||
|
||||
var titleSpan = $("#docbarpadtitle span");
|
||||
titleSpan.html(padutils.escapeHtml(self.title));
|
||||
$("#docbarpadtitle").attr('title', (pad.isPadPublic() ? "Public Pad: " : "") + self.title);
|
||||
$("#docbarpadtitle").show();
|
||||
|
||||
if (isTitleEditable)
|
||||
{
|
||||
var titleRight = $("#docbarpadtitle").position().left + $("#docbarpadtitle span").position().left + Math.min($("#docbarpadtitle").width(), $("#docbarpadtitle span").width());
|
||||
$("#docbarrenamelink").css('left', titleRight + 10).show();
|
||||
}
|
||||
|
||||
if (pad.isPadPublic())
|
||||
{
|
||||
$("#docbar").addClass("docbar-public");
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#docbar").removeClass("docbar-public");
|
||||
}
|
||||
}
|
||||
},
|
||||
disable: function()
|
||||
{
|
||||
enabled = false;
|
||||
self.render();
|
||||
},
|
||||
handleResizePage: function()
|
||||
{
|
||||
// Side-step circular reference. This should be injected.
|
||||
var padsavedrevs = require('./pad_savedrevs');
|
||||
padsavedrevs.handleResizePage();
|
||||
},
|
||||
hideLaterIfNoOtherInteraction: function()
|
||||
{
|
||||
return padutils.getCancellableAction('hide-docbar-panel', function()
|
||||
{
|
||||
self.setShownPanel(null);
|
||||
});
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.paddocbar = paddocbar;
|
266
src/static/js/pad_editbar.js
Normal file
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
var padeditor = require('./pad_editor').padeditor;
|
||||
var padsavedrevs = require('./pad_savedrevs');
|
||||
|
||||
function indexOf(array, value) {
|
||||
for (var i = 0, ii = array.length; i < ii; i++) {
|
||||
if (array[i] == value) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
var padeditbar = (function()
|
||||
{
|
||||
|
||||
var syncAnimation = (function()
|
||||
{
|
||||
var SYNCING = -100;
|
||||
var DONE = 100;
|
||||
var state = DONE;
|
||||
var fps = 25;
|
||||
var step = 1 / fps;
|
||||
var T_START = -0.5;
|
||||
var T_FADE = 1.0;
|
||||
var T_GONE = 1.5;
|
||||
var animator = padutils.makeAnimationScheduler(function()
|
||||
{
|
||||
if (state == SYNCING || state == DONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (state >= T_GONE)
|
||||
{
|
||||
state = DONE;
|
||||
$("#syncstatussyncing").css('display', 'none');
|
||||
$("#syncstatusdone").css('display', 'none');
|
||||
return false;
|
||||
}
|
||||
else if (state < 0)
|
||||
{
|
||||
state += step;
|
||||
if (state >= 0)
|
||||
{
|
||||
$("#syncstatussyncing").css('display', 'none');
|
||||
$("#syncstatusdone").css('display', 'block').css('opacity', 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
state += step;
|
||||
if (state >= T_FADE)
|
||||
{
|
||||
$("#syncstatusdone").css('opacity', (T_GONE - state) / (T_GONE - T_FADE));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}, step * 1000);
|
||||
return {
|
||||
syncing: function()
|
||||
{
|
||||
state = SYNCING;
|
||||
$("#syncstatussyncing").css('display', 'block');
|
||||
$("#syncstatusdone").css('display', 'none');
|
||||
},
|
||||
done: function()
|
||||
{
|
||||
state = T_START;
|
||||
animator.scheduleAnimation();
|
||||
}
|
||||
};
|
||||
}());
|
||||
|
||||
var self = {
|
||||
init: function()
|
||||
{
|
||||
var self = this;
|
||||
$("#editbar .editbarbutton").attr("unselectable", "on"); // for IE
|
||||
$("#editbar").removeClass("disabledtoolbar").addClass("enabledtoolbar");
|
||||
$("#editbar [data-key]").each(function (i, e) {
|
||||
$(e).click(function (event) {
|
||||
self.toolbarClick($(e).attr('data-key'));
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
},
|
||||
isEnabled: function()
|
||||
{
|
||||
// return !$("#editbar").hasClass('disabledtoolbar');
|
||||
return true;
|
||||
},
|
||||
disable: function()
|
||||
{
|
||||
$("#editbar").addClass('disabledtoolbar').removeClass("enabledtoolbar");
|
||||
},
|
||||
toolbarClick: function(cmd)
|
||||
{
|
||||
if (self.isEnabled())
|
||||
{
|
||||
if(cmd == "showusers")
|
||||
{
|
||||
self.toggleDropDown("users");
|
||||
}
|
||||
else if (cmd == 'settings')
|
||||
{
|
||||
self.toggleDropDown("settings");
|
||||
}
|
||||
else if (cmd == 'connectivity')
|
||||
{
|
||||
self.toggleDropDown("connectivity");
|
||||
}
|
||||
else if (cmd == 'embed')
|
||||
{
|
||||
self.setEmbedLinks();
|
||||
$('#linkinput').focus().select();
|
||||
self.toggleDropDown("embed");
|
||||
}
|
||||
else if (cmd == 'import_export')
|
||||
{
|
||||
self.toggleDropDown("importexport");
|
||||
}
|
||||
else if (cmd == 'savedRevision')
|
||||
{
|
||||
padsavedrevs.saveNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
padeditor.ace.callWithAce(function(ace)
|
||||
{
|
||||
if (cmd == 'bold' || cmd == 'italic' || cmd == 'underline' || cmd == 'strikethrough') ace.ace_toggleAttributeOnSelection(cmd);
|
||||
else if (cmd == 'undo' || cmd == 'redo') ace.ace_doUndoRedo(cmd);
|
||||
else if (cmd == 'insertunorderedlist') ace.ace_doInsertUnorderedList();
|
||||
else if (cmd == 'insertorderedlist') ace.ace_doInsertOrderedList();
|
||||
else if (cmd == 'indent')
|
||||
{
|
||||
if (!ace.ace_doIndentOutdent(false))
|
||||
{
|
||||
ace.ace_doInsertUnorderedList();
|
||||
}
|
||||
}
|
||||
else if (cmd == 'outdent')
|
||||
{
|
||||
ace.ace_doIndentOutdent(true);
|
||||
}
|
||||
else if (cmd == 'clearauthorship')
|
||||
{
|
||||
if ((!(ace.ace_getRep().selStart && ace.ace_getRep().selEnd)) || ace.ace_isCaret())
|
||||
{
|
||||
if (window.confirm("Clear authorship colors on entire document?"))
|
||||
{
|
||||
ace.ace_performDocumentApplyAttributesToCharRange(0, ace.ace_getRep().alltext.length, [
|
||||
['author', '']
|
||||
]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ace.ace_setAttributeOnSelection('author', '');
|
||||
}
|
||||
}
|
||||
}, cmd, true);
|
||||
}
|
||||
}
|
||||
if(padeditor.ace) padeditor.ace.focus();
|
||||
},
|
||||
toggleDropDown: function(moduleName, cb)
|
||||
{
|
||||
var modules = ["settings", "connectivity", "importexport", "embed", "users"];
|
||||
|
||||
// hide all modules and remove highlighting of all buttons
|
||||
if(moduleName == "none")
|
||||
{
|
||||
var returned = false
|
||||
for(var i=0;i<modules.length;i++)
|
||||
{
|
||||
//skip the userlist
|
||||
if(modules[i] == "users")
|
||||
continue;
|
||||
|
||||
var module = $("#" + modules[i]);
|
||||
|
||||
if(module.css('display') != "none")
|
||||
{
|
||||
$("#" + modules[i] + "link").removeClass("selected");
|
||||
module.slideUp("fast", cb);
|
||||
returned = true;
|
||||
}
|
||||
}
|
||||
if(!returned && cb) return cb();
|
||||
}
|
||||
else
|
||||
{
|
||||
// hide all modules that are not selected and remove highlighting
|
||||
// respectively add highlighting to the corresponding button
|
||||
for(var i=0;i<modules.length;i++)
|
||||
{
|
||||
var module = $("#" + modules[i]);
|
||||
|
||||
if(module.css('display') != "none")
|
||||
{
|
||||
$("#" + modules[i] + "link").removeClass("selected");
|
||||
module.slideUp("fast");
|
||||
}
|
||||
else if(modules[i]==moduleName)
|
||||
{
|
||||
$("#" + modules[i] + "link").addClass("selected");
|
||||
module.slideDown("fast", cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setSyncStatus: function(status)
|
||||
{
|
||||
if (status == "syncing")
|
||||
{
|
||||
syncAnimation.syncing();
|
||||
}
|
||||
else if (status == "done")
|
||||
{
|
||||
syncAnimation.done();
|
||||
}
|
||||
},
|
||||
setEmbedLinks: function()
|
||||
{
|
||||
if ($('#readonlyinput').is(':checked'))
|
||||
{
|
||||
var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/"));
|
||||
var readonlyLink = basePath + "/p/" + clientVars.readOnlyId;
|
||||
$('#embedinput').val("<iframe name='embed_readonly' src='" + readonlyLink + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>");
|
||||
$('#linkinput').val(readonlyLink);
|
||||
}
|
||||
else
|
||||
{
|
||||
var padurl = window.location.href.split("?")[0];
|
||||
$('#embedinput').val("<iframe name='embed_readwrite' src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>");
|
||||
$('#linkinput').val(padurl);
|
||||
}
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.padeditbar = padeditbar;
|
133
src/static/js/pad_editor.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var padcookie = require('./pad_cookie').padcookie;
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
|
||||
var padeditor = (function()
|
||||
{
|
||||
var Ace2Editor = undefined;
|
||||
var pad = undefined;
|
||||
var settings = undefined;
|
||||
var self = {
|
||||
ace: null,
|
||||
// this is accessed directly from other files
|
||||
viewZoom: 100,
|
||||
init: function(readyFunc, initialViewOptions, _pad)
|
||||
{
|
||||
Ace2Editor = require('./ace').Ace2Editor;
|
||||
pad = _pad;
|
||||
settings = pad.settings;
|
||||
|
||||
function aceReady()
|
||||
{
|
||||
$("#editorloadingbox").hide();
|
||||
if (readyFunc)
|
||||
{
|
||||
readyFunc();
|
||||
}
|
||||
}
|
||||
|
||||
self.ace = new Ace2Editor();
|
||||
self.ace.init("editorcontainer", "", aceReady);
|
||||
self.ace.setProperty("wraps", true);
|
||||
if (pad.getIsDebugEnabled())
|
||||
{
|
||||
self.ace.setProperty("dmesg", pad.dmesg);
|
||||
}
|
||||
self.initViewOptions();
|
||||
self.setViewOptions(initialViewOptions);
|
||||
|
||||
// view bar
|
||||
$("#viewbarcontents").show();
|
||||
},
|
||||
initViewOptions: function()
|
||||
{
|
||||
padutils.bindCheckboxChange($("#options-linenoscheck"), function()
|
||||
{
|
||||
pad.changeViewOption('showLineNumbers', padutils.getCheckbox($("#options-linenoscheck")));
|
||||
});
|
||||
padutils.bindCheckboxChange($("#options-colorscheck"), function()
|
||||
{
|
||||
padcookie.setPref('showAuthorshipColors', padutils.getCheckbox("#options-colorscheck"));
|
||||
pad.changeViewOption('showAuthorColors', padutils.getCheckbox("#options-colorscheck"));
|
||||
});
|
||||
$("#viewfontmenu").change(function()
|
||||
{
|
||||
pad.changeViewOption('useMonospaceFont', $("#viewfontmenu").val() == 'monospace');
|
||||
});
|
||||
},
|
||||
setViewOptions: function(newOptions)
|
||||
{
|
||||
function getOption(key, defaultValue)
|
||||
{
|
||||
var value = String(newOptions[key]);
|
||||
if (value == "true") return true;
|
||||
if (value == "false") return false;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
self.ace.setProperty("rtlIsTrue", settings.rtlIsTrue);
|
||||
|
||||
var v;
|
||||
|
||||
v = getOption('showLineNumbers', true);
|
||||
self.ace.setProperty("showslinenumbers", v);
|
||||
padutils.setCheckbox($("#options-linenoscheck"), v);
|
||||
|
||||
v = getOption('showAuthorColors', true);
|
||||
self.ace.setProperty("showsauthorcolors", v);
|
||||
padutils.setCheckbox($("#options-colorscheck"), v);
|
||||
// Override from parameters if true
|
||||
if (settings.noColors !== false)
|
||||
self.ace.setProperty("showsauthorcolors", !settings.noColors);
|
||||
|
||||
v = getOption('useMonospaceFont', false);
|
||||
self.ace.setProperty("textface", (v ? "monospace" : "Arial, sans-serif"));
|
||||
$("#viewfontmenu").val(v ? "monospace" : "normal");
|
||||
},
|
||||
dispose: function()
|
||||
{
|
||||
if (self.ace)
|
||||
{
|
||||
self.ace.destroy();
|
||||
self.ace = null;
|
||||
}
|
||||
},
|
||||
disable: function()
|
||||
{
|
||||
if (self.ace)
|
||||
{
|
||||
self.ace.setProperty("grayedOut", true);
|
||||
self.ace.setEditable(false);
|
||||
}
|
||||
},
|
||||
restoreRevisionText: function(dataFromServer)
|
||||
{
|
||||
pad.addHistoricalAuthors(dataFromServer.historicalAuthorData);
|
||||
self.ace.importAText(dataFromServer.atext, dataFromServer.apool, true);
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.padeditor = padeditor;
|
287
src/static/js/pad_impexp.js
Normal file
|
@ -0,0 +1,287 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var paddocbar = require('./pad_docbar').paddocbar;
|
||||
|
||||
var padimpexp = (function()
|
||||
{
|
||||
|
||||
///// import
|
||||
var currentImportTimer = null;
|
||||
var hidePanelCall = null;
|
||||
|
||||
function addImportFrames()
|
||||
{
|
||||
$("#import .importframe").remove();
|
||||
var iframe = $('<iframe style="display: none;" name="importiframe" class="importframe"></iframe>');
|
||||
$('#import').append(iframe);
|
||||
}
|
||||
|
||||
function fileInputUpdated()
|
||||
{
|
||||
$('#importformfilediv').addClass('importformenabled');
|
||||
$('#importsubmitinput').removeAttr('disabled');
|
||||
$('#importmessagefail').fadeOut("fast");
|
||||
$('#importarrow').show();
|
||||
$('#importarrow').animate(
|
||||
{
|
||||
paddingLeft: "0px"
|
||||
}, 500).animate(
|
||||
{
|
||||
paddingLeft: "10px"
|
||||
}, 150, 'swing').animate(
|
||||
{
|
||||
paddingLeft: "0px"
|
||||
}, 150, 'swing').animate(
|
||||
{
|
||||
paddingLeft: "10px"
|
||||
}, 150, 'swing').animate(
|
||||
{
|
||||
paddingLeft: "0px"
|
||||
}, 150, 'swing').animate(
|
||||
{
|
||||
paddingLeft: "10px"
|
||||
}, 150, 'swing').animate(
|
||||
{
|
||||
paddingLeft: "0px"
|
||||
}, 150, 'swing');
|
||||
}
|
||||
|
||||
function fileInputSubmit()
|
||||
{
|
||||
$('#importmessagefail').fadeOut("fast");
|
||||
var ret = window.confirm("Importing a file will overwrite the current text of the pad." + " Are you sure you want to proceed?");
|
||||
if (ret)
|
||||
{
|
||||
hidePanelCall = paddocbar.hideLaterIfNoOtherInteraction();
|
||||
currentImportTimer = window.setTimeout(function()
|
||||
{
|
||||
if (!currentImportTimer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
currentImportTimer = null;
|
||||
importFailed("Request timed out.");
|
||||
}, 25000); // time out after some number of seconds
|
||||
$('#importsubmitinput').attr(
|
||||
{
|
||||
disabled: true
|
||||
}).val("Importing...");
|
||||
window.setTimeout(function()
|
||||
{
|
||||
$('#importfileinput').attr(
|
||||
{
|
||||
disabled: true
|
||||
});
|
||||
}, 0);
|
||||
$('#importarrow').stop(true, true).hide();
|
||||
$('#importstatusball').show();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function importFailed(msg)
|
||||
{
|
||||
importErrorMessage(msg);
|
||||
}
|
||||
|
||||
function importDone()
|
||||
{
|
||||
$('#importsubmitinput').removeAttr('disabled').val("Import Now");
|
||||
window.setTimeout(function()
|
||||
{
|
||||
$('#importfileinput').removeAttr('disabled');
|
||||
}, 0);
|
||||
$('#importstatusball').hide();
|
||||
importClearTimeout();
|
||||
addImportFrames();
|
||||
}
|
||||
|
||||
function importClearTimeout()
|
||||
{
|
||||
if (currentImportTimer)
|
||||
{
|
||||
window.clearTimeout(currentImportTimer);
|
||||
currentImportTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function importErrorMessage(status)
|
||||
{
|
||||
var msg="";
|
||||
|
||||
if(status === "convertFailed"){
|
||||
msg = "We were not able to import this file. Please use a different document format or copy paste manually";
|
||||
} else if(status === "uploadFailed"){
|
||||
msg = "The upload failed, please try again";
|
||||
}
|
||||
|
||||
function showError(fade)
|
||||
{
|
||||
$('#importmessagefail').html('<strong style="color: red">Import failed:</strong> ' + (msg || 'Please copy paste'))[(fade ? "fadeIn" : "show")]();
|
||||
}
|
||||
|
||||
if ($('#importexport .importmessage').is(':visible'))
|
||||
{
|
||||
$('#importmessagesuccess').fadeOut("fast");
|
||||
$('#importmessagefail').fadeOut("fast", function()
|
||||
{
|
||||
showError(true);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
function importSuccessful(token)
|
||||
{
|
||||
$.ajax(
|
||||
{
|
||||
type: 'post',
|
||||
url: '/ep/pad/impexp/import2',
|
||||
data: {
|
||||
token: token,
|
||||
padId: pad.getPadId()
|
||||
},
|
||||
success: importApplicationSuccessful,
|
||||
error: importApplicationFailed,
|
||||
timeout: 25000
|
||||
});
|
||||
addImportFrames();
|
||||
}
|
||||
|
||||
function importApplicationFailed(xhr, textStatus, errorThrown)
|
||||
{
|
||||
importErrorMessage("Error during conversion.");
|
||||
importDone();
|
||||
}
|
||||
|
||||
///// export
|
||||
|
||||
function cantExport()
|
||||
{
|
||||
var type = $(this);
|
||||
if (type.hasClass("exporthrefpdf"))
|
||||
{
|
||||
type = "PDF";
|
||||
}
|
||||
else if (type.hasClass("exporthrefdoc"))
|
||||
{
|
||||
type = "Microsoft Word";
|
||||
}
|
||||
else if (type.hasClass("exporthrefodt"))
|
||||
{
|
||||
type = "OpenDocument";
|
||||
}
|
||||
else
|
||||
{
|
||||
type = "this file";
|
||||
}
|
||||
alert("Exporting as " + type + " format is disabled. Please contact your" + " system administrator for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/////
|
||||
var pad = undefined;
|
||||
var self = {
|
||||
init: function(_pad)
|
||||
{
|
||||
pad = _pad;
|
||||
|
||||
//get /p/padname
|
||||
var pad_root_path = new RegExp(/.*\/p\/[^\/]+/).exec(document.location.pathname)
|
||||
//get http://example.com/p/padname
|
||||
var pad_root_url = document.location.href.replace(document.location.pathname, pad_root_path)
|
||||
|
||||
// build the export links
|
||||
$("#exporthtmla").attr("href", pad_root_path + "/export/html");
|
||||
$("#exportplaina").attr("href", pad_root_path + "/export/txt");
|
||||
$("#exportdokuwikia").attr("href", pad_root_path + "/export/dokuwiki");
|
||||
|
||||
//hide stuff thats not avaible if abiword is disabled
|
||||
if(clientVars.abiwordAvailable == "no")
|
||||
{
|
||||
$("#exportworda").remove();
|
||||
$("#exportpdfa").remove();
|
||||
$("#exportopena").remove();
|
||||
$(".importformdiv").remove();
|
||||
$("#import").html("Import is not available. To enable import please install abiword");
|
||||
}
|
||||
else if(clientVars.abiwordAvailable == "withoutPDF")
|
||||
{
|
||||
$("#exportpdfa").remove();
|
||||
|
||||
$("#exportworda").attr("href", pad_root_path + "/export/doc");
|
||||
$("#exportopena").attr("href", pad_root_path + "/export/odt");
|
||||
|
||||
$("#importexport").css({"height":"142px"});
|
||||
$("#importexportline").css({"height":"142px"});
|
||||
|
||||
$("#importform").attr('action', pad_root_url + "/import");
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#exportworda").attr("href", pad_root_path + "/export/doc");
|
||||
$("#exportpdfa").attr("href", pad_root_path + "/export/pdf");
|
||||
$("#exportopena").attr("href", pad_root_path + "/export/odt");
|
||||
|
||||
$("#importform").attr('action', pad_root_path + "/import");
|
||||
}
|
||||
|
||||
$("#impexp-close").click(function()
|
||||
{
|
||||
paddocbar.setShownPanel(null);
|
||||
});
|
||||
|
||||
addImportFrames();
|
||||
$("#importfileinput").change(fileInputUpdated);
|
||||
$('#importform').submit(fileInputSubmit);
|
||||
$('.disabledexport').click(cantExport);
|
||||
},
|
||||
handleFrameCall: function(status)
|
||||
{
|
||||
if (status !== "ok")
|
||||
{
|
||||
importFailed(status);
|
||||
}
|
||||
|
||||
importDone();
|
||||
},
|
||||
disable: function()
|
||||
{
|
||||
$("#impexp-disabled-clickcatcher").show();
|
||||
$("#import").css('opacity', 0.5);
|
||||
$("#impexp-export").css('opacity', 0.5);
|
||||
},
|
||||
enable: function()
|
||||
{
|
||||
$("#impexp-disabled-clickcatcher").hide();
|
||||
$("#import").css('opacity', 1);
|
||||
$("#impexp-export").css('opacity', 1);
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.padimpexp = padimpexp;
|
64
src/static/js/pad_modals.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
var padeditbar = require('./pad_editbar').padeditbar;
|
||||
|
||||
var padmodals = (function()
|
||||
{
|
||||
var pad = undefined;
|
||||
var self = {
|
||||
init: function(_pad)
|
||||
{
|
||||
pad = _pad;
|
||||
},
|
||||
showModal: function(messageId)
|
||||
{
|
||||
padeditbar.toggleDropDown("none", function() {
|
||||
$("#connectivity .visible").removeClass('visible');
|
||||
$("#connectivity ."+messageId).addClass('visible');
|
||||
padeditbar.toggleDropDown("connectivity");
|
||||
});
|
||||
},
|
||||
showOverlay: function(duration) {
|
||||
$("#overlay").show().css(
|
||||
{
|
||||
'opacity': 0
|
||||
}).animate(
|
||||
{
|
||||
'opacity': 1
|
||||
}, duration);
|
||||
},
|
||||
hideOverlay: function(duration) {
|
||||
$("#overlay").animate(
|
||||
{
|
||||
'opacity': 0
|
||||
}, duration, function()
|
||||
{
|
||||
$("#modaloverlay").hide();
|
||||
});
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
exports.padmodals = padmodals;
|
26
src/static/js/pad_savedrevs.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Copyright 2012 Peter 'Pita' Martischka
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var pad;
|
||||
|
||||
exports.saveNow = function(){
|
||||
pad.collabClient.sendMessage({"type": "SAVE_REVISION"});
|
||||
alert("This revision is now marked as a saved revision");
|
||||
}
|
||||
|
||||
exports.init = function(_pad){
|
||||
pad = _pad;
|
||||
}
|
820
src/static/js/pad_userlist.js
Normal file
|
@ -0,0 +1,820 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
|
||||
var myUserInfo = {};
|
||||
|
||||
var colorPickerOpen = false;
|
||||
var colorPickerSetup = false;
|
||||
var previousColorId = 0;
|
||||
|
||||
|
||||
var paduserlist = (function()
|
||||
{
|
||||
|
||||
var rowManager = (function()
|
||||
{
|
||||
// The row manager handles rendering rows of the user list and animating
|
||||
// their insertion, removal, and reordering. It manipulates TD height
|
||||
// and TD opacity.
|
||||
|
||||
function nextRowId()
|
||||
{
|
||||
return "usertr" + (nextRowId.counter++);
|
||||
}
|
||||
nextRowId.counter = 1;
|
||||
// objects are shared; fields are "domId","data","animationStep"
|
||||
var rowsFadingOut = []; // unordered set
|
||||
var rowsFadingIn = []; // unordered set
|
||||
var rowsPresent = []; // in order
|
||||
var ANIMATION_START = -12; // just starting to fade in
|
||||
var ANIMATION_END = 12; // just finishing fading out
|
||||
|
||||
|
||||
function getAnimationHeight(step, power)
|
||||
{
|
||||
var a = Math.abs(step / 12);
|
||||
if (power == 2) a = a * a;
|
||||
else if (power == 3) a = a * a * a;
|
||||
else if (power == 4) a = a * a * a * a;
|
||||
else if (power >= 5) a = a * a * a * a * a;
|
||||
return Math.round(26 * (1 - a));
|
||||
}
|
||||
var OPACITY_STEPS = 6;
|
||||
|
||||
var ANIMATION_STEP_TIME = 20;
|
||||
var LOWER_FRAMERATE_FACTOR = 2;
|
||||
var scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR).scheduleAnimation;
|
||||
|
||||
var NUMCOLS = 4;
|
||||
|
||||
// we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
|
||||
// IE's poor handling when manipulating the DOM directly.
|
||||
|
||||
function getEmptyRowHtml(height)
|
||||
{
|
||||
return '<td colspan="' + NUMCOLS + '" style="border:0;height:' + height + 'px"><!-- --></td>';
|
||||
}
|
||||
|
||||
function isNameEditable(data)
|
||||
{
|
||||
return (!data.name) && (data.status != 'Disconnected');
|
||||
}
|
||||
|
||||
function replaceUserRowContents(tr, height, data)
|
||||
{
|
||||
var tds = getUserRowHtml(height, data).match(/<td.*?<\/td>/gi);
|
||||
if (isNameEditable(data) && tr.find("td.usertdname input:enabled").length > 0)
|
||||
{
|
||||
// preserve input field node
|
||||
for (var i = 0; i < tds.length; i++)
|
||||
{
|
||||
var oldTd = $(tr.find("td").get(i));
|
||||
if (!oldTd.hasClass('usertdname'))
|
||||
{
|
||||
oldTd.replaceWith(tds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tr.html(tds.join(''));
|
||||
}
|
||||
return tr;
|
||||
}
|
||||
|
||||
function getUserRowHtml(height, data)
|
||||
{
|
||||
var nameHtml;
|
||||
var isGuest = (data.id.charAt(0) != 'p');
|
||||
if (data.name)
|
||||
{
|
||||
nameHtml = padutils.escapeHtml(data.name);
|
||||
if (isGuest && pad.getIsProPad())
|
||||
{
|
||||
nameHtml += ' (Guest)';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nameHtml = '<input type="text" class="editempty newinput" value="unnamed" ' + (isNameEditable(data) ? '' : 'disabled="disabled" ') + '/>';
|
||||
}
|
||||
|
||||
return ['<td style="height:', height, 'px" class="usertdswatch"><div class="swatch" style="background:' + data.color + '"> </div></td>', '<td style="height:', height, 'px" class="usertdname">', nameHtml, '</td>', '<td style="height:', height, 'px" class="activity">', padutils.escapeHtml(data.activity), '</td>'].join('');
|
||||
}
|
||||
|
||||
function getRowHtml(id, innerHtml)
|
||||
{
|
||||
return '<tr id="' + id + '">' + innerHtml + '</tr>';
|
||||
}
|
||||
|
||||
function rowNode(row)
|
||||
{
|
||||
return $("#" + row.domId);
|
||||
}
|
||||
|
||||
function handleRowData(row)
|
||||
{
|
||||
if (row.data && row.data.status == 'Disconnected')
|
||||
{
|
||||
row.opacity = 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete row.opacity;
|
||||
}
|
||||
}
|
||||
|
||||
function handleRowNode(tr, data)
|
||||
{
|
||||
if (data.titleText)
|
||||
{
|
||||
var titleText = data.titleText;
|
||||
window.setTimeout(function()
|
||||
{
|
||||
/* tr.attr('title', titleText)*/
|
||||
}, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
tr.removeAttr('title');
|
||||
}
|
||||
}
|
||||
|
||||
function handleOtherUserInputs()
|
||||
{
|
||||
// handle 'INPUT' elements for naming other unnamed users
|
||||
$("#otheruserstable input.newinput").each(function()
|
||||
{
|
||||
var input = $(this);
|
||||
var tr = input.closest("tr");
|
||||
if (tr.length > 0)
|
||||
{
|
||||
var index = tr.parent().children().index(tr);
|
||||
if (index >= 0)
|
||||
{
|
||||
var userId = rowsPresent[index].data.id;
|
||||
rowManagerMakeNameEditor($(this), userId);
|
||||
}
|
||||
}
|
||||
}).removeClass('newinput');
|
||||
}
|
||||
|
||||
// animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
|
||||
|
||||
|
||||
function insertRow(position, data, animationPower)
|
||||
{
|
||||
position = Math.max(0, Math.min(rowsPresent.length, position));
|
||||
animationPower = (animationPower === undefined ? 4 : animationPower);
|
||||
|
||||
var domId = nextRowId();
|
||||
var row = {
|
||||
data: data,
|
||||
animationStep: ANIMATION_START,
|
||||
domId: domId,
|
||||
animationPower: animationPower
|
||||
};
|
||||
handleRowData(row);
|
||||
rowsPresent.splice(position, 0, row);
|
||||
var tr;
|
||||
if (animationPower == 0)
|
||||
{
|
||||
tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data)));
|
||||
row.animationStep = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
rowsFadingIn.push(row);
|
||||
tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START))));
|
||||
}
|
||||
handleRowNode(tr, data);
|
||||
if (position == 0)
|
||||
{
|
||||
$("table#otheruserstable").prepend(tr);
|
||||
}
|
||||
else
|
||||
{
|
||||
rowNode(rowsPresent[position - 1]).after(tr);
|
||||
}
|
||||
|
||||
if (animationPower != 0)
|
||||
{
|
||||
scheduleAnimation();
|
||||
}
|
||||
|
||||
handleOtherUserInputs();
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function updateRow(position, data)
|
||||
{
|
||||
var row = rowsPresent[position];
|
||||
if (row)
|
||||
{
|
||||
row.data = data;
|
||||
handleRowData(row);
|
||||
if (row.animationStep == 0)
|
||||
{
|
||||
// not currently animating
|
||||
var tr = rowNode(row);
|
||||
replaceUserRowContents(tr, getAnimationHeight(0), row.data).find("td").css('opacity', (row.opacity === undefined ? 1 : row.opacity));
|
||||
handleRowNode(tr, data);
|
||||
handleOtherUserInputs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeRow(position, animationPower)
|
||||
{
|
||||
animationPower = (animationPower === undefined ? 4 : animationPower);
|
||||
var row = rowsPresent[position];
|
||||
if (row)
|
||||
{
|
||||
rowsPresent.splice(position, 1); // remove
|
||||
if (animationPower == 0)
|
||||
{
|
||||
rowNode(row).remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
row.animationStep = -row.animationStep; // use symmetry
|
||||
row.animationPower = animationPower;
|
||||
rowsFadingOut.push(row);
|
||||
scheduleAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newPosition is position after the row has been removed
|
||||
|
||||
|
||||
function moveRow(oldPosition, newPosition, animationPower)
|
||||
{
|
||||
animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
|
||||
var row = rowsPresent[oldPosition];
|
||||
if (row && oldPosition != newPosition)
|
||||
{
|
||||
var rowData = row.data;
|
||||
removeRow(oldPosition, animationPower);
|
||||
insertRow(newPosition, rowData, animationPower);
|
||||
}
|
||||
}
|
||||
|
||||
function animateStep()
|
||||
{
|
||||
// animation must be symmetrical
|
||||
for (var i = rowsFadingIn.length - 1; i >= 0; i--)
|
||||
{ // backwards to allow removal
|
||||
var row = rowsFadingIn[i];
|
||||
var step = ++row.animationStep;
|
||||
var animHeight = getAnimationHeight(step, row.animationPower);
|
||||
var node = rowNode(row);
|
||||
var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
|
||||
if (step <= -OPACITY_STEPS)
|
||||
{
|
||||
node.find("td").height(animHeight);
|
||||
}
|
||||
else if (step == -OPACITY_STEPS + 1)
|
||||
{
|
||||
node.html(getUserRowHtml(animHeight, row.data)).find("td").css('opacity', baseOpacity * 1 / OPACITY_STEPS);
|
||||
handleRowNode(node, row.data);
|
||||
}
|
||||
else if (step < 0)
|
||||
{
|
||||
node.find("td").css('opacity', baseOpacity * (OPACITY_STEPS - (-step)) / OPACITY_STEPS).height(animHeight);
|
||||
}
|
||||
else if (step == 0)
|
||||
{
|
||||
// set HTML in case modified during animation
|
||||
node.html(getUserRowHtml(animHeight, row.data)).find("td").css('opacity', baseOpacity * 1).height(animHeight);
|
||||
handleRowNode(node, row.data);
|
||||
rowsFadingIn.splice(i, 1); // remove from set
|
||||
}
|
||||
}
|
||||
for (var i = rowsFadingOut.length - 1; i >= 0; i--)
|
||||
{ // backwards to allow removal
|
||||
var row = rowsFadingOut[i];
|
||||
var step = ++row.animationStep;
|
||||
var node = rowNode(row);
|
||||
var animHeight = getAnimationHeight(step, row.animationPower);
|
||||
var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
|
||||
if (step < OPACITY_STEPS)
|
||||
{
|
||||
node.find("td").css('opacity', baseOpacity * (OPACITY_STEPS - step) / OPACITY_STEPS).height(animHeight);
|
||||
}
|
||||
else if (step == OPACITY_STEPS)
|
||||
{
|
||||
node.html(getEmptyRowHtml(animHeight));
|
||||
}
|
||||
else if (step <= ANIMATION_END)
|
||||
{
|
||||
node.find("td").height(animHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
rowsFadingOut.splice(i, 1); // remove from set
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
|
||||
handleOtherUserInputs();
|
||||
|
||||
return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
|
||||
}
|
||||
|
||||
var self = {
|
||||
insertRow: insertRow,
|
||||
removeRow: removeRow,
|
||||
moveRow: moveRow,
|
||||
updateRow: updateRow
|
||||
};
|
||||
return self;
|
||||
}()); ////////// rowManager
|
||||
var otherUsersInfo = [];
|
||||
var otherUsersData = [];
|
||||
|
||||
function rowManagerMakeNameEditor(jnode, userId)
|
||||
{
|
||||
setUpEditable(jnode, function()
|
||||
{
|
||||
var existingIndex = findExistingIndex(userId);
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
return otherUsersInfo[existingIndex].name || '';
|
||||
}
|
||||
else
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}, function(newName)
|
||||
{
|
||||
if (!newName)
|
||||
{
|
||||
jnode.addClass("editempty");
|
||||
jnode.val("unnamed");
|
||||
}
|
||||
else
|
||||
{
|
||||
jnode.attr('disabled', 'disabled');
|
||||
pad.suggestUserName(userId, newName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findExistingIndex(userId)
|
||||
{
|
||||
var existingIndex = -1;
|
||||
for (var i = 0; i < otherUsersInfo.length; i++)
|
||||
{
|
||||
if (otherUsersInfo[i].userId == userId)
|
||||
{
|
||||
existingIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return existingIndex;
|
||||
}
|
||||
|
||||
function setUpEditable(jqueryNode, valueGetter, valueSetter)
|
||||
{
|
||||
jqueryNode.bind('focus', function(evt)
|
||||
{
|
||||
var oldValue = valueGetter();
|
||||
if (jqueryNode.val() !== oldValue)
|
||||
{
|
||||
jqueryNode.val(oldValue);
|
||||
}
|
||||
jqueryNode.addClass("editactive").removeClass("editempty");
|
||||
});
|
||||
jqueryNode.bind('blur', function(evt)
|
||||
{
|
||||
var newValue = jqueryNode.removeClass("editactive").val();
|
||||
valueSetter(newValue);
|
||||
});
|
||||
padutils.bindEnterAndEscape(jqueryNode, function onEnter()
|
||||
{
|
||||
jqueryNode.blur();
|
||||
}, function onEscape()
|
||||
{
|
||||
jqueryNode.val(valueGetter()).blur();
|
||||
});
|
||||
jqueryNode.removeAttr('disabled').addClass('editable');
|
||||
}
|
||||
|
||||
function updateInviteNotice()
|
||||
{
|
||||
if (otherUsersInfo.length == 0)
|
||||
{
|
||||
$("#otheruserstable").hide();
|
||||
$("#nootherusers").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#nootherusers").hide();
|
||||
$("#otheruserstable").show();
|
||||
}
|
||||
}
|
||||
|
||||
var knocksToIgnore = {};
|
||||
var guestPromptFlashState = 0;
|
||||
var guestPromptFlash = padutils.makeAnimationScheduler(
|
||||
|
||||
function()
|
||||
{
|
||||
var prompts = $("#guestprompts .guestprompt");
|
||||
if (prompts.length == 0)
|
||||
{
|
||||
return false; // no more to do
|
||||
}
|
||||
|
||||
guestPromptFlashState = 1 - guestPromptFlashState;
|
||||
if (guestPromptFlashState)
|
||||
{
|
||||
prompts.css('background', '#ffa');
|
||||
}
|
||||
else
|
||||
{
|
||||
prompts.css('background', '#ffe');
|
||||
}
|
||||
|
||||
return true;
|
||||
}, 1000);
|
||||
|
||||
var pad = undefined;
|
||||
var self = {
|
||||
init: function(myInitialUserInfo, _pad)
|
||||
{
|
||||
pad = _pad;
|
||||
|
||||
self.setMyUserInfo(myInitialUserInfo);
|
||||
|
||||
$("#otheruserstable tr").remove();
|
||||
|
||||
if (pad.getUserIsGuest())
|
||||
{
|
||||
$("#myusernameedit").addClass('myusernameedithoverable');
|
||||
setUpEditable($("#myusernameedit"), function()
|
||||
{
|
||||
return myUserInfo.name || '';
|
||||
}, function(newValue)
|
||||
{
|
||||
myUserInfo.name = newValue;
|
||||
pad.notifyChangeName(newValue);
|
||||
// wrap with setTimeout to do later because we get
|
||||
// a double "blur" fire in IE...
|
||||
window.setTimeout(function()
|
||||
{
|
||||
self.renderMyUserInfo();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// color picker
|
||||
$("#myswatchbox").click(showColorPicker);
|
||||
$("#mycolorpicker .pickerswatchouter").click(function()
|
||||
{
|
||||
$("#mycolorpicker .pickerswatchouter").removeClass('picked');
|
||||
$(this).addClass('picked');
|
||||
});
|
||||
$("#mycolorpickersave").click(function()
|
||||
{
|
||||
closeColorPicker(true);
|
||||
});
|
||||
$("#mycolorpickercancel").click(function()
|
||||
{
|
||||
closeColorPicker(false);
|
||||
});
|
||||
//
|
||||
},
|
||||
setMyUserInfo: function(info)
|
||||
{
|
||||
//translate the colorId
|
||||
if(typeof info.colorId == "number")
|
||||
{
|
||||
info.colorId = clientVars.colorPalette[info.colorId];
|
||||
}
|
||||
|
||||
myUserInfo = $.extend(
|
||||
{}, info);
|
||||
|
||||
self.renderMyUserInfo();
|
||||
},
|
||||
userJoinOrUpdate: function(info)
|
||||
{
|
||||
if ((!info.userId) || (info.userId == myUserInfo.userId))
|
||||
{
|
||||
// not sure how this would happen
|
||||
return;
|
||||
}
|
||||
|
||||
hooks.callAll('userJoinOrUpdate', {
|
||||
userInfo: info
|
||||
});
|
||||
|
||||
var userData = {};
|
||||
userData.color = typeof info.colorId == "number" ? clientVars.colorPalette[info.colorId] : info.colorId;
|
||||
userData.name = info.name;
|
||||
userData.status = '';
|
||||
userData.activity = '';
|
||||
userData.id = info.userId;
|
||||
// Firefox ignores \n in title text; Safari does a linebreak
|
||||
userData.titleText = [info.userAgent || '', info.ip || ''].join(' \n');
|
||||
|
||||
var existingIndex = findExistingIndex(info.userId);
|
||||
|
||||
var numUsersBesides = otherUsersInfo.length;
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
numUsersBesides--;
|
||||
}
|
||||
var newIndex = padutils.binarySearch(numUsersBesides, function(n)
|
||||
{
|
||||
if (existingIndex >= 0 && n >= existingIndex)
|
||||
{
|
||||
// pretend existingIndex isn't there
|
||||
n++;
|
||||
}
|
||||
var infoN = otherUsersInfo[n];
|
||||
var nameN = (infoN.name || '').toLowerCase();
|
||||
var nameThis = (info.name || '').toLowerCase();
|
||||
var idN = infoN.userId;
|
||||
var idThis = info.userId;
|
||||
return (nameN > nameThis) || (nameN == nameThis && idN > idThis);
|
||||
});
|
||||
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
// update
|
||||
if (existingIndex == newIndex)
|
||||
{
|
||||
otherUsersInfo[existingIndex] = info;
|
||||
otherUsersData[existingIndex] = userData;
|
||||
rowManager.updateRow(existingIndex, userData);
|
||||
}
|
||||
else
|
||||
{
|
||||
otherUsersInfo.splice(existingIndex, 1);
|
||||
otherUsersData.splice(existingIndex, 1);
|
||||
otherUsersInfo.splice(newIndex, 0, info);
|
||||
otherUsersData.splice(newIndex, 0, userData);
|
||||
rowManager.updateRow(existingIndex, userData);
|
||||
rowManager.moveRow(existingIndex, newIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
otherUsersInfo.splice(newIndex, 0, info);
|
||||
otherUsersData.splice(newIndex, 0, userData);
|
||||
rowManager.insertRow(newIndex, userData);
|
||||
}
|
||||
|
||||
updateInviteNotice();
|
||||
|
||||
self.updateNumberOfOnlineUsers();
|
||||
},
|
||||
updateNumberOfOnlineUsers: function()
|
||||
{
|
||||
var online = 1; // you are always online!
|
||||
for (var i = 0; i < otherUsersData.length; i++)
|
||||
{
|
||||
if (otherUsersData[i].status == "")
|
||||
{
|
||||
online++;
|
||||
}
|
||||
}
|
||||
$("#online_count").text(online);
|
||||
|
||||
return online;
|
||||
},
|
||||
userLeave: function(info)
|
||||
{
|
||||
var existingIndex = findExistingIndex(info.userId);
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
var userData = otherUsersData[existingIndex];
|
||||
userData.status = 'Disconnected';
|
||||
rowManager.updateRow(existingIndex, userData);
|
||||
if (userData.leaveTimer)
|
||||
{
|
||||
window.clearTimeout(userData.leaveTimer);
|
||||
}
|
||||
// set up a timer that will only fire if no leaves,
|
||||
// joins, or updates happen for this user in the
|
||||
// next N seconds, to remove the user from the list.
|
||||
var thisUserId = info.userId;
|
||||
var thisLeaveTimer = window.setTimeout(function()
|
||||
{
|
||||
var newExistingIndex = findExistingIndex(thisUserId);
|
||||
if (newExistingIndex >= 0)
|
||||
{
|
||||
var newUserData = otherUsersData[newExistingIndex];
|
||||
if (newUserData.status == 'Disconnected' && newUserData.leaveTimer == thisLeaveTimer)
|
||||
{
|
||||
otherUsersInfo.splice(newExistingIndex, 1);
|
||||
otherUsersData.splice(newExistingIndex, 1);
|
||||
rowManager.removeRow(newExistingIndex);
|
||||
updateInviteNotice();
|
||||
}
|
||||
}
|
||||
}, 8000); // how long to wait
|
||||
userData.leaveTimer = thisLeaveTimer;
|
||||
}
|
||||
updateInviteNotice();
|
||||
|
||||
self.updateNumberOfOnlineUsers();
|
||||
},
|
||||
showGuestPrompt: function(userId, displayName)
|
||||
{
|
||||
if (knocksToIgnore[userId])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var encodedUserId = padutils.encodeUserId(userId);
|
||||
|
||||
var actionName = 'hide-guest-prompt-' + encodedUserId;
|
||||
padutils.cancelActions(actionName);
|
||||
|
||||
var box = $("#guestprompt-" + encodedUserId);
|
||||
if (box.length == 0)
|
||||
{
|
||||
// make guest prompt box
|
||||
box = $('<div id="'+padutils.escapeHtml('guestprompt-' + encodedUserId) + '" class="guestprompt"><div class="choices"><a href="' + padutils.escapeHtml('javascript:void(require('+JSON.stringify(module.id)+').paduserlist.answerGuestPrompt(' + JSON.stringify(encodedUserId) + ',false))')+'">Deny</a> <a href="' + padutils.escapeHtml('javascript:void(require('+JSON.stringify(module.id)+').paduserlist.answerGuestPrompt(' + JSON.stringify(encodedUserId) + ',true))') + '">Approve</a></div><div class="guestname"><strong>Guest:</strong> ' + padutils.escapeHtml(displayName) + '</div></div>');
|
||||
$("#guestprompts").append(box);
|
||||
}
|
||||
else
|
||||
{
|
||||
// update display name
|
||||
box.find(".guestname").html('<strong>Guest:</strong> ' + padutils.escapeHtml(displayName));
|
||||
}
|
||||
var hideLater = padutils.getCancellableAction(actionName, function()
|
||||
{
|
||||
self.removeGuestPrompt(userId);
|
||||
});
|
||||
window.setTimeout(hideLater, 15000); // time-out with no knock
|
||||
guestPromptFlash.scheduleAnimation();
|
||||
},
|
||||
removeGuestPrompt: function(userId)
|
||||
{
|
||||
var box = $("#guestprompt-" + padutils.encodeUserId(userId));
|
||||
// remove ID now so a new knock by same user gets new, unfaded box
|
||||
box.removeAttr('id').fadeOut("fast", function()
|
||||
{
|
||||
box.remove();
|
||||
});
|
||||
|
||||
knocksToIgnore[userId] = true;
|
||||
window.setTimeout(function()
|
||||
{
|
||||
delete knocksToIgnore[userId];
|
||||
}, 5000);
|
||||
},
|
||||
answerGuestPrompt: function(encodedUserId, approve)
|
||||
{
|
||||
var guestId = padutils.decodeUserId(encodedUserId);
|
||||
|
||||
var msg = {
|
||||
type: 'guestanswer',
|
||||
authId: pad.getUserId(),
|
||||
guestId: guestId,
|
||||
answer: (approve ? "approved" : "denied")
|
||||
};
|
||||
pad.sendClientMessage(msg);
|
||||
|
||||
self.removeGuestPrompt(guestId);
|
||||
},
|
||||
renderMyUserInfo: function()
|
||||
{
|
||||
if (myUserInfo.name)
|
||||
{
|
||||
$("#myusernameedit").removeClass("editempty").val(
|
||||
myUserInfo.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#myusernameedit").addClass("editempty").val("Enter your name");
|
||||
}
|
||||
if (colorPickerOpen)
|
||||
{
|
||||
$("#myswatchbox").addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable');
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#myswatchbox").addClass('myswatchboxhoverable').removeClass('myswatchboxunhoverable');
|
||||
}
|
||||
|
||||
$("#myswatch").css({'background-color': myUserInfo.colorId});
|
||||
|
||||
if ($.browser.msie && parseInt($.browser.version) <= 8) {
|
||||
$("#usericon a").css({'box-shadow': 'inset 0 0 30px ' + myUserInfo.colorId,'background-color': myUserInfo.colorId});
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#usericon a").css({'box-shadow': 'inset 0 0 30px ' + myUserInfo.colorId});
|
||||
}
|
||||
}
|
||||
};
|
||||
return self;
|
||||
}());
|
||||
|
||||
function getColorPickerSwatchIndex(jnode)
|
||||
{
|
||||
// return Number(jnode.get(0).className.match(/\bn([0-9]+)\b/)[1])-1;
|
||||
return $("#colorpickerswatches li").index(jnode);
|
||||
}
|
||||
|
||||
function closeColorPicker(accept)
|
||||
{
|
||||
if (accept)
|
||||
{
|
||||
var newColor = $("#mycolorpickerpreview").css("background-color");
|
||||
var parts = newColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
||||
// parts now should be ["rgb(0, 70, 255", "0", "70", "255"]
|
||||
if (parts) {
|
||||
delete (parts[0]);
|
||||
for (var i = 1; i <= 3; ++i) {
|
||||
parts[i] = parseInt(parts[i]).toString(16);
|
||||
if (parts[i].length == 1) parts[i] = '0' + parts[i];
|
||||
}
|
||||
var newColor = "#" +parts.join(''); // "0070ff"
|
||||
}
|
||||
myUserInfo.colorId = newColor;
|
||||
pad.notifyChangeColor(newColor);
|
||||
paduserlist.renderMyUserInfo();
|
||||
}
|
||||
else
|
||||
{
|
||||
//pad.notifyChangeColor(previousColorId);
|
||||
//paduserlist.renderMyUserInfo();
|
||||
}
|
||||
|
||||
colorPickerOpen = false;
|
||||
$("#mycolorpicker").fadeOut("fast");
|
||||
}
|
||||
|
||||
function showColorPicker()
|
||||
{
|
||||
previousColorId = myUserInfo.colorId;
|
||||
|
||||
if (!colorPickerOpen)
|
||||
{
|
||||
var palette = pad.getColorPalette();
|
||||
|
||||
if (!colorPickerSetup)
|
||||
{
|
||||
var colorsList = $("#colorpickerswatches")
|
||||
for (var i = 0; i < palette.length; i++)
|
||||
{
|
||||
|
||||
var li = $('<li>', {
|
||||
style: 'background: ' + palette[i] + ';'
|
||||
});
|
||||
|
||||
li.appendTo(colorsList);
|
||||
|
||||
li.bind('click', function(event)
|
||||
{
|
||||
$("#colorpickerswatches li").removeClass('picked');
|
||||
$(event.target).addClass("picked");
|
||||
|
||||
var newColorId = getColorPickerSwatchIndex($("#colorpickerswatches .picked"));
|
||||
pad.notifyChangeColor(newColorId);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
colorPickerSetup = true;
|
||||
}
|
||||
|
||||
$("#mycolorpicker").fadeIn();
|
||||
colorPickerOpen = true;
|
||||
|
||||
$("#colorpickerswatches li").removeClass('picked');
|
||||
$($("#colorpickerswatches li")[myUserInfo.colorId]).addClass("picked"); //seems weird
|
||||
}
|
||||
}
|
||||
|
||||
exports.paduserlist = paduserlist;
|
537
src/static/js/pad_utils.js
Normal file
|
@ -0,0 +1,537 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var Security = require('./security');
|
||||
|
||||
/**
|
||||
* Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids
|
||||
*/
|
||||
|
||||
function randomString(len)
|
||||
{
|
||||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
var randomstring = '';
|
||||
len = len || 20
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
var rnum = Math.floor(Math.random() * chars.length);
|
||||
randomstring += chars.substring(rnum, rnum + 1);
|
||||
}
|
||||
return randomstring;
|
||||
}
|
||||
|
||||
function createCookie(name, value, days, path)
|
||||
{
|
||||
if (days)
|
||||
{
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
var expires = "; expires=" + date.toGMTString();
|
||||
}
|
||||
else var expires = "";
|
||||
|
||||
if(!path)
|
||||
path = "/";
|
||||
|
||||
document.cookie = name + "=" + value + expires + "; path=" + path;
|
||||
}
|
||||
|
||||
function readCookie(name)
|
||||
{
|
||||
var nameEQ = name + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++)
|
||||
{
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var padutils = {
|
||||
escapeHtml: function(x)
|
||||
{
|
||||
return Security.escapeHTML(String(x));
|
||||
},
|
||||
uniqueId: function()
|
||||
{
|
||||
var pad = require('./pad').pad; // Sidestep circular dependency
|
||||
function encodeNum(n, width)
|
||||
{
|
||||
// returns string that is exactly 'width' chars, padding with zeros
|
||||
// and taking rightmost digits
|
||||
return (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
|
||||
}
|
||||
return [pad.getClientIp(), encodeNum(+new Date, 7), encodeNum(Math.floor(Math.random() * 1e9), 4)].join('.');
|
||||
},
|
||||
uaDisplay: function(ua)
|
||||
{
|
||||
var m;
|
||||
|
||||
function clean(a)
|
||||
{
|
||||
var maxlen = 16;
|
||||
a = a.replace(/[^a-zA-Z0-9\.]/g, '');
|
||||
if (a.length > maxlen)
|
||||
{
|
||||
a = a.substr(0, maxlen);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function checkver(name)
|
||||
{
|
||||
var m = ua.match(RegExp(name + '\\/([\\d\\.]+)'));
|
||||
if (m && m.length > 1)
|
||||
{
|
||||
return clean(name + m[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// firefox
|
||||
if (checkver('Firefox'))
|
||||
{
|
||||
return checkver('Firefox');
|
||||
}
|
||||
|
||||
// misc browsers, including IE
|
||||
m = ua.match(/compatible; ([^;]+);/);
|
||||
if (m && m.length > 1)
|
||||
{
|
||||
return clean(m[1]);
|
||||
}
|
||||
|
||||
// iphone
|
||||
if (ua.match(/\(iPhone;/))
|
||||
{
|
||||
return 'iPhone';
|
||||
}
|
||||
|
||||
// chrome
|
||||
if (checkver('Chrome'))
|
||||
{
|
||||
return checkver('Chrome');
|
||||
}
|
||||
|
||||
// safari
|
||||
m = ua.match(/Safari\/[\d\.]+/);
|
||||
if (m)
|
||||
{
|
||||
var v = '?';
|
||||
m = ua.match(/Version\/([\d\.]+)/);
|
||||
if (m && m.length > 1)
|
||||
{
|
||||
v = m[1];
|
||||
}
|
||||
return clean('Safari' + v);
|
||||
}
|
||||
|
||||
// everything else
|
||||
var x = ua.split(' ')[0];
|
||||
return clean(x);
|
||||
},
|
||||
// e.g. "Thu Jun 18 2009 13:09"
|
||||
simpleDateTime: function(date)
|
||||
{
|
||||
var d = new Date(+date); // accept either number or date
|
||||
var dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
|
||||
var month = (['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])[d.getMonth()];
|
||||
var dayOfMonth = d.getDate();
|
||||
var year = d.getFullYear();
|
||||
var hourmin = d.getHours() + ":" + ("0" + d.getMinutes()).slice(-2);
|
||||
return dayOfWeek + ' ' + month + ' ' + dayOfMonth + ' ' + year + ' ' + hourmin;
|
||||
},
|
||||
findURLs: function(text)
|
||||
{
|
||||
// copied from ACE
|
||||
var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
|
||||
var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')');
|
||||
var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g');
|
||||
|
||||
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
|
||||
|
||||
|
||||
function _findURLs(text)
|
||||
{
|
||||
_REGEX_URL.lastIndex = 0;
|
||||
var urls = null;
|
||||
var execResult;
|
||||
while ((execResult = _REGEX_URL.exec(text)))
|
||||
{
|
||||
urls = (urls || []);
|
||||
var startIndex = execResult.index;
|
||||
var url = execResult[0];
|
||||
urls.push([startIndex, url]);
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
return _findURLs(text);
|
||||
},
|
||||
escapeHtmlWithClickableLinks: function(text, target)
|
||||
{
|
||||
var idx = 0;
|
||||
var pieces = [];
|
||||
var urls = padutils.findURLs(text);
|
||||
|
||||
function advanceTo(i)
|
||||
{
|
||||
if (i > idx)
|
||||
{
|
||||
pieces.push(Security.escapeHTML(text.substring(idx, i)));
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
if (urls)
|
||||
{
|
||||
for (var j = 0; j < urls.length; j++)
|
||||
{
|
||||
var startIndex = urls[j][0];
|
||||
var href = urls[j][1];
|
||||
advanceTo(startIndex);
|
||||
pieces.push('<a ', (target ? 'target="' + Security.escapeHTMLAttribute(target) + '" ' : ''), 'href="', Security.escapeHTMLAttribute(href), '">');
|
||||
advanceTo(startIndex + href.length);
|
||||
pieces.push('</a>');
|
||||
}
|
||||
}
|
||||
advanceTo(text.length);
|
||||
return pieces.join('');
|
||||
},
|
||||
bindEnterAndEscape: function(node, onEnter, onEscape)
|
||||
{
|
||||
|
||||
// Use keypress instead of keyup in bindEnterAndEscape
|
||||
// Keyup event is fired on enter in IME (Input Method Editor), But
|
||||
// keypress is not. So, I changed to use keypress instead of keyup.
|
||||
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox 3.6.10, Chrome 6.0.472, Safari 5.0).
|
||||
if (onEnter)
|
||||
{
|
||||
node.keypress(function(evt)
|
||||
{
|
||||
if (evt.which == 13)
|
||||
{
|
||||
onEnter(evt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onEscape)
|
||||
{
|
||||
node.keydown(function(evt)
|
||||
{
|
||||
if (evt.which == 27)
|
||||
{
|
||||
onEscape(evt);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
timediff: function(d)
|
||||
{
|
||||
var pad = require('./pad').pad; // Sidestep circular dependency
|
||||
function format(n, word)
|
||||
{
|
||||
n = Math.round(n);
|
||||
return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago');
|
||||
}
|
||||
d = Math.max(0, (+(new Date) - (+d) - pad.clientTimeOffset) / 1000);
|
||||
if (d < 60)
|
||||
{
|
||||
return format(d, 'second');
|
||||
}
|
||||
d /= 60;
|
||||
if (d < 60)
|
||||
{
|
||||
return format(d, 'minute');
|
||||
}
|
||||
d /= 60;
|
||||
if (d < 24)
|
||||
{
|
||||
return format(d, 'hour');
|
||||
}
|
||||
d /= 24;
|
||||
return format(d, 'day');
|
||||
},
|
||||
makeAnimationScheduler: function(funcToAnimateOneStep, stepTime, stepsAtOnce)
|
||||
{
|
||||
if (stepsAtOnce === undefined)
|
||||
{
|
||||
stepsAtOnce = 1;
|
||||
}
|
||||
|
||||
var animationTimer = null;
|
||||
|
||||
function scheduleAnimation()
|
||||
{
|
||||
if (!animationTimer)
|
||||
{
|
||||
animationTimer = window.setTimeout(function()
|
||||
{
|
||||
animationTimer = null;
|
||||
var n = stepsAtOnce;
|
||||
var moreToDo = true;
|
||||
while (moreToDo && n > 0)
|
||||
{
|
||||
moreToDo = funcToAnimateOneStep();
|
||||
n--;
|
||||
}
|
||||
if (moreToDo)
|
||||
{
|
||||
// more to do
|
||||
scheduleAnimation();
|
||||
}
|
||||
}, stepTime * stepsAtOnce);
|
||||
}
|
||||
}
|
||||
return {
|
||||
scheduleAnimation: scheduleAnimation
|
||||
};
|
||||
},
|
||||
makeShowHideAnimator: function(funcToArriveAtState, initiallyShown, fps, totalMs)
|
||||
{
|
||||
var animationState = (initiallyShown ? 0 : -2); // -2 hidden, -1 to 0 fade in, 0 to 1 fade out
|
||||
var animationFrameDelay = 1000 / fps;
|
||||
var animationStep = animationFrameDelay / totalMs;
|
||||
|
||||
var scheduleAnimation = padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation;
|
||||
|
||||
function doShow()
|
||||
{
|
||||
animationState = -1;
|
||||
funcToArriveAtState(animationState);
|
||||
scheduleAnimation();
|
||||
}
|
||||
|
||||
function doQuickShow()
|
||||
{ // start showing without losing any fade-in progress
|
||||
if (animationState < -1)
|
||||
{
|
||||
animationState = -1;
|
||||
}
|
||||
else if (animationState <= 0)
|
||||
{
|
||||
animationState = animationState;
|
||||
}
|
||||
else
|
||||
{
|
||||
animationState = Math.max(-1, Math.min(0, -animationState));
|
||||
}
|
||||
funcToArriveAtState(animationState);
|
||||
scheduleAnimation();
|
||||
}
|
||||
|
||||
function doHide()
|
||||
{
|
||||
if (animationState >= -1 && animationState <= 0)
|
||||
{
|
||||
animationState = 1e-6;
|
||||
scheduleAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
function animateOneStep()
|
||||
{
|
||||
if (animationState < -1 || animationState == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (animationState < 0)
|
||||
{
|
||||
// animate show
|
||||
animationState += animationStep;
|
||||
if (animationState >= 0)
|
||||
{
|
||||
animationState = 0;
|
||||
funcToArriveAtState(animationState);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
funcToArriveAtState(animationState);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (animationState > 0)
|
||||
{
|
||||
// animate hide
|
||||
animationState += animationStep;
|
||||
if (animationState >= 1)
|
||||
{
|
||||
animationState = 1;
|
||||
funcToArriveAtState(animationState);
|
||||
animationState = -2;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
funcToArriveAtState(animationState);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
show: doShow,
|
||||
hide: doHide,
|
||||
quickShow: doQuickShow
|
||||
};
|
||||
},
|
||||
_nextActionId: 1,
|
||||
uncanceledActions: {},
|
||||
getCancellableAction: function(actionType, actionFunc)
|
||||
{
|
||||
var o = padutils.uncanceledActions[actionType];
|
||||
if (!o)
|
||||
{
|
||||
o = {};
|
||||
padutils.uncanceledActions[actionType] = o;
|
||||
}
|
||||
var actionId = (padutils._nextActionId++);
|
||||
o[actionId] = true;
|
||||
return function()
|
||||
{
|
||||
var p = padutils.uncanceledActions[actionType];
|
||||
if (p && p[actionId])
|
||||
{
|
||||
actionFunc();
|
||||
}
|
||||
};
|
||||
},
|
||||
cancelActions: function(actionType)
|
||||
{
|
||||
var o = padutils.uncanceledActions[actionType];
|
||||
if (o)
|
||||
{
|
||||
// clear it
|
||||
delete padutils.uncanceledActions[actionType];
|
||||
}
|
||||
},
|
||||
makeFieldLabeledWhenEmpty: function(field, labelText)
|
||||
{
|
||||
field = $(field);
|
||||
|
||||
function clear()
|
||||
{
|
||||
field.addClass('editempty');
|
||||
field.val(labelText);
|
||||
}
|
||||
field.focus(function()
|
||||
{
|
||||
if (field.hasClass('editempty'))
|
||||
{
|
||||
field.val('');
|
||||
}
|
||||
field.removeClass('editempty');
|
||||
});
|
||||
field.blur(function()
|
||||
{
|
||||
if (!field.val())
|
||||
{
|
||||
clear();
|
||||
}
|
||||
});
|
||||
return {
|
||||
clear: clear
|
||||
};
|
||||
},
|
||||
getCheckbox: function(node)
|
||||
{
|
||||
return $(node).is(':checked');
|
||||
},
|
||||
setCheckbox: function(node, value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
$(node).attr('checked', 'checked');
|
||||
}
|
||||
else
|
||||
{
|
||||
$(node).removeAttr('checked');
|
||||
}
|
||||
},
|
||||
bindCheckboxChange: function(node, func)
|
||||
{
|
||||
$(node).bind("click change", func);
|
||||
},
|
||||
encodeUserId: function(userId)
|
||||
{
|
||||
return userId.replace(/[^a-y0-9]/g, function(c)
|
||||
{
|
||||
if (c == ".") return "-";
|
||||
return 'z' + c.charCodeAt(0) + 'z';
|
||||
});
|
||||
},
|
||||
decodeUserId: function(encodedUserId)
|
||||
{
|
||||
return encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, function(cc)
|
||||
{
|
||||
if (cc == '-') return '.';
|
||||
else if (cc.charAt(0) == 'z')
|
||||
{
|
||||
return String.fromCharCode(Number(cc.slice(1, -1)));
|
||||
}
|
||||
else
|
||||
{
|
||||
return cc;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var globalExceptionHandler = undefined;
|
||||
function setupGlobalExceptionHandler() {
|
||||
if (!globalExceptionHandler) {
|
||||
globalExceptionHandler = function test (msg, url, linenumber)
|
||||
{
|
||||
var errorId = randomString(20);
|
||||
if ($("#editorloadingbox").attr("display") != "none"){
|
||||
//show javascript errors to the user
|
||||
$("#editorloadingbox").css("padding", "10px");
|
||||
$("#editorloadingbox").css("padding-top", "45px");
|
||||
$("#editorloadingbox").html("<div style='text-align:left;color:red;font-size:16px;'><b>An error occured</b><br>The error was reported with the following id: '" + errorId + "'<br><br><span style='color:black;font-weight:bold;font-size:16px'>Please send this error message to us: </span><div style='color:black;font-size:14px'>'"
|
||||
+ "ErrorId: " + errorId + "<br>UserAgent: " + navigator.userAgent + "<br>" + msg + " in " + url + " at line " + linenumber + "'</div></div>");
|
||||
}
|
||||
|
||||
//send javascript errors to the server
|
||||
var errObj = {errorInfo: JSON.stringify({errorId: errorId, msg: msg, url: url, linenumber: linenumber, userAgent: navigator.userAgent})};
|
||||
var loc = document.location;
|
||||
var url = loc.protocol + "//" + loc.hostname + ":" + loc.port + "/" + loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "jserror";
|
||||
|
||||
$.post(url, errObj);
|
||||
|
||||
return false;
|
||||
};
|
||||
window.onerror = globalExceptionHandler;
|
||||
}
|
||||
}
|
||||
|
||||
padutils.setupGlobalExceptionHandler = setupGlobalExceptionHandler;
|
||||
|
||||
padutils.binarySearch = require('./ace2_common').binarySearch;
|
||||
|
||||
exports.randomString = randomString;
|
||||
exports.createCookie = createCookie;
|
||||
exports.readCookie = readCookie;
|
||||
exports.padutils = padutils;
|
690
src/static/js/pluginfw/async.js
Normal file
|
@ -0,0 +1,690 @@
|
|||
/*global setTimeout: false, console: false */
|
||||
(function () {
|
||||
|
||||
var async = {};
|
||||
|
||||
// global on the server, window in the browser
|
||||
var root = this,
|
||||
previous_async = root.async;
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = async;
|
||||
}
|
||||
else {
|
||||
root.async = async;
|
||||
}
|
||||
|
||||
async.noConflict = function () {
|
||||
root.async = previous_async;
|
||||
return async;
|
||||
};
|
||||
|
||||
//// cross-browser compatiblity functions ////
|
||||
|
||||
var _forEach = function (arr, iterator) {
|
||||
if (arr.forEach) {
|
||||
return arr.forEach(iterator);
|
||||
}
|
||||
for (var i = 0; i < arr.length; i += 1) {
|
||||
iterator(arr[i], i, arr);
|
||||
}
|
||||
};
|
||||
|
||||
var _map = function (arr, iterator) {
|
||||
if (arr.map) {
|
||||
return arr.map(iterator);
|
||||
}
|
||||
var results = [];
|
||||
_forEach(arr, function (x, i, a) {
|
||||
results.push(iterator(x, i, a));
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
var _reduce = function (arr, iterator, memo) {
|
||||
if (arr.reduce) {
|
||||
return arr.reduce(iterator, memo);
|
||||
}
|
||||
_forEach(arr, function (x, i, a) {
|
||||
memo = iterator(memo, x, i, a);
|
||||
});
|
||||
return memo;
|
||||
};
|
||||
|
||||
var _keys = function (obj) {
|
||||
if (Object.keys) {
|
||||
return Object.keys(obj);
|
||||
}
|
||||
var keys = [];
|
||||
for (var k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
var _indexOf = function (arr, item) {
|
||||
if (arr.indexOf) {
|
||||
return arr.indexOf(item);
|
||||
}
|
||||
for (var i = 0; i < arr.length; i += 1) {
|
||||
if (arr[i] === item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
//// exported async module functions ////
|
||||
|
||||
//// nextTick implementation with browser-compatible fallback ////
|
||||
if (typeof process === 'undefined' || !(process.nextTick)) {
|
||||
async.nextTick = function (fn) {
|
||||
setTimeout(fn, 0);
|
||||
};
|
||||
}
|
||||
else {
|
||||
async.nextTick = process.nextTick;
|
||||
}
|
||||
|
||||
async.forEach = function (arr, iterator, callback) {
|
||||
if (!arr.length) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
_forEach(arr, function (x) {
|
||||
iterator(x, function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
if (completed === arr.length) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
async.forEachSeries = function (arr, iterator, callback) {
|
||||
if (!arr.length) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
var iterate = function () {
|
||||
iterator(arr[completed], function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
if (completed === arr.length) {
|
||||
callback();
|
||||
}
|
||||
else {
|
||||
iterate();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
iterate();
|
||||
};
|
||||
|
||||
async.forEachLimit = function (arr, limit, iterator, callback) {
|
||||
if (!arr.length || limit <= 0) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
var started = 0;
|
||||
var running = 0;
|
||||
|
||||
(function replenish () {
|
||||
if (completed === arr.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
while (running < limit && started < arr.length) {
|
||||
iterator(arr[started], function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
running -= 1;
|
||||
if (completed === arr.length) {
|
||||
callback();
|
||||
}
|
||||
else {
|
||||
replenish();
|
||||
}
|
||||
}
|
||||
});
|
||||
started += 1;
|
||||
running += 1;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
|
||||
var doParallel = function (fn) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return fn.apply(null, [async.forEach].concat(args));
|
||||
};
|
||||
};
|
||||
var doSeries = function (fn) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return fn.apply(null, [async.forEachSeries].concat(args));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
var _asyncMap = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (err, v) {
|
||||
results[x.index] = v;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
};
|
||||
async.map = doParallel(_asyncMap);
|
||||
async.mapSeries = doSeries(_asyncMap);
|
||||
|
||||
|
||||
// reduce only has a series version, as doing reduce in parallel won't
|
||||
// work in many situations.
|
||||
async.reduce = function (arr, memo, iterator, callback) {
|
||||
async.forEachSeries(arr, function (x, callback) {
|
||||
iterator(memo, x, function (err, v) {
|
||||
memo = v;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, memo);
|
||||
});
|
||||
};
|
||||
// inject alias
|
||||
async.inject = async.reduce;
|
||||
// foldl alias
|
||||
async.foldl = async.reduce;
|
||||
|
||||
async.reduceRight = function (arr, memo, iterator, callback) {
|
||||
var reversed = _map(arr, function (x) {
|
||||
return x;
|
||||
}).reverse();
|
||||
async.reduce(reversed, memo, iterator, callback);
|
||||
};
|
||||
// foldr alias
|
||||
async.foldr = async.reduceRight;
|
||||
|
||||
var _filter = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (v) {
|
||||
if (v) {
|
||||
results.push(x);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
callback(_map(results.sort(function (a, b) {
|
||||
return a.index - b.index;
|
||||
}), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
});
|
||||
};
|
||||
async.filter = doParallel(_filter);
|
||||
async.filterSeries = doSeries(_filter);
|
||||
// select alias
|
||||
async.select = async.filter;
|
||||
async.selectSeries = async.filterSeries;
|
||||
|
||||
var _reject = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (v) {
|
||||
if (!v) {
|
||||
results.push(x);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
callback(_map(results.sort(function (a, b) {
|
||||
return a.index - b.index;
|
||||
}), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
});
|
||||
};
|
||||
async.reject = doParallel(_reject);
|
||||
async.rejectSeries = doSeries(_reject);
|
||||
|
||||
var _detect = function (eachfn, arr, iterator, main_callback) {
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x, function (result) {
|
||||
if (result) {
|
||||
main_callback(x);
|
||||
main_callback = function () {};
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback();
|
||||
});
|
||||
};
|
||||
async.detect = doParallel(_detect);
|
||||
async.detectSeries = doSeries(_detect);
|
||||
|
||||
async.some = function (arr, iterator, main_callback) {
|
||||
async.forEach(arr, function (x, callback) {
|
||||
iterator(x, function (v) {
|
||||
if (v) {
|
||||
main_callback(true);
|
||||
main_callback = function () {};
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback(false);
|
||||
});
|
||||
};
|
||||
// any alias
|
||||
async.any = async.some;
|
||||
|
||||
async.every = function (arr, iterator, main_callback) {
|
||||
async.forEach(arr, function (x, callback) {
|
||||
iterator(x, function (v) {
|
||||
if (!v) {
|
||||
main_callback(false);
|
||||
main_callback = function () {};
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback(true);
|
||||
});
|
||||
};
|
||||
// all alias
|
||||
async.all = async.every;
|
||||
|
||||
async.sortBy = function (arr, iterator, callback) {
|
||||
async.map(arr, function (x, callback) {
|
||||
iterator(x, function (err, criteria) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
}
|
||||
else {
|
||||
callback(null, {value: x, criteria: criteria});
|
||||
}
|
||||
});
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
else {
|
||||
var fn = function (left, right) {
|
||||
var a = left.criteria, b = right.criteria;
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
};
|
||||
callback(null, _map(results.sort(fn), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.auto = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
var keys = _keys(tasks);
|
||||
if (!keys.length) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var results = {};
|
||||
|
||||
var listeners = [];
|
||||
var addListener = function (fn) {
|
||||
listeners.unshift(fn);
|
||||
};
|
||||
var removeListener = function (fn) {
|
||||
for (var i = 0; i < listeners.length; i += 1) {
|
||||
if (listeners[i] === fn) {
|
||||
listeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
var taskComplete = function () {
|
||||
_forEach(listeners, function (fn) {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
addListener(function () {
|
||||
if (_keys(results).length === keys.length) {
|
||||
callback(null, results);
|
||||
}
|
||||
});
|
||||
|
||||
_forEach(keys, function (k) {
|
||||
var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k];
|
||||
var taskCallback = function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
// stop subsequent errors hitting callback multiple times
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
taskComplete();
|
||||
}
|
||||
};
|
||||
var requires = task.slice(0, Math.abs(task.length - 1)) || [];
|
||||
var ready = function () {
|
||||
return _reduce(requires, function (a, x) {
|
||||
return (a && results.hasOwnProperty(x));
|
||||
}, true);
|
||||
};
|
||||
if (ready()) {
|
||||
task[task.length - 1](taskCallback, results);
|
||||
}
|
||||
else {
|
||||
var listener = function () {
|
||||
if (ready()) {
|
||||
removeListener(listener);
|
||||
task[task.length - 1](taskCallback, results);
|
||||
}
|
||||
};
|
||||
addListener(listener);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.waterfall = function (tasks, callback) {
|
||||
if (!tasks.length) {
|
||||
return callback();
|
||||
}
|
||||
callback = callback || function () {};
|
||||
var wrapIterator = function (iterator) {
|
||||
return function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
var next = iterator.next();
|
||||
if (next) {
|
||||
args.push(wrapIterator(next));
|
||||
}
|
||||
else {
|
||||
args.push(callback);
|
||||
}
|
||||
async.nextTick(function () {
|
||||
iterator.apply(null, args);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
wrapIterator(async.iterator(tasks))();
|
||||
};
|
||||
|
||||
async.parallel = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
if (tasks.constructor === Array) {
|
||||
async.map(tasks, function (fn, callback) {
|
||||
if (fn) {
|
||||
fn(function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
callback.call(null, err, args);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
else {
|
||||
var results = {};
|
||||
async.forEach(_keys(tasks), function (k, callback) {
|
||||
tasks[k](function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async.series = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
if (tasks.constructor === Array) {
|
||||
async.mapSeries(tasks, function (fn, callback) {
|
||||
if (fn) {
|
||||
fn(function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
callback.call(null, err, args);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
else {
|
||||
var results = {};
|
||||
async.forEachSeries(_keys(tasks), function (k, callback) {
|
||||
tasks[k](function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async.iterator = function (tasks) {
|
||||
var makeCallback = function (index) {
|
||||
var fn = function () {
|
||||
if (tasks.length) {
|
||||
tasks[index].apply(null, arguments);
|
||||
}
|
||||
return fn.next();
|
||||
};
|
||||
fn.next = function () {
|
||||
return (index < tasks.length - 1) ? makeCallback(index + 1): null;
|
||||
};
|
||||
return fn;
|
||||
};
|
||||
return makeCallback(0);
|
||||
};
|
||||
|
||||
async.apply = function (fn) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
return function () {
|
||||
return fn.apply(
|
||||
null, args.concat(Array.prototype.slice.call(arguments))
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
var _concat = function (eachfn, arr, fn, callback) {
|
||||
var r = [];
|
||||
eachfn(arr, function (x, cb) {
|
||||
fn(x, function (err, y) {
|
||||
r = r.concat(y || []);
|
||||
cb(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, r);
|
||||
});
|
||||
};
|
||||
async.concat = doParallel(_concat);
|
||||
async.concatSeries = doSeries(_concat);
|
||||
|
||||
async.whilst = function (test, iterator, callback) {
|
||||
if (test()) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.whilst(test, iterator, callback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
async.until = function (test, iterator, callback) {
|
||||
if (!test()) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.until(test, iterator, callback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
async.queue = function (worker, concurrency) {
|
||||
var workers = 0;
|
||||
var q = {
|
||||
tasks: [],
|
||||
concurrency: concurrency,
|
||||
saturated: null,
|
||||
empty: null,
|
||||
drain: null,
|
||||
push: function (data, callback) {
|
||||
q.tasks.push({data: data, callback: callback});
|
||||
if(q.saturated && q.tasks.length == concurrency) q.saturated();
|
||||
async.nextTick(q.process);
|
||||
},
|
||||
process: function () {
|
||||
if (workers < q.concurrency && q.tasks.length) {
|
||||
var task = q.tasks.shift();
|
||||
if(q.empty && q.tasks.length == 0) q.empty();
|
||||
workers += 1;
|
||||
worker(task.data, function () {
|
||||
workers -= 1;
|
||||
if (task.callback) {
|
||||
task.callback.apply(task, arguments);
|
||||
}
|
||||
if(q.drain && q.tasks.length + workers == 0) q.drain();
|
||||
q.process();
|
||||
});
|
||||
}
|
||||
},
|
||||
length: function () {
|
||||
return q.tasks.length;
|
||||
},
|
||||
running: function () {
|
||||
return workers;
|
||||
}
|
||||
};
|
||||
return q;
|
||||
};
|
||||
|
||||
var _console_fn = function (name) {
|
||||
return function (fn) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
fn.apply(null, args.concat([function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (typeof console !== 'undefined') {
|
||||
if (err) {
|
||||
if (console.error) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
else if (console[name]) {
|
||||
_forEach(args, function (x) {
|
||||
console[name](x);
|
||||
});
|
||||
}
|
||||
}
|
||||
}]));
|
||||
};
|
||||
};
|
||||
async.log = _console_fn('log');
|
||||
async.dir = _console_fn('dir');
|
||||
/*async.info = _console_fn('info');
|
||||
async.warn = _console_fn('warn');
|
||||
async.error = _console_fn('error');*/
|
||||
|
||||
async.memoize = function (fn, hasher) {
|
||||
var memo = {};
|
||||
var queues = {};
|
||||
hasher = hasher || function (x) {
|
||||
return x;
|
||||
};
|
||||
var memoized = function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args.pop();
|
||||
var key = hasher.apply(null, args);
|
||||
if (key in memo) {
|
||||
callback.apply(null, memo[key]);
|
||||
}
|
||||
else if (key in queues) {
|
||||
queues[key].push(callback);
|
||||
}
|
||||
else {
|
||||
queues[key] = [callback];
|
||||
fn.apply(null, args.concat([function () {
|
||||
memo[key] = arguments;
|
||||
var q = queues[key];
|
||||
delete queues[key];
|
||||
for (var i = 0, l = q.length; i < l; i++) {
|
||||
q[i].apply(null, arguments);
|
||||
}
|
||||
}]));
|
||||
}
|
||||
};
|
||||
memoized.unmemoized = fn;
|
||||
return memoized;
|
||||
};
|
||||
|
||||
async.unmemoize = function (fn) {
|
||||
return function () {
|
||||
return (fn.unmemoized || fn).apply(null, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
134
src/static/js/pluginfw/hooks.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
||||
var _;
|
||||
|
||||
/* FIXME: Ugly hack, in the future, use same code for server & client */
|
||||
if (plugins.isClient) {
|
||||
var async = require("ep_etherpad-lite/static/js/pluginfw/async");
|
||||
var _ = require("ep_etherpad-lite/static/js/underscore");
|
||||
} else {
|
||||
var async = require("async");
|
||||
var _ = require("underscore");
|
||||
}
|
||||
|
||||
exports.bubbleExceptions = true
|
||||
|
||||
var hookCallWrapper = function (hook, hook_name, args, cb) {
|
||||
if (cb === undefined) cb = function (x) { return x; };
|
||||
|
||||
// Normalize output to list for both sync and async cases
|
||||
var normalize = function(x) {
|
||||
if (x == undefined) return [];
|
||||
return x;
|
||||
}
|
||||
var normalizedhook = function () {
|
||||
return normalize(hook.hook_fn(hook_name, args, function (x) {
|
||||
return cb(normalize(x));
|
||||
}));
|
||||
}
|
||||
|
||||
if (exports.bubbleExceptions) {
|
||||
return normalizedhook();
|
||||
} else {
|
||||
try {
|
||||
return normalizedhook();
|
||||
} catch (ex) {
|
||||
console.error([hook_name, hook.part.full_name, ex.stack || ex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.syncMapFirst = function (lst, fn) {
|
||||
var i;
|
||||
var result;
|
||||
for (i = 0; i < lst.length; i++) {
|
||||
result = fn(lst[i])
|
||||
if (result.length) return result;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
exports.mapFirst = function (lst, fn, cb) {
|
||||
var i = 0;
|
||||
|
||||
next = function () {
|
||||
if (i >= lst.length) return cb(undefined);
|
||||
fn(lst[i++], function (err, result) {
|
||||
if (err) return cb(err);
|
||||
if (result.length) return cb(null, result);
|
||||
next();
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/* Don't use Array.concat as it flatterns arrays within the array */
|
||||
exports.flatten = function (lst) {
|
||||
var res = [];
|
||||
if (lst != undefined && lst != null) {
|
||||
for (var i = 0; i < lst.length; i++) {
|
||||
if (lst[i] != undefined && lst[i] != null) {
|
||||
for (var j = 0; j < lst[i].length; j++) {
|
||||
res.push(lst[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
exports.callAll = function (hook_name, args) {
|
||||
if (!args) args = {};
|
||||
if (plugins.hooks[hook_name] === undefined) return [];
|
||||
return _.flatten(_.map(plugins.hooks[hook_name], function (hook) {
|
||||
return hookCallWrapper(hook, hook_name, args);
|
||||
}), true);
|
||||
}
|
||||
|
||||
exports.aCallAll = function (hook_name, args, cb) {
|
||||
if (!args) args = {};
|
||||
if (!cb) cb = function () {};
|
||||
if (plugins.hooks[hook_name] === undefined) return cb(null, []);
|
||||
async.map(
|
||||
plugins.hooks[hook_name],
|
||||
function (hook, cb) {
|
||||
hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); });
|
||||
},
|
||||
function (err, res) {
|
||||
cb(null, _.flatten(res, true));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
exports.callFirst = function (hook_name, args) {
|
||||
if (!args) args = {};
|
||||
if (plugins.hooks[hook_name] === undefined) return [];
|
||||
return exports.syncMapFirst(plugins.hooks[hook_name], function (hook) {
|
||||
return hookCallWrapper(hook, hook_name, args);
|
||||
});
|
||||
}
|
||||
|
||||
exports.aCallFirst = function (hook_name, args, cb) {
|
||||
if (!args) args = {};
|
||||
if (!cb) cb = function () {};
|
||||
if (plugins.hooks[hook_name] === undefined) return cb(null, []);
|
||||
exports.mapFirst(
|
||||
plugins.hooks[hook_name],
|
||||
function (hook, cb) {
|
||||
hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); });
|
||||
},
|
||||
cb
|
||||
);
|
||||
}
|
||||
|
||||
exports.callAllStr = function(hook_name, args, sep, pre, post) {
|
||||
if (sep == undefined) sep = '';
|
||||
if (pre == undefined) pre = '';
|
||||
if (post == undefined) post = '';
|
||||
var newCallhooks = [];
|
||||
var callhooks = exports.callAll(hook_name, args);
|
||||
for (var i = 0, ii = callhooks.length; i < ii; i++) {
|
||||
newCallhooks[i] = pre + callhooks[i] + post;
|
||||
}
|
||||
return newCallhooks.join(sep || "");
|
||||
}
|
106
src/static/js/pluginfw/installer.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
||||
var npm = require("npm");
|
||||
var registry = require("npm/lib/utils/npm-registry-client/index.js");
|
||||
|
||||
var withNpm = function (npmfn, final, cb) {
|
||||
npm.load({}, function (er) {
|
||||
if (er) return cb({progress:1, error:er});
|
||||
npm.on("log", function (message) {
|
||||
cb({progress: 0.5, message:message.msg + ": " + message.pref});
|
||||
});
|
||||
npmfn(function (er, data) {
|
||||
if (er) return cb({progress:1, error:er.code + ": " + er.path});
|
||||
if (!data) data = {};
|
||||
data.progress = 1;
|
||||
data.message = "Done.";
|
||||
cb(data);
|
||||
final();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// All these functions call their callback multiple times with
|
||||
// {progress:[0,1], message:STRING, error:object}. They will call it
|
||||
// with progress = 1 at least once, and at all times will either
|
||||
// message or error be present, not both. It can be called multiple
|
||||
// times for all values of propgress except for 1.
|
||||
|
||||
exports.uninstall = function(plugin_name, cb) {
|
||||
withNpm(
|
||||
function (cb) {
|
||||
npm.commands.uninstall([plugin_name], function (er) {
|
||||
if (er) return cb(er);
|
||||
hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) {
|
||||
if (er) return cb(er);
|
||||
plugins.update(cb);
|
||||
});
|
||||
});
|
||||
},
|
||||
function () {
|
||||
hooks.aCallAll("restartServer", {}, function () {});
|
||||
},
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
exports.install = function(plugin_name, cb) {
|
||||
withNpm(
|
||||
function (cb) {
|
||||
npm.commands.install([plugin_name], function (er) {
|
||||
if (er) return cb(er);
|
||||
hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) {
|
||||
if (er) return cb(er);
|
||||
plugins.update(cb);
|
||||
});
|
||||
});
|
||||
},
|
||||
function () {
|
||||
hooks.aCallAll("restartServer", {}, function () {});
|
||||
},
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
exports.searchCache = null;
|
||||
|
||||
exports.search = function(query, cache, cb) {
|
||||
withNpm(
|
||||
function (cb) {
|
||||
var getData = function (cb) {
|
||||
if (cache && exports.searchCache) {
|
||||
cb(null, exports.searchCache);
|
||||
} else {
|
||||
registry.get(
|
||||
"/-/all", null, 600, false, true,
|
||||
function (er, data) {
|
||||
if (er) return cb(er);
|
||||
exports.searchCache = data;
|
||||
cb(er, data);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
getData(
|
||||
function (er, data) {
|
||||
if (er) return cb(er);
|
||||
var res = {};
|
||||
var i = 0;
|
||||
for (key in data) {
|
||||
if ( key.indexOf(plugins.prefix) == 0
|
||||
&& key.indexOf(query.pattern) != -1) {
|
||||
i++;
|
||||
if (i > query.offset
|
||||
&& i <= query.offset + query.limit) {
|
||||
res[key] = data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
cb(null, {results:res, query: query, total:i});
|
||||
}
|
||||
);
|
||||
},
|
||||
function () { },
|
||||
cb
|
||||
);
|
||||
};
|
39
src/static/js/pluginfw/parent_require.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* This module allows passing require modules instances to
|
||||
* embedded iframes in a page.
|
||||
* For example, if a page has the "plugins" module initialized,
|
||||
* it is important to use exactly the same "plugins" instance
|
||||
* inside iframes as well. Otherwise, plugins cannot save any
|
||||
* state.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Instructs the require object that when a reqModuleName module
|
||||
* needs to be loaded, that it iterates through the parents of the
|
||||
* current window until it finds one who can execute "require"
|
||||
* statements and asks it to perform require on reqModuleName.
|
||||
*
|
||||
* @params requireDefObj Require object which supports define
|
||||
* statements. This object is accessible after loading require-kernel.
|
||||
* @params reqModuleName Module name e.g. (ep_etherpad-lite/static/js/plugins)
|
||||
*/
|
||||
exports.getRequirementFromParent = function(requireDefObj, reqModuleName) {
|
||||
// Force the 'undefinition' of the modules (if they already have been loaded).
|
||||
delete (requireDefObj._definitions)[reqModuleName];
|
||||
delete (requireDefObj._modules)[reqModuleName];
|
||||
requireDefObj.define(reqModuleName, function(require, exports, module) {
|
||||
var t = parent;
|
||||
var max = 0; // make sure I don't go up more than 10 times
|
||||
while (typeof(t) != "undefined") {
|
||||
max++;
|
||||
if (max==10)
|
||||
break;
|
||||
if (typeof(t.require) != "undefined") {
|
||||
module.exports = t.require(reqModuleName);
|
||||
return;
|
||||
}
|
||||
t = t.parent;
|
||||
}
|
||||
});
|
||||
}
|
259
src/static/js/pluginfw/plugins.js
Normal file
|
@ -0,0 +1,259 @@
|
|||
exports.isClient = typeof global != "object";
|
||||
|
||||
var _;
|
||||
|
||||
if (!exports.isClient) {
|
||||
var npm = require("npm/lib/npm.js");
|
||||
var readInstalled = require("./read-installed.js");
|
||||
var relativize = require("npm/lib/utils/relativize.js");
|
||||
var readJson = require("npm/lib/utils/read-json.js");
|
||||
var path = require("path");
|
||||
var async = require("async");
|
||||
var fs = require("fs");
|
||||
var tsort = require("./tsort");
|
||||
var util = require("util");
|
||||
_ = require("underscore");
|
||||
}else{
|
||||
var $, jQuery;
|
||||
$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$;
|
||||
_ = require("ep_etherpad-lite/static/js/underscore");
|
||||
}
|
||||
|
||||
exports.prefix = 'ep_';
|
||||
exports.loaded = false;
|
||||
exports.plugins = {};
|
||||
exports.parts = [];
|
||||
exports.hooks = {};
|
||||
exports.baseURL = '';
|
||||
|
||||
exports.ensure = function (cb) {
|
||||
if (!exports.loaded)
|
||||
exports.update(cb);
|
||||
else
|
||||
cb();
|
||||
};
|
||||
|
||||
exports.formatPlugins = function () {
|
||||
return _.keys(exports.plugins).join(", ");
|
||||
};
|
||||
|
||||
exports.formatParts = function () {
|
||||
return _.map(exports.parts, function (part) { return part.full_name; }).join("\n");
|
||||
};
|
||||
|
||||
exports.formatHooks = function (hook_set_name) {
|
||||
var res = [];
|
||||
var hooks = exports.extractHooks(exports.parts, hook_set_name || "hooks");
|
||||
|
||||
_.chain(hooks).keys().forEach(function (hook_name) {
|
||||
_.forEach(hooks[hook_name], function (hook) {
|
||||
res.push("<dt>" + hook.hook_name + "</dt><dd>" + hook.hook_fn_name + " from " + hook.part.full_name + "</dd>");
|
||||
});
|
||||
});
|
||||
return "<dl>" + res.join("\n") + "</dl>";
|
||||
};
|
||||
|
||||
exports.loadFn = function (path, hookName) {
|
||||
var functionName
|
||||
, parts = path.split(":");
|
||||
|
||||
// on windows: C:\foo\bar:xyz
|
||||
if(parts[0].length == 1) {
|
||||
if(parts.length == 3)
|
||||
functionName = parts.pop();
|
||||
path = parts.join(":");
|
||||
}else{
|
||||
path = parts[0];
|
||||
functionName = parts[1];
|
||||
}
|
||||
|
||||
var fn = require(path);
|
||||
functionName = functionName ? functionName : hookName;
|
||||
|
||||
_.each(functionName.split("."), function (name) {
|
||||
fn = fn[name];
|
||||
});
|
||||
return fn;
|
||||
};
|
||||
|
||||
exports.extractHooks = function (parts, hook_set_name) {
|
||||
var hooks = {};
|
||||
_.each(parts,function (part) {
|
||||
_.chain(part[hook_set_name] || {})
|
||||
.keys()
|
||||
.each(function (hook_name) {
|
||||
if (hooks[hook_name] === undefined) hooks[hook_name] = [];
|
||||
|
||||
var hook_fn_name = part[hook_set_name][hook_name];
|
||||
|
||||
/* On the server side, you can't just
|
||||
* require("pluginname/whatever") if the plugin is installed as
|
||||
* a dependency of another plugin! Bah, pesky little details of
|
||||
* npm... */
|
||||
if (!exports.isClient) {
|
||||
hook_fn_name = path.normalize(path.join(path.dirname(exports.plugins[part.plugin].package.path), hook_fn_name));
|
||||
}
|
||||
|
||||
try {
|
||||
var hook_fn = exports.loadFn(hook_fn_name, hook_name);
|
||||
if (!hook_fn) {
|
||||
throw "Not a function";
|
||||
}
|
||||
} catch (exc) {
|
||||
console.error("Failed to load '" + hook_fn_name + "' for '" + part.full_name + "/" + hook_set_name + "/" + hook_name + "': " + exc.toString())
|
||||
}
|
||||
if (hook_fn) {
|
||||
hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part});
|
||||
}
|
||||
});
|
||||
});
|
||||
return hooks;
|
||||
};
|
||||
|
||||
|
||||
if (exports.isClient) {
|
||||
exports.update = function (cb) {
|
||||
// It appears that this response (see #620) may interrupt the current thread
|
||||
// of execution on Firefox. This schedules the response in the run-loop,
|
||||
// which appears to fix the issue.
|
||||
var callback = function () {setTimeout(cb, 0);};
|
||||
|
||||
jQuery.getJSON(exports.baseURL + 'pluginfw/plugin-definitions.json', function(data) {
|
||||
exports.plugins = data.plugins;
|
||||
exports.parts = data.parts;
|
||||
exports.hooks = exports.extractHooks(exports.parts, "client_hooks");
|
||||
exports.loaded = true;
|
||||
callback();
|
||||
}).error(function(xhr, s, err){
|
||||
console.error("Failed to load plugin-definitions: " + err);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
} else {
|
||||
|
||||
exports.callInit = function (cb) {
|
||||
var hooks = require("./hooks");
|
||||
async.map(
|
||||
Object.keys(exports.plugins),
|
||||
function (plugin_name, cb) {
|
||||
var plugin = exports.plugins[plugin_name];
|
||||
fs.stat(path.normalize(path.join(plugin.package.path, ".ep_initialized")), function (err, stats) {
|
||||
if (err) {
|
||||
async.waterfall([
|
||||
function (cb) { fs.writeFile(path.normalize(path.join(plugin.package.path, ".ep_initialized")), 'done', cb); },
|
||||
function (cb) { hooks.aCallAll("init_" + plugin_name, {}, cb); },
|
||||
cb,
|
||||
]);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
},
|
||||
function () { cb(); }
|
||||
);
|
||||
}
|
||||
|
||||
exports.update = function (cb) {
|
||||
exports.getPackages(function (er, packages) {
|
||||
var parts = [];
|
||||
var plugins = {};
|
||||
// Load plugin metadata ep.json
|
||||
async.forEach(
|
||||
Object.keys(packages),
|
||||
function (plugin_name, cb) {
|
||||
exports.loadPlugin(packages, plugin_name, plugins, parts, cb);
|
||||
},
|
||||
function (err) {
|
||||
if (err) cb(err);
|
||||
exports.plugins = plugins;
|
||||
exports.parts = exports.sortParts(parts);
|
||||
exports.hooks = exports.extractHooks(exports.parts, "hooks");
|
||||
exports.loaded = true;
|
||||
exports.callInit(cb);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
exports.getPackages = function (cb) {
|
||||
// Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that
|
||||
var dir = path.resolve(npm.dir, '..');
|
||||
readInstalled(dir, function (er, data) {
|
||||
if (er) cb(er, null);
|
||||
var packages = {};
|
||||
function flatten(deps) {
|
||||
_.chain(deps).keys().each(function (name) {
|
||||
if (name.indexOf(exports.prefix) === 0) {
|
||||
packages[name] = _.clone(deps[name]);
|
||||
// Delete anything that creates loops so that the plugin
|
||||
// list can be sent as JSON to the web client
|
||||
delete packages[name].dependencies;
|
||||
delete packages[name].parent;
|
||||
}
|
||||
|
||||
if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies);
|
||||
});
|
||||
}
|
||||
|
||||
var tmp = {};
|
||||
tmp[data.name] = data;
|
||||
flatten(tmp);
|
||||
cb(null, packages);
|
||||
});
|
||||
};
|
||||
|
||||
exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) {
|
||||
var plugin_path = path.resolve(packages[plugin_name].path, "ep.json");
|
||||
fs.readFile(
|
||||
plugin_path,
|
||||
function (er, data) {
|
||||
if (er) {
|
||||
console.error("Unable to load plugin definition file " + plugin_path);
|
||||
return cb();
|
||||
}
|
||||
try {
|
||||
var plugin = JSON.parse(data);
|
||||
plugin['package'] = packages[plugin_name];
|
||||
plugins[plugin_name] = plugin;
|
||||
_.each(plugin.parts, function (part) {
|
||||
part.plugin = plugin_name;
|
||||
part.full_name = plugin_name + "/" + part.name;
|
||||
parts[part.full_name] = part;
|
||||
});
|
||||
} catch (ex) {
|
||||
console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString());
|
||||
}
|
||||
cb();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
exports.partsToParentChildList = function (parts) {
|
||||
var res = [];
|
||||
_.chain(parts).keys().forEach(function (name) {
|
||||
_.each(parts[name].post || [], function (child_name) {
|
||||
res.push([name, child_name]);
|
||||
});
|
||||
_.each(parts[name].pre || [], function (parent_name) {
|
||||
res.push([parent_name, name]);
|
||||
});
|
||||
if (!parts[name].pre && !parts[name].post) {
|
||||
res.push([name, ":" + name]); // Include apps with no dependency info
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
|
||||
// Used only in Node, so no need for _
|
||||
exports.sortParts = function(parts) {
|
||||
return tsort(
|
||||
exports.partsToParentChildList(parts)
|
||||
).filter(
|
||||
function (name) { return parts[name] !== undefined; }
|
||||
).map(
|
||||
function (name) { return parts[name]; }
|
||||
);
|
||||
};
|
||||
|
||||
}
|
324
src/static/js/pluginfw/read-installed.js
Normal file
|
@ -0,0 +1,324 @@
|
|||
// A copy of npm/lib/utils/read-installed.js
|
||||
// that is hacked to not cache everything :)
|
||||
|
||||
// Walk through the file-system "database" of installed
|
||||
// packages, and create a data object related to the
|
||||
// installed versions of each package.
|
||||
|
||||
/*
|
||||
This will traverse through all node_modules folders,
|
||||
resolving the dependencies object to the object corresponding to
|
||||
the package that meets that dep, or just the version/range if
|
||||
unmet.
|
||||
|
||||
Assuming that you had this folder structure:
|
||||
|
||||
/path/to
|
||||
+-- package.json { name = "root" }
|
||||
`-- node_modules
|
||||
+-- foo {bar, baz, asdf}
|
||||
| +-- node_modules
|
||||
| +-- bar { baz }
|
||||
| `-- baz
|
||||
`-- asdf
|
||||
|
||||
where "foo" depends on bar, baz, and asdf, bar depends on baz,
|
||||
and bar and baz are bundled with foo, whereas "asdf" is at
|
||||
the higher level (sibling to foo), you'd get this object structure:
|
||||
|
||||
{ <package.json data>
|
||||
, path: "/path/to"
|
||||
, parent: null
|
||||
, dependencies:
|
||||
{ foo :
|
||||
{ version: "1.2.3"
|
||||
, path: "/path/to/node_modules/foo"
|
||||
, parent: <Circular: root>
|
||||
, dependencies:
|
||||
{ bar:
|
||||
{ parent: <Circular: foo>
|
||||
, path: "/path/to/node_modules/foo/node_modules/bar"
|
||||
, version: "2.3.4"
|
||||
, dependencies: { baz: <Circular: foo.dependencies.baz> }
|
||||
}
|
||||
, baz: { ... }
|
||||
, asdf: <Circular: asdf>
|
||||
}
|
||||
}
|
||||
, asdf: { ... }
|
||||
}
|
||||
}
|
||||
|
||||
Unmet deps are left as strings.
|
||||
Extraneous deps are marked with extraneous:true
|
||||
deps that don't meet a requirement are marked with invalid:true
|
||||
|
||||
to READ(packagefolder, parentobj, name, reqver)
|
||||
obj = read package.json
|
||||
installed = ./node_modules/*
|
||||
if parentobj is null, and no package.json
|
||||
obj = {dependencies:{<installed>:"*"}}
|
||||
deps = Object.keys(obj.dependencies)
|
||||
obj.path = packagefolder
|
||||
obj.parent = parentobj
|
||||
if name, && obj.name !== name, obj.invalid = true
|
||||
if reqver, && obj.version !satisfies reqver, obj.invalid = true
|
||||
if !reqver && parentobj, obj.extraneous = true
|
||||
for each folder in installed
|
||||
obj.dependencies[folder] = READ(packagefolder+node_modules+folder,
|
||||
obj, folder, obj.dependencies[folder])
|
||||
# walk tree to find unmet deps
|
||||
for each dep in obj.dependencies not in installed
|
||||
r = obj.parent
|
||||
while r
|
||||
if r.dependencies[dep]
|
||||
if r.dependencies[dep].verion !satisfies obj.dependencies[dep]
|
||||
WARN
|
||||
r.dependencies[dep].invalid = true
|
||||
obj.dependencies[dep] = r.dependencies[dep]
|
||||
r = null
|
||||
else r = r.parent
|
||||
return obj
|
||||
|
||||
|
||||
TODO:
|
||||
1. Find unmet deps in parent directories, searching as node does up
|
||||
as far as the left-most node_modules folder.
|
||||
2. Ignore anything in node_modules that isn't a package folder.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
var npm = require("npm/lib/npm.js")
|
||||
, fs = require("graceful-fs")
|
||||
, path = require("path")
|
||||
, asyncMap = require("slide").asyncMap
|
||||
, semver = require("semver")
|
||||
, readJson = require("npm/lib/utils/read-json.js")
|
||||
, log = require("npm/lib/utils/log.js")
|
||||
|
||||
module.exports = readInstalled
|
||||
|
||||
function readInstalled (folder, cb) {
|
||||
/* This is where we clear the cache, these three lines are all the
|
||||
* new code there is */
|
||||
rpSeen = {};
|
||||
riSeen = [];
|
||||
var fuSeen = [];
|
||||
|
||||
var d = npm.config.get("depth")
|
||||
readInstalled_(folder, null, null, null, 0, d, function (er, obj) {
|
||||
if (er) return cb(er)
|
||||
// now obj has all the installed things, where they're installed
|
||||
// figure out the inheritance links, now that the object is built.
|
||||
resolveInheritance(obj)
|
||||
cb(null, obj)
|
||||
})
|
||||
}
|
||||
|
||||
var rpSeen = {}
|
||||
function readInstalled_ (folder, parent, name, reqver, depth, maxDepth, cb) {
|
||||
//console.error(folder, name)
|
||||
|
||||
var installed
|
||||
, obj
|
||||
, real
|
||||
, link
|
||||
|
||||
fs.readdir(path.resolve(folder, "node_modules"), function (er, i) {
|
||||
// error indicates that nothing is installed here
|
||||
if (er) i = []
|
||||
installed = i.filter(function (f) { return f.charAt(0) !== "." })
|
||||
next()
|
||||
})
|
||||
|
||||
readJson(path.resolve(folder, "package.json"), function (er, data) {
|
||||
obj = copy(data)
|
||||
|
||||
if (!parent) {
|
||||
obj = obj || true
|
||||
er = null
|
||||
}
|
||||
return next(er)
|
||||
})
|
||||
|
||||
fs.lstat(folder, function (er, st) {
|
||||
if (er) {
|
||||
if (!parent) real = true
|
||||
return next(er)
|
||||
}
|
||||
fs.realpath(folder, function (er, rp) {
|
||||
//console.error("realpath(%j) = %j", folder, rp)
|
||||
real = rp
|
||||
if (st.isSymbolicLink()) link = rp
|
||||
next(er)
|
||||
})
|
||||
})
|
||||
|
||||
var errState = null
|
||||
, called = false
|
||||
function next (er) {
|
||||
if (errState) return
|
||||
if (er) {
|
||||
errState = er
|
||||
return cb(null, [])
|
||||
}
|
||||
//console.error('next', installed, obj && typeof obj, name, real)
|
||||
if (!installed || !obj || !real || called) return
|
||||
called = true
|
||||
if (rpSeen[real]) return cb(null, rpSeen[real])
|
||||
if (obj === true) {
|
||||
obj = {dependencies:{}, path:folder}
|
||||
installed.forEach(function (i) { obj.dependencies[i] = "*" })
|
||||
}
|
||||
if (name && obj.name !== name) obj.invalid = true
|
||||
obj.realName = name || obj.name
|
||||
obj.dependencies = obj.dependencies || {}
|
||||
|
||||
// "foo":"http://blah" is always presumed valid
|
||||
if (reqver
|
||||
&& semver.validRange(reqver)
|
||||
&& !semver.satisfies(obj.version, reqver)) {
|
||||
obj.invalid = true
|
||||
}
|
||||
|
||||
if (parent
|
||||
&& !(name in parent.dependencies)
|
||||
&& !(name in (parent.devDependencies || {}))) {
|
||||
obj.extraneous = true
|
||||
}
|
||||
obj.path = obj.path || folder
|
||||
obj.realPath = real
|
||||
obj.link = link
|
||||
if (parent && !obj.link) obj.parent = parent
|
||||
rpSeen[real] = obj
|
||||
obj.depth = depth
|
||||
if (depth >= maxDepth) return cb(null, obj)
|
||||
asyncMap(installed, function (pkg, cb) {
|
||||
var rv = obj.dependencies[pkg]
|
||||
if (!rv && obj.devDependencies) rv = obj.devDependencies[pkg]
|
||||
readInstalled_( path.resolve(folder, "node_modules/"+pkg)
|
||||
, obj, pkg, obj.dependencies[pkg], depth + 1, maxDepth
|
||||
, cb )
|
||||
}, function (er, installedData) {
|
||||
if (er) return cb(er)
|
||||
installedData.forEach(function (dep) {
|
||||
obj.dependencies[dep.realName] = dep
|
||||
})
|
||||
|
||||
// any strings here are unmet things. however, if it's
|
||||
// optional, then that's fine, so just delete it.
|
||||
if (obj.optionalDependencies) {
|
||||
Object.keys(obj.optionalDependencies).forEach(function (dep) {
|
||||
if (typeof obj.dependencies[dep] === "string") {
|
||||
delete obj.dependencies[dep]
|
||||
}
|
||||
})
|
||||
}
|
||||
return cb(null, obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// starting from a root object, call findUnmet on each layer of children
|
||||
var riSeen = []
|
||||
function resolveInheritance (obj) {
|
||||
if (typeof obj !== "object") return
|
||||
if (riSeen.indexOf(obj) !== -1) return
|
||||
riSeen.push(obj)
|
||||
if (typeof obj.dependencies !== "object") {
|
||||
obj.dependencies = {}
|
||||
}
|
||||
Object.keys(obj.dependencies).forEach(function (dep) {
|
||||
findUnmet(obj.dependencies[dep])
|
||||
})
|
||||
Object.keys(obj.dependencies).forEach(function (dep) {
|
||||
resolveInheritance(obj.dependencies[dep])
|
||||
})
|
||||
}
|
||||
|
||||
// find unmet deps by walking up the tree object.
|
||||
// No I/O
|
||||
var fuSeen = []
|
||||
function findUnmet (obj) {
|
||||
if (fuSeen.indexOf(obj) !== -1) return
|
||||
fuSeen.push(obj)
|
||||
//console.error("find unmet", obj.name, obj.parent && obj.parent.name)
|
||||
var deps = obj.dependencies = obj.dependencies || {}
|
||||
//console.error(deps)
|
||||
Object.keys(deps)
|
||||
.filter(function (d) { return typeof deps[d] === "string" })
|
||||
.forEach(function (d) {
|
||||
//console.error("find unmet", obj.name, d, deps[d])
|
||||
var r = obj.parent
|
||||
, found = null
|
||||
while (r && !found && typeof deps[d] === "string") {
|
||||
// if r is a valid choice, then use that.
|
||||
found = r.dependencies[d]
|
||||
if (!found && r.realName === d) found = r
|
||||
|
||||
if (!found) {
|
||||
r = r.link ? null : r.parent
|
||||
continue
|
||||
}
|
||||
if ( typeof deps[d] === "string"
|
||||
&& !semver.satisfies(found.version, deps[d])) {
|
||||
// the bad thing will happen
|
||||
log.warn(obj.path + " requires "+d+"@'"+deps[d]
|
||||
+"' but will load\n"
|
||||
+found.path+",\nwhich is version "+found.version
|
||||
,"unmet dependency")
|
||||
found.invalid = true
|
||||
}
|
||||
deps[d] = found
|
||||
}
|
||||
|
||||
})
|
||||
log.verbose([obj._id], "returning")
|
||||
return obj
|
||||
}
|
||||
|
||||
function copy (obj) {
|
||||
if (!obj || typeof obj !== 'object') return obj
|
||||
if (Array.isArray(obj)) return obj.map(copy)
|
||||
|
||||
var o = {}
|
||||
for (var i in obj) o[i] = copy(obj[i])
|
||||
return o
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
var util = require("util")
|
||||
console.error("testing")
|
||||
|
||||
var called = 0
|
||||
readInstalled(process.cwd(), function (er, map) {
|
||||
console.error(called ++)
|
||||
if (er) return console.error(er.stack || er.message)
|
||||
cleanup(map)
|
||||
console.error(util.inspect(map, true, 10, true))
|
||||
})
|
||||
|
||||
var seen = []
|
||||
function cleanup (map) {
|
||||
if (seen.indexOf(map) !== -1) return
|
||||
seen.push(map)
|
||||
for (var i in map) switch (i) {
|
||||
case "_id":
|
||||
case "path":
|
||||
case "extraneous": case "invalid":
|
||||
case "dependencies": case "name":
|
||||
continue
|
||||
default: delete map[i]
|
||||
}
|
||||
var dep = map.dependencies
|
||||
// delete map.dependencies
|
||||
if (dep) {
|
||||
// map.dependencies = dep
|
||||
for (var i in dep) if (typeof dep[i] === "object") {
|
||||
cleanup(dep[i])
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
}
|
112
src/static/js/pluginfw/tsort.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* general topological sort
|
||||
* from https://gist.github.com/1232505
|
||||
* @author SHIN Suzuki (shinout310@gmail.com)
|
||||
* @param Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
|
||||
*
|
||||
* @returns Array : topological sorted list of IDs
|
||||
**/
|
||||
|
||||
function tsort(edges) {
|
||||
var nodes = {}, // hash: stringified id of the node => { id: id, afters: lisf of ids }
|
||||
sorted = [], // sorted list of IDs ( returned value )
|
||||
visited = {}; // hash: id of already visited node => true
|
||||
|
||||
var Node = function(id) {
|
||||
this.id = id;
|
||||
this.afters = [];
|
||||
}
|
||||
|
||||
// 1. build data structures
|
||||
edges.forEach(function(v) {
|
||||
var from = v[0], to = v[1];
|
||||
if (!nodes[from]) nodes[from] = new Node(from);
|
||||
if (!nodes[to]) nodes[to] = new Node(to);
|
||||
nodes[from].afters.push(to);
|
||||
});
|
||||
|
||||
// 2. topological sort
|
||||
Object.keys(nodes).forEach(function visit(idstr, ancestors) {
|
||||
var node = nodes[idstr],
|
||||
id = node.id;
|
||||
|
||||
// if already exists, do nothing
|
||||
if (visited[idstr]) return;
|
||||
|
||||
if (!Array.isArray(ancestors)) ancestors = [];
|
||||
|
||||
ancestors.push(id);
|
||||
|
||||
visited[idstr] = true;
|
||||
|
||||
node.afters.forEach(function(afterID) {
|
||||
if (ancestors.indexOf(afterID) >= 0) // if already in ancestors, a closed chain exists.
|
||||
throw new Error('closed chain : ' + afterID + ' is in ' + id);
|
||||
|
||||
visit(afterID.toString(), ancestors.map(function(v) { return v })); // recursive call
|
||||
});
|
||||
|
||||
sorted.unshift(id);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* TEST
|
||||
**/
|
||||
function tsortTest() {
|
||||
|
||||
// example 1: success
|
||||
var edges = [
|
||||
[1, 2],
|
||||
[1, 3],
|
||||
[2, 4],
|
||||
[3, 4]
|
||||
];
|
||||
|
||||
var sorted = tsort(edges);
|
||||
console.log(sorted);
|
||||
|
||||
// example 2: failure ( A > B > C > A )
|
||||
edges = [
|
||||
['A', 'B'],
|
||||
['B', 'C'],
|
||||
['C', 'A']
|
||||
];
|
||||
|
||||
try {
|
||||
sorted = tsort(edges);
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
|
||||
// example 3: generate random edges
|
||||
var max = 100, iteration = 30;
|
||||
function randomInt(max) {
|
||||
return Math.floor(Math.random() * max) + 1;
|
||||
}
|
||||
|
||||
edges = (function() {
|
||||
var ret = [], i = 0;
|
||||
while (i++ < iteration) ret.push( [randomInt(max), randomInt(max)] );
|
||||
return ret;
|
||||
})();
|
||||
|
||||
try {
|
||||
sorted = tsort(edges);
|
||||
console.log("succeeded", sorted);
|
||||
}
|
||||
catch (e) {
|
||||
console.log("failed", e.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// for node.js
|
||||
if (typeof exports == 'object' && exports === this) {
|
||||
module.exports = tsort;
|
||||
if (process.argv[1] === __filename) tsortTest();
|
||||
}
|
5
src/static/js/rjquery.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
// Proviedes a require'able version of jQuery without leaking $ and jQuery;
|
||||
|
||||
require('./jquery');
|
||||
exports.jQuery = exports.$ = $.noConflict(true);
|
54
src/static/js/security.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var HTML_ENTITY_MAP = {
|
||||
'&': '&'
|
||||
, '<': '<'
|
||||
, '>': '>'
|
||||
, '"': '"'
|
||||
, "'": '''
|
||||
, '/': '/'
|
||||
};
|
||||
|
||||
// OSWASP Guidlines: &, <, >, ", ' plus forward slash.
|
||||
var HTML_CHARACTERS_EXPRESSION = /[&"'<>\/]/g;
|
||||
function escapeHTML(text) {
|
||||
return text && text.replace(HTML_CHARACTERS_EXPRESSION, function (c) {
|
||||
return HTML_ENTITY_MAP[c] || c;
|
||||
});
|
||||
}
|
||||
|
||||
// OSWASP Guidlines: escape all non alphanumeric characters in ASCII space.
|
||||
var HTML_ATTRIBUTE_CHARACTERS_EXPRESSION =
|
||||
/[\x00-\x2F\x3A-\x40\5B-\x60\x7B-\xFF]/g;
|
||||
function escapeHTMLAttribute(text) {
|
||||
return text && text.replace(HTML_ATTRIBUTE_CHARACTERS_EXPRESSION, function (c) {
|
||||
return "&#x" + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + ";";
|
||||
});
|
||||
};
|
||||
|
||||
// OSWASP Guidlines: escape all non alphanumeric characters in ASCII space.
|
||||
var JAVASCRIPT_CHARACTERS_EXPRESSION =
|
||||
/[\x00-\x2F\x3A-\x40\5B-\x60\x7B-\xFF]/g;
|
||||
function escapeJavaScriptData(text) {
|
||||
return text && text.replace(JAVASCRIPT_CHARACTERS_EXPRESSION, function (c) {
|
||||
return "\\x" + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
});
|
||||
}
|
||||
|
||||
exports.escapeHTML = escapeHTML;
|
||||
exports.escapeHTMLAttribute = escapeHTMLAttribute;
|
||||
exports.escapeJavaScriptData = escapeJavaScriptData;
|
472
src/static/js/skiplist.js
Normal file
|
@ -0,0 +1,472 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var Ace2Common = require('./ace2_common'),
|
||||
_ = require('./underscore');
|
||||
|
||||
var noop = Ace2Common.noop;
|
||||
|
||||
function SkipList()
|
||||
{
|
||||
var PROFILER = window.PROFILER;
|
||||
if (!PROFILER)
|
||||
{
|
||||
PROFILER = function()
|
||||
{
|
||||
return {
|
||||
start: noop,
|
||||
mark: noop,
|
||||
literal: noop,
|
||||
end: noop,
|
||||
cancel: noop
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// if there are N elements in the skiplist, "start" is element -1 and "end" is element N
|
||||
var start = {
|
||||
key: null,
|
||||
levels: 1,
|
||||
upPtrs: [null],
|
||||
downPtrs: [null],
|
||||
downSkips: [1],
|
||||
downSkipWidths: [0]
|
||||
};
|
||||
var end = {
|
||||
key: null,
|
||||
levels: 1,
|
||||
upPtrs: [null],
|
||||
downPtrs: [null],
|
||||
downSkips: [null],
|
||||
downSkipWidths: [null]
|
||||
};
|
||||
var numNodes = 0;
|
||||
var totalWidth = 0;
|
||||
var keyToNodeMap = {};
|
||||
start.downPtrs[0] = end;
|
||||
end.upPtrs[0] = start;
|
||||
// a "point" object at location x allows modifications immediately after the first
|
||||
// x elements of the skiplist, such as multiple inserts or deletes.
|
||||
// After an insert or delete using point P, the point is still valid and points
|
||||
// to the same index in the skiplist. Other operations with other points invalidate
|
||||
// this point.
|
||||
|
||||
|
||||
function _getPoint(targetLoc)
|
||||
{
|
||||
var numLevels = start.levels;
|
||||
var lvl = numLevels - 1;
|
||||
var i = -1,
|
||||
ws = 0;
|
||||
var nodes = new Array(numLevels);
|
||||
var idxs = new Array(numLevels);
|
||||
var widthSkips = new Array(numLevels);
|
||||
nodes[lvl] = start;
|
||||
idxs[lvl] = -1;
|
||||
widthSkips[lvl] = 0;
|
||||
while (lvl >= 0)
|
||||
{
|
||||
var n = nodes[lvl];
|
||||
while (n.downPtrs[lvl] && (i + n.downSkips[lvl] < targetLoc))
|
||||
{
|
||||
i += n.downSkips[lvl];
|
||||
ws += n.downSkipWidths[lvl];
|
||||
n = n.downPtrs[lvl];
|
||||
}
|
||||
nodes[lvl] = n;
|
||||
idxs[lvl] = i;
|
||||
widthSkips[lvl] = ws;
|
||||
lvl--;
|
||||
if (lvl >= 0)
|
||||
{
|
||||
nodes[lvl] = n;
|
||||
}
|
||||
}
|
||||
return {
|
||||
nodes: nodes,
|
||||
idxs: idxs,
|
||||
loc: targetLoc,
|
||||
widthSkips: widthSkips,
|
||||
toString: function()
|
||||
{
|
||||
return "getPoint(" + targetLoc + ")";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _getNodeAtOffset(targetOffset)
|
||||
{
|
||||
var i = 0;
|
||||
var n = start;
|
||||
var lvl = start.levels - 1;
|
||||
while (lvl >= 0 && n.downPtrs[lvl])
|
||||
{
|
||||
while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset))
|
||||
{
|
||||
i += n.downSkipWidths[lvl];
|
||||
n = n.downPtrs[lvl];
|
||||
}
|
||||
lvl--;
|
||||
}
|
||||
if (n === start) return (start.downPtrs[0] || null);
|
||||
else if (n === end) return (targetOffset == totalWidth ? (end.upPtrs[0] || null) : null);
|
||||
return n;
|
||||
}
|
||||
|
||||
function _entryWidth(e)
|
||||
{
|
||||
return (e && e.width) || 0;
|
||||
}
|
||||
|
||||
function _insertKeyAtPoint(point, newKey, entry)
|
||||
{
|
||||
var p = PROFILER("insertKey", false);
|
||||
var newNode = {
|
||||
key: newKey,
|
||||
levels: 0,
|
||||
upPtrs: [],
|
||||
downPtrs: [],
|
||||
downSkips: [],
|
||||
downSkipWidths: []
|
||||
};
|
||||
p.mark("donealloc");
|
||||
var pNodes = point.nodes;
|
||||
var pIdxs = point.idxs;
|
||||
var pLoc = point.loc;
|
||||
var widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0];
|
||||
var newWidth = _entryWidth(entry);
|
||||
p.mark("loop1");
|
||||
|
||||
// The new node will have at least level 1
|
||||
// With a proability of 0.01^(n-1) the nodes level will be >= n
|
||||
while (newNode.levels == 0 || Math.random() < 0.01)
|
||||
{
|
||||
var lvl = newNode.levels;
|
||||
newNode.levels++;
|
||||
if (lvl == pNodes.length)
|
||||
{
|
||||
// assume we have just passed the end of point.nodes, and reached one level greater
|
||||
// than the skiplist currently supports
|
||||
pNodes[lvl] = start;
|
||||
pIdxs[lvl] = -1;
|
||||
start.levels++;
|
||||
end.levels++;
|
||||
start.downPtrs[lvl] = end;
|
||||
end.upPtrs[lvl] = start;
|
||||
start.downSkips[lvl] = numNodes + 1;
|
||||
start.downSkipWidths[lvl] = totalWidth;
|
||||
point.widthSkips[lvl] = 0;
|
||||
}
|
||||
var me = newNode;
|
||||
var up = pNodes[lvl];
|
||||
var down = up.downPtrs[lvl];
|
||||
var skip1 = pLoc - pIdxs[lvl];
|
||||
var skip2 = up.downSkips[lvl] + 1 - skip1;
|
||||
up.downSkips[lvl] = skip1;
|
||||
up.downPtrs[lvl] = me;
|
||||
me.downSkips[lvl] = skip2;
|
||||
me.upPtrs[lvl] = up;
|
||||
me.downPtrs[lvl] = down;
|
||||
down.upPtrs[lvl] = me;
|
||||
var widthSkip1 = widthLoc - point.widthSkips[lvl];
|
||||
var widthSkip2 = up.downSkipWidths[lvl] + newWidth - widthSkip1;
|
||||
up.downSkipWidths[lvl] = widthSkip1;
|
||||
me.downSkipWidths[lvl] = widthSkip2;
|
||||
}
|
||||
p.mark("loop2");
|
||||
p.literal(pNodes.length, "PNL");
|
||||
for (var lvl = newNode.levels; lvl < pNodes.length; lvl++)
|
||||
{
|
||||
var up = pNodes[lvl];
|
||||
up.downSkips[lvl]++;
|
||||
up.downSkipWidths[lvl] += newWidth;
|
||||
}
|
||||
p.mark("map");
|
||||
keyToNodeMap['$KEY$' + newKey] = newNode;
|
||||
numNodes++;
|
||||
totalWidth += newWidth;
|
||||
p.end();
|
||||
}
|
||||
|
||||
function _getNodeAtPoint(point)
|
||||
{
|
||||
return point.nodes[0].downPtrs[0];
|
||||
}
|
||||
|
||||
function _incrementPoint(point)
|
||||
{
|
||||
point.loc++;
|
||||
for (var i = 0; i < point.nodes.length; i++)
|
||||
{
|
||||
if (point.idxs[i] + point.nodes[i].downSkips[i] < point.loc)
|
||||
{
|
||||
point.idxs[i] += point.nodes[i].downSkips[i];
|
||||
point.widthSkips[i] += point.nodes[i].downSkipWidths[i];
|
||||
point.nodes[i] = point.nodes[i].downPtrs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _deleteKeyAtPoint(point)
|
||||
{
|
||||
var elem = point.nodes[0].downPtrs[0];
|
||||
var elemWidth = _entryWidth(elem.entry);
|
||||
for (var i = 0; i < point.nodes.length; i++)
|
||||
{
|
||||
if (i < elem.levels)
|
||||
{
|
||||
var up = elem.upPtrs[i];
|
||||
var down = elem.downPtrs[i];
|
||||
var totalSkip = up.downSkips[i] + elem.downSkips[i] - 1;
|
||||
up.downPtrs[i] = down;
|
||||
down.upPtrs[i] = up;
|
||||
up.downSkips[i] = totalSkip;
|
||||
var totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth;
|
||||
up.downSkipWidths[i] = totalWidthSkip;
|
||||
}
|
||||
else
|
||||
{
|
||||
var up = point.nodes[i];
|
||||
var down = up.downPtrs[i];
|
||||
up.downSkips[i]--;
|
||||
up.downSkipWidths[i] -= elemWidth;
|
||||
}
|
||||
}
|
||||
delete keyToNodeMap['$KEY$' + elem.key];
|
||||
numNodes--;
|
||||
totalWidth -= elemWidth;
|
||||
}
|
||||
|
||||
function _propagateWidthChange(node)
|
||||
{
|
||||
var oldWidth = node.downSkipWidths[0];
|
||||
var newWidth = _entryWidth(node.entry);
|
||||
var widthChange = newWidth - oldWidth;
|
||||
var n = node;
|
||||
var lvl = 0;
|
||||
while (lvl < n.levels)
|
||||
{
|
||||
n.downSkipWidths[lvl] += widthChange;
|
||||
lvl++;
|
||||
while (lvl >= n.levels && n.upPtrs[lvl - 1])
|
||||
{
|
||||
n = n.upPtrs[lvl - 1];
|
||||
}
|
||||
}
|
||||
totalWidth += widthChange;
|
||||
}
|
||||
|
||||
function _getNodeIndex(node, byWidth)
|
||||
{
|
||||
var dist = (byWidth ? 0 : -1);
|
||||
var n = node;
|
||||
while (n !== start)
|
||||
{
|
||||
var lvl = n.levels - 1;
|
||||
n = n.upPtrs[lvl];
|
||||
if (byWidth) dist += n.downSkipWidths[lvl];
|
||||
else dist += n.downSkips[lvl];
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function _getNodeByKey(key)
|
||||
{
|
||||
return keyToNodeMap['$KEY$' + key];
|
||||
}
|
||||
|
||||
// Returns index of first entry such that entryFunc(entry) is truthy,
|
||||
// or length() if no such entry. Assumes all falsy entries come before
|
||||
// all truthy entries.
|
||||
|
||||
|
||||
function _search(entryFunc)
|
||||
{
|
||||
var low = start;
|
||||
var lvl = start.levels - 1;
|
||||
var lowIndex = -1;
|
||||
|
||||
function f(node)
|
||||
{
|
||||
if (node === start) return false;
|
||||
else if (node === end) return true;
|
||||
else return entryFunc(node.entry);
|
||||
}
|
||||
while (lvl >= 0)
|
||||
{
|
||||
var nextLow = low.downPtrs[lvl];
|
||||
while (!f(nextLow))
|
||||
{
|
||||
lowIndex += low.downSkips[lvl];
|
||||
low = nextLow;
|
||||
nextLow = low.downPtrs[lvl];
|
||||
}
|
||||
lvl--;
|
||||
}
|
||||
return lowIndex + 1;
|
||||
}
|
||||
|
||||
/*
|
||||
The skip-list contains "entries", JavaScript objects that each must have a unique "key" property
|
||||
that is a string.
|
||||
*/
|
||||
var self = this;
|
||||
_.extend(this, {
|
||||
length: function()
|
||||
{
|
||||
return numNodes;
|
||||
},
|
||||
atIndex: function(i)
|
||||
{
|
||||
if (i < 0) console.warn("atIndex(" + i + ")");
|
||||
if (i >= numNodes) console.warn("atIndex(" + i + ">=" + numNodes + ")");
|
||||
return _getNodeAtPoint(_getPoint(i)).entry;
|
||||
},
|
||||
// differs from Array.splice() in that new elements are in an array, not varargs
|
||||
splice: function(start, deleteCount, newEntryArray)
|
||||
{
|
||||
if (start < 0) console.warn("splice(" + start + ", ...)");
|
||||
if (start + deleteCount > numNodes)
|
||||
{
|
||||
console.warn("splice(" + start + ", " + deleteCount + ", ...), N=" + numNodes);
|
||||
console.warn("%s %s %s", typeof start, typeof deleteCount, typeof numNodes);
|
||||
console.trace();
|
||||
}
|
||||
|
||||
if (!newEntryArray) newEntryArray = [];
|
||||
var pt = _getPoint(start);
|
||||
for (var i = 0; i < deleteCount; i++)
|
||||
{
|
||||
_deleteKeyAtPoint(pt);
|
||||
}
|
||||
for (var i = (newEntryArray.length - 1); i >= 0; i--)
|
||||
{
|
||||
var entry = newEntryArray[i];
|
||||
_insertKeyAtPoint(pt, entry.key, entry);
|
||||
var node = _getNodeByKey(entry.key);
|
||||
node.entry = entry;
|
||||
}
|
||||
},
|
||||
next: function(entry)
|
||||
{
|
||||
return _getNodeByKey(entry.key).downPtrs[0].entry || null;
|
||||
},
|
||||
prev: function(entry)
|
||||
{
|
||||
return _getNodeByKey(entry.key).upPtrs[0].entry || null;
|
||||
},
|
||||
push: function(entry)
|
||||
{
|
||||
self.splice(numNodes, 0, [entry]);
|
||||
},
|
||||
slice: function(start, end)
|
||||
{
|
||||
// act like Array.slice()
|
||||
if (start === undefined) start = 0;
|
||||
else if (start < 0) start += numNodes;
|
||||
if (end === undefined) end = numNodes;
|
||||
else if (end < 0) end += numNodes;
|
||||
|
||||
if (start < 0) start = 0;
|
||||
if (start > numNodes) start = numNodes;
|
||||
if (end < 0) end = 0;
|
||||
if (end > numNodes) end = numNodes;
|
||||
|
||||
dmesg(String([start, end, numNodes]));
|
||||
if (end <= start) return [];
|
||||
var n = self.atIndex(start);
|
||||
var array = [n];
|
||||
for (var i = 1; i < (end - start); i++)
|
||||
{
|
||||
n = self.next(n);
|
||||
array.push(n);
|
||||
}
|
||||
return array;
|
||||
},
|
||||
atKey: function(key)
|
||||
{
|
||||
return _getNodeByKey(key).entry;
|
||||
},
|
||||
indexOfKey: function(key)
|
||||
{
|
||||
return _getNodeIndex(_getNodeByKey(key));
|
||||
},
|
||||
indexOfEntry: function(entry)
|
||||
{
|
||||
return self.indexOfKey(entry.key);
|
||||
},
|
||||
containsKey: function(key)
|
||||
{
|
||||
return !!(_getNodeByKey(key));
|
||||
},
|
||||
// gets the last entry starting at or before the offset
|
||||
atOffset: function(offset)
|
||||
{
|
||||
return _getNodeAtOffset(offset).entry;
|
||||
},
|
||||
keyAtOffset: function(offset)
|
||||
{
|
||||
return self.atOffset(offset).key;
|
||||
},
|
||||
offsetOfKey: function(key)
|
||||
{
|
||||
return _getNodeIndex(_getNodeByKey(key), true);
|
||||
},
|
||||
offsetOfEntry: function(entry)
|
||||
{
|
||||
return self.offsetOfKey(entry.key);
|
||||
},
|
||||
setEntryWidth: function(entry, width)
|
||||
{
|
||||
entry.width = width;
|
||||
_propagateWidthChange(_getNodeByKey(entry.key));
|
||||
},
|
||||
totalWidth: function()
|
||||
{
|
||||
return totalWidth;
|
||||
},
|
||||
offsetOfIndex: function(i)
|
||||
{
|
||||
if (i < 0) return 0;
|
||||
if (i >= numNodes) return totalWidth;
|
||||
return self.offsetOfEntry(self.atIndex(i));
|
||||
},
|
||||
indexOfOffset: function(offset)
|
||||
{
|
||||
if (offset <= 0) return 0;
|
||||
if (offset >= totalWidth) return numNodes;
|
||||
return self.indexOfEntry(self.atOffset(offset));
|
||||
},
|
||||
search: function(entryFunc)
|
||||
{
|
||||
return _search(entryFunc);
|
||||
},
|
||||
//debugToString: _debugToString,
|
||||
debugGetPoint: _getPoint,
|
||||
debugDepth: function()
|
||||
{
|
||||
return start.levels;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SkipList;
|
165
src/static/js/timeslider.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// These jQuery things should create local references, but for now `require()`
|
||||
// assigns to the global `$` and augments it with plugins.
|
||||
require('./jquery');
|
||||
JSON = require('./json2');
|
||||
|
||||
var createCookie = require('./pad_utils').createCookie;
|
||||
var readCookie = require('./pad_utils').readCookie;
|
||||
var randomString = require('./pad_utils').randomString;
|
||||
var _ = require('./underscore');
|
||||
|
||||
var socket, token, padId, export_links;
|
||||
|
||||
function init() {
|
||||
$(document).ready(function ()
|
||||
{
|
||||
// start the custom js
|
||||
if (typeof customStart == "function") customStart();
|
||||
|
||||
//get the padId out of the url
|
||||
var urlParts= document.location.pathname.split("/");
|
||||
padId = decodeURIComponent(urlParts[urlParts.length-2]);
|
||||
|
||||
//set the title
|
||||
document.title = padId.replace(/_+/g, ' ') + " | " + document.title;
|
||||
|
||||
//ensure we have a token
|
||||
token = readCookie("token");
|
||||
if(token == null)
|
||||
{
|
||||
token = "t." + randomString();
|
||||
createCookie("token", token, 60);
|
||||
}
|
||||
|
||||
var loc = document.location;
|
||||
//get the correct port
|
||||
var port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port;
|
||||
//create the url
|
||||
var url = loc.protocol + "//" + loc.hostname + ":" + port + "/";
|
||||
//find out in which subfolder we are
|
||||
var resource = exports.baseURL.substring(1) + 'socket.io';
|
||||
|
||||
//build up the socket io connection
|
||||
socket = io.connect(url, {resource: resource});
|
||||
|
||||
//send the ready message once we're connected
|
||||
socket.on('connect', function()
|
||||
{
|
||||
sendSocketMsg("CLIENT_READY", {});
|
||||
});
|
||||
|
||||
socket.on('disconnect', function()
|
||||
{
|
||||
BroadcastSlider.showReconnectUI();
|
||||
});
|
||||
|
||||
//route the incoming messages
|
||||
socket.on('message', function(message)
|
||||
{
|
||||
if(window.console) console.log(message);
|
||||
|
||||
if(message.type == "CLIENT_VARS")
|
||||
{
|
||||
handleClientVars(message);
|
||||
}
|
||||
else if(message.accessStatus)
|
||||
{
|
||||
$("body").html("<h2>You have no permission to access this pad</h2>")
|
||||
} else {
|
||||
changesetLoader.handleMessageFromServer(message);
|
||||
}
|
||||
});
|
||||
|
||||
//get all the export links
|
||||
export_links = $('#export > .exportlink')
|
||||
|
||||
if(document.referrer.length > 0 && document.referrer.substring(document.referrer.lastIndexOf("/")-1,document.referrer.lastIndexOf("/")) === "p") {
|
||||
$("#returnbutton").attr("href", document.referrer);
|
||||
} else {
|
||||
$("#returnbutton").attr("href", document.location.href.substring(0,document.location.href.lastIndexOf("/")));
|
||||
}
|
||||
|
||||
$('button#forcereconnect').click(function()
|
||||
{
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
//sends a message over the socket
|
||||
function sendSocketMsg(type, data)
|
||||
{
|
||||
var sessionID = readCookie("sessionID");
|
||||
var password = readCookie("password");
|
||||
|
||||
var msg = { "component" : "pad", // FIXME: Remove this stupidity!
|
||||
"type": type,
|
||||
"data": data,
|
||||
"padId": padId,
|
||||
"token": token,
|
||||
"sessionID": sessionID,
|
||||
"password": password,
|
||||
"protocolVersion": 2};
|
||||
|
||||
socket.json.send(msg);
|
||||
}
|
||||
|
||||
var fireWhenAllScriptsAreLoaded = [];
|
||||
|
||||
var BroadcastSlider, changesetLoader;
|
||||
function handleClientVars(message)
|
||||
{
|
||||
//save the client Vars
|
||||
clientVars = message.data;
|
||||
|
||||
//load all script that doesn't work without the clientVars
|
||||
BroadcastSlider = require('./broadcast_slider').loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
|
||||
require('./broadcast_revisions').loadBroadcastRevisionsJS();
|
||||
changesetLoader = require('./broadcast').loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
|
||||
|
||||
//initialize export ui
|
||||
require('./pad_impexp').padimpexp.init();
|
||||
|
||||
//change export urls when the slider moves
|
||||
var export_rev_regex = /(\/\d+)?\/export/
|
||||
BroadcastSlider.onSlider(function(revno)
|
||||
{
|
||||
// export_links is a jQuery Array, so .each is allowed.
|
||||
export_links.each(function()
|
||||
{
|
||||
this.setAttribute('href', this.href.replace(export_rev_regex, '/' + revno + '/export'));
|
||||
});
|
||||
});
|
||||
|
||||
//fire all start functions of these scripts, formerly fired with window.load
|
||||
for(var i=0;i < fireWhenAllScriptsAreLoaded.length;i++)
|
||||
{
|
||||
fireWhenAllScriptsAreLoaded[i]();
|
||||
}
|
||||
}
|
||||
|
||||
exports.baseURL = '';
|
||||
exports.init = init;
|
237
src/static/js/tinycon.js
Normal file
|
@ -0,0 +1,237 @@
|
|||
/*!
|
||||
* Tinycon - A small library for manipulating the Favicon
|
||||
* Tom Moor, http://tommoor.com
|
||||
* Copyright (c) 2012 Tom Moor
|
||||
* MIT Licensed
|
||||
* @version 0.2.6
|
||||
*/
|
||||
|
||||
(function(){
|
||||
|
||||
var Tinycon = {};
|
||||
var currentFavicon = null;
|
||||
var originalFavicon = null;
|
||||
var originalTitle = document.title;
|
||||
var faviconImage = null;
|
||||
var canvas = null;
|
||||
var options = {};
|
||||
var defaults = {
|
||||
width: 7,
|
||||
height: 9,
|
||||
font: '10px arial',
|
||||
colour: '#ffffff',
|
||||
background: '#F03D25',
|
||||
fallback: true
|
||||
};
|
||||
|
||||
var ua = (function () {
|
||||
var agent = navigator.userAgent.toLowerCase();
|
||||
// New function has access to 'agent' via closure
|
||||
return function (browser) {
|
||||
return agent.indexOf(browser) !== -1;
|
||||
};
|
||||
}());
|
||||
|
||||
var browser = {
|
||||
ie: ua('msie'),
|
||||
chrome: ua('chrome'),
|
||||
webkit: ua('chrome') || ua('safari'),
|
||||
safari: ua('safari') && !ua('chrome'),
|
||||
mozilla: ua('mozilla') && !ua('chrome') && !ua('safari')
|
||||
};
|
||||
|
||||
// private methods
|
||||
var getFaviconTag = function(){
|
||||
|
||||
var links = document.getElementsByTagName('link');
|
||||
|
||||
for(var i=0, len=links.length; i < len; i++) {
|
||||
if ((links[i].getAttribute('rel') || '').match(/\bicon\b/)) {
|
||||
return links[i];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
var removeFaviconTag = function(){
|
||||
|
||||
var links = document.getElementsByTagName('link');
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
|
||||
for(var i=0, len=links.length; i < len; i++) {
|
||||
var exists = (typeof(links[i]) !== 'undefined');
|
||||
if (exists && links[i].getAttribute('rel') === 'icon') {
|
||||
head.removeChild(links[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var getCurrentFavicon = function(){
|
||||
|
||||
if (!originalFavicon || !currentFavicon) {
|
||||
var tag = getFaviconTag();
|
||||
originalFavicon = currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico';
|
||||
}
|
||||
|
||||
return currentFavicon;
|
||||
};
|
||||
|
||||
var getCanvas = function (){
|
||||
|
||||
if (!canvas) {
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = 16;
|
||||
canvas.height = 16;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
var setFaviconTag = function(url){
|
||||
removeFaviconTag();
|
||||
|
||||
var link = document.createElement('link');
|
||||
link.type = 'image/x-icon';
|
||||
link.rel = 'icon';
|
||||
link.href = url;
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
};
|
||||
|
||||
var log = function(message){
|
||||
if (window.console) window.console.log(message);
|
||||
};
|
||||
|
||||
var drawFavicon = function(num, colour) {
|
||||
|
||||
// fallback to updating the browser title if unsupported
|
||||
if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') {
|
||||
return updateTitle(num);
|
||||
}
|
||||
|
||||
var context = getCanvas().getContext("2d");
|
||||
var colour = colour || '#000000';
|
||||
var num = num || 0;
|
||||
var src = getCurrentFavicon();
|
||||
|
||||
faviconImage = new Image();
|
||||
faviconImage.onload = function() {
|
||||
|
||||
// clear canvas
|
||||
context.clearRect(0, 0, 16, 16);
|
||||
|
||||
// draw original favicon
|
||||
context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, 16, 16);
|
||||
|
||||
// draw bubble over the top
|
||||
if (num > 0) drawBubble(context, num, colour);
|
||||
|
||||
// refresh tag in page
|
||||
refreshFavicon();
|
||||
};
|
||||
|
||||
// allow cross origin resource requests if the image is not a data:uri
|
||||
// as detailed here: https://github.com/mrdoob/three.js/issues/1305
|
||||
if (!src.match(/^data/)) {
|
||||
faviconImage.crossOrigin = 'anonymous';
|
||||
}
|
||||
|
||||
faviconImage.src = src;
|
||||
};
|
||||
|
||||
var updateTitle = function(num) {
|
||||
|
||||
if (options.fallback) {
|
||||
if (num > 0) {
|
||||
document.title = '('+num+') ' + originalTitle;
|
||||
} else {
|
||||
document.title = originalTitle;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var drawBubble = function(context, num, colour) {
|
||||
|
||||
// bubble needs to be larger for double digits
|
||||
var len = (num+"").length-1;
|
||||
var width = options.width + (6*len);
|
||||
var w = 16-width;
|
||||
var h = 16-options.height;
|
||||
|
||||
// webkit seems to render fonts lighter than firefox
|
||||
context.font = (browser.webkit ? 'bold ' : '') + options.font;
|
||||
context.fillStyle = options.background;
|
||||
context.strokeStyle = options.background;
|
||||
context.lineWidth = 1;
|
||||
|
||||
// bubble
|
||||
context.fillRect(w,h,width-1,options.height);
|
||||
|
||||
// rounded left
|
||||
context.beginPath();
|
||||
context.moveTo(w-0.5,h+1);
|
||||
context.lineTo(w-0.5,15);
|
||||
context.stroke();
|
||||
|
||||
// rounded right
|
||||
context.beginPath();
|
||||
context.moveTo(15.5,h+1);
|
||||
context.lineTo(15.5,15);
|
||||
context.stroke();
|
||||
|
||||
// bottom shadow
|
||||
context.beginPath();
|
||||
context.strokeStyle = "rgba(0,0,0,0.3)";
|
||||
context.moveTo(w,16);
|
||||
context.lineTo(15,16);
|
||||
context.stroke();
|
||||
|
||||
// number
|
||||
context.fillStyle = options.colour;
|
||||
context.textAlign = "right";
|
||||
context.textBaseline = "top";
|
||||
|
||||
// unfortunately webkit/mozilla are a pixel different in text positioning
|
||||
context.fillText(num, 15, browser.mozilla ? 7 : 6);
|
||||
};
|
||||
|
||||
var refreshFavicon = function(){
|
||||
// check support
|
||||
if (!getCanvas().getContext) return;
|
||||
|
||||
setFaviconTag(getCanvas().toDataURL());
|
||||
};
|
||||
|
||||
|
||||
// public methods
|
||||
Tinycon.setOptions = function(custom){
|
||||
options = {};
|
||||
|
||||
for(var key in defaults){
|
||||
options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key];
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Tinycon.setImage = function(url){
|
||||
currentFavicon = url;
|
||||
refreshFavicon();
|
||||
return this;
|
||||
};
|
||||
|
||||
Tinycon.setBubble = function(num, colour){
|
||||
|
||||
// validate
|
||||
if(isNaN(parseFloat(num)) || !isFinite(num)) return log('Bubble must be a number');
|
||||
|
||||
drawFavicon(num, colour);
|
||||
return this;
|
||||
};
|
||||
|
||||
Tinycon.reset = function(){
|
||||
Tinycon.setImage(originalFavicon);
|
||||
};
|
||||
|
||||
Tinycon.setOptions(defaults);
|
||||
window.Tinycon = Tinycon;
|
||||
})();
|
999
src/static/js/underscore.js
Normal file
|
@ -0,0 +1,999 @@
|
|||
// Underscore.js 1.3.1
|
||||
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Underscore is freely distributable under the MIT license.
|
||||
// Portions of Underscore are inspired or borrowed from Prototype,
|
||||
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
||||
// For all details and documentation:
|
||||
// http://documentcloud.github.com/underscore
|
||||
|
||||
(function() {
|
||||
|
||||
// Baseline setup
|
||||
// --------------
|
||||
|
||||
// Establish the root object, `window` in the browser, or `global` on the server.
|
||||
var root = this;
|
||||
|
||||
// Save the previous value of the `_` variable.
|
||||
var previousUnderscore = root._;
|
||||
|
||||
// Establish the object that gets returned to break out of a loop iteration.
|
||||
var breaker = {};
|
||||
|
||||
// Save bytes in the minified (but not gzipped) version:
|
||||
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
|
||||
|
||||
// Create quick reference variables for speed access to core prototypes.
|
||||
var slice = ArrayProto.slice,
|
||||
unshift = ArrayProto.unshift,
|
||||
toString = ObjProto.toString,
|
||||
hasOwnProperty = ObjProto.hasOwnProperty;
|
||||
|
||||
// All **ECMAScript 5** native function implementations that we hope to use
|
||||
// are declared here.
|
||||
var
|
||||
nativeForEach = ArrayProto.forEach,
|
||||
nativeMap = ArrayProto.map,
|
||||
nativeReduce = ArrayProto.reduce,
|
||||
nativeReduceRight = ArrayProto.reduceRight,
|
||||
nativeFilter = ArrayProto.filter,
|
||||
nativeEvery = ArrayProto.every,
|
||||
nativeSome = ArrayProto.some,
|
||||
nativeIndexOf = ArrayProto.indexOf,
|
||||
nativeLastIndexOf = ArrayProto.lastIndexOf,
|
||||
nativeIsArray = Array.isArray,
|
||||
nativeKeys = Object.keys,
|
||||
nativeBind = FuncProto.bind;
|
||||
|
||||
// Create a safe reference to the Underscore object for use below.
|
||||
var _ = function(obj) { return new wrapper(obj); };
|
||||
|
||||
// Export the Underscore object for **Node.js**, with
|
||||
// backwards-compatibility for the old `require()` API. If we're in
|
||||
// the browser, add `_` as a global object via a string identifier,
|
||||
// for Closure Compiler "advanced" mode.
|
||||
if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
exports = module.exports = _;
|
||||
}
|
||||
exports._ = _;
|
||||
} else {
|
||||
root['_'] = _;
|
||||
}
|
||||
|
||||
// Current version.
|
||||
_.VERSION = '1.3.1';
|
||||
|
||||
// Collection Functions
|
||||
// --------------------
|
||||
|
||||
// The cornerstone, an `each` implementation, aka `forEach`.
|
||||
// Handles objects with the built-in `forEach`, arrays, and raw objects.
|
||||
// Delegates to **ECMAScript 5**'s native `forEach` if available.
|
||||
var each = _.each = _.forEach = function(obj, iterator, context) {
|
||||
if (obj == null) return;
|
||||
if (nativeForEach && obj.forEach === nativeForEach) {
|
||||
obj.forEach(iterator, context);
|
||||
} else if (obj.length === +obj.length) {
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
|
||||
}
|
||||
} else {
|
||||
for (var key in obj) {
|
||||
if (_.has(obj, key)) {
|
||||
if (iterator.call(context, obj[key], key, obj) === breaker) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Return the results of applying the iterator to each element.
|
||||
// Delegates to **ECMAScript 5**'s native `map` if available.
|
||||
_.map = _.collect = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
results[results.length] = iterator.call(context, value, index, list);
|
||||
});
|
||||
if (obj.length === +obj.length) results.length = obj.length;
|
||||
return results;
|
||||
};
|
||||
|
||||
// **Reduce** builds up a single result from a list of values, aka `inject`,
|
||||
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
|
||||
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
|
||||
var initial = arguments.length > 2;
|
||||
if (obj == null) obj = [];
|
||||
if (nativeReduce && obj.reduce === nativeReduce) {
|
||||
if (context) iterator = _.bind(iterator, context);
|
||||
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
|
||||
}
|
||||
each(obj, function(value, index, list) {
|
||||
if (!initial) {
|
||||
memo = value;
|
||||
initial = true;
|
||||
} else {
|
||||
memo = iterator.call(context, memo, value, index, list);
|
||||
}
|
||||
});
|
||||
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
|
||||
return memo;
|
||||
};
|
||||
|
||||
// The right-associative version of reduce, also known as `foldr`.
|
||||
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
|
||||
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
|
||||
var initial = arguments.length > 2;
|
||||
if (obj == null) obj = [];
|
||||
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
|
||||
if (context) iterator = _.bind(iterator, context);
|
||||
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
|
||||
}
|
||||
var reversed = _.toArray(obj).reverse();
|
||||
if (context && !initial) iterator = _.bind(iterator, context);
|
||||
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
|
||||
};
|
||||
|
||||
// Return the first value which passes a truth test. Aliased as `detect`.
|
||||
_.find = _.detect = function(obj, iterator, context) {
|
||||
var result;
|
||||
any(obj, function(value, index, list) {
|
||||
if (iterator.call(context, value, index, list)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Return all the elements that pass a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `filter` if available.
|
||||
// Aliased as `select`.
|
||||
_.filter = _.select = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (iterator.call(context, value, index, list)) results[results.length] = value;
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
// Return all the elements for which a truth test fails.
|
||||
_.reject = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
each(obj, function(value, index, list) {
|
||||
if (!iterator.call(context, value, index, list)) results[results.length] = value;
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
// Determine whether all of the elements match a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `every` if available.
|
||||
// Aliased as `all`.
|
||||
_.every = _.all = function(obj, iterator, context) {
|
||||
var result = true;
|
||||
if (obj == null) return result;
|
||||
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Determine if at least one element in the object matches a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `some` if available.
|
||||
// Aliased as `any`.
|
||||
var any = _.some = _.any = function(obj, iterator, context) {
|
||||
iterator || (iterator = _.identity);
|
||||
var result = false;
|
||||
if (obj == null) return result;
|
||||
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (result || (result = iterator.call(context, value, index, list))) return breaker;
|
||||
});
|
||||
return !!result;
|
||||
};
|
||||
|
||||
// Determine if a given value is included in the array or object using `===`.
|
||||
// Aliased as `contains`.
|
||||
_.include = _.contains = function(obj, target) {
|
||||
var found = false;
|
||||
if (obj == null) return found;
|
||||
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
|
||||
found = any(obj, function(value) {
|
||||
return value === target;
|
||||
});
|
||||
return found;
|
||||
};
|
||||
|
||||
// Invoke a method (with arguments) on every item in a collection.
|
||||
_.invoke = function(obj, method) {
|
||||
var args = slice.call(arguments, 2);
|
||||
return _.map(obj, function(value) {
|
||||
return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
|
||||
});
|
||||
};
|
||||
|
||||
// Convenience version of a common use case of `map`: fetching a property.
|
||||
_.pluck = function(obj, key) {
|
||||
return _.map(obj, function(value){ return value[key]; });
|
||||
};
|
||||
|
||||
// Return the maximum element or (element-based computation).
|
||||
_.max = function(obj, iterator, context) {
|
||||
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
|
||||
if (!iterator && _.isEmpty(obj)) return -Infinity;
|
||||
var result = {computed : -Infinity};
|
||||
each(obj, function(value, index, list) {
|
||||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||||
computed >= result.computed && (result = {value : value, computed : computed});
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
// Return the minimum element (or element-based computation).
|
||||
_.min = function(obj, iterator, context) {
|
||||
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
|
||||
if (!iterator && _.isEmpty(obj)) return Infinity;
|
||||
var result = {computed : Infinity};
|
||||
each(obj, function(value, index, list) {
|
||||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||||
computed < result.computed && (result = {value : value, computed : computed});
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
// Shuffle an array.
|
||||
_.shuffle = function(obj) {
|
||||
var shuffled = [], rand;
|
||||
each(obj, function(value, index, list) {
|
||||
if (index == 0) {
|
||||
shuffled[0] = value;
|
||||
} else {
|
||||
rand = Math.floor(Math.random() * (index + 1));
|
||||
shuffled[index] = shuffled[rand];
|
||||
shuffled[rand] = value;
|
||||
}
|
||||
});
|
||||
return shuffled;
|
||||
};
|
||||
|
||||
// Sort the object's values by a criterion produced by an iterator.
|
||||
_.sortBy = function(obj, iterator, context) {
|
||||
return _.pluck(_.map(obj, function(value, index, list) {
|
||||
return {
|
||||
value : value,
|
||||
criteria : iterator.call(context, value, index, list)
|
||||
};
|
||||
}).sort(function(left, right) {
|
||||
var a = left.criteria, b = right.criteria;
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}), 'value');
|
||||
};
|
||||
|
||||
// Groups the object's values by a criterion. Pass either a string attribute
|
||||
// to group by, or a function that returns the criterion.
|
||||
_.groupBy = function(obj, val) {
|
||||
var result = {};
|
||||
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
|
||||
each(obj, function(value, index) {
|
||||
var key = iterator(value, index);
|
||||
(result[key] || (result[key] = [])).push(value);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Use a comparator function to figure out at what index an object should
|
||||
// be inserted so as to maintain order. Uses binary search.
|
||||
_.sortedIndex = function(array, obj, iterator) {
|
||||
iterator || (iterator = _.identity);
|
||||
var low = 0, high = array.length;
|
||||
while (low < high) {
|
||||
var mid = (low + high) >> 1;
|
||||
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
|
||||
}
|
||||
return low;
|
||||
};
|
||||
|
||||
// Safely convert anything iterable into a real, live array.
|
||||
_.toArray = function(iterable) {
|
||||
if (!iterable) return [];
|
||||
if (iterable.toArray) return iterable.toArray();
|
||||
if (_.isArray(iterable)) return slice.call(iterable);
|
||||
if (_.isArguments(iterable)) return slice.call(iterable);
|
||||
return _.values(iterable);
|
||||
};
|
||||
|
||||
// Return the number of elements in an object.
|
||||
_.size = function(obj) {
|
||||
return _.toArray(obj).length;
|
||||
};
|
||||
|
||||
// Array Functions
|
||||
// ---------------
|
||||
|
||||
// Get the first element of an array. Passing **n** will return the first N
|
||||
// values in the array. Aliased as `head`. The **guard** check allows it to work
|
||||
// with `_.map`.
|
||||
_.first = _.head = function(array, n, guard) {
|
||||
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
|
||||
};
|
||||
|
||||
// Returns everything but the last entry of the array. Especcialy useful on
|
||||
// the arguments object. Passing **n** will return all the values in
|
||||
// the array, excluding the last N. The **guard** check allows it to work with
|
||||
// `_.map`.
|
||||
_.initial = function(array, n, guard) {
|
||||
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
|
||||
};
|
||||
|
||||
// Get the last element of an array. Passing **n** will return the last N
|
||||
// values in the array. The **guard** check allows it to work with `_.map`.
|
||||
_.last = function(array, n, guard) {
|
||||
if ((n != null) && !guard) {
|
||||
return slice.call(array, Math.max(array.length - n, 0));
|
||||
} else {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
};
|
||||
|
||||
// Returns everything but the first entry of the array. Aliased as `tail`.
|
||||
// Especially useful on the arguments object. Passing an **index** will return
|
||||
// the rest of the values in the array from that index onward. The **guard**
|
||||
// check allows it to work with `_.map`.
|
||||
_.rest = _.tail = function(array, index, guard) {
|
||||
return slice.call(array, (index == null) || guard ? 1 : index);
|
||||
};
|
||||
|
||||
// Trim out all falsy values from an array.
|
||||
_.compact = function(array) {
|
||||
return _.filter(array, function(value){ return !!value; });
|
||||
};
|
||||
|
||||
// Return a completely flattened version of an array.
|
||||
_.flatten = function(array, shallow) {
|
||||
return _.reduce(array, function(memo, value) {
|
||||
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
|
||||
memo[memo.length] = value;
|
||||
return memo;
|
||||
}, []);
|
||||
};
|
||||
|
||||
// Return a version of the array that does not contain the specified value(s).
|
||||
_.without = function(array) {
|
||||
return _.difference(array, slice.call(arguments, 1));
|
||||
};
|
||||
|
||||
// Produce a duplicate-free version of the array. If the array has already
|
||||
// been sorted, you have the option of using a faster algorithm.
|
||||
// Aliased as `unique`.
|
||||
_.uniq = _.unique = function(array, isSorted, iterator) {
|
||||
var initial = iterator ? _.map(array, iterator) : array;
|
||||
var result = [];
|
||||
_.reduce(initial, function(memo, el, i) {
|
||||
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
|
||||
memo[memo.length] = el;
|
||||
result[result.length] = array[i];
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Produce an array that contains the union: each distinct element from all of
|
||||
// the passed-in arrays.
|
||||
_.union = function() {
|
||||
return _.uniq(_.flatten(arguments, true));
|
||||
};
|
||||
|
||||
// Produce an array that contains every item shared between all the
|
||||
// passed-in arrays. (Aliased as "intersect" for back-compat.)
|
||||
_.intersection = _.intersect = function(array) {
|
||||
var rest = slice.call(arguments, 1);
|
||||
return _.filter(_.uniq(array), function(item) {
|
||||
return _.every(rest, function(other) {
|
||||
return _.indexOf(other, item) >= 0;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Take the difference between one array and a number of other arrays.
|
||||
// Only the elements present in just the first array will remain.
|
||||
_.difference = function(array) {
|
||||
var rest = _.flatten(slice.call(arguments, 1));
|
||||
return _.filter(array, function(value){ return !_.include(rest, value); });
|
||||
};
|
||||
|
||||
// Zip together multiple lists into a single array -- elements that share
|
||||
// an index go together.
|
||||
_.zip = function() {
|
||||
var args = slice.call(arguments);
|
||||
var length = _.max(_.pluck(args, 'length'));
|
||||
var results = new Array(length);
|
||||
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
|
||||
return results;
|
||||
};
|
||||
|
||||
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
|
||||
// we need this function. Return the position of the first occurrence of an
|
||||
// item in an array, or -1 if the item is not included in the array.
|
||||
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
|
||||
// If the array is large and already in sort order, pass `true`
|
||||
// for **isSorted** to use binary search.
|
||||
_.indexOf = function(array, item, isSorted) {
|
||||
if (array == null) return -1;
|
||||
var i, l;
|
||||
if (isSorted) {
|
||||
i = _.sortedIndex(array, item);
|
||||
return array[i] === item ? i : -1;
|
||||
}
|
||||
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
|
||||
for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
|
||||
_.lastIndexOf = function(array, item) {
|
||||
if (array == null) return -1;
|
||||
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
|
||||
var i = array.length;
|
||||
while (i--) if (i in array && array[i] === item) return i;
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Generate an integer Array containing an arithmetic progression. A port of
|
||||
// the native Python `range()` function. See
|
||||
// [the Python documentation](http://docs.python.org/library/functions.html#range).
|
||||
_.range = function(start, stop, step) {
|
||||
if (arguments.length <= 1) {
|
||||
stop = start || 0;
|
||||
start = 0;
|
||||
}
|
||||
step = arguments[2] || 1;
|
||||
|
||||
var len = Math.max(Math.ceil((stop - start) / step), 0);
|
||||
var idx = 0;
|
||||
var range = new Array(len);
|
||||
|
||||
while(idx < len) {
|
||||
range[idx++] = start;
|
||||
start += step;
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
// Function (ahem) Functions
|
||||
// ------------------
|
||||
|
||||
// Reusable constructor function for prototype setting.
|
||||
var ctor = function(){};
|
||||
|
||||
// Create a function bound to a given object (assigning `this`, and arguments,
|
||||
// optionally). Binding with arguments is also known as `curry`.
|
||||
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
|
||||
// We check for `func.bind` first, to fail fast when `func` is undefined.
|
||||
_.bind = function bind(func, context) {
|
||||
var bound, args;
|
||||
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
|
||||
if (!_.isFunction(func)) throw new TypeError;
|
||||
args = slice.call(arguments, 2);
|
||||
return bound = function() {
|
||||
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
|
||||
ctor.prototype = func.prototype;
|
||||
var self = new ctor;
|
||||
var result = func.apply(self, args.concat(slice.call(arguments)));
|
||||
if (Object(result) === result) return result;
|
||||
return self;
|
||||
};
|
||||
};
|
||||
|
||||
// Bind all of an object's methods to that object. Useful for ensuring that
|
||||
// all callbacks defined on an object belong to it.
|
||||
_.bindAll = function(obj) {
|
||||
var funcs = slice.call(arguments, 1);
|
||||
if (funcs.length == 0) funcs = _.functions(obj);
|
||||
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Memoize an expensive function by storing its results.
|
||||
_.memoize = function(func, hasher) {
|
||||
var memo = {};
|
||||
hasher || (hasher = _.identity);
|
||||
return function() {
|
||||
var key = hasher.apply(this, arguments);
|
||||
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
|
||||
};
|
||||
};
|
||||
|
||||
// Delays a function for the given number of milliseconds, and then calls
|
||||
// it with the arguments supplied.
|
||||
_.delay = function(func, wait) {
|
||||
var args = slice.call(arguments, 2);
|
||||
return setTimeout(function(){ return func.apply(func, args); }, wait);
|
||||
};
|
||||
|
||||
// Defers a function, scheduling it to run after the current call stack has
|
||||
// cleared.
|
||||
_.defer = function(func) {
|
||||
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
|
||||
};
|
||||
|
||||
// Returns a function, that, when invoked, will only be triggered at most once
|
||||
// during a given window of time.
|
||||
_.throttle = function(func, wait) {
|
||||
var context, args, timeout, throttling, more;
|
||||
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
|
||||
return function() {
|
||||
context = this; args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (more) func.apply(context, args);
|
||||
whenDone();
|
||||
};
|
||||
if (!timeout) timeout = setTimeout(later, wait);
|
||||
if (throttling) {
|
||||
more = true;
|
||||
} else {
|
||||
func.apply(context, args);
|
||||
}
|
||||
whenDone();
|
||||
throttling = true;
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds.
|
||||
_.debounce = function(func, wait) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that will be executed at most one time, no matter how
|
||||
// often you call it. Useful for lazy initialization.
|
||||
_.once = function(func) {
|
||||
var ran = false, memo;
|
||||
return function() {
|
||||
if (ran) return memo;
|
||||
ran = true;
|
||||
return memo = func.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns the first function passed as an argument to the second,
|
||||
// allowing you to adjust arguments, run code before and after, and
|
||||
// conditionally execute the original function.
|
||||
_.wrap = function(func, wrapper) {
|
||||
return function() {
|
||||
var args = [func].concat(slice.call(arguments, 0));
|
||||
return wrapper.apply(this, args);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that is the composition of a list of functions, each
|
||||
// consuming the return value of the function that follows.
|
||||
_.compose = function() {
|
||||
var funcs = arguments;
|
||||
return function() {
|
||||
var args = arguments;
|
||||
for (var i = funcs.length - 1; i >= 0; i--) {
|
||||
args = [funcs[i].apply(this, args)];
|
||||
}
|
||||
return args[0];
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that will only be executed after being called N times.
|
||||
_.after = function(times, func) {
|
||||
if (times <= 0) return func();
|
||||
return function() {
|
||||
if (--times < 1) { return func.apply(this, arguments); }
|
||||
};
|
||||
};
|
||||
|
||||
// Object Functions
|
||||
// ----------------
|
||||
|
||||
// Retrieve the names of an object's properties.
|
||||
// Delegates to **ECMAScript 5**'s native `Object.keys`
|
||||
_.keys = nativeKeys || function(obj) {
|
||||
if (obj !== Object(obj)) throw new TypeError('Invalid object');
|
||||
var keys = [];
|
||||
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
|
||||
return keys;
|
||||
};
|
||||
|
||||
// Retrieve the values of an object's properties.
|
||||
_.values = function(obj) {
|
||||
return _.map(obj, _.identity);
|
||||
};
|
||||
|
||||
// Return a sorted list of the function names available on the object.
|
||||
// Aliased as `methods`
|
||||
_.functions = _.methods = function(obj) {
|
||||
var names = [];
|
||||
for (var key in obj) {
|
||||
if (_.isFunction(obj[key])) names.push(key);
|
||||
}
|
||||
return names.sort();
|
||||
};
|
||||
|
||||
// Extend a given object with all the properties in passed-in object(s).
|
||||
_.extend = function(obj) {
|
||||
each(slice.call(arguments, 1), function(source) {
|
||||
for (var prop in source) {
|
||||
obj[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Fill in a given object with default properties.
|
||||
_.defaults = function(obj) {
|
||||
each(slice.call(arguments, 1), function(source) {
|
||||
for (var prop in source) {
|
||||
if (obj[prop] == null) obj[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Create a (shallow-cloned) duplicate of an object.
|
||||
_.clone = function(obj) {
|
||||
if (!_.isObject(obj)) return obj;
|
||||
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
|
||||
};
|
||||
|
||||
// Invokes interceptor with the obj, and then returns obj.
|
||||
// The primary purpose of this method is to "tap into" a method chain, in
|
||||
// order to perform operations on intermediate results within the chain.
|
||||
_.tap = function(obj, interceptor) {
|
||||
interceptor(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Internal recursive comparison function.
|
||||
function eq(a, b, stack) {
|
||||
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
||||
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
|
||||
if (a === b) return a !== 0 || 1 / a == 1 / b;
|
||||
// A strict comparison is necessary because `null == undefined`.
|
||||
if (a == null || b == null) return a === b;
|
||||
// Unwrap any wrapped objects.
|
||||
if (a._chain) a = a._wrapped;
|
||||
if (b._chain) b = b._wrapped;
|
||||
// Invoke a custom `isEqual` method if one is provided.
|
||||
if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
|
||||
if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
|
||||
// Compare `[[Class]]` names.
|
||||
var className = toString.call(a);
|
||||
if (className != toString.call(b)) return false;
|
||||
switch (className) {
|
||||
// Strings, numbers, dates, and booleans are compared by value.
|
||||
case '[object String]':
|
||||
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
|
||||
// equivalent to `new String("5")`.
|
||||
return a == String(b);
|
||||
case '[object Number]':
|
||||
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
|
||||
// other numeric values.
|
||||
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
|
||||
case '[object Date]':
|
||||
case '[object Boolean]':
|
||||
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
|
||||
// millisecond representations. Note that invalid dates with millisecond representations
|
||||
// of `NaN` are not equivalent.
|
||||
return +a == +b;
|
||||
// RegExps are compared by their source patterns and flags.
|
||||
case '[object RegExp]':
|
||||
return a.source == b.source &&
|
||||
a.global == b.global &&
|
||||
a.multiline == b.multiline &&
|
||||
a.ignoreCase == b.ignoreCase;
|
||||
}
|
||||
if (typeof a != 'object' || typeof b != 'object') return false;
|
||||
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
||||
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
||||
var length = stack.length;
|
||||
while (length--) {
|
||||
// Linear search. Performance is inversely proportional to the number of
|
||||
// unique nested structures.
|
||||
if (stack[length] == a) return true;
|
||||
}
|
||||
// Add the first object to the stack of traversed objects.
|
||||
stack.push(a);
|
||||
var size = 0, result = true;
|
||||
// Recursively compare objects and arrays.
|
||||
if (className == '[object Array]') {
|
||||
// Compare array lengths to determine if a deep comparison is necessary.
|
||||
size = a.length;
|
||||
result = size == b.length;
|
||||
if (result) {
|
||||
// Deep compare the contents, ignoring non-numeric properties.
|
||||
while (size--) {
|
||||
// Ensure commutative equality for sparse arrays.
|
||||
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Objects with different constructors are not equivalent.
|
||||
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
|
||||
// Deep compare objects.
|
||||
for (var key in a) {
|
||||
if (_.has(a, key)) {
|
||||
// Count the expected number of properties.
|
||||
size++;
|
||||
// Deep compare each member.
|
||||
if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
|
||||
}
|
||||
}
|
||||
// Ensure that both objects contain the same number of properties.
|
||||
if (result) {
|
||||
for (key in b) {
|
||||
if (_.has(b, key) && !(size--)) break;
|
||||
}
|
||||
result = !size;
|
||||
}
|
||||
}
|
||||
// Remove the first object from the stack of traversed objects.
|
||||
stack.pop();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Perform a deep comparison to check if two objects are equal.
|
||||
_.isEqual = function(a, b) {
|
||||
return eq(a, b, []);
|
||||
};
|
||||
|
||||
// Is a given array, string, or object empty?
|
||||
// An "empty" object has no enumerable own-properties.
|
||||
_.isEmpty = function(obj) {
|
||||
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
|
||||
for (var key in obj) if (_.has(obj, key)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Is a given value a DOM element?
|
||||
_.isElement = function(obj) {
|
||||
return !!(obj && obj.nodeType == 1);
|
||||
};
|
||||
|
||||
// Is a given value an array?
|
||||
// Delegates to ECMA5's native Array.isArray
|
||||
_.isArray = nativeIsArray || function(obj) {
|
||||
return toString.call(obj) == '[object Array]';
|
||||
};
|
||||
|
||||
// Is a given variable an object?
|
||||
_.isObject = function(obj) {
|
||||
return obj === Object(obj);
|
||||
};
|
||||
|
||||
// Is a given variable an arguments object?
|
||||
_.isArguments = function(obj) {
|
||||
return toString.call(obj) == '[object Arguments]';
|
||||
};
|
||||
if (!_.isArguments(arguments)) {
|
||||
_.isArguments = function(obj) {
|
||||
return !!(obj && _.has(obj, 'callee'));
|
||||
};
|
||||
}
|
||||
|
||||
// Is a given value a function?
|
||||
_.isFunction = function(obj) {
|
||||
return toString.call(obj) == '[object Function]';
|
||||
};
|
||||
|
||||
// Is a given value a string?
|
||||
_.isString = function(obj) {
|
||||
return toString.call(obj) == '[object String]';
|
||||
};
|
||||
|
||||
// Is a given value a number?
|
||||
_.isNumber = function(obj) {
|
||||
return toString.call(obj) == '[object Number]';
|
||||
};
|
||||
|
||||
// Is the given value `NaN`?
|
||||
_.isNaN = function(obj) {
|
||||
// `NaN` is the only value for which `===` is not reflexive.
|
||||
return obj !== obj;
|
||||
};
|
||||
|
||||
// Is a given value a boolean?
|
||||
_.isBoolean = function(obj) {
|
||||
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
|
||||
};
|
||||
|
||||
// Is a given value a date?
|
||||
_.isDate = function(obj) {
|
||||
return toString.call(obj) == '[object Date]';
|
||||
};
|
||||
|
||||
// Is the given value a regular expression?
|
||||
_.isRegExp = function(obj) {
|
||||
return toString.call(obj) == '[object RegExp]';
|
||||
};
|
||||
|
||||
// Is a given value equal to null?
|
||||
_.isNull = function(obj) {
|
||||
return obj === null;
|
||||
};
|
||||
|
||||
// Is a given variable undefined?
|
||||
_.isUndefined = function(obj) {
|
||||
return obj === void 0;
|
||||
};
|
||||
|
||||
// Has own property?
|
||||
_.has = function(obj, key) {
|
||||
return hasOwnProperty.call(obj, key);
|
||||
};
|
||||
|
||||
// Utility Functions
|
||||
// -----------------
|
||||
|
||||
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
|
||||
// previous owner. Returns a reference to the Underscore object.
|
||||
_.noConflict = function() {
|
||||
root._ = previousUnderscore;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Keep the identity function around for default iterators.
|
||||
_.identity = function(value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
// Run a function **n** times.
|
||||
_.times = function (n, iterator, context) {
|
||||
for (var i = 0; i < n; i++) iterator.call(context, i);
|
||||
};
|
||||
|
||||
// Escape a string for HTML interpolation.
|
||||
_.escape = function(string) {
|
||||
return (''+string).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
|
||||
};
|
||||
|
||||
// Add your own custom functions to the Underscore object, ensuring that
|
||||
// they're correctly added to the OOP wrapper as well.
|
||||
_.mixin = function(obj) {
|
||||
each(_.functions(obj), function(name){
|
||||
addToWrapper(name, _[name] = obj[name]);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate a unique integer id (unique within the entire client session).
|
||||
// Useful for temporary DOM ids.
|
||||
var idCounter = 0;
|
||||
_.uniqueId = function(prefix) {
|
||||
var id = idCounter++;
|
||||
return prefix ? prefix + id : id;
|
||||
};
|
||||
|
||||
// By default, Underscore uses ERB-style template delimiters, change the
|
||||
// following template settings to use alternative delimiters.
|
||||
_.templateSettings = {
|
||||
evaluate : /<%([\s\S]+?)%>/g,
|
||||
interpolate : /<%=([\s\S]+?)%>/g,
|
||||
escape : /<%-([\s\S]+?)%>/g
|
||||
};
|
||||
|
||||
// When customizing `templateSettings`, if you don't want to define an
|
||||
// interpolation, evaluation or escaping regex, we need one that is
|
||||
// guaranteed not to match.
|
||||
var noMatch = /.^/;
|
||||
|
||||
// Within an interpolation, evaluation, or escaping, remove HTML escaping
|
||||
// that had been previously added.
|
||||
var unescape = function(code) {
|
||||
return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
|
||||
};
|
||||
|
||||
// JavaScript micro-templating, similar to John Resig's implementation.
|
||||
// Underscore templating handles arbitrary delimiters, preserves whitespace,
|
||||
// and correctly escapes quotes within interpolated code.
|
||||
_.template = function(str, data) {
|
||||
var c = _.templateSettings;
|
||||
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
|
||||
'with(obj||{}){__p.push(\'' +
|
||||
str.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(c.escape || noMatch, function(match, code) {
|
||||
return "',_.escape(" + unescape(code) + "),'";
|
||||
})
|
||||
.replace(c.interpolate || noMatch, function(match, code) {
|
||||
return "'," + unescape(code) + ",'";
|
||||
})
|
||||
.replace(c.evaluate || noMatch, function(match, code) {
|
||||
return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
|
||||
})
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\t/g, '\\t')
|
||||
+ "');}return __p.join('');";
|
||||
var func = new Function('obj', '_', tmpl);
|
||||
if (data) return func(data, _);
|
||||
return function(data) {
|
||||
return func.call(this, data, _);
|
||||
};
|
||||
};
|
||||
|
||||
// Add a "chain" function, which will delegate to the wrapper.
|
||||
_.chain = function(obj) {
|
||||
return _(obj).chain();
|
||||
};
|
||||
|
||||
// The OOP Wrapper
|
||||
// ---------------
|
||||
|
||||
// If Underscore is called as a function, it returns a wrapped object that
|
||||
// can be used OO-style. This wrapper holds altered versions of all the
|
||||
// underscore functions. Wrapped objects may be chained.
|
||||
var wrapper = function(obj) { this._wrapped = obj; };
|
||||
|
||||
// Expose `wrapper.prototype` as `_.prototype`
|
||||
_.prototype = wrapper.prototype;
|
||||
|
||||
// Helper function to continue chaining intermediate results.
|
||||
var result = function(obj, chain) {
|
||||
return chain ? _(obj).chain() : obj;
|
||||
};
|
||||
|
||||
// A method to easily add functions to the OOP wrapper.
|
||||
var addToWrapper = function(name, func) {
|
||||
wrapper.prototype[name] = function() {
|
||||
var args = slice.call(arguments);
|
||||
unshift.call(args, this._wrapped);
|
||||
return result(func.apply(_, args), this._chain);
|
||||
};
|
||||
};
|
||||
|
||||
// Add all of the Underscore functions to the wrapper object.
|
||||
_.mixin(_);
|
||||
|
||||
// Add all mutator Array functions to the wrapper.
|
||||
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
|
||||
var method = ArrayProto[name];
|
||||
wrapper.prototype[name] = function() {
|
||||
var wrapped = this._wrapped;
|
||||
method.apply(wrapped, arguments);
|
||||
var length = wrapped.length;
|
||||
if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
|
||||
return result(wrapped, this._chain);
|
||||
};
|
||||
});
|
||||
|
||||
// Add all accessor Array functions to the wrapper.
|
||||
each(['concat', 'join', 'slice'], function(name) {
|
||||
var method = ArrayProto[name];
|
||||
wrapper.prototype[name] = function() {
|
||||
return result(method.apply(this._wrapped, arguments), this._chain);
|
||||
};
|
||||
});
|
||||
|
||||
// Start chaining a wrapped Underscore object.
|
||||
wrapper.prototype.chain = function() {
|
||||
this._chain = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Extracts the result from a wrapped and chained object.
|
||||
wrapper.prototype.value = function() {
|
||||
return this._wrapped;
|
||||
};
|
||||
|
||||
}).call(this);
|
335
src/static/js/undomodule.js
Normal file
|
@ -0,0 +1,335 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var Changeset = require('./Changeset');
|
||||
var _ = require('./underscore');
|
||||
|
||||
var undoModule = (function()
|
||||
{
|
||||
var stack = (function()
|
||||
{
|
||||
var stackElements = [];
|
||||
// two types of stackElements:
|
||||
// 1) { elementType: UNDOABLE_EVENT, eventType: "anything", [backset: <changeset>,]
|
||||
// [selStart: <char number>, selEnd: <char number>, selFocusAtStart: <boolean>] }
|
||||
// 2) { elementType: EXTERNAL_CHANGE, changeset: <changeset> }
|
||||
// invariant: no two consecutive EXTERNAL_CHANGEs
|
||||
var numUndoableEvents = 0;
|
||||
|
||||
var UNDOABLE_EVENT = "undoableEvent";
|
||||
var EXTERNAL_CHANGE = "externalChange";
|
||||
|
||||
function clearStack()
|
||||
{
|
||||
stackElements.length = 0;
|
||||
stackElements.push(
|
||||
{
|
||||
elementType: UNDOABLE_EVENT,
|
||||
eventType: "bottom"
|
||||
});
|
||||
numUndoableEvents = 1;
|
||||
}
|
||||
clearStack();
|
||||
|
||||
function pushEvent(event)
|
||||
{
|
||||
var e = _.extend(
|
||||
{}, event);
|
||||
e.elementType = UNDOABLE_EVENT;
|
||||
stackElements.push(e);
|
||||
numUndoableEvents++;
|
||||
//dmesg("pushEvent backset: "+event.backset);
|
||||
}
|
||||
|
||||
function pushExternalChange(cs)
|
||||
{
|
||||
var idx = stackElements.length - 1;
|
||||
if (stackElements[idx].elementType == EXTERNAL_CHANGE)
|
||||
{
|
||||
stackElements[idx].changeset = Changeset.compose(stackElements[idx].changeset, cs, getAPool());
|
||||
}
|
||||
else
|
||||
{
|
||||
stackElements.push(
|
||||
{
|
||||
elementType: EXTERNAL_CHANGE,
|
||||
changeset: cs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _exposeEvent(nthFromTop)
|
||||
{
|
||||
// precond: 0 <= nthFromTop < numUndoableEvents
|
||||
var targetIndex = stackElements.length - 1 - nthFromTop;
|
||||
var idx = stackElements.length - 1;
|
||||
while (idx > targetIndex || stackElements[idx].elementType == EXTERNAL_CHANGE)
|
||||
{
|
||||
if (stackElements[idx].elementType == EXTERNAL_CHANGE)
|
||||
{
|
||||
var ex = stackElements[idx];
|
||||
var un = stackElements[idx - 1];
|
||||
if (un.backset)
|
||||
{
|
||||
var excs = ex.changeset;
|
||||
var unbs = un.backset;
|
||||
un.backset = Changeset.follow(excs, un.backset, false, getAPool());
|
||||
ex.changeset = Changeset.follow(unbs, ex.changeset, true, getAPool());
|
||||
if ((typeof un.selStart) == "number")
|
||||
{
|
||||
var newSel = Changeset.characterRangeFollow(excs, un.selStart, un.selEnd);
|
||||
un.selStart = newSel[0];
|
||||
un.selEnd = newSel[1];
|
||||
if (un.selStart == un.selEnd)
|
||||
{
|
||||
un.selFocusAtStart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
stackElements[idx - 1] = ex;
|
||||
stackElements[idx] = un;
|
||||
if (idx >= 2 && stackElements[idx - 2].elementType == EXTERNAL_CHANGE)
|
||||
{
|
||||
ex.changeset = Changeset.compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
|
||||
stackElements.splice(idx - 2, 1);
|
||||
idx--;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
idx--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNthFromTop(n)
|
||||
{
|
||||
// precond: 0 <= n < numEvents()
|
||||
_exposeEvent(n);
|
||||
return stackElements[stackElements.length - 1 - n];
|
||||
}
|
||||
|
||||
function numEvents()
|
||||
{
|
||||
return numUndoableEvents;
|
||||
}
|
||||
|
||||
function popEvent()
|
||||
{
|
||||
// precond: numEvents() > 0
|
||||
_exposeEvent(0);
|
||||
numUndoableEvents--;
|
||||
return stackElements.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
numEvents: numEvents,
|
||||
popEvent: popEvent,
|
||||
pushEvent: pushEvent,
|
||||
pushExternalChange: pushExternalChange,
|
||||
clearStack: clearStack,
|
||||
getNthFromTop: getNthFromTop
|
||||
};
|
||||
})();
|
||||
|
||||
// invariant: stack always has at least one undoable event
|
||||
var undoPtr = 0; // zero-index from top of stack, 0 == top
|
||||
|
||||
function clearHistory()
|
||||
{
|
||||
stack.clearStack();
|
||||
undoPtr = 0;
|
||||
}
|
||||
|
||||
function _charOccurrences(str, c)
|
||||
{
|
||||
var i = 0;
|
||||
var count = 0;
|
||||
while (i >= 0 && i < str.length)
|
||||
{
|
||||
i = str.indexOf(c, i);
|
||||
if (i >= 0)
|
||||
{
|
||||
count++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function _opcodeOccurrences(cs, opcode)
|
||||
{
|
||||
return _charOccurrences(Changeset.unpack(cs).ops, opcode);
|
||||
}
|
||||
|
||||
function _mergeChangesets(cs1, cs2)
|
||||
{
|
||||
if (!cs1) return cs2;
|
||||
if (!cs2) return cs1;
|
||||
|
||||
// Rough heuristic for whether changesets should be considered one action:
|
||||
// each does exactly one insertion, no dels, and the composition does also; or
|
||||
// each does exactly one deletion, no ins, and the composition does also.
|
||||
// A little weird in that it won't merge "make bold" with "insert char"
|
||||
// but will merge "make bold and insert char" with "insert char",
|
||||
// though that isn't expected to come up.
|
||||
var plusCount1 = _opcodeOccurrences(cs1, '+');
|
||||
var plusCount2 = _opcodeOccurrences(cs2, '+');
|
||||
var minusCount1 = _opcodeOccurrences(cs1, '-');
|
||||
var minusCount2 = _opcodeOccurrences(cs2, '-');
|
||||
if (plusCount1 == 1 && plusCount2 == 1 && minusCount1 == 0 && minusCount2 == 0)
|
||||
{
|
||||
var merge = Changeset.compose(cs1, cs2, getAPool());
|
||||
var plusCount3 = _opcodeOccurrences(merge, '+');
|
||||
var minusCount3 = _opcodeOccurrences(merge, '-');
|
||||
if (plusCount3 == 1 && minusCount3 == 0)
|
||||
{
|
||||
return merge;
|
||||
}
|
||||
}
|
||||
else if (plusCount1 == 0 && plusCount2 == 0 && minusCount1 == 1 && minusCount2 == 1)
|
||||
{
|
||||
var merge = Changeset.compose(cs1, cs2, getAPool());
|
||||
var plusCount3 = _opcodeOccurrences(merge, '+');
|
||||
var minusCount3 = _opcodeOccurrences(merge, '-');
|
||||
if (plusCount3 == 0 && minusCount3 == 1)
|
||||
{
|
||||
return merge;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function reportEvent(event)
|
||||
{
|
||||
var topEvent = stack.getNthFromTop(0);
|
||||
|
||||
function applySelectionToTop()
|
||||
{
|
||||
if ((typeof event.selStart) == "number")
|
||||
{
|
||||
topEvent.selStart = event.selStart;
|
||||
topEvent.selEnd = event.selEnd;
|
||||
topEvent.selFocusAtStart = event.selFocusAtStart;
|
||||
}
|
||||
}
|
||||
|
||||
if ((!event.backset) || Changeset.isIdentity(event.backset))
|
||||
{
|
||||
applySelectionToTop();
|
||||
}
|
||||
else
|
||||
{
|
||||
var merged = false;
|
||||
if (topEvent.eventType == event.eventType)
|
||||
{
|
||||
var merge = _mergeChangesets(event.backset, topEvent.backset);
|
||||
if (merge)
|
||||
{
|
||||
topEvent.backset = merge;
|
||||
//dmesg("reportEvent merge: "+merge);
|
||||
applySelectionToTop();
|
||||
merged = true;
|
||||
}
|
||||
}
|
||||
if (!merged)
|
||||
{
|
||||
stack.pushEvent(event);
|
||||
}
|
||||
undoPtr = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function reportExternalChange(changeset)
|
||||
{
|
||||
if (changeset && !Changeset.isIdentity(changeset))
|
||||
{
|
||||
stack.pushExternalChange(changeset);
|
||||
}
|
||||
}
|
||||
|
||||
function _getSelectionInfo(event)
|
||||
{
|
||||
if ((typeof event.selStart) != "number")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return {
|
||||
selStart: event.selStart,
|
||||
selEnd: event.selEnd,
|
||||
selFocusAtStart: event.selFocusAtStart
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For "undo" and "redo", the change event must be returned
|
||||
// by eventFunc and NOT reported through the normal mechanism.
|
||||
// "eventFunc" should take a changeset and an optional selection info object,
|
||||
// or can be called with no arguments to mean that no undo is possible.
|
||||
// "eventFunc" will be called exactly once.
|
||||
|
||||
function performUndo(eventFunc)
|
||||
{
|
||||
if (undoPtr < stack.numEvents() - 1)
|
||||
{
|
||||
var backsetEvent = stack.getNthFromTop(undoPtr);
|
||||
var selectionEvent = stack.getNthFromTop(undoPtr + 1);
|
||||
var undoEvent = eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
|
||||
stack.pushEvent(undoEvent);
|
||||
undoPtr += 2;
|
||||
}
|
||||
else eventFunc();
|
||||
}
|
||||
|
||||
function performRedo(eventFunc)
|
||||
{
|
||||
if (undoPtr >= 2)
|
||||
{
|
||||
var backsetEvent = stack.getNthFromTop(0);
|
||||
var selectionEvent = stack.getNthFromTop(1);
|
||||
eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
|
||||
stack.popEvent();
|
||||
undoPtr -= 2;
|
||||
}
|
||||
else eventFunc();
|
||||
}
|
||||
|
||||
function getAPool()
|
||||
{
|
||||
return undoModule.apool;
|
||||
}
|
||||
|
||||
return {
|
||||
clearHistory: clearHistory,
|
||||
reportEvent: reportEvent,
|
||||
reportExternalChange: reportExternalChange,
|
||||
performUndo: performUndo,
|
||||
performRedo: performRedo,
|
||||
enabled: true,
|
||||
apool: null
|
||||
}; // apool is filled in by caller
|
||||
})();
|
||||
|
||||
exports.undoModule = undoModule;
|
404
src/static/js/unorm.js
Normal file
388
src/static/js/virtual_lines.js
Normal file
|
@ -0,0 +1,388 @@
|
|||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
* This helps other people to understand this code better and helps them to improve it.
|
||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function makeVirtualLineView(lineNode)
|
||||
{
|
||||
|
||||
// how much to jump forward or backward at once in a charSeeker before
|
||||
// constructing a DOM node and checking the coordinates (which takes a
|
||||
// significant fraction of a millisecond). From the
|
||||
// coordinates and the approximate line height we can estimate how
|
||||
// many lines we have moved. We risk being off if the number of lines
|
||||
// we move is on the order of the line height in pixels. Fortunately,
|
||||
// when the user boosts the font-size they increase both.
|
||||
var maxCharIncrement = 20;
|
||||
var seekerAtEnd = null;
|
||||
|
||||
function getNumChars()
|
||||
{
|
||||
return lineNode.textContent.length;
|
||||
}
|
||||
|
||||
function getNumVirtualLines()
|
||||
{
|
||||
if (!seekerAtEnd)
|
||||
{
|
||||
var seeker = makeCharSeeker();
|
||||
seeker.forwardByWhile(maxCharIncrement);
|
||||
seekerAtEnd = seeker;
|
||||
}
|
||||
return seekerAtEnd.getVirtualLine() + 1;
|
||||
}
|
||||
|
||||
function getVLineAndOffsetForChar(lineChar)
|
||||
{
|
||||
var seeker = makeCharSeeker();
|
||||
seeker.forwardByWhile(maxCharIncrement, null, lineChar);
|
||||
var theLine = seeker.getVirtualLine();
|
||||
seeker.backwardByWhile(8, function()
|
||||
{
|
||||
return seeker.getVirtualLine() == theLine;
|
||||
});
|
||||
seeker.forwardByWhile(1, function()
|
||||
{
|
||||
return seeker.getVirtualLine() != theLine;
|
||||
});
|
||||
var lineStartChar = seeker.getOffset();
|
||||
return {
|
||||
vline: theLine,
|
||||
offset: (lineChar - lineStartChar)
|
||||
};
|
||||
}
|
||||
|
||||
function getCharForVLineAndOffset(vline, offset)
|
||||
{
|
||||
// returns revised vline and offset as well as absolute char index within line.
|
||||
// if offset is beyond end of line, for example, will give new offset at end of line.
|
||||
var seeker = makeCharSeeker();
|
||||
// go to start of line
|
||||
seeker.binarySearch(function()
|
||||
{
|
||||
return seeker.getVirtualLine() >= vline;
|
||||
});
|
||||
var lineStart = seeker.getOffset();
|
||||
var theLine = seeker.getVirtualLine();
|
||||
// go to offset, overshooting the virtual line only if offset is too large for it
|
||||
seeker.forwardByWhile(maxCharIncrement, null, lineStart + offset);
|
||||
// get back into line
|
||||
seeker.backwardByWhile(1, function()
|
||||
{
|
||||
return seeker.getVirtualLine() != theLine;
|
||||
}, lineStart);
|
||||
var lineChar = seeker.getOffset();
|
||||
var theOffset = lineChar - lineStart;
|
||||
// handle case of last virtual line; should be able to be at end of it
|
||||
if (theOffset < offset && theLine == (getNumVirtualLines() - 1))
|
||||
{
|
||||
var lineLen = getNumChars();
|
||||
theOffset += lineLen - lineChar;
|
||||
lineChar = lineLen;
|
||||
}
|
||||
|
||||
return {
|
||||
vline: theLine,
|
||||
offset: theOffset,
|
||||
lineChar: lineChar
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getNumVirtualLines: getNumVirtualLines,
|
||||
getVLineAndOffsetForChar: getVLineAndOffsetForChar,
|
||||
getCharForVLineAndOffset: getCharForVLineAndOffset,
|
||||
makeCharSeeker: function()
|
||||
{
|
||||
return makeCharSeeker();
|
||||
}
|
||||
};
|
||||
|
||||
function deepFirstChildTextNode(nd)
|
||||
{
|
||||
nd = nd.firstChild;
|
||||
while (nd && nd.firstChild) nd = nd.firstChild;
|
||||
if (nd.data) return nd;
|
||||
return null;
|
||||
}
|
||||
|
||||
function makeCharSeeker( /*lineNode*/ )
|
||||
{
|
||||
|
||||
function charCoords(tnode, i)
|
||||
{
|
||||
var container = tnode.parentNode;
|
||||
|
||||
// treat space specially; a space at the end of a virtual line
|
||||
// will have weird coordinates
|
||||
var isSpace = (tnode.nodeValue.charAt(i) === " ");
|
||||
if (isSpace)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
if (container.previousSibling && deepFirstChildTextNode(container.previousSibling))
|
||||
{
|
||||
tnode = deepFirstChildTextNode(container.previousSibling);
|
||||
i = tnode.length - 1;
|
||||
container = tnode.parentNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
return {
|
||||
top: container.offsetTop,
|
||||
left: container.offsetLeft
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
i--; // use previous char
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var charWrapper = document.createElement("SPAN");
|
||||
|
||||
// wrap the character
|
||||
var tnodeText = tnode.nodeValue;
|
||||
var frag = document.createDocumentFragment();
|
||||
frag.appendChild(document.createTextNode(tnodeText.substring(0, i)));
|
||||
charWrapper.appendChild(document.createTextNode(tnodeText.substr(i, 1)));
|
||||
frag.appendChild(charWrapper);
|
||||
frag.appendChild(document.createTextNode(tnodeText.substring(i + 1)));
|
||||
container.replaceChild(frag, tnode);
|
||||
|
||||
var result = {
|
||||
top: charWrapper.offsetTop,
|
||||
left: charWrapper.offsetLeft + (isSpace ? charWrapper.offsetWidth : 0),
|
||||
height: charWrapper.offsetHeight
|
||||
};
|
||||
|
||||
while (container.firstChild) container.removeChild(container.firstChild);
|
||||
container.appendChild(tnode);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var lineText = lineNode.textContent;
|
||||
var lineLength = lineText.length;
|
||||
|
||||
var curNode = null;
|
||||
var curChar = 0;
|
||||
var curCharWithinNode = 0
|
||||
var curTop;
|
||||
var curLeft;
|
||||
var approxLineHeight;
|
||||
var whichLine = 0;
|
||||
|
||||
function nextNode()
|
||||
{
|
||||
var n = curNode;
|
||||
if (!n) n = lineNode.firstChild;
|
||||
else n = n.nextSibling;
|
||||
while (n && !deepFirstChildTextNode(n))
|
||||
{
|
||||
n = n.nextSibling;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function prevNode()
|
||||
{
|
||||
var n = curNode;
|
||||
if (!n) n = lineNode.lastChild;
|
||||
else n = n.previousSibling;
|
||||
while (n && !deepFirstChildTextNode(n))
|
||||
{
|
||||
n = n.previousSibling;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
var seeker;
|
||||
if (lineLength > 0)
|
||||
{
|
||||
curNode = nextNode();
|
||||
var firstCharData = charCoords(deepFirstChildTextNode(curNode), 0);
|
||||
approxLineHeight = firstCharData.height;
|
||||
curTop = firstCharData.top;
|
||||
curLeft = firstCharData.left;
|
||||
|
||||
function updateCharData(tnode, i)
|
||||
{
|
||||
var coords = charCoords(tnode, i);
|
||||
whichLine += Math.round((coords.top - curTop) / approxLineHeight);
|
||||
curTop = coords.top;
|
||||
curLeft = coords.left;
|
||||
}
|
||||
|
||||
seeker = {
|
||||
forward: function(numChars)
|
||||
{
|
||||
var oldChar = curChar;
|
||||
var newChar = curChar + numChars;
|
||||
if (newChar > (lineLength - 1)) newChar = lineLength - 1;
|
||||
while (curChar < newChar)
|
||||
{
|
||||
var curNodeLength = deepFirstChildTextNode(curNode).length;
|
||||
var toGo = curNodeLength - curCharWithinNode;
|
||||
if (curChar + toGo > newChar || !nextNode())
|
||||
{
|
||||
// going to next node would be too far
|
||||
var n = newChar - curChar;
|
||||
if (n >= toGo) n = toGo - 1;
|
||||
curChar += n;
|
||||
curCharWithinNode += n;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// go to next node
|
||||
curChar += toGo;
|
||||
curCharWithinNode = 0;
|
||||
curNode = nextNode();
|
||||
}
|
||||
}
|
||||
updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
|
||||
return curChar - oldChar;
|
||||
},
|
||||
backward: function(numChars)
|
||||
{
|
||||
var oldChar = curChar;
|
||||
var newChar = curChar - numChars;
|
||||
if (newChar < 0) newChar = 0;
|
||||
while (curChar > newChar)
|
||||
{
|
||||
if (curChar - curCharWithinNode <= newChar || !prevNode())
|
||||
{
|
||||
// going to prev node would be too far
|
||||
var n = curChar - newChar;
|
||||
if (n > curCharWithinNode) n = curCharWithinNode;
|
||||
curChar -= n;
|
||||
curCharWithinNode -= n;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// go to prev node
|
||||
curChar -= curCharWithinNode + 1;
|
||||
curNode = prevNode();
|
||||
curCharWithinNode = deepFirstChildTextNode(curNode).length - 1;
|
||||
}
|
||||
}
|
||||
updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
|
||||
return oldChar - curChar;
|
||||
},
|
||||
getVirtualLine: function()
|
||||
{
|
||||
return whichLine;
|
||||
},
|
||||
getLeftCoord: function()
|
||||
{
|
||||
return curLeft;
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
curLeft = lineNode.offsetLeft;
|
||||
seeker = {
|
||||
forward: function(numChars)
|
||||
{
|
||||
return 0;
|
||||
},
|
||||
backward: function(numChars)
|
||||
{
|
||||
return 0;
|
||||
},
|
||||
getVirtualLine: function()
|
||||
{
|
||||
return 0;
|
||||
},
|
||||
getLeftCoord: function()
|
||||
{
|
||||
return curLeft;
|
||||
}
|
||||
};
|
||||
}
|
||||
seeker.getOffset = function()
|
||||
{
|
||||
return curChar;
|
||||
};
|
||||
seeker.getLineLength = function()
|
||||
{
|
||||
return lineLength;
|
||||
};
|
||||
seeker.toString = function()
|
||||
{
|
||||
return "seeker[curChar: " + curChar + "(" + lineText.charAt(curChar) + "), left: " + seeker.getLeftCoord() + ", vline: " + seeker.getVirtualLine() + "]";
|
||||
};
|
||||
|
||||
function moveByWhile(isBackward, amount, optCondFunc, optCharLimit)
|
||||
{
|
||||
var charsMovedLast = null;
|
||||
var hasCondFunc = ((typeof optCondFunc) == "function");
|
||||
var condFunc = optCondFunc;
|
||||
var hasCharLimit = ((typeof optCharLimit) == "number");
|
||||
var charLimit = optCharLimit;
|
||||
while (charsMovedLast !== 0 && ((!hasCondFunc) || condFunc()))
|
||||
{
|
||||
var toMove = amount;
|
||||
if (hasCharLimit)
|
||||
{
|
||||
var untilLimit = (isBackward ? curChar - charLimit : charLimit - curChar);
|
||||
if (untilLimit < toMove) toMove = untilLimit;
|
||||
}
|
||||
if (toMove < 0) break;
|
||||
charsMovedLast = (isBackward ? seeker.backward(toMove) : seeker.forward(toMove));
|
||||
}
|
||||
}
|
||||
|
||||
seeker.forwardByWhile = function(amount, optCondFunc, optCharLimit)
|
||||
{
|
||||
moveByWhile(false, amount, optCondFunc, optCharLimit);
|
||||
}
|
||||
seeker.backwardByWhile = function(amount, optCondFunc, optCharLimit)
|
||||
{
|
||||
moveByWhile(true, amount, optCondFunc, optCharLimit);
|
||||
}
|
||||
seeker.binarySearch = function(condFunc)
|
||||
{
|
||||
// returns index of boundary between false chars and true chars;
|
||||
// positions seeker at first true char, or else last char
|
||||
var trueFunc = condFunc;
|
||||
var falseFunc = function()
|
||||
{
|
||||
return !condFunc();
|
||||
};
|
||||
seeker.forwardByWhile(20, falseFunc);
|
||||
seeker.backwardByWhile(20, trueFunc);
|
||||
seeker.forwardByWhile(10, falseFunc);
|
||||
seeker.backwardByWhile(5, trueFunc);
|
||||
seeker.forwardByWhile(1, falseFunc);
|
||||
return seeker.getOffset() + (condFunc() ? 0 : 1);
|
||||
}
|
||||
|
||||
return seeker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.makeVirtualLineView = makeVirtualLineView;
|
3
src/static/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
User-agent: *
|
||||
Disallow: /p/
|
||||
Disallow: /newpad
|
167
src/static/tests.html
Normal file
|
@ -0,0 +1,167 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>API Test and Examples Page</title>
|
||||
<script type="text/javascript" src="js/jquery.js"></script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-size:9pt;
|
||||
background: rgba(0, 0, 0, .05);
|
||||
color: #333;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
font: 14px helvetica,sans-serif;
|
||||
background: #ccc;
|
||||
background: -moz-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
|
||||
background: -webkit-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
|
||||
background: -ms-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
|
||||
background: -o-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
|
||||
width: 1000px;
|
||||
}
|
||||
.define, #template {
|
||||
display: none;
|
||||
}
|
||||
.test_group {
|
||||
overflow: auto;
|
||||
width: 300px;
|
||||
float:left;
|
||||
color: #555;
|
||||
|
||||
border-top: 1px solid #999;
|
||||
margin: 4px;
|
||||
padding: 4px 10px 4px 10px;
|
||||
background: #eee;
|
||||
background: -webkit-linear-gradient(#fff, #ccc);
|
||||
background: -moz-linear-gradient(#fff, #ccc);
|
||||
background: -ms-linear-gradient(#fff, #ccc);
|
||||
background: -o-linear-gradient(#fff, #ccc);
|
||||
opacity: .9;
|
||||
box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.test_group h2 {
|
||||
font-size: 10pt;
|
||||
}
|
||||
.test_group table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#apikeyDIV {
|
||||
width: 100%
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$('input[type=button]').live('click', function() {
|
||||
var $test_group = $(this).closest('.test_group');
|
||||
var name = parseName($test_group.find('h2').text());
|
||||
|
||||
var results_node = $test_group.find('.results');
|
||||
|
||||
var params = {};
|
||||
$test_group.find('input[type=text]').each(function() {
|
||||
params[$(this).attr('name')] = $(this).val();
|
||||
});
|
||||
|
||||
callFunction(name, results_node, params);
|
||||
});
|
||||
|
||||
var template = $('#template')
|
||||
$('.define').each(function() {
|
||||
var functionName = parseName($(this).text());
|
||||
var parameters = parseParameters($(this).text());
|
||||
|
||||
var testGroup = template.clone();
|
||||
|
||||
testGroup.find('h2').text(functionName + "()");
|
||||
|
||||
var table = testGroup.find('table');
|
||||
|
||||
$(parameters).each(function(index, el) {
|
||||
table.prepend('<tr><td>' + el + ':</td>' +
|
||||
'<td style="width:200px"><input type="text" size="10" name="' + el + '" /></td></tr>');
|
||||
});
|
||||
|
||||
testGroup.css({display: "block"});
|
||||
testGroup.appendTo('body');
|
||||
});
|
||||
});
|
||||
|
||||
function parseName(str)
|
||||
{
|
||||
return str.substring(0, str.indexOf('('));
|
||||
}
|
||||
|
||||
function parseParameters(str)
|
||||
{
|
||||
// parse out the parameters by looking for parens
|
||||
var parens = str.substring(str.indexOf("("));
|
||||
|
||||
// return empty array if there are no paremeters
|
||||
if(parens.length < 3)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// remove parens from string
|
||||
parens = parens.substring(1);
|
||||
parens = parens.substring(0, parens.length-1);
|
||||
|
||||
return parens.split(',');
|
||||
}
|
||||
|
||||
function callFunction(memberName, results_node, params)
|
||||
{
|
||||
$('#result').text('Calling ' + memberName + "()...");
|
||||
|
||||
params["apikey"]=$("#apikey").val();
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "/api/1/" + memberName,
|
||||
data: params,
|
||||
success: function(json,status,xhr) {
|
||||
results_node.text(xhr.responseText);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
results_node.html("textStatus: " + textStatus + "<br />errorThrown: " + errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="apikeyDIV" class="test_group"><b>APIKEY: </b><input type="text" id="apikey"></div>
|
||||
<div class="test_group" id="template">
|
||||
<h2>createGroup()</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="buttonBox" colspan="2" style="text-align:right;"><input type="button" value="Run" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="results"></div>
|
||||
|
||||
</div>
|
||||
<div class="define">createGroup()</div>
|
||||
<div class="define">deleteGroup(groupID)</div>
|
||||
<div class="define">createGroupIfNotExistsFor(groupMapper)</div>
|
||||
<div class="define">listPads(groupID)</div>
|
||||
<div class="define">createPad(padID,text)</div>
|
||||
<div class="define">createGroupPad(groupID,padName,text)</div>
|
||||
<div class="define">createAuthor(name)</div>
|
||||
<div class="define">createAuthorIfNotExistsFor(authorMapper,name)</div>
|
||||
<div class="define">createSession(groupID,authorID,validUntil)</div>
|
||||
<div class="define">deleteSession(sessionID)</div>
|
||||
<div class="define">getSessionInfo(sessionID)</div>
|
||||
<div class="define">listSessionsOfGroup(groupID)</div>
|
||||
<div class="define">listSessionsOfAuthor(authorID)</div>
|
||||
<div class="define">getText(padID,rev)</div>
|
||||
<div class="define">setText(padID,text)</div>
|
||||
<div class="define">getRevisionsCount(padID)</div>
|
||||
<div class="define">getLastEdited(padID)</div>
|
||||
<div class="define">deletePad(padID)</div>
|
||||
<div class="define">getReadOnlyID(padID)</div>
|
||||
<div class="define">setPublicStatus(padID,publicStatus)</div>
|
||||
<div class="define">getPublicStatus(padID)</div>
|
||||
<div class="define">setPassword(padID,password)</div>
|
||||
<div class="define">isPasswordProtected(padID)</div>
|
||||
</body>
|
||||
</html>
|