Compare commits

...
48 Commits
Author SHA1 Message Date
or-else f2cb7d1e8b updated screenshots 2017-09-13 13:36:07 -07:00
or-else 7870fb06a2 better use of local cache 2017-09-13 12:17:24 -07:00
or-else 9adff7572e message pagination 2017-09-12 18:21:33 -07:00
or-else d50da89403 updated screenshot 2017-09-07 19:21:08 -07:00
or-else 8ddfb93f7a topic invites work correctly now, UI fixes 2017-09-07 19:10:50 -07:00
or-else b718aa9513 more context menu fixes and features 2017-09-05 22:04:14 -07:00
or-else 7782021c12 refactoring context menus 2017-09-03 21:52:59 -07:00
or-else 73b2a85eea cleaning up drafty and UI fixes in permissions editing 2017-09-02 15:57:03 -07:00
or-else d34f51a9d0 better formatted example 2017-08-24 20:16:53 -07:00
or-else 0a2abc080f a lot of refactoring of drafty to male it usable as a universal media container 2017-08-24 12:35:14 -07:00
or-else 9d1786ab67 better error handling 2017-08-21 19:53:41 -07:00
or-else fb6b4899cd rich formatting works 2017-08-21 19:45:48 -07:00
or-else 0c7549819a adding rich formatting to messags 2017-08-19 20:40:25 -07:00
or-else fd8c58e837 bug fix 2017-08-18 11:20:20 -07:00
or-else 5c74be339a added a couple of supporting methods and made representation a bit more compact 2017-08-18 11:01:51 -07:00
or-else c88c821002 added samples 2017-08-17 13:20:20 -07:00
or-else f20f83b936 urls, mentions, and hashtags are extracted and formatted correctly 2017-08-17 13:07:32 -07:00
or-else 88cc3145c4 parser and formatter work as expected 2017-08-16 17:53:39 -07:00
or-else 5d1bf5f624 first pass at defining rich text formatting 2017-08-15 20:54:59 -07:00
or-else 9fa998709d small tweaks to the logo 2017-08-10 22:35:20 +03:00
or-else 0f90626a49 make permissions editor UI clearer 2017-08-09 10:30:59 +03:00
or-else 40108d5201 incorrect value used for user2 2017-08-08 22:13:29 +03:00
or-else 0dde03cf6f more refactoring of permissions editor. still does not work 2017-08-08 18:33:30 +03:00
or-else 83fe8ea02b refactoring permissions editor 2017-08-08 11:16:50 +03:00
or-else 17876fc4b0 bug fix: an extra copy of a p2p topic was sometimes created 2017-08-07 21:14:42 +03:00
or-else eca16c8474 making default permissions work as expected 2017-08-07 17:12:43 +03:00
or-else ff0dfed2e8 change auto-login order: try token first 2017-08-07 10:06:03 +03:00
Gene db8e718002 Merge pull request #6 from riandyrn/attempt-generalized-number-of-tags-sent-to-server
[Attempt to Generalize Number of Tags Sent to Server]
2017-08-05 13:05:14 +03:00
Riandy Rahman Nugraha f49f0424d5 generalized to_tag() into to_tags() 2017-08-05 16:59:56 +07:00
or-else 998ddfd54b changed new code for style and made it usable for other non-email tags 2017-08-04 14:47:18 +03:00
Gene 53e67de669 Merge pull request #5 from riandyrn/riandy-branch
[Attempt to Make New User Discoverable]
2017-08-04 14:37:36 +03:00
Riandy Rahman Nugraha 897bb0719a remove tinode.js from example app —> tinode.js already has its own repo 2017-08-04 09:11:29 +07:00
Riandy Rahman Nugraha d051c84737 explicitly set defacs for newly created account to “JRWPS”; add email as tags in newly created account, so it could be discovered by others; 2017-08-03 09:15:31 +07:00
or-else 578df160d1 cosmetic change 2017-07-23 18:23:35 +03:00
or-else ac335a7a5c newer screenshot 2017-07-15 18:22:53 +03:00
or-else 5868915f2c permissions editing works now 2017-07-15 12:35:53 +03:00
or-else 1054c81de3 Merge branch 'devel' 2017-07-10 20:34:40 +03:00
or-else 79756602a7 topic member management complete 2017-07-10 19:29:35 +03:00
or-else 8d5f446ccd member management is complete now 2017-07-09 11:22:27 +03:00
or-else b689d08e71 subscriber management is somewhat functional 2017-07-06 16:15:56 +03:00
or-else 58ae90b574 added mateerial chips widget 2017-07-04 19:23:07 +03:00
or-else e79444e5f7 lets of bug fixes 2017-07-02 16:16:45 +03:00
or-else 45eb91765b more refactoring of topic and account editing 2017-06-29 14:02:16 +03:00
or-else 933ede1220 separated contact list into a component 2017-06-27 20:58:27 +03:00
or-else 506bd42201 documentation update 2017-06-25 20:40:17 +03:00
or-else b3ef0a856b name and version 2017-06-25 15:33:20 +03:00
or-else 732c17ad76 make mobile screenshots bigger 2017-06-25 15:32:15 +03:00
or-else 8ee22694f1 fit three images in a row 2017-06-25 15:29:25 +03:00
15 changed files with 2495 additions and 512 deletions
+1
View File
@@ -0,0 +1 @@
tinode.js
+10 -11
View File
@@ -2,39 +2,38 @@
Example using [Tinode](https://github.com/tinode/chat/) to build a single-page chat application. The app uses
[React](https://facebook.github.io/react/), Google's material design [fonts](https://www.google.com/fonts/)
and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). The [Tinode javascript library](https://github.com/tinode/tinode-js/) has no external dependencies.
and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). The [Tinode javascript library](https://github.com/tinode/tinode-js/) has no external dependencies. Overall it's a lot like open source WhatsApp or Telegram web apps.
This is work in progress. Bugs are expected. The app was tested in the latest Chrome & Firefox only.
Try it (possibly older version) live at http://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`.
Try it (possibly older version) live at http://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `17025550001` through `17025550009`. The demo is reset every night at 2:15 am Pacific time.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
Photos from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
Sample avatar photos from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
## Missing functionality
* Cannot manage group members.
* Editing chat parameters is incomplete and buggy.
* Cannot send images or other media.
* Cannot delete messages.
* No typing indicators.
* No indicators of chat status (muted, blocked, etc.).
* Long polling as a wire transport does not work.
* No indication of chat status (muted, blocked, etc.).
* Browser push notifications are missing.
* No encryption.
* Editing chat permissions is incomplete.
## Responsive design
### Desktop
### Desktop screenshot
<p align="center">
<img src="web-desktop.png" alt="Desktop web: full app" width=866 />
<img src="web-desktop-1.png" alt="Desktop web: full app" width=866 />
</p>
### Mobile
### Mobile screenshots
<p align="center">
<img src="web-mob-contacts.png" alt="Mobile web: contacts" width=323 /> <img src="web-mob-chat.png" alt="Mobile web: chat" width=323 /> <img src="web-mob-topicinfo.png" alt="Mobile web: topic info" width=323 />
<img src="web-mob-contacts-1.png" alt="Mobile web: contacts" width=323 /> <img src="web-mob-chat-1.png" alt="Mobile web: chat" width=323 /> <img src="web-mob-info-1.png" alt="Mobile web: topic info" width=323 /> <img src="web-mob-new-chat-1.png" alt="Mobile web: start new chat" width=323 />
</p>
+348 -26
View File
@@ -37,17 +37,41 @@ a:focus {
outline-offset: -2px;
}
a.red:hover,
a.red:focus {
color: #F44336;
}
a.flat-button {
display: flex;
align-items: center;
margin: 0.15rem 0.35rem;
height: 2em;
line-height: 2em;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
white-space: nowrap;
text-transform: uppercase;
}
a.flat-button:hover,
a.flat-button:focus {
text-decoration: none;
}
form {
font-size: 1.1rem;
padding: 0.5rem 0.75rem;
line-height: 1.75;
line-height: 1.5;
}
form label {
label {
font-size: 1.1rem;
margin-right: auto;
}
form label.small {
label.small {
color: #1e88e5;
font-size: 0.95rem;
}
@@ -159,7 +183,7 @@ textarea {
height: 2.5rem;
border-bottom: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
transition: all 0.1s ease-in-out;
width: 100%;
}
textarea:focus,
@@ -225,6 +249,26 @@ ul.menu > li.separator:hover {
}
/* END popup/dropdown menu */
/* Generic text colors */
.blue {
color: #2196f3; /* blue 500 */
}
.red {
color: #D32F2F; /* red 700 */
}
.hr {
display: block;
width: 100%;
height: 0.6rem;
background-color: #f0f0f0;
box-shadow: inset 0 0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15),
inset 0 -0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15);
}
/* END generic text colors */
/* END of basic elements */
#mountPoint {
@@ -251,7 +295,7 @@ ul.menu > li.separator:hover {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 0.3;
flex-grow: 0;
flex-shrink: 0.3;
width: 24rem;
min-width: 18rem;
@@ -268,7 +312,7 @@ ul.menu > li.separator:hover {
#info-view {
display: flex;
flex-direction: column;
flex-grow: 0.3;
flex-grow: 0;
flex-shrink: 0.3;
width: 24rem;
min-width: 18rem;
@@ -349,6 +393,37 @@ ul.menu > li.separator:hover {
}
/* END of InPlaceEdit styles */
table.permission-editor {
width: 100%;
}
table.permission-editor td {
margin: 0.35rem 0;
font-size: 1.1rem;
vertical-align: middle;
}
table.permission-editor td.checkbox {
text-align: center;
color: #999;
}
.permission-editor .material-icons {
font-size: 1.1rem;
line-height: 1.1rem;
vertical-align: middle;
}
.material-icons.clickable {
cursor: pointer;
border-bottom: none;
}
.clickable {
border-bottom: 1px dashed #ddd;
cursor: pointer;
}
/* END of top-level layout */
/* Sidepanel styles */
@@ -373,7 +448,12 @@ ul.menu > li.separator:hover {
justify-content: flex-end;
}
.dialog-buttons button {
margin: 0 0.5rem;
}
.avatar-box {
position: relative;
flex-grow: 0;
flex-shrink: 0;
}
@@ -446,21 +526,27 @@ ul.menu > li.separator:hover {
overflow: hidden;
}
.contacts-panel {
.scrollable-panel {
overflow-y: auto;
margin-top: 1rem;:
margin-top: 0.25rem;
margin-bottom: auto;
height: 100%;
}
.contact-box {
list-style: none;
overflow-x: hidden;
}
.contact-box > li {
display: flex;
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
min-height: 3.5rem;
flex-grow: 0;
flex-shrink: 0;
border-bottom: 1px solid #eee;
border-collapse: collapse;
overflow-x: hidden;
}
.contact-box > li:hover {
background-color: #eceff1; /* blue-gray 50 highlight */
@@ -469,8 +555,20 @@ ul.menu > li.separator:hover {
background-color: #cfd8dc; /* blue-gray 100 highlight */
}
.contact-box .text-box {
overflow-x: hidden;
}
.contact-box .contact-title {
font-size: 1.05rem;
text-overflow: ellipsis;
overflow: hidden
}
.contact-box .contact-comment {
text-overflow: ellipsis;
overflow: hidden;
font-weight: lighter;
}
/* The down arrow head - trigger for context menu in contacts list */
@@ -521,7 +619,7 @@ ul.menu > li.separator:hover {
border-radius: 50%;
position: absolute;
right: 0;
bottom: 0;
bottom: 0.1rem;
}
.contact-box .online {
background-color: #4caf50;
@@ -529,6 +627,22 @@ ul.menu > li.separator:hover {
.contact-box .offline {
background-color: #C0C0C0;
}
.contact-box .checkmark.material-icons {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
font-size: 1.5rem;
line-height: 1.5rem;
vertical-align: middle;
color: #0097A7;
position: absolute;
right: 0.05rem;
bottom: 0.05rem;
background-color: white;
overflow: visible;
border: 0.0px solid transparent;
}
/* END of avatar box */
#add-topic {
@@ -543,20 +657,35 @@ ul.menu > li.separator:hover {
flex-direction: column;
}
.panel-form-one {
.panel-form-row {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.5rem 0.75rem;
line-height: 1.5;
flex-grow: 0;
flex-shrink: 0;
}
.panel-form-two {
.panel-form-column {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-grow: 0;
flex-shrink: 0;
padding: 0.5rem 0.75rem;
line-height: 1.5;
}
/* Make sure padding is only applied at
the top level panel */
.panel-form-row > .panel-form-column {
padding: 0!important;
flex-grow: 1;
}
.panel-form-column > .panel-form-row {
padding: 0!important;
}
/* Styling of avatar upload area in the New Account panel */
/* Styling of avatar upload control */
.avatar-upload {
display: flex;
flex-direction: column;
@@ -564,8 +693,16 @@ ul.menu > li.separator:hover {
width: 7rem;
height: 9rem;
margin-left: 0.5rem;
font-size: 4.5rem;
padding: 0;
}
.avatar-upload .avatar-box {
position: relative;
width: 7rem;
height: 7rem;
}
/* Clearfix */
.avatar-upload:after {
content: ".";
@@ -649,6 +786,7 @@ ul.menu > li.separator:hover {
#topic-caption-panel .avatar-box {
position: relative;
width: 3rem;
min-width: 3rem;
height: 3rem;
@@ -680,10 +818,15 @@ ul.menu > li.separator:hover {
display: flex;
align-items: center;
flex: none;
padding: 0.5rem;
padding: 0.4rem;
background-color: #eceff1;
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 2;
height: 4rem;
}
#sendMessage {
overflow: hidden;
}
/* A static panel shown when no topic is selected */
@@ -886,6 +1029,24 @@ ul.menu > li.separator:hover {
margin-right: auto;
}
#group-manager {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
}
#group-manager-buttons {
display: flex;
background-color: #eceff1;
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 2;
justify-content: space-around;
padding: 0.4rem;
height: 4rem;
border-left: 1px solid #ccc;
}
/* END of InfoView */
/* Modal dialog */
@@ -967,7 +1128,7 @@ ul.tabbar > li > a {
font-size: 1.1rem;
color: rgba(33,150,243,0.6);
overflow: hidden;
box-shadow: inset 0 -2px 2.5px -1px rgba(0,0,0,0.5);
box-shadow: inset 0 -2px 2.5px -1px rgba(0,0,0,0.25);
}
ul.tabbar li.active > a {
@@ -1000,24 +1161,52 @@ ul.tabbar li.active > a:after {
/* Miscellaneous */
/* Badge with the number of unread messages */
.badge {
.unread {
display: inline-block;
font-size: 0.75rem;
font-weight: bolder;
background-color: #0097A7;
color: white;
border-radius: 50%;
padding: 0.25rem;
min-width: 1.4rem;
width: 1.35rem;
height: 1.35rem;
margin-left: 0.5rem;
line-height: 1;
line-height: 1.35rem;
text-align: center;
vertical-align: middle;
}
/* Color of buttons */
.blue {
color: #2196f3; /* blue 500 */
/* Badge with a label, like [you], [muted], etc */
.badge {
font-family: monospace;
font-size: 0.75rem;
border: 1px solid #666;
background-color: #eee;
color: #666;
padding: 0 0.15rem 0 0.15rem;
margin-left: 0.1rem;
margin-right: 0.1rem;
border-radius: 0.15rem;
display: inline-block;
align-self: flex-start;
}
.badge.green {
border-color: #383;
background-color: #efe;
color: #383;
}
.badge.yellow {
border-color: #660;
background-color: #ffe;
color: #660;
}
.badge.red {
border-color: #933;
background-color: #fee;
color: #933;
}
/* Letter tile base style, responsive square box */
@@ -1025,7 +1214,6 @@ ul.tabbar li.active > a:after {
width: 100%;
padding-bottom: 100%;
border-radius: 50%;
position: relative;
}
/* Div with letter tile content */
@@ -1044,6 +1232,10 @@ ul.tabbar li.active > a:after {
color: rgba(250, 250, 250, 0.8); /* default text color #FAFAFA, gray 50 */
}
.lettertile:hover {
color: white;
}
/* Colors of letter tiles */
.lettertile.color0 {
background-color: #ef9a9a;
@@ -1073,7 +1265,7 @@ ul.tabbar li.active > a:after {
background-color: #c5e1a5;
}
.lettertile.color9 {
background-color: #fff59d;
background-color: #f0e57d;
}
.lettertile.color10 {
background-color: #f48fb1;
@@ -1154,10 +1346,140 @@ ul.tabbar li.active > a:after {
}
#self-avatar.material-icons, .avatar-box .material-icons {
font-size: 3rem;
font-size: 2.4rem;
}
.avatar-box .material-icons {
color: rgba(250, 250, 250, 0.8); /* #fafafa, gray 50 */
background-color: #c0c0c0;
border-radius: 50%;
}
.chip-input {
display: flex;
flex-wrap: wrap;
flex-grow: 1;
cursor: text;
font-size: 1.1rem;
font-weight: normal;
background: transparent;
border-bottom: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
transition: all 0.1s ease-in-out;
width: 100%;
max-height: 10rem;
min-height: 4rem;
overflow-y: scroll;
margin: 0.25rem 0rem 0.25rem 0rem;
padding: 0 0 0.5rem 0.5rem;
outline: none;
color: #666;
align-items: center;
justify-content: flex-start;
text-align: center;
}
.chip-input.focused {
border-bottom: 1px solid #2196f3;
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
}
.chip-input > input[type="text"] {
display: inline-block;
width: auto;
border: none;
line-height: 1.8;
box-shadow: none;
min-width: 4rem;
flex: 1 0 auto;
height: 1.8rem;
margin: 0.125rem;
}
.chip {
display: flex;
flex: 0 0 auto;
align-items: center;
height: 1.8rem;
line-height: 1.8;
border-radius: 0.9rem;
margin: 0.125rem;
background-color: #eee;
white-space: nowrap;
font-size: 90%;
}
.chip > a {
display: inline-block;
color: #ccc;
background-color: #aaa;
border-radius: 50%;
margin: 0.3rem;
width: 1.175rem;
height: 1.175rem;
line-height: 1.15;
text-decoration: none;
}
.chip > a:focus,
.chip > a:hover {
text-decoration: none;
color: #ddd;
background-color: #bbb;
}
.chip .avatar-box {
position: relative;
width: 1.8rem;
min-width: 1.8rem;
height: 1.8rem;
min-height: 1.8rem;
font-size: 1rem;
margin-right: 0.2rem;
}
.chip .avatar-box .material-icons {
font-size: 1.8rem;
}
.chip .spacer {
display: inline-block;
margin: 0.3rem;
width: 0.25rem;
height: 1.175rem;
}
.load-spinner-box {
position: absolute;
box-shadow: 0.1rem 0.1rem 0.15rem 1px rgba(0,0,0,0.25);
border-radius: 50%;
padding: 0.25rem;
width: 2.5rem;
height: 2.5rem;
left: 0;
right: 0;
top: 5rem;
margin-left: auto;
margin-right: auto;
background-color: white;
z-index: 2;
}
.loader-spinner {
border: 0.35rem solid #f3f3f3;
border-radius: 50%;
border-top: 0.35rem solid #1e88e5;
width: 2rem;
height: 2rem;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
+592
View File
@@ -0,0 +1,592 @@
// Basic parser and formatter for very simple text markup. Mostly targeted at
// mobile use cases similar to Telegram and WhatsApp.
//
// Supports:
// *abc* -> <b>abc</b>
// _abc_ -> <i>abc</i>
// ~abc~ -> <del>abc</del>
// `abc` -> <tt>abc</tt>
// Nested frmatting is supported, e.g. *abc _def_* -> <b>abc <i>def</i></b>
//
// URLs, @mentions, and #hashtags are extracted and converted into links.
//
// JSON data representation is inspired by Draft.js raw formatting.
/*
Text:
this is *bold*, `code` and _italic_, ~strike~
combined *bold and _italic_*
an url: https://www.example.com/abc#fragment and another _www.tinode.co_
this is a @mention and a #hashtag in a string
second #hashtag
Sample JSON representation of the text above:
{
"txt": "this is bold, code and italic, strike combined bold and italic an url: https://www.example.com/abc#fragment " +
"and another www.tinode.co this is a @mention and a #hashtag in a string second #hashtag",
"fmt": [
{ "at":8, "len":4,"tp":"ST" },{ "at":14, "len":4, "tp":"CO" },{ "at":23, "len":6, "tp":"EM"},
{ "at":31, "len":6, "tp":"DL" },{ "tp":"BR", "len":1, "at":37 },{ "at":56, "len":6, "tp":"EM" },
{ "at":47, "len":15, "tp":"ST" },{ "tp":"BR", "len":1, "at":62 },{ "at":120, "len":13, "tp":"EM" },
{ "at":71, "len":36, "key":0 },{ "at":120, "len":13, "key":1 },{ "tp":"BR", "len":1, "at":133 },
{ "at":144, "len":8, "key":2 },{ "at":159, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":179 },
{ "at":187, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":195 }
],
"ent": [
{ "tp":"LN", "data":{ "url":"https://www.example.com/abc#fragment" } },
{ "tp":"LN", "data":{ "url":"http://www.tinode.co" } },
{ "tp":"MN", "data":{ "val":"mention" } },
{ "tp":"HT", "data":{ "val":"hashtag" } }
]
}
*/
(function(environment) { // closure for web browsers
'use strict';
// Regular expressions for parsing inline formats. Javascript does not support lookbehind,
// so it's a bit messy.
var INLINE_STYLES = [
// Strong = bold, *bold text*
{name: "ST", start: /(?:^|\W)(\*)[^\s*]/, end: /[^\s*](\*)(?=$|\W)/},
// Emphesized = italic, _italic text_
{name: "EM", start: /(?:^|[\W_])(_)[^\s_]/, end: /[^\s_](_)(?=$|[\W_])/},
// Deleted, ~strike this though~
{name: "DL", start: /(?:^|\W)(~)[^\s~]/, end: /[^\s~](~)(?=$|\W)/},
// Code block `this is monospace`
{name: "CO", start: /(?:^|\W)(`)[^`]/, end: /[^`](`)(?=$|\W)/}
];
// RegExps for entity extraction (RF = reference)
var ENTITY_TYPES = [
// URLs
{name: "LN", dataName: "url",
pack: function(val) {
// Check if the protocol is specified, if not use http
if (!/^[a-z]+:\/\//i.test(val)) {
val = 'http://' + val;
}
return {url: val};
},
re: /(https?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=]*)/g},
// Mentions @user (must be 2 or more characters)
{name: "MN", dataName: "val",
pack: function(val) { return {val: val.slice(1)}; },
re: /\B@(\w\w+)/g},
// Hashtags #hashtag
{name: "HT", dataName: "val",
pack: function(val) { return {val: val.slice(1)}; },
re: /\B#(\w\w+)/g}
];
// HTML tag name suggestions
var HTML_TAGS = {
ST: { name: 'b', isVoid: false },
EM: { name: 'i', isVoid: false },
DL: { name: 'del', isVoid: false },
CO: { name: 'tt', isVoid: false },
BR: { name: 'br', isVoid: true },
LN: { name: 'a', isVoid: false },
MN: { name: 'a', isVoid: false },
HT: { name: 'a', isVoid: false }
};
// Helpers for converting Drafty to HTML.
var DECORATORS = {
ST: { open: function() { return '<b>'; }, close: function() { return '</b>'; }},
EM: { open: function() { return '<i>'; }, close: function() { return '</i>'}},
DL: { open: function() { return '<del>'; }, close: function() { return '</del>'}},
CO: { open: function() { return '<tt>'; }, close: function() { return '</tt>'}},
BR: { open: function() { return ''; }, close: function() { return '<br/>'}},
LN: {
open: function(data) { return '<a href="' + data.url + '">'; },
close: function(data) { return '</a>'; },
props: function(data) { return { href: data.url, target: "_blank" }; },
},
MN: {
open: function(data) { return '<a href="#' + data.val + '">'; },
close: function(data) { return '</a>'; },
props: function(data) { return { name: data.val }; },
},
HT: {
open: function(data) { return '<a href="#' + data.val + '">'; },
close: function(data) { return '</a>'; },
props: function(data) { return { name: data.val }; },
}
};
var Drafty = (function() {
// Take a string and defined earlier style spans, re-compose them into a tree where each leaf is
// a same-style (including unstyled) string. I.e. 'hello *bold _italic_* and ~more~ world' ->
// ('hello ', (b: 'bold ', (i: 'italic')), ' and ', (s: 'more'), ' world');
//
// This is needed in order to clear markup, i.e. 'hello *world*' -> 'hello world' and convert
// ranges from markup-ed offsets to plain text offsets.
function chunkify(line, start, end, spans) {
var chunks = [];
if (spans.length == 0) {
return [];
}
for (var i in spans) {
// Get the next chunk from the queue
var span = spans[i];
// Grab the initial unstyled chunk
if (span.start > start) {
chunks.push({text: line.slice(start, span.start)});
}
// Grab the styled chunk. It may include subchunks.
var chunk = {type: span.type};
var chld = chunkify(line, span.start + 1, span.end - 1, span.children);
if (chld.length > 0) {
chunk.children = chld;
} else {
chunk.text = span.text;
}
chunks.push(chunk);
start = span.end + 1; // '+1' is to skip the formatting character
}
// Grab the remaining unstyled chunk, after the last span
if (start < end) {
chunks.push({text: line.slice(start, end)});
}
return chunks;
}
// Same as chunkify but used for formatting.
function forEach(line, start, end, spans, formatter, context) {
// Add un-styled range before the styled span starts.
// Process ranges calling formatter for each range.
var result = [];
for (var i = 0; i < spans.length; i++) {
var span = spans[i];
// Add un-styled range before the styled span starts.
if (start < span.at) {
result.push(formatter.call(context, null, undefined, line.slice(start, span.at)));
start = span.at;
}
// Get all spans which are within current span.
var subspans = [];
for (var si = i + 1; si < spans.length && spans[si].at < span.at + span.len; si++) {
subspans.push(spans[si]);
i = si;
}
var tag = HTML_TAGS[span.tp] || {};
result.push(formatter.call(context, span.tp, span.data,
tag.isVoid ? null : forEach(line, start, span.at + span.len, subspans, formatter, context)));
start = span.at + span.len;
}
// Add the last unformatted range.
if (start < end) {
result.push(formatter.call(context, null, undefined, line.slice(start, end)));
}
return result;
}
// Detect starts and ends of formatting spans. Unformatted spans are
// ignored at this stage.
function spannify(original, re_start, re_end, type) {
var result = [];
var index = 0;
var line = original.slice(0); // make a copy;
while (line.length > 0) {
// match[0]; // match, like '*abc*'
// match[1]; // match captured in parenthesis, like 'abc'
// match['index']; // offset where the match started.
// Find the opening token.
var start = re_start.exec(line);
if (start == null) {
break;
}
// Because javascript RegExp does not support lookbehind, the actual offset may not point
// at the markup character. Find it in the matched string.
var start_offset = start['index'] + start[0].lastIndexOf(start[1]);
// Clip the processed part of the string.
line = line.slice(start_offset + 1);
// start_offset is an offset within the clipped string. Convert to original index.
start_offset += index;
// Index now point to the beginning of 'line' within the 'original' string.
index = start_offset + 1;
// Find the matching closing token.
var end = re_end ? re_end.exec(line) : null;
if (end == null) {
break;
}
var end_offset = end['index'] + end[0].indexOf(end[1]);
// Clip the processed part of the string.
line = line.slice(end_offset + 1);
// Update offsets
end_offset += index;
// Index now point to the beginning of 'line' within the 'original' string.
index = end_offset + 1;
result.push({
text: original.slice(start_offset+1, end_offset),
children: [],
start: start_offset,
end: end_offset,
type: type
});
}
return result;
}
// Convert linear array or spans into a tree representation.
// Keep standalone and nested spans, throw away partially overlapping spans.
function toTree(spans) {
if (spans.length == 0) {
return [];
}
var tree = [spans[0]];
var last = spans[0];
for (var i = 1; i < spans.length; i++) {
// Keep spans which start after the end of the previous span or those which
// are complete within the previous span.
if (spans[i].start > last.end) {
// Span is completely outside of the previous span.
tree.push(spans[i]);
last = spans[i];
} else if (spans[i].end < last.end) {
// Span is fully inside of the previous span. Push to subnode.
last.children.push(spans[i]);
}
// Span could partially overlap, ignoring it as invalid.
}
// Recursively rearrange the subnodes.
for (var i in tree) {
tree[i].children = toTree(tree[i].children);
}
return tree;
}
// Get a list of entities from a text.
function extractEntities(line) {
var match;
var extracted = [];
ENTITY_TYPES.map(function(entity) {
while ((match = entity.re.exec(line)) !== null) {
extracted.push({
offset: match['index'],
len: match[0].length,
unique: match[0],
data: entity.pack(match[0]),
type: entity.name});
}
});
if (extracted.length == 0) {
return extracted;
}
// Remove entities detected inside other entities, like #hashtag in a URL.
extracted.sort(function(a,b) {
return a.offset - b.offset;
});
var idx = -1;
extracted = extracted.filter(function(el) {
var result = (el.offset > idx);
idx = el.offset + el.len;
return result;
});
return extracted;
}
// Convert the chunks into format suitable for serialization.
function draftify(chunks, startAt) {
var plain = "";
var ranges = [];
for (var i in chunks) {
var chunk = chunks[i];
if (!chunk.text) {
var drafty = draftify(chunk.children, plain.length + startAt);
chunk.text = drafty.txt;
ranges = ranges.concat(drafty.fmt);
}
if (chunk.type) {
ranges.push({at: plain.length + startAt, len: chunk.text.length, tp: chunk.type});
}
plain += chunk.text;
}
return {txt: plain, fmt: ranges};
}
// Splice two strings: insert second string into the first one at the given index
function splice(src, at, insert) {
return src.slice(0, at) + insert + src.slice(at);
}
return {
/**
* Parse text into structured representation.
*/
parse: function(content) {
// Split text into lines. It makes further processing easier.
var lines = content.split(/\r?\n/);
// Holds entities referenced from text
var entityMap = [];
var entityIndex = {};
// Processing lines one by one, hold intermediate result in blx.
var blx = [];
lines.map(function(line) {
var spans = [];
var entities = [];
// Find formatted spans in the string.
// Try to match each style.
INLINE_STYLES.map(function(style) {
// Each style could be matched multiple times.
spans = spans.concat(spannify(line, style.start, style.end, style.name));
});
var block;
if (spans.length == 0) {
block = {txt: line};
} else {
// Sort spans by style occurence early -> late
spans.sort(function(a,b) {
return a.start - b.start;
});
// Convert an array of possibly overlapping spans into a tree
spans = toTree(spans);
// Build a tree representation of the entire string, not
// just the formatted parts.
var chunks = chunkify(line, 0, line.length, spans);
var drafty = draftify(chunks, 0);
block = {txt: drafty.txt, fmt: drafty.fmt};
}
// Extract entities from the cleaned up string.
entities = extractEntities(block.txt);
if (entities.length > 0) {
var ranges = [];
for (var i in entities) {
// {offset: match['index'], unique: match[0], len: match[0].length, data: ent.packer(), type: ent.name}
var entity = entities[i];
var index = entityIndex[entity.unique];
if (!index) {
index = entityMap.length;
entityIndex[entity.unique] = index;
entityMap.push({tp: entity.type, data: entity.data});
}
ranges.push({at: entity.offset, len: entity.len, key: index});
}
block.ent = ranges;
}
blx.push(block);
});
var result = {txt: ""};
// Merge lines and save line breaks as BR inline formatting.
if (blx.length > 0) {
result.txt = blx[0].txt;
result.fmt = (blx[0].fmt || []).concat(blx[0].ent || []);
for (var i = 1; i<blx.length; i++) {
var block = blx[i];
var offset = result.txt.length + 1;
result.fmt.push({tp: "BR", len: 1, at: offset - 1});
result.txt += " " + block.txt;
if (block.fmt) {
result.fmt = result.fmt.concat(block.fmt.map(function(s) {
s.at += offset; return s;
}));
}
if (block.ent) {
result.fmt = result.fmt.concat(block.ent.map(function(s) {
s.at += offset; return s;
}));
}
}
if (result.fmt.length == 0) {
delete result.fmt;
}
if (entityMap.length > 0) {
result.ent = entityMap;
}
}
return result;
},
/**
* Given the structured representation of rich text, convert it to HTML.
* No attempt is made to strip pre-existing html markup.
* This is potentially unsafe because block.txt may contain malicious
* markup.
*
* @param {drafy} content - structured representation of rich text.
*
* @return HTML-representation of content
*/
unsafeToHTML: function(content) {
var {txt, fmt, ent} = content;
var markup = [];
if (fmt) {
for (var i in fmt) {
var range = fmt[i];
var tp = range.tp, data;
if (!tp) {
var entity = ent[range.key];
if (entity) {
tp = entity.tp;
data = entity.data;
}
}
if (DECORATORS[tp]) {
markup.push({idx: range.at, what: DECORATORS[tp].open(data)});
markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)});
}
}
}
markup.sort(function(a, b) {
return b.idx - a.idx; // in descending order
});
for (var i in markup) {
if (markup[i].what) {
txt = splice(txt, markup[i].idx, markup[i].what);
}
}
return txt;
},
/**
* Transform using custom formatting.
*
* @param {Drafty} content - content to transform.
* @param {function} formatter - callback which transforms individual elements
* @param {Object} context - context provided to formatter as 'this'.
*
* @return {Object} context
*/
format: function(content, formatter, context) {
var {txt, fmt, ent} = content;
if (!fmt) {
return [txt];
}
var spans = [].concat(fmt);
// Soft spans first by start index (asc) then by length (desc).
spans.sort(function(a, b) {
if (a.at - b.at == 0) {
return b.len - a.len; // longer one comes first (<0)
}
return a.at - b.at;
});
// Denormalize entities into spans. Create a copy of the objects to leave
// original Drafty object unchanged.
spans = spans.map(function(s) {
var data;
var tp = s.tp;
if (s.key + 1 > 0) {
data = ent[s.key].data;
tp = ent[s.key].tp;
}
return {tp: tp, data: data, at: s.at, len: s.len};
});
return forEach(txt, 0, txt.length, spans, formatter, context);
},
/**
* Given structured representation of rich text, convert it to plain text.
*
* @param {drafty} content - content to convert to plain text.
*/
toPlainText: function(content) {
return content.text;
},
/**
* Returns true if content has no markup and no entities.
*
* @param {drafty} content - content to check for presence of markup.
* @returns true is content is plain text, false otherwise.
*/
isPlainText: function(content, noLineBreaks) {
return !(content.fmt || content.ent);
},
/**
* Get HTML tag for a given two-letter style name
* @param {string} style - two-letter style, like ST or LN
* @returns
*/
tagName: function(style) {
return HTML_TAGS[style] ? HTML_TAGS[style].name : undefined;
},
/**
* For a given data bundle generate an object with HTML attributes,
* for instance, given {url: "http://www.example.com/"} return
* {href: "http://www.example.com/"}
* @param {stting} style - tw-letter style to generate attributes for.
* @param {Object} data - data bundle to convert to attributes
* @returns object with HTML attributes.
*/
attrValue: function(style, data) {
if (data && DECORATORS[style]) {
return DECORATORS[style].props(data);
}
return undefined;
},
/**
* Content MIME type.
* @returns string suitabe for HTTP Content-Type field.
*/
getContentType: function() {
return "text/x-drafty";
}
};
});
// Export for the window object or node; Check that is not already defined.
if (typeof(environment.Drafty) === 'undefined') {
environment.Drafty = Drafty();
}
})(this); // 'this' will be a window object for the browsers.
+22 -92
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 77 KiB

+1522 -383
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB