├── .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 | ![Demo](/img/chat.gif) 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 --------------------------------------------------------------------------------