Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9b7a2671 | ||
|
|
1a3362f1cd | ||
|
|
9009667c5a | ||
|
|
a59023575b | ||
|
|
b2b1ca2934 | ||
|
|
afd1bbb58a | ||
|
|
de655a105a | ||
|
|
2c1c0b2544 | ||
|
|
ccf5318e4c | ||
|
|
7322abfd55 | ||
|
|
09518bbac2 | ||
|
|
b6a831ec91 | ||
|
|
378cd622c4 | ||
|
|
939e3e59cf | ||
|
|
22e82b1c7f | ||
|
|
e495917356 | ||
|
|
cf4b58cab2 | ||
|
|
0a96014484 | ||
|
|
3e8fd9d6e6 | ||
|
|
a06d29863d | ||
|
|
de24e23127 | ||
|
|
9417e1aa49 | ||
|
|
1f8d26a215 | ||
|
|
e4a0b60f1e | ||
|
|
19612e4236 | ||
|
|
af1e9c609b | ||
|
|
5d18c71475 | ||
|
|
20fc9ae343 | ||
|
|
2e8b9145b7 | ||
|
|
859dec8410 | ||
|
|
8f13d2c856 | ||
|
|
d2a226852b | ||
|
|
9c7a05f616 | ||
|
|
a6e59936e2 | ||
|
|
e3d95eafe1 | ||
|
|
e30568ecd5 | ||
|
|
9ac8ea8f93 | ||
|
|
1bac018961 | ||
|
|
1aba91d6f3 | ||
|
|
fd750558ac |
@@ -1 +0,0 @@
|
||||
tinode.js
|
||||
@@ -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/)
|
||||
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)
|
||||
|
||||
@@ -16,24 +16,23 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia
|
||||
|
||||
## Missing functionality
|
||||
|
||||
* Cannot send images or other media.
|
||||
* Cannot delete messages.
|
||||
* No typing indicators.
|
||||
* No indication of chat status (muted, blocked, etc.).
|
||||
* Browser push notifications are missing.
|
||||
* No encryption.
|
||||
* Previews not generated for videos, audio, links or docs.
|
||||
* No indication of some chat statuses (muted, blocked, etc.).
|
||||
* Browser push notifications are not implemented.
|
||||
* No end-to-end encryption.
|
||||
* Editing chat permissions is incomplete.
|
||||
* Emoji support is weak.
|
||||
|
||||
## Responsive design
|
||||
|
||||
### Desktop screenshot
|
||||
|
||||
<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>
|
||||
|
||||
### Mobile screenshots
|
||||
|
||||
<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>
|
||||
|
||||
+213
-36
@@ -247,6 +247,36 @@ ul.menu > li.separator {
|
||||
ul.menu > li.separator:hover {
|
||||
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 */
|
||||
|
||||
/* Generic text colors */
|
||||
@@ -258,6 +288,14 @@ ul.menu > li.separator:hover {
|
||||
color: #D32F2F; /* red 700 */
|
||||
}
|
||||
|
||||
.gray {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.hr {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -411,7 +449,6 @@ table.permission-editor td.checkbox {
|
||||
.permission-editor .material-icons {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.material-icons.clickable {
|
||||
@@ -505,17 +542,51 @@ table.permission-editor td.checkbox {
|
||||
|
||||
|
||||
/* Area which displays an error message */
|
||||
#alert-box {
|
||||
background-color: transparent;
|
||||
height: 0;
|
||||
.alert-box {
|
||||
position: relative;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
padding: 1rem 1.5rem 1rem 0.5rem;
|
||||
}
|
||||
#alert-box.error-alert {
|
||||
padding: 0.5rem;
|
||||
.alert-box .cancel {
|
||||
position: absolute;
|
||||
right: 0.25rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-box.error {
|
||||
display: flex;
|
||||
background-color: #ffcdd2;
|
||||
border-color: #d32f2f;
|
||||
border-bottom: 1px solid #ffbfc8;
|
||||
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 */
|
||||
@@ -540,6 +611,7 @@ table.permission-editor td.checkbox {
|
||||
|
||||
.contact-box > li {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
|
||||
min-height: 3.5rem;
|
||||
flex-grow: 0;
|
||||
@@ -571,20 +643,6 @@ table.permission-editor td.checkbox {
|
||||
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 */
|
||||
.contact-box .avatar-box {
|
||||
margin-right: 0.35rem;
|
||||
@@ -609,7 +667,7 @@ table.permission-editor td.checkbox {
|
||||
}
|
||||
|
||||
/* Online-offline indicator positioned inside avatar-box */
|
||||
.contact-box .online, .offline {
|
||||
.avatar-box .online, .offline {
|
||||
display: inline-block;
|
||||
width: 0.75rem;
|
||||
min-width: 0.75rem;
|
||||
@@ -621,20 +679,37 @@ table.permission-editor td.checkbox {
|
||||
right: 0;
|
||||
bottom: 0.1rem;
|
||||
}
|
||||
.contact-box .online {
|
||||
.avatar-box .online {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
.contact-box .offline {
|
||||
.avatar-box .offline {
|
||||
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 {
|
||||
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;
|
||||
@@ -772,6 +847,10 @@ table.permission-editor td.checkbox {
|
||||
/* END of sidepanel styles */
|
||||
|
||||
/* Topic/messages view */
|
||||
#topic-view {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#topic-caption-panel {
|
||||
background-color: #b0bec5;
|
||||
}
|
||||
@@ -784,7 +863,6 @@ table.permission-editor td.checkbox {
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
#topic-caption-panel .avatar-box {
|
||||
position: relative;
|
||||
width: 3rem;
|
||||
@@ -806,6 +884,7 @@ table.permission-editor td.checkbox {
|
||||
|
||||
/* Scrollable panel where the messages are shown */
|
||||
#messages-panel {
|
||||
position: relative;
|
||||
padding: 0.75rem;
|
||||
overflow-y: scroll;
|
||||
background-color: #eceff1;
|
||||
@@ -813,12 +892,37 @@ table.permission-editor td.checkbox {
|
||||
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 */
|
||||
#send-message-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
padding: 0.4rem;
|
||||
padding: 0.4rem 0.4rem 0.4rem 0.6rem;
|
||||
background-color: #eceff1;
|
||||
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
||||
z-index: 2;
|
||||
@@ -829,6 +933,18 @@ table.permission-editor td.checkbox {
|
||||
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 */
|
||||
#dummy-view {
|
||||
width: 100%;
|
||||
@@ -845,6 +961,49 @@ table.permission-editor td.checkbox {
|
||||
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 */
|
||||
.chat-box {
|
||||
list-style: none;
|
||||
@@ -869,6 +1028,7 @@ table.permission-editor td.checkbox {
|
||||
}
|
||||
|
||||
.chat-box .bubble {
|
||||
display: flex;
|
||||
position: relative;
|
||||
max-width: 40rem;
|
||||
min-width: 12rem;
|
||||
@@ -980,7 +1140,7 @@ table.permission-editor td.checkbox {
|
||||
|
||||
.chat-box .timestamp {
|
||||
display: inline-block;
|
||||
position:relative;
|
||||
position: relative;
|
||||
font-size: 80%;
|
||||
color: #777;
|
||||
padding-left: 1rem;
|
||||
@@ -1009,6 +1169,24 @@ table.permission-editor td.checkbox {
|
||||
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 topic/mesages view */
|
||||
|
||||
@@ -1053,12 +1231,13 @@ table.permission-editor td.checkbox {
|
||||
.dialog-overlay {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
background-color: rgba(0,0,0,0.25);
|
||||
background-color: rgba(255,255,255,0.90);
|
||||
}
|
||||
.modal-dialog {
|
||||
display: flex;
|
||||
@@ -1340,10 +1519,14 @@ ul.tabbar li.active > a:after {
|
||||
.material-icons {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.material-icons.small {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.material-icons.big {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
|
||||
#self-avatar.material-icons, .avatar-box .material-icons {
|
||||
font-size: 2.4rem;
|
||||
@@ -1470,15 +1653,9 @@ ul.tabbar li.active > a:after {
|
||||
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); }
|
||||
|
||||
@@ -73,7 +73,7 @@ Sample JSON representation of the text above:
|
||||
{name: "MN", dataName: "val",
|
||||
pack: function(val) { return {val: val.slice(1)}; },
|
||||
re: /\B@(\w\w+)/g},
|
||||
// Hashtags #hashtag
|
||||
// Hashtags #hashtag, like metion 2 or more characters.
|
||||
{name: "HT", dataName: "val",
|
||||
pack: function(val) { return {val: val.slice(1)}; },
|
||||
re: /\B#(\w\w+)/g}
|
||||
@@ -88,9 +88,23 @@ Sample JSON representation of the text above:
|
||||
BR: { name: 'br', isVoid: true },
|
||||
LN: { 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.
|
||||
var DECORATORS = {
|
||||
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 + '">'; },
|
||||
close: function(data) { return '</a>'; },
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Parse text into structured representation.
|
||||
* Parse plain text into structured representation.
|
||||
*/
|
||||
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.
|
||||
var lines = content.split(/\r?\n/);
|
||||
|
||||
@@ -443,6 +489,64 @@ Sample JSON representation of the text above:
|
||||
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.
|
||||
* No attempt is made to strip pre-existing html markup.
|
||||
@@ -470,8 +574,10 @@ Sample JSON representation of the text above:
|
||||
}
|
||||
|
||||
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, what: DECORATORS[tp].open(data)});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,12 +607,20 @@ Sample JSON representation of the text above:
|
||||
format: function(content, formatter, context) {
|
||||
var {txt, fmt, ent} = content;
|
||||
|
||||
txt = txt || "";
|
||||
|
||||
if (!fmt) {
|
||||
return [txt];
|
||||
}
|
||||
|
||||
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).
|
||||
spans.sort(function(a, b) {
|
||||
if (a.at - b.at == 0) {
|
||||
@@ -520,7 +634,8 @@ Sample JSON representation of the text above:
|
||||
spans = spans.map(function(s) {
|
||||
var data;
|
||||
var tp = s.tp;
|
||||
if (s.key + 1 > 0) {
|
||||
if (!tp) {
|
||||
s.key = s.key || 0;
|
||||
data = ent[s.key].data;
|
||||
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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
isPlainText: function(content, noLineBreaks) {
|
||||
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
|
||||
* @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 instance, given {url: "http://www.example.com/"} return
|
||||
* {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
|
||||
* @returns object with HTML attributes.
|
||||
*/
|
||||
@@ -581,7 +743,6 @@ Sample JSON representation of the text above:
|
||||
getContentType: function() {
|
||||
return "text/x-drafty";
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
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 |
+817
-285
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 748 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user