├── .gitignore
├── README.md
├── chat.coffee
├── example.framer
├── .gitignore
├── app.coffee
├── framer
│ ├── .bookmark
│ ├── coffee-script.js
│ ├── config.json
│ ├── framer.generated.js
│ ├── framer.init.js
│ ├── framer.js
│ ├── framer.js.map
│ ├── framer.modules.js
│ ├── images
│ │ ├── cursor-active.png
│ │ ├── cursor-active@2x.png
│ │ ├── cursor.png
│ │ ├── cursor@2x.png
│ │ ├── icon-120.png
│ │ ├── icon-152.png
│ │ ├── icon-180.png
│ │ ├── icon-192.png
│ │ └── icon-76.png
│ ├── style.css
│ └── version
├── images
│ ├── .gitkeep
│ ├── andrew.jpg
│ ├── engly.jpg
│ ├── garron.jpg
│ ├── isaak.jpg
│ └── ningxia.jpg
├── index.html
└── modules
│ ├── chat.coffee
│ └── input.coffee
├── example
└── sampleChat.framer
│ └── modules
│ └── chat.coffee
└── img
└── chat.gif
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Framer-chat
2 |
3 | A Framer module to easily build an interactive chat interface.
4 |
5 | 
6 |
7 | ## Add it to your Framer Studio project
8 |
9 | * Download the project from Github.
10 | * Copy `chat.coffee` into `modules/` folder.
11 | * Download a copy of [Input-Framer](https://github.com/ajimix/Input-Framer)
12 | * Import the module into your project by adding `{ Chat } = require 'chat'` to the top of your project's code.
13 | * Instantiate a new instance of the chat module with `chat = new Chat`
14 |
15 | ## How to use it
16 |
17 | Getting started is pretty easy. Follow the instructions above to create a new instance of the chat module...
18 |
19 | ```coffeescript
20 | { Chat } = require 'chat'
21 |
22 | chat = new Chat
23 | fontSize: 24
24 | lineHeight: 36
25 | ```
26 |
27 | By default, the chat module will have styling and a sample message. You can override any of these defaults you want.
28 |
29 | ```coffeescript
30 | { Chat } = require 'chat'
31 |
32 | chat = new Chat
33 | fontSize: 24 # Text size in pixels
34 | lineHeight: 36 # Line height in pixels
35 | padding: 20 # Space between text and bubble, bubble and screen, etc.
36 | borderRadius: 20 # Radius of the chat bubbles
37 | maxWidth: Screen.width * 0.6 # Maximum width of chat bubbles
38 | avatarSize: 60 # Height and width of user avatars
39 | avatarBorderRadius: 30 # Border radius of avatars (circular by default)
40 | inputBorderColor: '#ccc' # Color of the top border of the input container
41 | inputHeight: 80 # Height of the input container
42 | placeholder: 'Start chatting' # Placeholder text for the input container
43 | defaultUserId: 1 # The user doing the chatting. The ID references the collection below
44 | authorTextColor: '#999' # Text color of user names
45 | bubbleColor: # Text bubble colors. Usually the left bubble is grey
46 | right: '#4080FF'
47 | left: '#eee'
48 | bubbleText: # Color of the bubble text
49 | right: 'white'
50 | left: 'black'
51 | data: [ # The conversation that's initially rendered
52 | {
53 | author: 1
54 | message: 'Lorem ipsum dolor sit amet, ei has impetus vituperata adversarium, nihil populo semper eu ius, an eam vero sensibus.'
55 | }
56 | ]
57 | users: [ # A collection of users
58 | {
59 | id: 1 # Referenced in `author` in the `data` collection
60 | name: 'Ningxia' # Display name
61 | avatar: 'ningxia.jpg' # User avatar images should be in /images directory
62 | }
63 | ]
64 | ```
65 |
66 | A default initial conversation is included in the module, but you can pass your own. The conversation should be an array of objects (a collection). For example:
67 |
68 | ```coffeescript
69 | { Chat } = require 'chat'
70 |
71 | # Default conversation
72 | data = [
73 | {
74 | author: 1
75 | message: 'Lorem ipsum dolor sit amet, ei has impetus vituperata adversarium, nihil populo semper eu ius, an eam vero sensibus.'
76 | }
77 | {
78 | author: 2
79 | message: 'In facilisis dignissim mea, no cum blandit accusata contentiones. Luptatum inimicus at usu.'
80 | }
81 | {
82 | author: 3
83 | message: 'Nec dolorum mediocrem at.'
84 | }
85 | {
86 | author: 1
87 | message: 'Te mazim.'
88 | }
89 | ]
90 |
91 | chat = new Chat
92 | data: data
93 | ```
94 |
95 | Additionally, the module contains a default collection of one user. You might want to add your own. Don't forget to add your avatar images to your `/images` directory.
96 |
97 | ```coffeescript
98 | { Chat } = require 'chat'
99 |
100 | conversation = [...]
101 |
102 | # Default users
103 | users = [
104 | {
105 | id: 1
106 | name: 'Isaak'
107 | avatar: 'isaak.jpg'
108 | }
109 | {
110 | id: 2
111 | name: 'Garron'
112 | avatar: 'garron.jpg'
113 | }
114 | {
115 | id: 3
116 | name: 'Engly'
117 | avatar: 'engly.jpg'
118 | }
119 | {
120 | id: 4
121 | name: 'Ningxia'
122 | avatar: 'ningxia.jpg'
123 | }
124 | ]
125 |
126 | chat = new Chat
127 | data: data
128 | users: users
129 | defaultUserId: 4
130 | ```
131 |
132 | ## Adding user chats
133 |
134 | Out of the box, in the input field is hooked up and allows you to add comments to the chat log. It's also possible to add comments programmatically.
135 |
136 | ```coffeescript
137 | { Chat } = require 'chat'
138 |
139 | chat = new Chat
140 |
141 | newComment =
142 | author: 1
143 | message: 'This is a programmatically added comment!'
144 |
145 | chat.renderComment newComment, 'right'
146 | ```
147 |
148 | ## Usage example
149 |
150 | Check out the example project.
151 |
152 | ## Todo
153 |
154 | * ⬜️ Group right aligned comments
155 | * ⬜️ Timestamps
156 | * ⬜️ Ability to pass in comments with special formats
157 |
--------------------------------------------------------------------------------
/chat.coffee:
--------------------------------------------------------------------------------
1 | InputModule = require 'input'
2 |
3 | class exports.Chat
4 | defaults =
5 | fontSize: 24
6 | lineHeight: 36
7 | padding: 20
8 | borderRadius: 20
9 | maxWidth: Screen.width * 0.6
10 | avatarSize: 60
11 | avatarBorderRadius: 30
12 | inputBorderColor: '#ccc'
13 | inputHeight: 80
14 | placeholder: 'Start chatting'
15 | defaultUserId: 1
16 | authorTextColor: '#999'
17 | bubbleColor:
18 | right: '#4080FF'
19 | left: '#eee'
20 | bubbleText:
21 | right: 'white'
22 | left: 'black'
23 | data: [
24 | {
25 | author: 1
26 | message: 'Lorem ipsum dolor sit amet, ei has impetus vituperata adversarium, nihil populo semper eu ius, an eam vero sensibus.'
27 | }
28 | ]
29 | users: [
30 | {
31 | id: 1
32 | name: 'Ningxia'
33 | avatar: 'ningxia.jpg'
34 | }
35 | ]
36 |
37 | constructor: (options) ->
38 | if options == undefined then options = {}
39 | options = _.defaults options, defaults
40 | @options = options
41 | @_group = 0
42 |
43 | @commentsScroll = new ScrollComponent
44 | name: 'comments'
45 | backgroundColor: null
46 | width: Screen.width
47 | height: Screen.height - @options.inputHeight
48 | mouseWheelEnabled: true
49 | scrollHorizontal: false
50 |
51 | @commentsScroll.contentInset =
52 | top: @options.padding
53 |
54 | @renderComment = (comment, align) =>
55 | # Calcuate the message size
56 | @_messageSize = Utils.textSize comment.message,
57 | {'padding': "#{@options.padding}px"},
58 | {width: @options.maxWidth}
59 |
60 | @_leftPadding = @options.padding * 2 + @options.avatarSize
61 |
62 | # Find the author
63 | @_author = _.find @options.users, {id: comment.author}
64 |
65 | # Find comments by the same author
66 | # Only works on left comments so far, need to do for right
67 | @_commentIndex = _.findIndex @options.data, comment
68 |
69 | @_previousComment = @_nextComment = @options.data[@_commentIndex - 1]
70 | @_nextComment = @options.data[@_commentIndex + 1]
71 |
72 | @_sameNextAuthor = if @_nextComment and @_nextComment.author is comment.author then true else false
73 | @_samePreviousAuthor = if @_previousComment and @_previousComment.author is comment.author then true else false
74 |
75 | if @_samePreviousAuthor or @_sameNextAuthor
76 | @_group = @_group + 1
77 |
78 | @_messageMargin = if @_sameNextAuthor and align is 'left' then @options.lineHeight * 0.25 else @options.lineHeight * 2
79 |
80 | # Construct the comment
81 | @comment = new Layer
82 | parent: @commentsScroll.content
83 | name: 'comment'
84 | backgroundColor: null
85 | width: Screen.width
86 | height: @_messageSize.height + @_messageMargin
87 |
88 | unless @_samePreviousAuthor
89 | @author = new Layer
90 | name: 'comment:author'
91 | html: @_author.name
92 | parent: @comment
93 | x: if align is 'right' then Align.right(-@options.padding) else @_leftPadding
94 | width: @comment.width
95 | color: @options.authorTextColor
96 | backgroundColor: null
97 | style:
98 | 'font-weight': 'bold'
99 | 'font-size': '90%'
100 | 'text-align': align
101 |
102 | @message = new Layer
103 | name: 'comment:message'
104 | parent: @comment
105 | html: comment.message
106 | height: @_messageSize.height
107 | y: @options.lineHeight
108 | backgroundColor: @options.bubbleColor[align]
109 | color: @options.bubbleText[align]
110 | borderRadius: @options.borderRadius
111 | style:
112 | 'padding': "#{@options.padding}px"
113 | 'width': 'auto'
114 | 'max-width': "#{@_messageSize.width}px"
115 | 'text-align': align
116 |
117 |
118 | # Special stuff for alignment
119 | if align is 'right'
120 | @_width = parseInt @message.computedStyle()['width']
121 | @message.x = Screen.width - @_width - @options.padding
122 | else
123 | # Avatar
124 | @.message.x = @_leftPadding
125 |
126 | unless @_sameNextAuthor
127 | @avatar = new Layer
128 | parent: @comment
129 | name: 'comment:avatar'
130 | size: @options.avatarSize
131 | borderRadius: @options.avatarBorderRadius
132 | image: "images/#{@_author.avatar}"
133 | x: @options.padding
134 | y: Align.bottom(-@options.padding * 2)
135 |
136 | # Grouped comments border
137 | if @_samePreviousAuthor and @_sameNextAuthor
138 | @message.style =
139 | "border-top-#{align}-radius": '3px'
140 | "border-bottom-#{align}-radius": '3px'
141 |
142 | if @_group is 1
143 | @message.style = "border-bottom-#{align}-radius": '3px'
144 |
145 | if @_group > 1 and !@_sameNextAuthor
146 | @message.style = "border-top-#{align}-radius": '3px'
147 |
148 | # Recalcuate position
149 | @reflow()
150 |
151 | @reflow = () =>
152 | @commentsHeight = 0
153 | @comments = @commentsScroll.content.children
154 |
155 | # Loop through all the comments
156 | for comment, i in @comments
157 | commentsHeight = @commentsHeight + comment.height
158 | @yOffset = 0
159 |
160 | # Add up the height of the sibling layers to the left of the current layer
161 | for layer in _.take(@comments, i)
162 | @yOffset = @yOffset + layer.height
163 |
164 | # Set the current comment position to the height of left siblings
165 | comment.y = @yOffset
166 |
167 | # Scroll stuff
168 | @commentsScroll.updateContent()
169 | @commentsScroll.scrollToLayer @comments[@comments.length - 1]
170 |
171 |
172 | # Draw everything
173 | _.map @options.data, (comment) =>
174 | @renderComment(comment, 'left')
175 |
176 |
177 | # New commpents
178 | @inputWrapper = new Layer
179 | name: 'input'
180 | backgroundColor: null
181 | height: @options.inputHeight
182 | width: Screen.width
183 | y: Align.bottom
184 | style:
185 | 'border-top': "1px solid #{@options.inputBorderColor}"
186 |
187 | @input = new InputModule.Input
188 | name: 'input:field'
189 | parent: @inputWrapper
190 | width: Screen.width
191 | placeholder: @options.placeholder
192 | virtualKeyboard: false
193 |
194 | createComment = (value) =>
195 | newComment =
196 | author: @options.defaultUserId
197 | message: value
198 |
199 | @renderComment newComment, 'right'
200 |
201 | @input.on 'keyup', (event) ->
202 | # Add new comments
203 | if event.which is 13
204 | createComment(@value)
205 | @value = ''
206 |
207 | @input.form.addEventListener 'submit', (event) ->
208 | event.preventDefault()
209 |
--------------------------------------------------------------------------------
/example.framer/.gitignore:
--------------------------------------------------------------------------------
1 | # Framer Git Ignore
2 |
3 | # General OSX
4 |
5 | .DS_Store
6 | .AppleDouble
7 | .LSOverride
8 |
9 | # Icon must end with two \r
10 | Icon
11 |
12 | # Thumbnails
13 | ._*
14 |
15 | # Files that might appear in the root of a volume
16 | .DocumentRevisions-V100
17 | .fseventsd
18 | .Spotlight-V100
19 | .TemporaryItems
20 | .Trashes
21 | .VolumeIcon.icns
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 |
30 | # Framer Specific
31 | .*.html
32 | .app.js
33 | framer/*.old*
34 | framer/.*.hash
35 | framer/backup.coffee
36 | framer/backups/*
37 | framer/manifest.txt
38 | framer/metadata.json
39 | framer/preview.png
40 | framer/social-880x460.png
41 | framer/social-1200x630.png
42 |
--------------------------------------------------------------------------------
/example.framer/app.coffee:
--------------------------------------------------------------------------------
1 | { Chat } = require 'chat'
2 |
3 | # Data
4 | users = [
5 | {
6 | id: 1
7 | name: 'Isaak'
8 | avatar: 'isaak.jpg'
9 | }
10 | {
11 | id: 2
12 | name: 'Garron'
13 | avatar: 'garron.jpg'
14 | }
15 | {
16 | id: 3
17 | name: 'Engly'
18 | avatar: 'engly.jpg'
19 | }
20 | {
21 | id: 4
22 | name: 'Ningxia'
23 | avatar: 'ningxia.jpg'
24 | }
25 | {
26 | id: 5
27 | name: 'Andrew'
28 | avatar: 'andrew.jpg'
29 | }
30 | ]
31 |
32 | data = [
33 | {
34 | author: 1
35 | message: 'Lorem ipsum dolor sit amet, ei has impetus vituperata adversarium, nihil populo semper eu ius, an eam vero sensibus.'
36 | }
37 | {
38 | author: 2
39 | message: 'In facilisis dignissim mea, no cum blandit accusata contentiones. Luptatum inimicus at usu, ceteros quaerendum eu sed, id eirmod audire epicuri eum. Ea eos idque voluptatum.'
40 | }
41 | {
42 | author: 3
43 | message: 'Nec dolorum mediocrem at.'
44 | }
45 | {
46 | author: 4
47 | message: 'Nec an sonet euismod equidem, velit postulant intellegebat mei eu, eos ea offendit noluisse. Convenire definiebas constituam usu ut.'
48 | }
49 | {
50 | author: 1
51 | message: 'Te mazim.'
52 | }
53 | {
54 | author: 2
55 | message: 'Mel quidam deserunt an.'
56 | }
57 | {
58 | author: 3
59 | message: 'Ea ius tale audiam definitiones. Inani suavitate reprehendunt mea no. Nam quod rebum ea. At erant eruditi antiopam eos. In has idque tempor doctus, sit legere detracto cu.'
60 | }
61 | {
62 | author: 4
63 | message: 'Vel iudico aperiam invenire ne, graecis offendit principes id vix. Est ad sonet tibique, discere volumus oporteat ea vis. At option incorrupte scriptorem ius, in verear meliore vivendo usu, ut cum rebum fugit.'
64 | }
65 | {
66 | author: 1
67 | message: 'Amet eirmod ius ut, cu eum recteque facilisis complectitur.'
68 | }
69 | {
70 | author: 4
71 | message: 'Ex nullam deseruisse duo, no vim novum omittam definitiones, vix mollis indoctum scripserit an. Id persius efficiendi mel, solet iracundia disputationi est ne.'
72 | }
73 | {
74 | author: 4
75 | message: 'Mundi obliqu.'
76 | }
77 | {
78 | author: 4
79 | message: 'Ex saepe explicari pri, iriure appareat constituam te vix. Dicant postulant ocurreret cum eu, nam ne sumo integre assentior. In sumo mundi verterem ius, mel prima placerat praesent te.'
80 | }
81 | ]
82 |
83 |
84 | # Setup
85 | background = new BackgroundLayer
86 | backgroundColor: 'white'
87 |
88 | chat = new Chat
89 | data: data
90 | users: users
91 |
92 |
--------------------------------------------------------------------------------
/example.framer/framer/.bookmark:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/.bookmark
--------------------------------------------------------------------------------
/example.framer/framer/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "orientation" : 0,
3 | "updateDelay" : 0.3,
4 | "contentScale" : 1,
5 | "fullScreen" : false,
6 | "sharedPrototype" : 0,
7 | "propertyPanelToggleStates" : {
8 |
9 | },
10 | "deviceType" : "apple-iphone-7-silver",
11 | "projectId" : "781D503B-74C7-419A-A9C1-94E3AA77994F",
12 | "deviceOrientation" : 0,
13 | "selectedHand" : "",
14 | "foldedCodeRanges" : [
15 | "{27, 1963}"
16 | ],
17 | "deviceScale" : "fit"
18 | }
--------------------------------------------------------------------------------
/example.framer/framer/framer.generated.js:
--------------------------------------------------------------------------------
1 | // This is autogenerated by Framer
2 |
3 |
4 | if (!window.Framer && window._bridge) {window._bridge('runtime.error', {message:'[framer.js] Framer library missing or corrupt. Select File → Update Framer Library.'})}
5 | if (DeviceComponent) {DeviceComponent.Devices["iphone-6-silver"].deviceImageJP2 = false};
6 | if (window.Framer) {window.Framer.Defaults.DeviceView = {"deviceScale":"","selectedHand":"","deviceType":"custom","contentScale":"","orientation":0};
7 | }
8 | if (window.Framer) {window.Framer.Defaults.DeviceComponent = {"deviceScale":"","selectedHand":"","deviceType":"custom","contentScale":"","orientation":0};
9 | }
10 | window.FramerStudioInfo = {"deviceImagesUrl":"\/_server\/resources\/DeviceImages","documentTitle":"example.framer"};
11 |
12 | Framer.Device = new Framer.DeviceView();
13 | Framer.Device.setupContext();
--------------------------------------------------------------------------------
/example.framer/framer/framer.init.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | function isFileLoadingAllowed() {
4 | return (window.location.protocol.indexOf("file") == -1)
5 | }
6 |
7 | function isHomeScreened() {
8 | return ("standalone" in window.navigator) && window.navigator.standalone == true
9 | }
10 |
11 | function isCompatibleBrowser() {
12 | return Utils.isWebKit()
13 | }
14 |
15 | var alertNode;
16 |
17 | function dismissAlert() {
18 | alertNode.parentElement.removeChild(alertNode)
19 | loadProject()
20 | }
21 |
22 | function showAlert(html) {
23 |
24 | alertNode = document.createElement("div")
25 |
26 | alertNode.classList.add("framerAlertBackground")
27 | alertNode.innerHTML = html
28 |
29 | document.addEventListener("DOMContentLoaded", function(event) {
30 | document.body.appendChild(alertNode)
31 | })
32 |
33 | window.dismissAlert = dismissAlert;
34 | }
35 |
36 | function showBrowserAlert() {
37 | var html = ""
38 | html += "
"
39 | html += "
Error: Not A WebKit Browser "
40 | html += "Your browser is not supported.
Please use Safari or Chrome.
"
41 | html += "
Try anyway "
42 | html += "
"
43 |
44 | showAlert(html)
45 | }
46 |
47 | function showFileLoadingAlert() {
48 | var html = ""
49 | html += ""
50 | html += "
Error: Local File Restrictions "
51 | html += "Preview this prototype with Framer Mirror or learn more about "
52 | html += "
file restrictions .
"
53 | html += "
Try anyway "
54 | html += "
"
55 |
56 | showAlert(html)
57 | }
58 |
59 | function loadProject() {
60 | CoffeeScript.load("app.coffee")
61 | }
62 |
63 | function setDefaultPageTitle() {
64 | // If no title was set we set it to the project folder name so
65 | // you get a nice name on iOS if you bookmark to desktop.
66 | document.addEventListener("DOMContentLoaded", function() {
67 | if (document.title == "") {
68 | if (window.FramerStudioInfo && window.FramerStudioInfo.documentTitle) {
69 | document.title = window.FramerStudioInfo.documentTitle
70 | } else {
71 | document.title = window.location.pathname.replace(/\//g, "")
72 | }
73 | }
74 | })
75 | }
76 |
77 | function init() {
78 |
79 | if (Utils.isFramerStudio()) {
80 | return
81 | }
82 |
83 | setDefaultPageTitle()
84 |
85 | if (!isCompatibleBrowser()) {
86 | return showBrowserAlert()
87 | }
88 |
89 | if (!isFileLoadingAllowed()) {
90 | return showFileLoadingAlert()
91 | }
92 |
93 | loadProject()
94 |
95 | }
96 |
97 | init()
98 |
99 | })()
100 |
--------------------------------------------------------------------------------
/example.framer/framer/framer.modules.js:
--------------------------------------------------------------------------------
1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1 && !_this._sameNextAuthor) {
156 | _this.message.style = (
157 | obj2 = {},
158 | obj2["border-top-" + align + "-radius"] = '3px',
159 | obj2
160 | );
161 | }
162 | }
163 | return _this.reflow();
164 | };
165 | })(this);
166 | this.reflow = (function(_this) {
167 | return function() {
168 | var comment, commentsHeight, i, j, k, layer, len, len1, ref, ref1;
169 | _this.commentsHeight = 0;
170 | _this.comments = _this.commentsScroll.content.children;
171 | ref = _this.comments;
172 | for (i = j = 0, len = ref.length; j < len; i = ++j) {
173 | comment = ref[i];
174 | commentsHeight = _this.commentsHeight + comment.height;
175 | _this.yOffset = 0;
176 | ref1 = _.take(_this.comments, i);
177 | for (k = 0, len1 = ref1.length; k < len1; k++) {
178 | layer = ref1[k];
179 | _this.yOffset = _this.yOffset + layer.height;
180 | }
181 | comment.y = _this.yOffset;
182 | }
183 | _this.commentsScroll.updateContent();
184 | return _this.commentsScroll.scrollToLayer(_this.comments[_this.comments.length - 1]);
185 | };
186 | })(this);
187 | _.map(this.options.data, (function(_this) {
188 | return function(comment) {
189 | return _this.renderComment(comment, 'left');
190 | };
191 | })(this));
192 | this.inputWrapper = new Layer({
193 | name: 'input',
194 | backgroundColor: null,
195 | height: this.options.inputHeight,
196 | width: Screen.width,
197 | y: Align.bottom,
198 | style: {
199 | 'border-top': "1px solid " + this.options.inputBorderColor
200 | }
201 | });
202 | this.input = new InputModule.Input({
203 | name: 'input:field',
204 | parent: this.inputWrapper,
205 | width: Screen.width,
206 | placeholder: this.options.placeholder,
207 | virtualKeyboard: false
208 | });
209 | createComment = (function(_this) {
210 | return function(value) {
211 | var newComment;
212 | newComment = {
213 | author: _this.options.defaultUserId,
214 | message: value
215 | };
216 | return _this.renderComment(newComment, 'right');
217 | };
218 | })(this);
219 | this.input.on('keyup', function(event) {
220 | if (event.which === 13) {
221 | createComment(this.value);
222 | return this.value = '';
223 | }
224 | });
225 | this.input.form.addEventListener('submit', function(event) {
226 | return event.preventDefault();
227 | });
228 | }
229 |
230 | return Chat;
231 |
232 | })();
233 |
234 |
235 | },{"input":"input"}],"input":[function(require,module,exports){
236 | var growthRatio, imageHeight,
237 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
238 | hasProp = {}.hasOwnProperty;
239 |
240 | exports.keyboardLayer = new Layer({
241 | x: 0,
242 | y: Screen.height,
243 | width: Screen.width,
244 | height: 432,
245 | html: " "
246 | });
247 |
248 | growthRatio = Screen.width / 732;
249 |
250 | imageHeight = growthRatio * 432;
251 |
252 | exports.keyboardLayer.states = {
253 | shown: {
254 | y: Screen.height - imageHeight
255 | }
256 | };
257 |
258 | exports.keyboardLayer.states.animationOptions = {
259 | curve: "spring(500,50,15)"
260 | };
261 |
262 | exports.Input = (function(superClass) {
263 | extend(Input, superClass);
264 |
265 | Input.define("style", {
266 | get: function() {
267 | return this.input.style;
268 | },
269 | set: function(value) {
270 | return _.extend(this.input.style, value);
271 | }
272 | });
273 |
274 | Input.define("value", {
275 | get: function() {
276 | return this.input.value;
277 | },
278 | set: function(value) {
279 | return this.input.value = value;
280 | }
281 | });
282 |
283 | function Input(options) {
284 | if (options == null) {
285 | options = {};
286 | }
287 | if (options.setup == null) {
288 | options.setup = false;
289 | }
290 | if (options.width == null) {
291 | options.width = Screen.width;
292 | }
293 | if (options.clip == null) {
294 | options.clip = false;
295 | }
296 | if (options.height == null) {
297 | options.height = 60;
298 | }
299 | if (options.backgroundColor == null) {
300 | options.backgroundColor = options.setup ? "rgba(255, 60, 47, .5)" : "transparent";
301 | }
302 | if (options.fontSize == null) {
303 | options.fontSize = 30;
304 | }
305 | if (options.lineHeight == null) {
306 | options.lineHeight = 30;
307 | }
308 | if (options.padding == null) {
309 | options.padding = 10;
310 | }
311 | if (options.text == null) {
312 | options.text = "";
313 | }
314 | if (options.placeholder == null) {
315 | options.placeholder = "";
316 | }
317 | if (options.virtualKeyboard == null) {
318 | options.virtualKeyboard = Utils.isMobile() ? false : true;
319 | }
320 | if (options.type == null) {
321 | options.type = "text";
322 | }
323 | if (options.goButton == null) {
324 | options.goButton = false;
325 | }
326 | Input.__super__.constructor.call(this, options);
327 | if (options.placeholderColor != null) {
328 | this.placeholderColor = options.placeholderColor;
329 | }
330 | this.input = document.createElement("input");
331 | this.input.id = "input-" + (_.now());
332 | this.input.style.cssText = "font-size: " + options.fontSize + "px; line-height: " + options.lineHeight + "px; padding: " + options.padding + "px; width: " + options.width + "px; height: " + options.height + "px; border: none; outline-width: 0; background-image: url(about:blank); background-color: " + options.backgroundColor + ";";
333 | this.input.value = options.text;
334 | this.input.type = options.type;
335 | this.input.placeholder = options.placeholder;
336 | this.form = document.createElement("form");
337 | if (options.goButton) {
338 | this.form.action = "#";
339 | this.form.addEventListener("submit", function(event) {
340 | return event.preventDefault();
341 | });
342 | }
343 | this.form.appendChild(this.input);
344 | this._element.appendChild(this.form);
345 | this.backgroundColor = "transparent";
346 | if (this.placeholderColor) {
347 | this.updatePlaceholderColor(options.placeholderColor);
348 | }
349 | if (!Utils.isMobile() && options.virtualKeyboard === true) {
350 | this.input.addEventListener("focus", function() {
351 | exports.keyboardLayer.bringToFront();
352 | return exports.keyboardLayer.states.next();
353 | });
354 | this.input.addEventListener("blur", function() {
355 | return exports.keyboardLayer.states["switch"]("default");
356 | });
357 | }
358 | }
359 |
360 | Input.prototype.updatePlaceholderColor = function(color) {
361 | var css;
362 | this.placeholderColor = color;
363 | if (this.pageStyle != null) {
364 | document.head.removeChild(this.pageStyle);
365 | }
366 | this.pageStyle = document.createElement("style");
367 | this.pageStyle.type = "text/css";
368 | css = "#" + this.input.id + "::-webkit-input-placeholder { color: " + this.placeholderColor + "; }";
369 | this.pageStyle.appendChild(document.createTextNode(css));
370 | return document.head.appendChild(this.pageStyle);
371 | };
372 |
373 | Input.prototype.focus = function() {
374 | return this.input.focus();
375 | };
376 |
377 | Input.prototype.onFocus = function(cb) {
378 | return this.input.addEventListener("focus", function() {
379 | return cb.apply(this);
380 | });
381 | };
382 |
383 | Input.prototype.onBlur = function(cb) {
384 | return this.input.addEventListener("blur", function() {
385 | return cb.apply(this);
386 | });
387 | };
388 |
389 | return Input;
390 |
391 | })(Layer);
392 |
393 |
394 | },{}]},{},[])
395 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZnJhbWVyLm1vZHVsZXMuanMiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL1VzZXJzL2xpZWJjaGVuL0Ryb3Bib3ggKEZhY2Vib29rKS9QZXJzb25hbC9GcmFtZXIgY2hhdC9tb2R1bGUvZXhhbXBsZS5mcmFtZXIvbW9kdWxlcy9pbnB1dC5jb2ZmZWUiLCIuLi8uLi8uLi8uLi8uLi9Vc2Vycy9saWViY2hlbi9Ecm9wYm94IChGYWNlYm9vaykvUGVyc29uYWwvRnJhbWVyIGNoYXQvbW9kdWxlL2V4YW1wbGUuZnJhbWVyL21vZHVsZXMvY2hhdC5jb2ZmZWUiLCJub2RlX21vZHVsZXMvYnJvd3Nlci1wYWNrL19wcmVsdWRlLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydHMua2V5Ym9hcmRMYXllciA9IG5ldyBMYXllclxuXHR4OjAsIHk6U2NyZWVuLmhlaWdodCwgd2lkdGg6U2NyZWVuLndpZHRoLCBoZWlnaHQ6NDMyXG5cdGh0bWw6XCI8aW1nIHN0eWxlPSd3aWR0aDogMTAwJTsnIHNyYz0nbW9kdWxlcy9rZXlib2FyZC5wbmcnLz5cIlxuXG4jc2NyZWVuIHdpZHRoIHZzLiBzaXplIG9mIGltYWdlIHdpZHRoXG5ncm93dGhSYXRpbyA9IFNjcmVlbi53aWR0aCAvIDczMlxuaW1hZ2VIZWlnaHQgPSBncm93dGhSYXRpbyAqIDQzMlxuXG5leHBvcnRzLmtleWJvYXJkTGF5ZXIuc3RhdGVzID1cblx0c2hvd246IFxuXHRcdHk6IFNjcmVlbi5oZWlnaHQgLSBpbWFnZUhlaWdodFxuXG5leHBvcnRzLmtleWJvYXJkTGF5ZXIuc3RhdGVzLmFuaW1hdGlvbk9wdGlvbnMgPVxuXHRjdXJ2ZTogXCJzcHJpbmcoNTAwLDUwLDE1KVwiXG5cbmNsYXNzIGV4cG9ydHMuSW5wdXQgZXh0ZW5kcyBMYXllclxuXHRAZGVmaW5lIFwic3R5bGVcIixcblx0XHRnZXQ6IC0+IEBpbnB1dC5zdHlsZVxuXHRcdHNldDogKHZhbHVlKSAtPlxuXHRcdFx0Xy5leHRlbmQgQGlucHV0LnN0eWxlLCB2YWx1ZVxuXG5cdEBkZWZpbmUgXCJ2YWx1ZVwiLFxuXHRcdGdldDogLT4gQGlucHV0LnZhbHVlXG5cdFx0c2V0OiAodmFsdWUpIC0+XG5cdFx0XHRAaW5wdXQudmFsdWUgPSB2YWx1ZVxuXG5cdGNvbnN0cnVjdG9yOiAob3B0aW9ucyA9IHt9KSAtPlxuXHRcdG9wdGlvbnMuc2V0dXAgPz0gZmFsc2Vcblx0XHRvcHRpb25zLndpZHRoID89IFNjcmVlbi53aWR0aFxuXHRcdG9wdGlvbnMuY2xpcCA/PSBmYWxzZVxuXHRcdG9wdGlvbnMuaGVpZ2h0ID89IDYwXG5cdFx0b3B0aW9ucy5iYWNrZ3JvdW5kQ29sb3IgPz0gaWYgb3B0aW9ucy5zZXR1cCB0aGVuIFwicmdiYSgyNTUsIDYwLCA0NywgLjUpXCIgZWxzZSBcInRyYW5zcGFyZW50XCJcblx0XHRvcHRpb25zLmZvbnRTaXplID89IDMwXG5cdFx0b3B0aW9ucy5saW5lSGVpZ2h0ID89IDMwXG5cdFx0b3B0aW9ucy5wYWRkaW5nID89IDEwXG5cdFx0b3B0aW9ucy50ZXh0ID89IFwiXCJcblx0XHRvcHRpb25zLnBsYWNlaG9sZGVyID89IFwiXCJcblx0XHRvcHRpb25zLnZpcnR1YWxLZXlib2FyZCA/PSBpZiBVdGlscy5pc01vYmlsZSgpIHRoZW4gZmFsc2UgZWxzZSB0cnVlXG5cdFx0b3B0aW9ucy50eXBlID89IFwidGV4dFwiXG5cdFx0b3B0aW9ucy5nb0J1dHRvbiA/PSBmYWxzZVxuXG5cdFx0c3VwZXIgb3B0aW9uc1xuXG5cdFx0QHBsYWNlaG9sZGVyQ29sb3IgPSBvcHRpb25zLnBsYWNlaG9sZGVyQ29sb3IgaWYgb3B0aW9ucy5wbGFjZWhvbGRlckNvbG9yP1xuXHRcdEBpbnB1dCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQgXCJpbnB1dFwiXG5cdFx0QGlucHV0LmlkID0gXCJpbnB1dC0je18ubm93KCl9XCJcblx0XHRAaW5wdXQuc3R5bGUuY3NzVGV4dCA9IFwiZm9udC1zaXplOiAje29wdGlvbnMuZm9udFNpemV9cHg7IGxpbmUtaGVpZ2h0OiAje29wdGlvbnMubGluZUhlaWdodH1weDsgcGFkZGluZzogI3tvcHRpb25zLnBhZGRpbmd9cHg7IHdpZHRoOiAje29wdGlvbnMud2lkdGh9cHg7IGhlaWdodDogI3tvcHRpb25zLmhlaWdodH1weDsgYm9yZGVyOiBub25lOyBvdXRsaW5lLXdpZHRoOiAwOyBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoYWJvdXQ6YmxhbmspOyBiYWNrZ3JvdW5kLWNvbG9yOiAje29wdGlvbnMuYmFja2dyb3VuZENvbG9yfTtcIlxuXHRcdEBpbnB1dC52YWx1ZSA9IG9wdGlvbnMudGV4dFxuXHRcdEBpbnB1dC50eXBlID0gb3B0aW9ucy50eXBlXG5cdFx0QGlucHV0LnBsYWNlaG9sZGVyID0gb3B0aW9ucy5wbGFjZWhvbGRlclxuXHRcdEBmb3JtID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCBcImZvcm1cIlxuXG5cdFx0aWYgb3B0aW9ucy5nb0J1dHRvblxuXHRcdFx0QGZvcm0uYWN0aW9uID0gXCIjXCJcblx0XHRcdEBmb3JtLmFkZEV2ZW50TGlzdGVuZXIgXCJzdWJtaXRcIiwgKGV2ZW50KSAtPlxuXHRcdFx0XHRldmVudC5wcmV2ZW50RGVmYXVsdCgpXG5cblx0XHRAZm9ybS5hcHBlbmRDaGlsZCBAaW5wdXRcblx0XHRAX2VsZW1lbnQuYXBwZW5kQ2hpbGQgQGZvcm1cblxuXHRcdEBiYWNrZ3JvdW5kQ29sb3IgPSBcInRyYW5zcGFyZW50XCJcblx0XHRAdXBkYXRlUGxhY2Vob2xkZXJDb2xvciBvcHRpb25zLnBsYWNlaG9sZGVyQ29sb3IgaWYgQHBsYWNlaG9sZGVyQ29sb3JcblxuXHRcdCNvbmx5IHNob3cgaG9ub3IgdmlydHVhbCBrZXlib2FyZCBvcHRpb24gd2hlbiBub3Qgb24gbW9iaWxlLFxuXHRcdCNvdGhlcndpc2UgaWdub3JlXG5cdFx0aWYgIVV0aWxzLmlzTW9iaWxlKCkgJiYgb3B0aW9ucy52aXJ0dWFsS2V5Ym9hcmQgaXMgdHJ1ZVxuXHRcdFx0QGlucHV0LmFkZEV2ZW50TGlzdGVuZXIgXCJmb2N1c1wiLCAtPlxuXHRcdFx0XHRleHBvcnRzLmtleWJvYXJkTGF5ZXIuYnJpbmdUb0Zyb250KClcblx0XHRcdFx0ZXhwb3J0cy5rZXlib2FyZExheWVyLnN0YXRlcy5uZXh0KClcblx0XHRcdEBpbnB1dC5hZGRFdmVudExpc3RlbmVyIFwiYmx1clwiLCAtPlxuXHRcdFx0XHRleHBvcnRzLmtleWJvYXJkTGF5ZXIuc3RhdGVzLnN3aXRjaCBcImRlZmF1bHRcIlxuXG5cdHVwZGF0ZVBsYWNlaG9sZGVyQ29sb3I6IChjb2xvcikgLT5cblx0XHRAcGxhY2Vob2xkZXJDb2xvciA9IGNvbG9yXG5cdFx0aWYgQHBhZ2VTdHlsZT9cblx0XHRcdGRvY3VtZW50LmhlYWQucmVtb3ZlQ2hpbGQgQHBhZ2VTdHlsZVxuXHRcdEBwYWdlU3R5bGUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50IFwic3R5bGVcIlxuXHRcdEBwYWdlU3R5bGUudHlwZSA9IFwidGV4dC9jc3NcIlxuXHRcdGNzcyA9IFwiIyN7QGlucHV0LmlkfTo6LXdlYmtpdC1pbnB1dC1wbGFjZWhvbGRlciB7IGNvbG9yOiAje0BwbGFjZWhvbGRlckNvbG9yfTsgfVwiXG5cdFx0QHBhZ2VTdHlsZS5hcHBlbmRDaGlsZChkb2N1bWVudC5jcmVhdGVUZXh0Tm9kZSBjc3MpXG5cdFx0ZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZCBAcGFnZVN0eWxlXG5cblx0Zm9jdXM6ICgpIC0+XG5cdFx0QGlucHV0LmZvY3VzKClcblxuXHRvbkZvY3VzOiAoY2IpIC0+XG5cdFx0QGlucHV0LmFkZEV2ZW50TGlzdGVuZXIgXCJmb2N1c1wiLCAtPlxuXHRcdFx0Y2IuYXBwbHkoQClcblxuXHRvbkJsdXI6IChjYikgLT5cblx0XHRAaW5wdXQuYWRkRXZlbnRMaXN0ZW5lciBcImJsdXJcIiwgLT5cblx0XHRcdGNiLmFwcGx5KEApXG4iLCJJbnB1dE1vZHVsZSA9IHJlcXVpcmUgJ2lucHV0J1xuXG5jbGFzcyBleHBvcnRzLkNoYXRcblx0ZGVmYXVsdHMgPVxuXHRcdGZvbnRTaXplOiAyNFxuXHRcdGxpbmVIZWlnaHQ6IDM2XG5cdFx0cGFkZGluZzogMjBcblx0XHRib3JkZXJSYWRpdXM6IDIwXG5cdFx0bWF4V2lkdGg6IFNjcmVlbi53aWR0aCAqIDAuNlxuXHRcdGF2YXRhclNpemU6IDYwXG5cdFx0YXZhdGFyQm9yZGVyUmFkaXVzOiAzMFxuXHRcdGlucHV0Qm9yZGVyQ29sb3I6ICcjY2NjJ1xuXHRcdGlucHV0SGVpZ2h0OiA4MFxuXHRcdHBsYWNlaG9sZGVyOiAnU3RhcnQgY2hhdHRpbmcnXG5cdFx0ZGVmYXVsdFVzZXJJZDogMVxuXHRcdGF1dGhvclRleHRDb2xvcjogJyM5OTknXG5cdFx0YnViYmxlQ29sb3I6XG5cdFx0XHRyaWdodDogJyM0MDgwRkYnXG5cdFx0XHRsZWZ0OiAnI2VlZSdcblx0XHRidWJibGVUZXh0OlxuXHRcdFx0cmlnaHQ6ICd3aGl0ZSdcblx0XHRcdGxlZnQ6ICdibGFjaydcblx0XHRkYXRhOiBbXG5cdFx0XHR7XG5cdFx0XHRcdGF1dGhvcjogMVxuXHRcdFx0XHRtZXNzYWdlOiAnTG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGVpIGhhcyBpbXBldHVzIHZpdHVwZXJhdGEgYWR2ZXJzYXJpdW0sIG5paGlsIHBvcHVsbyBzZW1wZXIgZXUgaXVzLCBhbiBlYW0gdmVybyBzZW5zaWJ1cy4nXG5cdFx0XHR9XG5cdFx0XVxuXHRcdHVzZXJzOiBbXG5cdFx0XHR7XG5cdFx0XHRcdGlkOiAxXG5cdFx0XHRcdG5hbWU6ICdOaW5neGlhJ1xuXHRcdFx0XHRhdmF0YXI6ICduaW5neGlhLmpwZydcblx0XHRcdH1cblx0XHRdXG5cblx0Y29uc3RydWN0b3I6IChvcHRpb25zKSAtPlxuXHRcdGlmIG9wdGlvbnMgPT0gdW5kZWZpbmVkIHRoZW4gb3B0aW9ucyA9IHt9XG5cdFx0b3B0aW9ucyA9IF8uZGVmYXVsdHMgb3B0aW9ucywgZGVmYXVsdHNcblx0XHRAb3B0aW9ucyA9IG9wdGlvbnNcblx0XHRAX2dyb3VwID0gMFxuXG5cdFx0QGNvbW1lbnRzU2Nyb2xsID0gbmV3IFNjcm9sbENvbXBvbmVudFxuXHRcdFx0bmFtZTogJ2NvbW1lbnRzJ1xuXHRcdFx0YmFja2dyb3VuZENvbG9yOiBudWxsXG5cdFx0XHR3aWR0aDogU2NyZWVuLndpZHRoXG5cdFx0XHRoZWlnaHQ6IFNjcmVlbi5oZWlnaHQgLSBAb3B0aW9ucy5pbnB1dEhlaWdodFxuXHRcdFx0bW91c2VXaGVlbEVuYWJsZWQ6IHRydWVcblx0XHRcdHNjcm9sbEhvcml6b250YWw6IGZhbHNlXG5cblx0XHRAY29tbWVudHNTY3JvbGwuY29udGVudEluc2V0ID1cblx0XHRcdHRvcDogQG9wdGlvbnMucGFkZGluZ1xuXG5cdFx0QHJlbmRlckNvbW1lbnQgPSAoY29tbWVudCwgYWxpZ24pID0+XG5cdFx0XHQjIENhbGN1YXRlIHRoZSBtZXNzYWdlIHNpemVcblx0XHRcdEBfbWVzc2FnZVNpemUgPSBVdGlscy50ZXh0U2l6ZSBjb21tZW50Lm1lc3NhZ2UsXG5cdFx0XHRcdHsncGFkZGluZyc6IFwiI3tAb3B0aW9ucy5wYWRkaW5nfXB4XCJ9LFxuXHRcdFx0XHR7d2lkdGg6IEBvcHRpb25zLm1heFdpZHRofVxuXG5cdFx0XHRAX2xlZnRQYWRkaW5nID0gQG9wdGlvbnMucGFkZGluZyAqIDIgKyBAb3B0aW9ucy5hdmF0YXJTaXplXG5cblx0XHRcdCMgRmluZCB0aGUgYXV0aG9yXG5cdFx0XHRAX2F1dGhvciA9IF8uZmluZCBAb3B0aW9ucy51c2Vycywge2lkOiBjb21tZW50LmF1dGhvcn1cblxuXHRcdFx0IyBGaW5kIGNvbW1lbnRzIGJ5IHRoZSBzYW1lIGF1dGhvclxuXHRcdFx0IyBPbmx5IHdvcmtzIG9uIGxlZnQgY29tbWVudHMgc28gZmFyLCBuZWVkIHRvIGRvIGZvciByaWdodFxuXHRcdFx0QF9jb21tZW50SW5kZXggPSBfLmZpbmRJbmRleCBAb3B0aW9ucy5kYXRhLCBjb21tZW50XG5cdFx0XHRcblx0XHRcdEBfcHJldmlvdXNDb21tZW50ID0gQF9uZXh0Q29tbWVudCA9IEBvcHRpb25zLmRhdGFbQF9jb21tZW50SW5kZXggLSAxXVxuXHRcdFx0QF9uZXh0Q29tbWVudCA9IEBvcHRpb25zLmRhdGFbQF9jb21tZW50SW5kZXggKyAxXVxuXG5cdFx0XHRAX3NhbWVOZXh0QXV0aG9yID0gaWYgQF9uZXh0Q29tbWVudCBhbmQgQF9uZXh0Q29tbWVudC5hdXRob3IgaXMgY29tbWVudC5hdXRob3IgdGhlbiB0cnVlIGVsc2UgZmFsc2Vcblx0XHRcdEBfc2FtZVByZXZpb3VzQXV0aG9yID0gaWYgQF9wcmV2aW91c0NvbW1lbnQgYW5kIEBfcHJldmlvdXNDb21tZW50LmF1dGhvciBpcyBjb21tZW50LmF1dGhvciB0aGVuIHRydWUgZWxzZSBmYWxzZVxuXG5cdFx0XHRpZiBAX3NhbWVQcmV2aW91c0F1dGhvciBvciBAX3NhbWVOZXh0QXV0aG9yXG5cdFx0XHRcdEBfZ3JvdXAgPSBAX2dyb3VwICsgMVxuXG5cdFx0XHRAX21lc3NhZ2VNYXJnaW4gPSBpZiBAX3NhbWVOZXh0QXV0aG9yIGFuZCBhbGlnbiBpcyAnbGVmdCcgdGhlbiBAb3B0aW9ucy5saW5lSGVpZ2h0ICogMC4yNSBlbHNlIEBvcHRpb25zLmxpbmVIZWlnaHQgKiAyXG5cblx0XHRcdCMgQ29uc3RydWN0IHRoZSBjb21tZW50XG5cdFx0XHRAY29tbWVudCA9IG5ldyBMYXllclxuXHRcdFx0XHRwYXJlbnQ6IEBjb21tZW50c1Njcm9sbC5jb250ZW50XG5cdFx0XHRcdG5hbWU6ICdjb21tZW50J1xuXHRcdFx0XHRiYWNrZ3JvdW5kQ29sb3I6IG51bGxcblx0XHRcdFx0d2lkdGg6IFNjcmVlbi53aWR0aFxuXHRcdFx0XHRoZWlnaHQ6IEBfbWVzc2FnZVNpemUuaGVpZ2h0ICsgQF9tZXNzYWdlTWFyZ2luXG5cblx0XHRcdHVubGVzcyBAX3NhbWVQcmV2aW91c0F1dGhvclxuXHRcdFx0XHRAYXV0aG9yID0gbmV3IExheWVyXG5cdFx0XHRcdFx0bmFtZTogJ2NvbW1lbnQ6YXV0aG9yJ1xuXHRcdFx0XHRcdGh0bWw6IEBfYXV0aG9yLm5hbWVcblx0XHRcdFx0XHRwYXJlbnQ6IEBjb21tZW50XG5cdFx0XHRcdFx0eDogaWYgYWxpZ24gaXMgJ3JpZ2h0JyB0aGVuIEFsaWduLnJpZ2h0KC1Ab3B0aW9ucy5wYWRkaW5nKSBlbHNlIEBfbGVmdFBhZGRpbmdcblx0XHRcdFx0XHR3aWR0aDogQGNvbW1lbnQud2lkdGhcblx0XHRcdFx0XHRjb2xvcjogQG9wdGlvbnMuYXV0aG9yVGV4dENvbG9yXG5cdFx0XHRcdFx0YmFja2dyb3VuZENvbG9yOiBudWxsXG5cdFx0XHRcdFx0c3R5bGU6XG5cdFx0XHRcdFx0XHQnZm9udC13ZWlnaHQnOiAnYm9sZCdcblx0XHRcdFx0XHRcdCdmb250LXNpemUnOiAnOTAlJ1xuXHRcdFx0XHRcdFx0J3RleHQtYWxpZ24nOiBhbGlnblxuXG5cdFx0XHRAbWVzc2FnZSA9IG5ldyBMYXllclxuXHRcdFx0XHRuYW1lOiAnY29tbWVudDptZXNzYWdlJ1xuXHRcdFx0XHRwYXJlbnQ6IEBjb21tZW50XG5cdFx0XHRcdGh0bWw6IGNvbW1lbnQubWVzc2FnZVxuXHRcdFx0XHRoZWlnaHQ6IEBfbWVzc2FnZVNpemUuaGVpZ2h0XG5cdFx0XHRcdHk6IEBvcHRpb25zLmxpbmVIZWlnaHRcblx0XHRcdFx0YmFja2dyb3VuZENvbG9yOiBAb3B0aW9ucy5idWJibGVDb2xvclthbGlnbl1cblx0XHRcdFx0Y29sb3I6IEBvcHRpb25zLmJ1YmJsZVRleHRbYWxpZ25dXG5cdFx0XHRcdGJvcmRlclJhZGl1czogQG9wdGlvbnMuYm9yZGVyUmFkaXVzXG5cdFx0XHRcdHN0eWxlOlxuXHRcdFx0XHRcdCdwYWRkaW5nJzogXCIje0BvcHRpb25zLnBhZGRpbmd9cHhcIlxuXHRcdFx0XHRcdCd3aWR0aCc6ICdhdXRvJ1xuXHRcdFx0XHRcdCdtYXgtd2lkdGgnOiBcIiN7QF9tZXNzYWdlU2l6ZS53aWR0aH1weFwiXG5cdFx0XHRcdFx0J3RleHQtYWxpZ24nOiBhbGlnblxuXG5cblx0XHRcdCMgU3BlY2lhbCBzdHVmZiBmb3IgYWxpZ25tZW50XG5cdFx0XHRpZiBhbGlnbiBpcyAncmlnaHQnXG5cdFx0XHRcdEBfd2lkdGggPSBwYXJzZUludCBAbWVzc2FnZS5jb21wdXRlZFN0eWxlKClbJ3dpZHRoJ11cblx0XHRcdFx0QG1lc3NhZ2UueCA9IFNjcmVlbi53aWR0aCAtIEBfd2lkdGggLSBAb3B0aW9ucy5wYWRkaW5nXG5cdFx0XHRlbHNlXG5cdFx0XHRcdCMgQXZhdGFyXG5cdFx0XHRcdEAubWVzc2FnZS54ID0gQF9sZWZ0UGFkZGluZ1xuXG5cdFx0XHRcdHVubGVzcyBAX3NhbWVOZXh0QXV0aG9yXG5cdFx0XHRcdFx0QGF2YXRhciA9IG5ldyBMYXllclxuXHRcdFx0XHRcdFx0cGFyZW50OiBAY29tbWVudFxuXHRcdFx0XHRcdFx0bmFtZTogJ2NvbW1lbnQ6YXZhdGFyJ1xuXHRcdFx0XHRcdFx0c2l6ZTogQG9wdGlvbnMuYXZhdGFyU2l6ZVxuXHRcdFx0XHRcdFx0Ym9yZGVyUmFkaXVzOiBAb3B0aW9ucy5hdmF0YXJCb3JkZXJSYWRpdXNcblx0XHRcdFx0XHRcdGltYWdlOiBcImltYWdlcy8je0BfYXV0aG9yLmF2YXRhcn1cIlxuXHRcdFx0XHRcdFx0eDogQG9wdGlvbnMucGFkZGluZ1xuXHRcdFx0XHRcdFx0eTogQWxpZ24uYm90dG9tKC1Ab3B0aW9ucy5wYWRkaW5nICogMilcblxuXHRcdFx0XHQjIEdyb3VwZWQgY29tbWVudHMgYm9yZGVyXG5cdFx0XHRcdGlmIEBfc2FtZVByZXZpb3VzQXV0aG9yIGFuZCBAX3NhbWVOZXh0QXV0aG9yXG5cdFx0XHRcdFx0QG1lc3NhZ2Uuc3R5bGUgPVxuXHRcdFx0XHRcdFx0XCJib3JkZXItdG9wLSN7YWxpZ259LXJhZGl1c1wiOiAnM3B4J1xuXHRcdFx0XHRcdFx0XCJib3JkZXItYm90dG9tLSN7YWxpZ259LXJhZGl1c1wiOiAnM3B4J1xuXG5cdFx0XHRcdGlmIEBfZ3JvdXAgaXMgMVxuXHRcdFx0XHRcdEBtZXNzYWdlLnN0eWxlID0gXCJib3JkZXItYm90dG9tLSN7YWxpZ259LXJhZGl1c1wiOiAnM3B4J1xuXG5cdFx0XHRcdGlmIEBfZ3JvdXAgPiAxIGFuZCAhQF9zYW1lTmV4dEF1dGhvclxuXHRcdFx0XHRcdEBtZXNzYWdlLnN0eWxlID0gXCJib3JkZXItdG9wLSN7YWxpZ259LXJhZGl1c1wiOiAnM3B4J1xuXG5cdFx0XHQjIFJlY2FsY3VhdGUgcG9zaXRpb25cblx0XHRcdEByZWZsb3coKVxuXG5cdFx0QHJlZmxvdyA9ICgpID0+XG5cdFx0XHRAY29tbWVudHNIZWlnaHQgPSAwXG5cdFx0XHRAY29tbWVudHMgPSBAY29tbWVudHNTY3JvbGwuY29udGVudC5jaGlsZHJlblxuXG5cdFx0XHQjIExvb3AgdGhyb3VnaCBhbGwgdGhlIGNvbW1lbnRzXG5cdFx0XHRmb3IgY29tbWVudCwgaSBpbiBAY29tbWVudHNcblx0XHRcdFx0Y29tbWVudHNIZWlnaHQgPSBAY29tbWVudHNIZWlnaHQgKyBjb21tZW50LmhlaWdodFxuXHRcdFx0XHRAeU9mZnNldCA9IDBcblxuXHRcdFx0XHQjIEFkZCB1cCB0aGUgaGVpZ2h0IG9mIHRoZSBzaWJsaW5nIGxheWVycyB0byB0aGUgbGVmdCBvZiB0aGUgY3VycmVudCBsYXllclxuXHRcdFx0XHRmb3IgbGF5ZXIgaW4gXy50YWtlKEBjb21tZW50cywgaSlcblx0XHRcdFx0XHRAeU9mZnNldCA9IEB5T2Zmc2V0ICsgbGF5ZXIuaGVpZ2h0XG5cblx0XHRcdFx0IyBTZXQgdGhlIGN1cnJlbnQgY29tbWVudCBwb3NpdGlvbiB0byB0aGUgaGVpZ2h0IG9mIGxlZnQgc2libGluZ3Ncblx0XHRcdFx0Y29tbWVudC55ID0gQHlPZmZzZXRcblxuXHRcdFx0IyBTY3JvbGwgc3R1ZmZcblx0XHRcdEBjb21tZW50c1Njcm9sbC51cGRhdGVDb250ZW50KClcblx0XHRcdEBjb21tZW50c1Njcm9sbC5zY3JvbGxUb0xheWVyIEBjb21tZW50c1tAY29tbWVudHMubGVuZ3RoIC0gMV1cblxuXG5cdFx0IyBEcmF3IGV2ZXJ5dGhpbmdcblx0XHRfLm1hcCBAb3B0aW9ucy5kYXRhLCAoY29tbWVudCkgPT5cblx0XHRcdEByZW5kZXJDb21tZW50KGNvbW1lbnQsICdsZWZ0JylcblxuXG5cdFx0IyBOZXcgY29tbXBlbnRzXG5cdFx0QGlucHV0V3JhcHBlciA9IG5ldyBMYXllclxuXHRcdFx0bmFtZTogJ2lucHV0J1xuXHRcdFx0YmFja2dyb3VuZENvbG9yOiBudWxsXG5cdFx0XHRoZWlnaHQ6IEBvcHRpb25zLmlucHV0SGVpZ2h0XG5cdFx0XHR3aWR0aDogU2NyZWVuLndpZHRoXG5cdFx0XHR5OiBBbGlnbi5ib3R0b21cblx0XHRcdHN0eWxlOlxuXHRcdFx0XHQnYm9yZGVyLXRvcCc6IFwiMXB4IHNvbGlkICN7QG9wdGlvbnMuaW5wdXRCb3JkZXJDb2xvcn1cIlxuXG5cdFx0QGlucHV0ID0gbmV3IElucHV0TW9kdWxlLklucHV0XG5cdFx0XHRuYW1lOiAnaW5wdXQ6ZmllbGQnXG5cdFx0XHRwYXJlbnQ6IEBpbnB1dFdyYXBwZXJcblx0XHRcdHdpZHRoOiBTY3JlZW4ud2lkdGhcblx0XHRcdHBsYWNlaG9sZGVyOiBAb3B0aW9ucy5wbGFjZWhvbGRlclxuXHRcdFx0dmlydHVhbEtleWJvYXJkOiBmYWxzZVxuXG5cdFx0Y3JlYXRlQ29tbWVudCA9ICh2YWx1ZSkgPT5cblx0XHRcdG5ld0NvbW1lbnQgPVxuXHRcdFx0XHRhdXRob3I6IEBvcHRpb25zLmRlZmF1bHRVc2VySWRcblx0XHRcdFx0bWVzc2FnZTogdmFsdWVcblxuXHRcdFx0QHJlbmRlckNvbW1lbnQgbmV3Q29tbWVudCwgJ3JpZ2h0J1xuXG5cdFx0QGlucHV0Lm9uICdrZXl1cCcsIChldmVudCkgLT5cblx0XHRcdCMgQWRkIG5ldyBjb21tZW50c1xuXHRcdFx0aWYgZXZlbnQud2hpY2ggaXMgMTNcblx0XHRcdFx0Y3JlYXRlQ29tbWVudChAdmFsdWUpXG5cdFx0XHRcdEB2YWx1ZSA9ICcnXG5cblx0XHRAaW5wdXQuZm9ybS5hZGRFdmVudExpc3RlbmVyICdzdWJtaXQnLCAoZXZlbnQpIC0+XG5cdFx0XHRldmVudC5wcmV2ZW50RGVmYXVsdCgpXG4iLCIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUVBQTtBREFBLElBQUE7O0FBQUEsV0FBQSxHQUFjLE9BQUEsQ0FBUSxPQUFSOztBQUVSLE9BQU8sQ0FBQztBQUNiLE1BQUE7O0VBQUEsUUFBQSxHQUNDO0lBQUEsUUFBQSxFQUFVLEVBQVY7SUFDQSxVQUFBLEVBQVksRUFEWjtJQUVBLE9BQUEsRUFBUyxFQUZUO0lBR0EsWUFBQSxFQUFjLEVBSGQ7SUFJQSxRQUFBLEVBQVUsTUFBTSxDQUFDLEtBQVAsR0FBZSxHQUp6QjtJQUtBLFVBQUEsRUFBWSxFQUxaO0lBTUEsa0JBQUEsRUFBb0IsRUFOcEI7SUFPQSxnQkFBQSxFQUFrQixNQVBsQjtJQVFBLFdBQUEsRUFBYSxFQVJiO0lBU0EsV0FBQSxFQUFhLGdCQVRiO0lBVUEsYUFBQSxFQUFlLENBVmY7SUFXQSxlQUFBLEVBQWlCLE1BWGpCO0lBWUEsV0FBQSxFQUNDO01BQUEsS0FBQSxFQUFPLFNBQVA7TUFDQSxJQUFBLEVBQU0sTUFETjtLQWJEO0lBZUEsVUFBQSxFQUNDO01BQUEsS0FBQSxFQUFPLE9BQVA7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
396 |
--------------------------------------------------------------------------------
/example.framer/framer/images/cursor-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/cursor-active.png
--------------------------------------------------------------------------------
/example.framer/framer/images/cursor-active@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/cursor-active@2x.png
--------------------------------------------------------------------------------
/example.framer/framer/images/cursor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/cursor.png
--------------------------------------------------------------------------------
/example.framer/framer/images/cursor@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/cursor@2x.png
--------------------------------------------------------------------------------
/example.framer/framer/images/icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/icon-120.png
--------------------------------------------------------------------------------
/example.framer/framer/images/icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/icon-152.png
--------------------------------------------------------------------------------
/example.framer/framer/images/icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/icon-180.png
--------------------------------------------------------------------------------
/example.framer/framer/images/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/icon-192.png
--------------------------------------------------------------------------------
/example.framer/framer/images/icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/framer/images/icon-76.png
--------------------------------------------------------------------------------
/example.framer/framer/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | border: none;
5 | -webkit-user-select: none;
6 | -webkit-tap-highlight-color: rgba(0,0,0,0);
7 | }
8 |
9 | body {
10 | background-color: #fff;
11 | font: 28px/1em "Helvetica";
12 | color: gray;
13 | overflow: hidden;
14 | }
15 |
16 | a {
17 | color: gray;
18 | }
19 |
20 | body {
21 | cursor: url('images/cursor.png') 32 32, auto;
22 | cursor: -webkit-image-set(
23 | url('images/cursor.png') 1x,
24 | url('images/cursor@2x.png') 2x
25 | ) 32 32, auto;
26 | }
27 |
28 | body:active {
29 | cursor: url('images/cursor-active.png') 32 32, auto;
30 | cursor: -webkit-image-set(
31 | url('images/cursor-active.png') 1x,
32 | url('images/cursor-active@2x.png') 2x
33 | ) 32 32, auto;
34 | }
35 |
36 | .framerAlertBackground {
37 | position: absolute; top:0px; left:0px; right:0px; bottom:0px;
38 | z-index: 1000;
39 | background-color: #fff;
40 | }
41 |
42 | .framerAlert {
43 | font:400 14px/1.4 "Helvetica Neue", Helvetica, Arial, sans-serif;
44 | -webkit-font-smoothing:antialiased;
45 | color:#616367; text-align:center;
46 | position: absolute; top:40%; left:50%; width:260px; margin-left:-130px;
47 | }
48 | .framerAlert strong { font-weight:500; color:#000; margin-bottom:8px; display:block; }
49 | .framerAlert a { color:#28AFFA; }
50 | .framerAlert .btn {
51 | font-weight:500; text-decoration:none; line-height:1;
52 | display:inline-block; padding:6px 12px 7px 12px;
53 | border-radius:3px; margin-top:12px;
54 | background:#28AFFA; color:#fff;
55 | }
56 |
57 | ::-webkit-scrollbar {
58 | display: none;
59 | }
--------------------------------------------------------------------------------
/example.framer/framer/version:
--------------------------------------------------------------------------------
1 | 5
--------------------------------------------------------------------------------
/example.framer/images/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/images/.gitkeep
--------------------------------------------------------------------------------
/example.framer/images/andrew.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/images/andrew.jpg
--------------------------------------------------------------------------------
/example.framer/images/engly.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/images/engly.jpg
--------------------------------------------------------------------------------
/example.framer/images/garron.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/images/garron.jpg
--------------------------------------------------------------------------------
/example.framer/images/isaak.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/images/isaak.jpg
--------------------------------------------------------------------------------
/example.framer/images/ningxia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/example.framer/images/ningxia.jpg
--------------------------------------------------------------------------------
/example.framer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/example.framer/modules/chat.coffee:
--------------------------------------------------------------------------------
1 | InputModule = require 'input'
2 |
3 | class exports.Chat
4 | defaults =
5 | fontSize: 24
6 | lineHeight: 36
7 | padding: 20
8 | borderRadius: 20
9 | maxWidth: Screen.width * 0.6
10 | avatarSize: 60
11 | avatarBorderRadius: 30
12 | inputBorderColor: '#ccc'
13 | inputHeight: 80
14 | placeholder: 'Start chatting'
15 | defaultUserId: 1
16 | authorTextColor: '#999'
17 | bubbleColor:
18 | right: '#4080FF'
19 | left: '#eee'
20 | bubbleText:
21 | right: 'white'
22 | left: 'black'
23 | data: [
24 | {
25 | author: 1
26 | message: 'Lorem ipsum dolor sit amet, ei has impetus vituperata adversarium, nihil populo semper eu ius, an eam vero sensibus.'
27 | }
28 | ]
29 | users: [
30 | {
31 | id: 1
32 | name: 'Ningxia'
33 | avatar: 'ningxia.jpg'
34 | }
35 | ]
36 |
37 | constructor: (options) ->
38 | if options == undefined then options = {}
39 | options = _.defaults options, defaults
40 | @options = options
41 | @_group = 0
42 |
43 | @commentsScroll = new ScrollComponent
44 | name: 'comments'
45 | backgroundColor: null
46 | width: Screen.width
47 | height: Screen.height - @options.inputHeight
48 | mouseWheelEnabled: true
49 | scrollHorizontal: false
50 |
51 | @commentsScroll.contentInset =
52 | top: @options.padding
53 |
54 | @renderComment = (comment, align) =>
55 | # Calcuate the message size
56 | @_messageSize = Utils.textSize comment.message,
57 | {'padding': "#{@options.padding}px"},
58 | {width: @options.maxWidth}
59 |
60 | @_leftPadding = @options.padding * 2 + @options.avatarSize
61 |
62 | # Find the author
63 | @_author = _.find @options.users, {id: comment.author}
64 |
65 | # Find comments by the same author
66 | # Only works on left comments so far, need to do for right
67 | @_commentIndex = _.findIndex @options.data, comment
68 |
69 | @_previousComment = @_nextComment = @options.data[@_commentIndex - 1]
70 | @_nextComment = @options.data[@_commentIndex + 1]
71 |
72 | @_sameNextAuthor = if @_nextComment and @_nextComment.author is comment.author then true else false
73 | @_samePreviousAuthor = if @_previousComment and @_previousComment.author is comment.author then true else false
74 |
75 | if @_samePreviousAuthor or @_sameNextAuthor
76 | @_group = @_group + 1
77 |
78 | @_messageMargin = if @_sameNextAuthor and align is 'left' then @options.lineHeight * 0.25 else @options.lineHeight * 2
79 |
80 | # Construct the comment
81 | @comment = new Layer
82 | parent: @commentsScroll.content
83 | name: 'comment'
84 | backgroundColor: null
85 | width: Screen.width
86 | height: @_messageSize.height + @_messageMargin
87 |
88 | unless @_samePreviousAuthor
89 | @author = new Layer
90 | name: 'comment:author'
91 | html: @_author.name
92 | parent: @comment
93 | x: if align is 'right' then Align.right(-@options.padding) else @_leftPadding
94 | width: @comment.width
95 | color: @options.authorTextColor
96 | backgroundColor: null
97 | style:
98 | 'font-weight': 'bold'
99 | 'font-size': '90%'
100 | 'text-align': align
101 |
102 | @message = new Layer
103 | name: 'comment:message'
104 | parent: @comment
105 | html: comment.message
106 | height: @_messageSize.height
107 | y: @options.lineHeight
108 | backgroundColor: @options.bubbleColor[align]
109 | color: @options.bubbleText[align]
110 | borderRadius: @options.borderRadius
111 | style:
112 | 'padding': "#{@options.padding}px"
113 | 'width': 'auto'
114 | 'max-width': "#{@_messageSize.width}px"
115 | 'text-align': align
116 |
117 |
118 | # Special stuff for alignment
119 | if align is 'right'
120 | @_width = parseInt @message.computedStyle()['width']
121 | @message.x = Screen.width - @_width - @options.padding
122 | else
123 | # Avatar
124 | @.message.x = @_leftPadding
125 |
126 | unless @_sameNextAuthor
127 | @avatar = new Layer
128 | parent: @comment
129 | name: 'comment:avatar'
130 | size: @options.avatarSize
131 | borderRadius: @options.avatarBorderRadius
132 | image: "images/#{@_author.avatar}"
133 | x: @options.padding
134 | y: Align.bottom(-@options.padding * 2)
135 |
136 | # Grouped comments border
137 | if @_samePreviousAuthor and @_sameNextAuthor
138 | @message.style =
139 | "border-top-#{align}-radius": '3px'
140 | "border-bottom-#{align}-radius": '3px'
141 |
142 | if @_group is 1
143 | @message.style = "border-bottom-#{align}-radius": '3px'
144 |
145 | if @_group > 1 and !@_sameNextAuthor
146 | @message.style = "border-top-#{align}-radius": '3px'
147 |
148 | # Recalcuate position
149 | @reflow()
150 |
151 | @reflow = () =>
152 | @commentsHeight = 0
153 | @comments = @commentsScroll.content.children
154 |
155 | # Loop through all the comments
156 | for comment, i in @comments
157 | commentsHeight = @commentsHeight + comment.height
158 | @yOffset = 0
159 |
160 | # Add up the height of the sibling layers to the left of the current layer
161 | for layer in _.take(@comments, i)
162 | @yOffset = @yOffset + layer.height
163 |
164 | # Set the current comment position to the height of left siblings
165 | comment.y = @yOffset
166 |
167 | # Scroll stuff
168 | @commentsScroll.updateContent()
169 | @commentsScroll.scrollToLayer @comments[@comments.length - 1]
170 |
171 |
172 | # Draw everything
173 | _.map @options.data, (comment) =>
174 | @renderComment(comment, 'left')
175 |
176 |
177 | # New commpents
178 | @inputWrapper = new Layer
179 | name: 'input'
180 | backgroundColor: null
181 | height: @options.inputHeight
182 | width: Screen.width
183 | y: Align.bottom
184 | style:
185 | 'border-top': "1px solid #{@options.inputBorderColor}"
186 |
187 | @input = new InputModule.Input
188 | name: 'input:field'
189 | parent: @inputWrapper
190 | width: Screen.width
191 | placeholder: @options.placeholder
192 | virtualKeyboard: false
193 |
194 | createComment = (value) =>
195 | newComment =
196 | author: @options.defaultUserId
197 | message: value
198 |
199 | @renderComment newComment, 'right'
200 |
201 | @input.on 'keyup', (event) ->
202 | # Add new comments
203 | if event.which is 13
204 | createComment(@value)
205 | @value = ''
206 |
207 | @input.form.addEventListener 'submit', (event) ->
208 | event.preventDefault()
209 |
--------------------------------------------------------------------------------
/example.framer/modules/input.coffee:
--------------------------------------------------------------------------------
1 | exports.keyboardLayer = new Layer
2 | x:0, y:Screen.height, width:Screen.width, height:432
3 | html:" "
4 |
5 | #screen width vs. size of image width
6 | growthRatio = Screen.width / 732
7 | imageHeight = growthRatio * 432
8 |
9 | exports.keyboardLayer.states =
10 | shown:
11 | y: Screen.height - imageHeight
12 |
13 | exports.keyboardLayer.states.animationOptions =
14 | curve: "spring(500,50,15)"
15 |
16 | class exports.Input extends Layer
17 | @define "style",
18 | get: -> @input.style
19 | set: (value) ->
20 | _.extend @input.style, value
21 |
22 | @define "value",
23 | get: -> @input.value
24 | set: (value) ->
25 | @input.value = value
26 |
27 | constructor: (options = {}) ->
28 | options.setup ?= false
29 | options.width ?= Screen.width
30 | options.clip ?= false
31 | options.height ?= 60
32 | options.backgroundColor ?= if options.setup then "rgba(255, 60, 47, .5)" else "transparent"
33 | options.fontSize ?= 30
34 | options.lineHeight ?= 30
35 | options.padding ?= 10
36 | options.text ?= ""
37 | options.placeholder ?= ""
38 | options.virtualKeyboard ?= if Utils.isMobile() then false else true
39 | options.type ?= "text"
40 | options.goButton ?= false
41 |
42 | super options
43 |
44 | @placeholderColor = options.placeholderColor if options.placeholderColor?
45 | @input = document.createElement "input"
46 | @input.id = "input-#{_.now()}"
47 | @input.style.cssText = "font-size: #{options.fontSize}px; line-height: #{options.lineHeight}px; padding: #{options.padding}px; width: #{options.width}px; height: #{options.height}px; border: none; outline-width: 0; background-image: url(about:blank); background-color: #{options.backgroundColor};"
48 | @input.value = options.text
49 | @input.type = options.type
50 | @input.placeholder = options.placeholder
51 | @form = document.createElement "form"
52 |
53 | if options.goButton
54 | @form.action = "#"
55 | @form.addEventListener "submit", (event) ->
56 | event.preventDefault()
57 |
58 | @form.appendChild @input
59 | @_element.appendChild @form
60 |
61 | @backgroundColor = "transparent"
62 | @updatePlaceholderColor options.placeholderColor if @placeholderColor
63 |
64 | #only show honor virtual keyboard option when not on mobile,
65 | #otherwise ignore
66 | if !Utils.isMobile() && options.virtualKeyboard is true
67 | @input.addEventListener "focus", ->
68 | exports.keyboardLayer.bringToFront()
69 | exports.keyboardLayer.states.next()
70 | @input.addEventListener "blur", ->
71 | exports.keyboardLayer.states.switch "default"
72 |
73 | updatePlaceholderColor: (color) ->
74 | @placeholderColor = color
75 | if @pageStyle?
76 | document.head.removeChild @pageStyle
77 | @pageStyle = document.createElement "style"
78 | @pageStyle.type = "text/css"
79 | css = "##{@input.id}::-webkit-input-placeholder { color: #{@placeholderColor}; }"
80 | @pageStyle.appendChild(document.createTextNode css)
81 | document.head.appendChild @pageStyle
82 |
83 | focus: () ->
84 | @input.focus()
85 |
86 | onFocus: (cb) ->
87 | @input.addEventListener "focus", ->
88 | cb.apply(@)
89 |
90 | onBlur: (cb) ->
91 | @input.addEventListener "blur", ->
92 | cb.apply(@)
93 |
--------------------------------------------------------------------------------
/example/sampleChat.framer/modules/chat.coffee:
--------------------------------------------------------------------------------
1 | InputModule = require 'input'
2 |
3 | class exports.Chat
4 | defaults =
5 | fontSize: 24
6 | lineHeight: 36
7 | padding: 20
8 | borderRadius: 20
9 | maxWidth: Screen.width * 0.6
10 | avatarSize: 60
11 | avatarBorderRadius: 30
12 | inputBorderColor: '#ccc'
13 | inputHeight: 80
14 | placeholder: 'Start chatting'
15 | defaultUserId: 1
16 | authorTextColor: '#999'
17 | bubbleColor:
18 | right: '#4080FF'
19 | left: '#eee'
20 | bubbleText:
21 | right: 'white'
22 | left: 'black'
23 | data: [
24 | {
25 | author: 1
26 | message: 'Lorem ipsum dolor sit amet, ei has impetus vituperata adversarium, nihil populo semper eu ius, an eam vero sensibus.'
27 | }
28 | ]
29 | users: [
30 | {
31 | id: 1
32 | name: 'Ningxia'
33 | avatar: 'ningxia.jpg'
34 | }
35 | ]
36 |
37 | constructor: (options) ->
38 | if options == undefined then options = {}
39 | options = _.defaults options, defaults
40 | @options = options
41 | @_group = 0
42 |
43 | @commentsScroll = new ScrollComponent
44 | name: 'comments'
45 | backgroundColor: null
46 | width: Screen.width
47 | height: Screen.height - @options.inputHeight
48 | mouseWheelEnabled: true
49 | scrollHorizontal: false
50 |
51 | @commentsScroll.contentInset =
52 | top: @options.padding
53 |
54 | @renderComment = (comment, align) =>
55 | # Calcuate the message size
56 | @_messageSize = Utils.textSize comment.message,
57 | {'padding': "#{@options.padding}px"},
58 | {width: @options.maxWidth}
59 |
60 | @_leftPadding = @options.padding * 2 + @options.avatarSize
61 |
62 | # Find the author
63 | @_author = _.find @options.users, {id: comment.author}
64 |
65 | # Find comments by the same author
66 | # Only works on left comments so far, need to do for right
67 | @_commentIndex = _.findIndex @options.data, comment
68 |
69 | @_previousComment = @_nextComment = @options.data[@_commentIndex - 1]
70 | @_nextComment = @options.data[@_commentIndex + 1]
71 |
72 | @_sameNextAuthor = if @_nextComment and @_nextComment.author is comment.author then true else false
73 | @_samePreviousAuthor = if @_previousComment and @_previousComment.author is comment.author then true else false
74 |
75 | if @_samePreviousAuthor or @_sameNextAuthor
76 | @_group = @_group + 1
77 |
78 | @_messageMargin = if @_sameNextAuthor and align is 'left' then @options.lineHeight * 0.25 else @options.lineHeight * 2
79 |
80 | # Construct the comment
81 | @comment = new Layer
82 | parent: @commentsScroll.content
83 | name: 'comment'
84 | backgroundColor: null
85 | width: Screen.width
86 | height: @_messageSize.height + @_messageMargin
87 |
88 | unless @_samePreviousAuthor
89 | @author = new Layer
90 | name: 'comment:author'
91 | html: @_author.name
92 | parent: @comment
93 | x: if align is 'right' then Align.right(-@options.padding) else @_leftPadding
94 | width: @comment.width
95 | color: @options.authorTextColor
96 | backgroundColor: null
97 | style:
98 | 'font-weight': 'bold'
99 | 'font-size': '90%'
100 | 'text-align': align
101 |
102 | @message = new Layer
103 | name: 'comment:message'
104 | parent: @comment
105 | html: comment.message
106 | height: @_messageSize.height
107 | y: @options.lineHeight
108 | backgroundColor: @options.bubbleColor[align]
109 | color: @options.bubbleText[align]
110 | borderRadius: @options.borderRadius
111 | style:
112 | 'padding': "#{@options.padding}px"
113 | 'width': 'auto'
114 | 'max-width': "#{@_messageSize.width}px"
115 | 'text-align': align
116 |
117 |
118 | # Special stuff for alignment
119 | if align is 'right'
120 | @_width = parseInt @message.computedStyle()['width']
121 | @message.x = Screen.width - @_width - @options.padding
122 | else
123 | # Avatar
124 | @.message.x = @_leftPadding
125 |
126 | unless @_sameNextAuthor
127 | @avatar = new Layer
128 | parent: @comment
129 | name: 'comment:avatar'
130 | size: @options.avatarSize
131 | borderRadius: @options.avatarBorderRadius
132 | image: "images/#{@_author.avatar}"
133 | x: @options.padding
134 | y: Align.bottom(-@options.padding * 2)
135 |
136 | # Grouped comments border
137 | if @_samePreviousAuthor and @_sameNextAuthor
138 | @message.style =
139 | "border-top-#{align}-radius": '3px'
140 | "border-bottom-#{align}-radius": '3px'
141 |
142 | if @_group is 1
143 | @message.style = "border-bottom-#{align}-radius": '3px'
144 |
145 | if @_group > 1 and !@_sameNextAuthor
146 | @message.style = "border-top-#{align}-radius": '3px'
147 |
148 | # Recalcuate position
149 | @reflow()
150 |
151 | @reflow = () =>
152 | @commentsHeight = 0
153 | @comments = @commentsScroll.content.children
154 |
155 | # Loop through all the comments
156 | for comment, i in @comments
157 | commentsHeight = @commentsHeight + comment.height
158 | @yOffset = 0
159 |
160 | # Add up the height of the sibling layers to the left of the current layer
161 | for layer in _.take(@comments, i)
162 | @yOffset = @yOffset + layer.height
163 |
164 | # Set the current comment position to the height of left siblings
165 | comment.y = @yOffset
166 |
167 | # Scroll stuff
168 | @commentsScroll.updateContent()
169 | @commentsScroll.scrollToLayer @comments[@comments.length - 1]
170 |
171 |
172 | # Draw everything
173 | _.map @options.data, (comment) =>
174 | @renderComment(comment, 'left')
175 |
176 |
177 | # New commpents
178 | @inputWrapper = new Layer
179 | name: 'input'
180 | backgroundColor: null
181 | height: @options.inputHeight
182 | width: Screen.width
183 | y: Align.bottom
184 | style:
185 | 'border-top': "1px solid #{@options.inputBorderColor}"
186 |
187 | @input = new InputModule.Input
188 | name: 'input:field'
189 | parent: @inputWrapper
190 | width: Screen.width
191 | placeholder: @options.placeholder
192 | virtualKeyboard: false
193 |
194 | createComment = (value) =>
195 | newComment =
196 | author: @options.defaultUserId
197 | message: value
198 |
199 | @renderComment newComment, 'right'
200 |
201 | @input.on 'keyup', (event) ->
202 | # Add new comments
203 | if event.which is 13
204 | createComment(@value)
205 | @value = ''
206 |
207 | @input.form.addEventListener 'submit', (event) ->
208 | event.preventDefault()
209 |
--------------------------------------------------------------------------------
/img/chat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewliebchen/framer-chat/f63e6fce84f34a7ceb26ffafd3028389e43c91f6/img/chat.gif
--------------------------------------------------------------------------------