Compare commits

..
40 Commits
Author SHA1 Message Date
or-else 1b9b7a2671 bump app version 2017-12-14 11:18:41 -08:00
or-else 1a3362f1cd fix online status 2017-12-05 22:23:18 -08:00
or-else 9009667c5a bug fix: correct handling of hard-delete 2017-12-03 14:43:54 -08:00
or-else a59023575b bug fix in topic member menu 2017-11-25 19:02:36 -08:00
or-else b2b1ca2934 kind of works but buggy 2017-11-25 14:24:18 -08:00
or-else afd1bbb58a making new delete scheme work 2017-11-24 20:23:49 -08:00
or-else de655a105a bug fix 2017-10-25 14:00:21 -07:00
or-else 2c1c0b2544 fix for very long file names 2017-10-22 17:37:43 -07:00
or-else ccf5318e4c mention than deleting messages works now 2017-10-13 16:42:46 -07:00
or-else 7322abfd55 messages can be deleted one by one now 2017-10-13 13:04:54 -07:00
or-else 09518bbac2 adding message deletion - incomplete yet 2017-10-13 10:23:19 -07:00
or-else b6a831ec91 big fix in error handling 2017-10-01 16:08:02 -07:00
or-else 378cd622c4 dropping support for .ico because they are not well supported on android 2017-09-30 22:03:05 -07:00
or-else 939e3e59cf attach some images without converting them first 2017-09-30 16:47:02 -07:00
or-else 22e82b1c7f corrected inline image width on mobile 2017-09-26 14:54:00 -07:00
or-else e495917356 simplified handling of inline images 2017-09-26 14:43:46 -07:00
or-else cf4b58cab2 added missing screenshot 2017-09-25 21:01:36 -07:00
or-else 0a96014484 inline images work now 2017-09-25 21:00:35 -07:00
or-else 3e8fd9d6e6 file attachments work now 2017-09-24 18:21:14 -07:00
or-else a06d29863d add UI elements for attaching images and other docs 2017-09-23 13:26:58 -07:00
or-else de24e23127 bug fix: exception in changing permissions 2017-09-21 19:49:21 -07:00
or-else 9417e1aa49 bug fix: correct display of online status in MessagesView 2017-09-21 19:38:00 -07:00
or-else 1f8d26a215 bug fix: send private value in topic creation 2017-09-21 18:23:26 -07:00
or-else e4a0b60f1e better indication of write-only topics 2017-09-21 09:41:24 -07:00
or-else 19612e4236 add blob generation from base64 to be able to deal with large images 2017-09-20 17:13:13 -07:00
or-else af1e9c609b forgot to add the new icon 2017-09-20 15:27:37 -07:00
or-else 5d18c71475 add green dot to shortcut icon to indicate unread messages 2017-09-20 15:01:19 -07:00
or-else 20fc9ae343 scroll only on material changes 2017-09-20 12:00:54 -07:00
or-else 2e8b9145b7 better handling of lost/restored connection 2017-09-19 20:15:40 -07:00
or-else 859dec8410 removed dead code 2017-09-17 20:57:19 -07:00
or-else 8f13d2c856 better error handling and reporting 2017-09-17 18:36:01 -07:00
or-else d2a226852b fixed drafty's handling of undefined values 2017-09-15 18:04:02 -07:00
or-else 9c7a05f616 bug fix: lost topic parameters after reconnect 2017-09-14 19:26:45 -07:00
or-else a6e59936e2 typing indicators work now 2017-09-14 15:36:34 -07:00
or-else e3d95eafe1 bug fixes 2017-09-14 11:43:27 -07:00
or-else e30568ecd5 correctly propagate update to all components 2017-09-14 11:09:25 -07:00
or-else 9ac8ea8f93 adding borders to screenshots 2017-09-13 14:06:31 -07:00
or-else 1bac018961 adding borders to screenshots 2017-09-13 14:05:54 -07:00
or-else 1aba91d6f3 adding borders to screenshots 2017-09-13 14:03:20 -07:00
or-else fd750558ac adding borders to screenshots 2017-09-13 14:00:19 -07:00
9 changed files with 1208 additions and 340 deletions
-1
View File
@@ -1 +0,0 @@
tinode.js
+9 -10
View File
@@ -4,9 +4,9 @@ Example using [Tinode](https://github.com/tinode/chat/) to build a single-page c
[React](https://facebook.github.io/react/), Google's material design [fonts](https://www.google.com/fonts/) [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. Overall it's a lot like open source WhatsApp or Telegram web apps. 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. Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist. 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`. 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. Try possibly newer or older version live at https://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:15am Pacific time.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
@@ -16,24 +16,23 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia
## Missing functionality ## Missing functionality
* Cannot send images or other media. * Previews not generated for videos, audio, links or docs.
* Cannot delete messages. * No indication of some chat statuses (muted, blocked, etc.).
* No typing indicators. * Browser push notifications are not implemented.
* No indication of chat status (muted, blocked, etc.). * No end-to-end encryption.
* Browser push notifications are missing.
* No encryption.
* Editing chat permissions is incomplete. * Editing chat permissions is incomplete.
* Emoji support is weak.
## Responsive design ## Responsive design
### Desktop screenshot ### Desktop screenshot
<p align="center"> <p align="center">
<img src="web-desktop-1.png" alt="Desktop web: full app" width=866 /> <img src="web-desktop-2.png" alt="Desktop web: full app" width=866 />
</p> </p>
### Mobile screenshots ### Mobile screenshots
<p align="center"> <p align="center">
<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 /> <kbd><img src="web-mob-contacts-1.png" alt="Mobile web: contacts" width=323 /></kbd> <kbd><img src="web-mob-chat-1.png" alt="Mobile web: chat" width=323 /></kbd> <kbd><img src="web-mob-info-1.png" alt="Mobile web: topic info" width=323 /></kbd> <kbd><img src="web-mob-new-chat-1.png" alt="Mobile web: start new chat" width=323 /></kbd>
</p> </p>
+212 -35
View File
@@ -247,6 +247,36 @@ ul.menu > li.separator {
ul.menu > li.separator:hover { ul.menu > li.separator:hover {
background-color: #ccc; background-color: #ccc;
} }
/* The down arrow head - trigger for context menu */
li .menuTrigger {
position: absolute;
top: 0.25rem;
right: 0.25rem;
width: 1.5rem;
height: 1.2rem;
line-height: 1.2rem;
border-radius: 0.6rem;
text-align: center;
opacity: 0;
}
li:hover .menuTrigger {
opacity: 1.0;
}
li:hover .menuTrigger a {
color: #757575;
}
li.left:hover .menuTrigger {
-text-shadow: #c5e1a5 0 0 15px, #c5e1a5 0 0 5px;
background-image: radial-gradient(#c5e1a5 40%, transparent 75%);
}
li.right:hover .menuTrigger {
-text-shadow: #fafafa 0 0 15px, #fafafa 0 0 5px;
background-image: radial-gradient(#fafafa 40%, transparent 75%);
}
/* END popup/dropdown menu */ /* END popup/dropdown menu */
/* Generic text colors */ /* Generic text colors */
@@ -258,6 +288,14 @@ ul.menu > li.separator:hover {
color: #D32F2F; /* red 700 */ color: #D32F2F; /* red 700 */
} }
.gray {
color: #757575;
}
.small {
font-size: 90%;
}
.hr { .hr {
display: block; display: block;
width: 100%; width: 100%;
@@ -411,7 +449,6 @@ table.permission-editor td.checkbox {
.permission-editor .material-icons { .permission-editor .material-icons {
font-size: 1.1rem; font-size: 1.1rem;
line-height: 1.1rem; line-height: 1.1rem;
vertical-align: middle;
} }
.material-icons.clickable { .material-icons.clickable {
@@ -505,17 +542,51 @@ table.permission-editor td.checkbox {
/* Area which displays an error message */ /* Area which displays an error message */
#alert-box { .alert-box {
background-color: transparent; position: relative;
height: 0; display: none;
overflow: hidden; overflow: hidden;
padding: 1rem 1.5rem 1rem 0.5rem;
} }
#alert-box.error-alert { .alert-box .cancel {
padding: 0.5rem; position: absolute;
right: 0.25rem;
top: 0.5rem;
}
.alert-box.error {
display: flex;
background-color: #ffcdd2; background-color: #ffcdd2;
border-color: #d32f2f; border-bottom: 1px solid #ffbfc8;
color: #b71c1c; color: #b71c1c;
height: auto; }
.alert-box.warning {
display: flex;
background-color: #fff9c4;
border-bottom: 1px solid #fff59d;
color: #607d8b;
}
.alert-box .cancel .material-icons {
font-size: 1rem;
}
.alert-box.error .cancel .material-icons {
color: #b71c1c;
}
.alert-box.warning .cancel .material-icons {
color: #757575;
}
.alert-box .icon {
display: block;
margin-right: 0.5rem;
}
.alert-box .icon .material-icons {
font-size: 1.5rem;
}
.alert-box.warning .icon .material-icons {
color: #fbc02d;
}
.alert-box.error .icon .material-icons {
color: #ef5350;
} }
/* List of contacts */ /* List of contacts */
@@ -540,6 +611,7 @@ table.permission-editor td.checkbox {
.contact-box > li { .contact-box > li {
display: flex; display: flex;
position: relative;
padding: 0.35rem 0.25rem 0.5rem 0.35rem; padding: 0.35rem 0.25rem 0.5rem 0.35rem;
min-height: 3.5rem; min-height: 3.5rem;
flex-grow: 0; flex-grow: 0;
@@ -571,20 +643,6 @@ table.permission-editor td.checkbox {
font-weight: lighter; font-weight: lighter;
} }
/* The down arrow head - trigger for context menu in contacts list */
.contact-box li .menuTrigger {
position: relative;
margin-left: auto;
width: 1.5rem;
height: 1.5rem;
}
.contact-box li .menuTrigger a {
color: transparent;
}
.contact-box li:hover .menuTrigger a {
color: #757575;
}
/* Avatar and online-offline indicator */ /* Avatar and online-offline indicator */
.contact-box .avatar-box { .contact-box .avatar-box {
margin-right: 0.35rem; margin-right: 0.35rem;
@@ -609,7 +667,7 @@ table.permission-editor td.checkbox {
} }
/* Online-offline indicator positioned inside avatar-box */ /* Online-offline indicator positioned inside avatar-box */
.contact-box .online, .offline { .avatar-box .online, .offline {
display: inline-block; display: inline-block;
width: 0.75rem; width: 0.75rem;
min-width: 0.75rem; min-width: 0.75rem;
@@ -621,20 +679,37 @@ table.permission-editor td.checkbox {
right: 0; right: 0;
bottom: 0.1rem; bottom: 0.1rem;
} }
.contact-box .online { .avatar-box .online {
background-color: #4caf50; background-color: #4caf50;
} }
.contact-box .offline { .avatar-box .offline {
background-color: #C0C0C0; background-color: #C0C0C0;
} }
/* Typing indicator: animation of the online dot */
.online.typing {
animation: typing 1.5s infinite;
}
@keyframes typing {
0% { background-color: #40C040; }
25% { background-image: radial-gradient(#30D030 0%, #40C040 67%); }
33.3% { background-image: radial-gradient(#10EE10 0%, #40C040 50%); }
41.7% { background-image: radial-gradient(#00FF00 0%, #40C040 35%); }
50% { background-image: radial-gradient(#30D030 0%, #40C040 67%); }
58.3% { background-image: radial-gradient(#10EE10 0%, #40C040 50%); }
66.7% { background-image: radial-gradient(#00FF00 0%, #40C040 35%); }
75% { background-image: radial-gradient(#30D030 0%, #40C040 67%); }
83.3% { background-image: radial-gradient(#10EE10 0%, #40C040 50%); }
91.6% { background-image: radial-gradient(#00FF00 0%, #40C040 35%); }
}
.contact-box .checkmark.material-icons { .contact-box .checkmark.material-icons {
display: inline-block; display: inline-block;
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.5rem; line-height: 1.5rem;
vertical-align: middle;
color: #0097A7; color: #0097A7;
position: absolute; position: absolute;
right: 0.05rem; right: 0.05rem;
@@ -772,6 +847,10 @@ table.permission-editor td.checkbox {
/* END of sidepanel styles */ /* END of sidepanel styles */
/* Topic/messages view */ /* Topic/messages view */
#topic-view {
position: relative;
}
#topic-caption-panel { #topic-caption-panel {
background-color: #b0bec5; background-color: #b0bec5;
} }
@@ -784,7 +863,6 @@ table.permission-editor td.checkbox {
color: black; color: black;
} }
#topic-caption-panel .avatar-box { #topic-caption-panel .avatar-box {
position: relative; position: relative;
width: 3rem; width: 3rem;
@@ -806,6 +884,7 @@ table.permission-editor td.checkbox {
/* Scrollable panel where the messages are shown */ /* Scrollable panel where the messages are shown */
#messages-panel { #messages-panel {
position: relative;
padding: 0.75rem; padding: 0.75rem;
overflow-y: scroll; overflow-y: scroll;
background-color: #eceff1; background-color: #eceff1;
@@ -813,12 +892,37 @@ table.permission-editor td.checkbox {
background-image: url("../img/ignasi_pattern_s.png"); background-image: url("../img/ignasi_pattern_s.png");
} }
#messages-panel.write-only::after {
content: "";
display: block;
position: absolute;
top:0;
right: 0;
bottom: 0;
left: 0;
background-image: repeating-linear-gradient(45deg,
transparent, transparent 0.5rem, rgba(255,255,255,.5) 0.5rem, rgba(255,255,255,.5) 1rem);
}
#write-only-note {
position: absolute;
background-color: white;
color: #666;
height: auto;
width: auto;
line-height: 2rem;
text-align: center;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 0.25rem 1.5rem;
}
/* Footer-panel with the input for the new message */ /* Footer-panel with the input for the new message */
#send-message-panel { #send-message-panel {
display: flex; display: flex;
align-items: center; align-items: center;
flex: none; flex: none;
padding: 0.4rem; padding: 0.4rem 0.4rem 0.4rem 0.6rem;
background-color: #eceff1; background-color: #eceff1;
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */ box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 2; z-index: 2;
@@ -829,6 +933,18 @@ table.permission-editor td.checkbox {
overflow: hidden; overflow: hidden;
} }
#send-message-panel .material-icons {
min-width: 1.75rem;
}
#send-message-panel .material-icons.secondary, .material-icons.disabled {
color: #999!important;
}
#send-message-panel .material-icons.secondary:hover {
color: #2196f3!important;
}
/* A static panel shown when no topic is selected */ /* A static panel shown when no topic is selected */
#dummy-view { #dummy-view {
width: 100%; width: 100%;
@@ -845,6 +961,49 @@ table.permission-editor td.checkbox {
vertical-align: middle; vertical-align: middle;
} }
#image-preview {
position: absolute;
display: flex;
flex-direction: column;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background-color: rgba(255,255,255,0.90);
}
#image-preview-caption-panel {
display: flex;
justify-content: space-between;
background-color: transparent;
line-height: 2.5rem;
height: 2.5rem;
padding: 1rem 0.75rem 0 0.75rem;
}
#image-preview-caption-panel a {
color: #757575;
text-decoration: none;
}
#image-preview-caption-panel a:hover {
color: black;
}
#image-preview-container {
margin: auto;
overflow: hidden;
}
#image-preview-footer {
display: flex;
justify-content: space-around;
height: 3.2rem;
line-height: 1.5rem;
background-color: transparent;
}
/* Talk bubbles */ /* Talk bubbles */
.chat-box { .chat-box {
list-style: none; list-style: none;
@@ -869,6 +1028,7 @@ table.permission-editor td.checkbox {
} }
.chat-box .bubble { .chat-box .bubble {
display: flex;
position: relative; position: relative;
max-width: 40rem; max-width: 40rem;
min-width: 12rem; min-width: 12rem;
@@ -1009,6 +1169,24 @@ table.permission-editor td.checkbox {
color:#777; color:#777;
} }
/* Inline image styles */
.inline-image {
max-width: 36rem;
max-height: 24rem;
overflow: hidden;
border-radius: 0.1rem;
cursor: pointer;
display: block;
}
/* Attachment styles */
.attachment {
display: flex;
}
.attachment:last-of-type {
margin-bottom: -1rem;
}
/* END of talk bubbles */ /* END of talk bubbles */
/* END of topic/mesages view */ /* END of topic/mesages view */
@@ -1053,12 +1231,13 @@ table.permission-editor td.checkbox {
.dialog-overlay { .dialog-overlay {
position: absolute; position: absolute;
display: flex; display: flex;
flex-direction: column;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 10; z-index: 10;
background-color: rgba(0,0,0,0.25); background-color: rgba(255,255,255,0.90);
} }
.modal-dialog { .modal-dialog {
display: flex; display: flex;
@@ -1340,10 +1519,14 @@ ul.tabbar li.active > a:after {
.material-icons { .material-icons {
font-size: 1.4rem; font-size: 1.4rem;
line-height: 1; line-height: 1;
vertical-align: middle;
} }
.material-icons.small { .material-icons.small {
font-size: 1rem; font-size: 1rem;
} }
.material-icons.big {
font-size: 2.4rem;
}
#self-avatar.material-icons, .avatar-box .material-icons { #self-avatar.material-icons, .avatar-box .material-icons {
font-size: 2.4rem; font-size: 2.4rem;
@@ -1470,15 +1653,9 @@ ul.tabbar li.active > a:after {
border-top: 0.35rem solid #1e88e5; border-top: 0.35rem solid #1e88e5;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
-webkit-animation: spin 1.5s linear infinite;
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 { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
+169 -8
View File
@@ -73,7 +73,7 @@ Sample JSON representation of the text above:
{name: "MN", dataName: "val", {name: "MN", dataName: "val",
pack: function(val) { return {val: val.slice(1)}; }, pack: function(val) { return {val: val.slice(1)}; },
re: /\B@(\w\w+)/g}, re: /\B@(\w\w+)/g},
// Hashtags #hashtag // Hashtags #hashtag, like metion 2 or more characters.
{name: "HT", dataName: "val", {name: "HT", dataName: "val",
pack: function(val) { return {val: val.slice(1)}; }, pack: function(val) { return {val: val.slice(1)}; },
re: /\B#(\w\w+)/g} re: /\B#(\w\w+)/g}
@@ -88,9 +88,23 @@ Sample JSON representation of the text above:
BR: { name: 'br', isVoid: true }, BR: { name: 'br', isVoid: true },
LN: { name: 'a', isVoid: false }, LN: { name: 'a', isVoid: false },
MN: { name: 'a', isVoid: false }, MN: { name: 'a', isVoid: false },
HT: { name: 'a', isVoid: false } HT: { name: 'a', isVoid: false },
IM: { name: 'img', isVoid: true }
}; };
// Convert base64-encoded string into Blob.
function base64toObjectUrl(b64, contentType) {
var bin = atob(b64);
var length = bin.length;
var buf = new ArrayBuffer(length);
var arr = new Uint8Array(buf);
for (var i = 0; i < length; i++) {
arr[i] = bin.charCodeAt(i);
}
return URL.createObjectURL(new Blob([buf], {type: contentType}));
}
// Helpers for converting Drafty to HTML. // Helpers for converting Drafty to HTML.
var DECORATORS = { var DECORATORS = {
ST: { open: function() { return '<b>'; }, close: function() { return '</b>'; }}, ST: { open: function() { return '<b>'; }, close: function() { return '</b>'; }},
@@ -112,6 +126,32 @@ Sample JSON representation of the text above:
open: function(data) { return '<a href="#' + data.val + '">'; }, open: function(data) { return '<a href="#' + data.val + '">'; },
close: function(data) { return '</a>'; }, close: function(data) { return '</a>'; },
props: function(data) { return { name: data.val }; }, props: function(data) { return { name: data.val }; },
},
IM: {
open: function(data) {
var blobUrl = base64toObjectUrl(data.val, data.mime);
var res = (data.name ? '<a href="' + blobUrl + '" download="' + data.name + '">' : '') +
'<img src="' + blobUrl + '"' +
(data.width ? ' width="' + data.width + '"' : '') +
(data.height ? ' height="' + data.height + '"' : '') + ' border="0" />';
console.log("open: " + res);
return res;
},
close: function(data) {
return (data.name ? '</a>' : '');
},
props: function(data) {
var url = base64toObjectUrl(data.val, data.mime);
return {
src: url,
title: data.name,
'data-width': data.width,
'data-height': data.height,
'data-name': data.name,
'data-size': (data.val.length * 0.75) | 0,
'data-mime': data.mime
};
},
} }
}; };
@@ -340,10 +380,16 @@ Sample JSON representation of the text above:
} }
return { return {
/** /**
* Parse text into structured representation. * Parse plain text into structured representation.
*/ */
parse: function(content) { parse: function(content) {
// Make sure we are parsing strings only.
if (typeof content != "string") {
return null;
}
// Split text into lines. It makes further processing easier. // Split text into lines. It makes further processing easier.
var lines = content.split(/\r?\n/); var lines = content.split(/\r?\n/);
@@ -443,6 +489,64 @@ Sample JSON representation of the text above:
return result; return result;
}, },
/**
* Add inline image to drafty content
*
* @param {Drafty} content object to add image to
* @param {integer} at index where the object is inserted
* @param {string} mime mime-type of the image, e.g. "image/png"
* @param {string} bits base64-encoded image content
* @param {integer} width width of the image
* @param {integer} height height of the image
* @param {string} fname file name suggestion for downloading the image.
*/
insertImage: function(content, at, mime, base64bits, width, height, fname) {
content = content || {txt: " "};
content.ent = content.ent || [];
content.fmt = content.fmt || [];
content.fmt.push({
at: at,
len: 1,
key: content.ent.length
});
content.ent.push({
tp: "IM",
data: {
mime: mime,
val: base64bits,
width: width,
height: height,
name: fname
}
});
return content;
},
attachFile: function(content, mime, base64bits, fname) {
content = content || {txt: ""};
content.ent = content.ent || [];
content.fmt = content.fmt || [];
content.fmt.push({
at: -1,
len: 0,
key: content.ent.length
});
content.ent.push({
tp: "EX",
data: {
mime: mime,
val: base64bits,
name: fname
}
});
return content;
},
/** /**
* Given the structured representation of rich text, convert it to HTML. * Given the structured representation of rich text, convert it to HTML.
* No attempt is made to strip pre-existing html markup. * No attempt is made to strip pre-existing html markup.
@@ -470,8 +574,10 @@ Sample JSON representation of the text above:
} }
if (DECORATORS[tp]) { if (DECORATORS[tp]) {
markup.push({idx: range.at, what: DECORATORS[tp].open(data)}); // Because we later sort in descending order, closing markup must come first.
// Otherwise zero-length objects will not be represented correctly.
markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)}); markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)});
markup.push({idx: range.at, what: DECORATORS[tp].open(data)});
} }
} }
} }
@@ -501,12 +607,20 @@ Sample JSON representation of the text above:
format: function(content, formatter, context) { format: function(content, formatter, context) {
var {txt, fmt, ent} = content; var {txt, fmt, ent} = content;
txt = txt || "";
if (!fmt) { if (!fmt) {
return [txt]; return [txt];
} }
var spans = [].concat(fmt); var spans = [].concat(fmt);
// Zero values may have been stripped. Restore them.
spans.map(function(s) {
s.at = s.at || 0;
s.len = s.len || 0;
});
// Soft spans first by start index (asc) then by length (desc). // Soft spans first by start index (asc) then by length (desc).
spans.sort(function(a, b) { spans.sort(function(a, b) {
if (a.at - b.at == 0) { if (a.at - b.at == 0) {
@@ -520,7 +634,8 @@ Sample JSON representation of the text above:
spans = spans.map(function(s) { spans = spans.map(function(s) {
var data; var data;
var tp = s.tp; var tp = s.tp;
if (s.key + 1 > 0) { if (!tp) {
s.key = s.key || 0;
data = ent[s.key].data; data = ent[s.key].data;
tp = ent[s.key].tp; tp = ent[s.key].tp;
} }
@@ -542,13 +657,60 @@ Sample JSON representation of the text above:
/** /**
* Returns true if content has no markup and no entities. * Returns true if content has no markup and no entities.
* *
* @param {drafty} content - content to check for presence of markup. * @param {Drafty} content - content to check for presence of markup.
* @returns true is content is plain text, false otherwise. * @returns true is content is plain text, false otherwise.
*/ */
isPlainText: function(content, noLineBreaks) { isPlainText: function(content, noLineBreaks) {
return !(content.fmt || content.ent); return !(content.fmt || content.ent);
}, },
/**
* Check if the drafty content has attachments.
*
* @param {Drafty} content - content to check for attachments.
* @returns true if there are attachments.
*/
hasAttachment: function(content) {
if (content.ent && content.ent.length > 0) {
for (var i in content.ent) {
if (content.ent[i].tp == "EX") {
return true;
}
}
}
return false;
},
/**
* Enumerate attachments
*
* @param {Drafty} content - drafty object to process for attachments.
* @param {function} callback - callback to call for each attachment.
* @param {Object} content - value of "this" for callback.
*/
attachments: function(content, callback, context) {
if (content.ent && content.ent.length > 0) {
for (var i in content.ent) {
if (content.ent[i].tp == "EX") {
callback.call(context, content.ent[i].data, i);
}
}
}
},
// Get blob URL for a given entity
getBlobUrl: function(entity) {
return base64toObjectUrl(entity.val, entity.mime);
},
getAttachmentSize: function(entity) {
// Value is base64 encoded. The actual object size is smaller than the encoded length.
return entity.val ? (entity.val.length * 0.75) | 0 : 0;
},
getAttachmentType: function(entity) {
return entity.mime || "text/plain";
},
/** /**
* Get HTML tag for a given two-letter style name * Get HTML tag for a given two-letter style name
* @param {string} style - two-letter style, like ST or LN * @param {string} style - two-letter style, like ST or LN
@@ -562,7 +724,7 @@ Sample JSON representation of the text above:
* For a given data bundle generate an object with HTML attributes, * For a given data bundle generate an object with HTML attributes,
* for instance, given {url: "http://www.example.com/"} return * for instance, given {url: "http://www.example.com/"} return
* {href: "http://www.example.com/"} * {href: "http://www.example.com/"}
* @param {stting} style - tw-letter style to generate attributes for. * @param {string} style - tw-letter style to generate attributes for.
* @param {Object} data - data bundle to convert to attributes * @param {Object} data - data bundle to convert to attributes
* @returns object with HTML attributes. * @returns object with HTML attributes.
*/ */
@@ -581,7 +743,6 @@ Sample JSON representation of the text above:
getContentType: function() { getContentType: function() {
return "text/x-drafty"; return "text/x-drafty";
} }
}; };
}); });
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Before

Width:  |  Height:  |  Size: 748 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB