├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── app ├── boot │ └── page.js ├── component_data │ ├── compose_box.js │ ├── mail_items.js │ └── move_to.js ├── component_ui │ ├── compose_box.js │ ├── folders.js │ ├── mail_controls.js │ ├── mail_items.js │ ├── move_to_selector.js │ └── with_select.js ├── css │ └── custom.css ├── data.js └── templates.js ├── bower_components ├── bootstrap │ ├── css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap-responsive.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js ├── es5-shim │ ├── .gitignore │ ├── CHANGES │ ├── CONTRIBUTORS.md │ ├── LICENSE │ ├── README.md │ ├── component.json │ ├── es5-sham.js │ ├── es5-sham.min.js │ ├── es5-shim.js │ ├── es5-shim.min.js │ ├── package.json │ └── tests │ │ ├── helpers │ │ ├── h-kill.js │ │ ├── h-matchers.js │ │ └── h.js │ │ ├── index.html │ │ ├── lib │ │ ├── jasmine-html.js │ │ ├── jasmine.css │ │ ├── jasmine.js │ │ ├── jasmine_favicon.png │ │ └── json2.js │ │ └── spec │ │ ├── s-array.js │ │ ├── s-date.js │ │ ├── s-function.js │ │ ├── s-object.js │ │ └── s-string.js ├── flight │ ├── .travis.yml │ └── lib │ │ ├── advice.js │ │ ├── base.js │ │ ├── component.js │ │ ├── compose.js │ │ ├── debug.js │ │ ├── index.js │ │ ├── logger.js │ │ ├── registry.js │ │ └── utils.js ├── jasmine-flight │ ├── .bower.json │ ├── LICENSE.md │ ├── README.md │ ├── bower.json │ └── lib │ │ └── jasmine-flight.js ├── jasmine-jquery │ ├── .bower.json │ ├── CONTRIBUTING.md │ ├── Gruntfile.js │ ├── MIT.LICENSE │ ├── README.md │ ├── bower.json │ ├── lib │ │ └── jasmine-jquery.js │ ├── package.json │ └── spec │ │ ├── fixtures │ │ ├── json │ │ │ └── jasmine_json_test.json │ │ ├── real_non_mocked_fixture.html │ │ └── real_non_mocked_fixture_style.css │ │ └── suites │ │ └── jasmine-jquery-spec.js ├── jquery │ ├── component.json │ ├── composer.json │ ├── jquery.js │ └── jquery.min.js ├── mustache │ └── mustache.js └── requirejs │ └── require.js ├── index.html ├── karma.conf.js ├── package.json ├── requireMain.js └── test ├── fixtures ├── compose_box.html ├── mail_controls.html ├── mail_items.html └── move_to_selector.html ├── spec ├── component_data │ ├── compose_box_spec.js │ ├── mail_items_spec.js │ └── move_to_spec.js └── component_ui │ ├── compose_box_spec.js │ ├── folders_spec.js │ ├── mail_controls_spec.js │ ├── mail_items_spec.js │ ├── move_to_selector_spec.js │ └── with_select_spec.js └── test-main.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "node_js" 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | before_script: 6 | - "export DISPLAY=:99.0" 7 | - "sh -e /etc/init.d/xvfb start" 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Twitter Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flight example app 2 | 3 | [](http://travis-ci.org/flightjs/example-app) 4 | 5 | An example Flight application. [Demo here](http://flightjs.github.io/example-app/) 6 | -------------------------------------------------------------------------------- /app/boot/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'app/component_data/mail_items', 7 | 'app/component_data/compose_box', 8 | 'app/component_data/move_to', 9 | 'app/component_ui/mail_items', 10 | 'app/component_ui/mail_controls', 11 | 'app/component_ui/compose_box', 12 | 'app/component_ui/folders', 13 | 'app/component_ui/move_to_selector' 14 | ], 15 | 16 | function( 17 | MailItemsData, 18 | ComposeBoxData, 19 | MoveToData, 20 | MailItemsUI, 21 | MailControlsUI, 22 | ComposeBoxUI, 23 | FoldersUI, 24 | MoveToSelectorUI) { 25 | 26 | function initialize() { 27 | MailItemsData.attachTo(document); 28 | ComposeBoxData.attachTo(document, { 29 | selectedFolders: ['inbox'] 30 | }); 31 | MoveToData.attachTo(document); 32 | MailItemsUI.attachTo('#mail_items', { 33 | itemContainerSelector: '#mail_items_TB', 34 | selectedFolders: ['inbox'] 35 | }); 36 | MailControlsUI.attachTo('#mail_controls'); 37 | ComposeBoxUI.attachTo('#compose_box'); 38 | FoldersUI.attachTo('#folders'); 39 | MoveToSelectorUI.attachTo('#move_to_selector', { 40 | moveActionSelector: '#move_mail', 41 | selectedFolders: ['inbox'] 42 | }); 43 | } 44 | 45 | return initialize; 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /app/component_data/compose_box.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'flight/lib/component', 7 | 'bower_components/mustache/mustache', 8 | 'app/data', 9 | 'app/templates' 10 | ], 11 | 12 | function(defineComponent, Mustache, dataStore, templates) { 13 | return defineComponent(composeBox); 14 | 15 | function composeBox() { 16 | 17 | this.defaultAttrs({ 18 | dataStore: dataStore, 19 | recipientHintId: 'recipient_hint', 20 | subjectHint: 'Subject', 21 | messageHint: 'Message', 22 | toHint: 'To', 23 | forwardPrefix: 'Fw', 24 | replyPrefix: 'Re' 25 | }); 26 | 27 | this.serveComposeBox = function(ev, data) { 28 | this.trigger("dataComposeBoxServed", { 29 | markup: this.renderComposeBox(data.type, data.relatedMailId), 30 | type: data.type}); 31 | }; 32 | 33 | this.getSubject = function(type, relatedMailId) { 34 | var relatedMail = this.attr.dataStore.mail.filter(function(each) { 35 | return each.id == relatedMailId; 36 | })[0]; 37 | 38 | var subject = relatedMail && relatedMail.subject; 39 | 40 | var subjectLookup = { 41 | newMail: this.attr.subjectHint, 42 | forward: this.attr.forwardPrefix + ": " + subject, 43 | reply: this.attr.replyPrefix + ": " + subject 44 | } 45 | 46 | return subjectLookup[type]; 47 | }; 48 | 49 | this.renderComposeBox = function(type, relatedMailId) { 50 | var recipientId = this.getRecipientId(type, relatedMailId); 51 | var contacts = this.attr.dataStore.contacts.map(function(contact) { 52 | contact.recipient = (contact.id == recipientId); 53 | return contact; 54 | }); 55 | 56 | return Mustache.render(templates.composeBox, { 57 | newMail: type == 'newMail', 58 | reply: type == 'reply', 59 | subject: this.getSubject(type, relatedMailId), 60 | message: this.attr.messageHint, 61 | contacts: contacts 62 | }); 63 | }; 64 | 65 | this.getRecipientId = function(type, relatedMailId) { 66 | var relatedMail = (type == 'reply') && this.attr.dataStore.mail.filter(function(each) { 67 | return each.id == relatedMailId; 68 | })[0]; 69 | 70 | return relatedMail && relatedMail.contact_id || this.attr.recipientHintId; 71 | }; 72 | 73 | 74 | this.send = function(ev, data) { 75 | this.attr.dataStore.mail.push({ 76 | id: String(Date.now()), 77 | contact_id: data.to_id, 78 | folders: ["sent"], 79 | time: Date.now(), 80 | subject: data.subject, 81 | message: data.message 82 | }); 83 | this.trigger('dataMailItemsRefreshRequested', {folder: data.currentFolder}); 84 | }; 85 | 86 | this.after("initialize", function() { 87 | this.on("uiComposeBoxRequested", this.serveComposeBox); 88 | this.on("uiSendRequested", this.send); 89 | }); 90 | } 91 | 92 | } 93 | ); 94 | -------------------------------------------------------------------------------- /app/component_data/mail_items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'flight/lib/component', 7 | 'bower_components/mustache/mustache', 8 | 'app/data', 9 | 'app/templates' 10 | ], 11 | 12 | function(defineComponent, Mustache, dataStore, templates) { 13 | return defineComponent(mailItems); 14 | 15 | function mailItems() { 16 | 17 | this.defaultAttrs({ 18 | folder: 'inbox', 19 | dataStore: dataStore 20 | }); 21 | 22 | this.serveMailItems = function(ev, data) { 23 | var folder = (data && data.folder) || this.attr.folder; 24 | this.trigger("dataMailItemsServed", {markup: this.renderItems(this.assembleItems(folder))}) 25 | }; 26 | 27 | this.renderItems = function(items) { 28 | return Mustache.render(templates.mailItem, {mailItems: items}); 29 | }; 30 | 31 | this.assembleItems = function(folder) { 32 | var items = []; 33 | 34 | this.attr.dataStore.mail.forEach(function(each) { 35 | if (each.folders && each.folders.indexOf(folder) > -1) { 36 | items.push(this.getItemForView(each)); 37 | } 38 | }, this); 39 | 40 | return items; 41 | }; 42 | 43 | this.getItemForView = function(itemData) { 44 | var thisItem, thisContact, msg 45 | 46 | thisItem = {id: itemData.id, important: itemData.important}; 47 | 48 | thisContact = this.attr.dataStore.contacts.filter(function(contact) { 49 | return contact.id == itemData.contact_id 50 | })[0]; 51 | thisItem.name = [thisContact.firstName, thisContact.lastName].join(' '); 52 | 53 | var subj = itemData.subject; 54 | thisItem.formattedSubject = subj.length > 70 ? subj.slice(0, 70) + "..." : subj; 55 | 56 | var msg = itemData.message; 57 | thisItem.formattedMessage = msg.length > 70 ? msg.slice(0, 70) + "..." : msg; 58 | 59 | return thisItem; 60 | }; 61 | 62 | this.after("initialize", function() { 63 | this.on("uiMailItemsRequested", this.serveMailItems); 64 | this.on("dataMailItemsRefreshRequested", this.serveMailItems); 65 | }); 66 | } 67 | } 68 | ); 69 | -------------------------------------------------------------------------------- /app/component_data/move_to.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'flight/lib/component', 7 | 'bower_components/mustache/mustache', 8 | 'app/data', 9 | 'app/templates' 10 | ], 11 | 12 | function(defineComponent, Mustache, dataStore, templates) { 13 | return defineComponent(moveTo); 14 | 15 | function moveTo() { 16 | 17 | this.defaultAttrs({ 18 | dataStore: dataStore 19 | }); 20 | 21 | this.serveAvailableFolders = function(ev, data) { 22 | this.trigger("dataMoveToItemsServed", { 23 | markup: this.renderFolderSelector(this.getOtherFolders(data.folder)) 24 | }) 25 | }; 26 | 27 | this.renderFolderSelector = function(items) { 28 | return Mustache.render(templates.moveToSelector, {moveToItems: items}); 29 | }; 30 | 31 | this.moveItems = function(ev, data) { 32 | var itemsToMoveIds = data.itemIds 33 | this.attr.dataStore.mail.forEach(function(item) { 34 | if (itemsToMoveIds.indexOf(item.id) > -1) { 35 | item.folders = [data.toFolder]; 36 | } 37 | }); 38 | this.trigger('dataMailItemsRefreshRequested', {folder: data.fromFolder}); 39 | }; 40 | 41 | this.getOtherFolders = function(folder) { 42 | return this.attr.dataStore.folders.filter(function(e) {return e != folder}); 43 | }; 44 | 45 | this.after("initialize", function() { 46 | this.on("uiAvailableFoldersRequested", this.serveAvailableFolders); 47 | this.on("uiMoveItemsRequested", this.moveItems); 48 | }); 49 | } 50 | 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /app/component_ui/compose_box.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'flight/lib/component' 7 | ], 8 | 9 | function(defineComponent) { 10 | 11 | return defineComponent(composeBox); 12 | 13 | function composeBox() { 14 | 15 | this.defaultAttrs({ 16 | newMailType: 'newMail', 17 | forwardMailType: 'forward', 18 | replyMailType: 'reply', 19 | hintClass: 'hint', 20 | selectedFolders: [], 21 | selectedMailItems: [], 22 | 23 | //selectors 24 | composeControl: '.compose', 25 | newControlSelector: '#new_mail', 26 | cancelSelector: '#cancel_composed', 27 | sendSelector: '#send_composed', 28 | toSelector: '#compose_to', 29 | subjectSelector: '#compose_subject', 30 | messageSelector: '#compose_message', 31 | recipientSelector: '#recipient_select', 32 | recipientHintSelector: '#recipient_hint', 33 | selectedRecipientSelector: '#recipient_select :selected', 34 | hintSelector: 'div.hint' 35 | }); 36 | 37 | this.newMail = function() { 38 | this.requestComposeBox(this.attr.newMailType); 39 | }; 40 | 41 | this.forward = function() { 42 | this.requestComposeBox(this.attr.forwardMailType, this.attr.selectedMailItems); 43 | }; 44 | 45 | this.reply = function() { 46 | this.requestComposeBox(this.attr.replyMailType, this.attr.selectedMailItems); 47 | }; 48 | 49 | this.requestComposeBox = function(type, relatedMailId) { 50 | this.trigger('uiComposeBoxRequested', {type: type, relatedMailId: relatedMailId}); 51 | }; 52 | 53 | this.launchComposeBox = function(ev, data) { 54 | var focusSelector = (data.type == this.attr.replyMailType) ? 'messageSelector' : 'toSelector'; 55 | this.$node.html(data.markup).show(); 56 | this.select(focusSelector).focus(); 57 | }; 58 | 59 | this.cancel = function() { 60 | this.$node.html('').hide(); 61 | }; 62 | 63 | this.requestSend = function() { 64 | var data = { 65 | to_id: this.select('selectedRecipientSelector').attr('id'), 66 | subject: this.select('subjectSelector').text(), 67 | message: this.select('messageSelector').text(), 68 | currentFolder: this.attr.selectedFolders[0] 69 | }; 70 | this.trigger('uiSendRequested', data); 71 | this.$node.hide(); 72 | }; 73 | 74 | this.enableSend = function() { 75 | this.select('recipientHintSelector').attr('disabled', 'disabled'); 76 | this.select('sendSelector').removeAttr('disabled'); 77 | }; 78 | 79 | this.removeHint = function(ev, data) { 80 | $(ev.target).html('').removeClass(this.attr.hintClass); 81 | }; 82 | 83 | this.updateMailItemSelections = function(ev, data) { 84 | this.attr.selectedMailItems = data.selectedIds; 85 | } 86 | 87 | this.updateFolderSelections = function(ev, data) { 88 | this.attr.selectedFolders = data.selectedIds; 89 | } 90 | 91 | this.after('initialize', function() { 92 | this.on(document, 'dataComposeBoxServed', this.launchComposeBox); 93 | this.on(document, 'uiForwardMail', this.forward); 94 | this.on(document, 'uiReplyToMail', this.reply); 95 | this.on(document, 'uiMailItemSelectionChanged', this.updateMailItemSelections); 96 | this.on(document, 'uiFolderSelectionChanged', this.updateFolderSelections); 97 | 98 | //the following bindings use delegation so that the event target is read at event time 99 | this.on(document, "click", { 100 | 'cancelSelector': this.cancel, 101 | 'sendSelector': this.requestSend, 102 | 'newControlSelector': this.newMail 103 | }); 104 | this.on('change', { 105 | 'recipientSelector': this.enableSend 106 | }); 107 | this.on('keydown', { 108 | 'hintSelector': this.removeHint 109 | }); 110 | }); 111 | } 112 | } 113 | ); 114 | -------------------------------------------------------------------------------- /app/component_ui/folders.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'flight/lib/component', 7 | './with_select' 8 | ], 9 | 10 | function(defineComponent, withSelect) { 11 | 12 | return defineComponent(folders, withSelect); 13 | 14 | function folders() { 15 | 16 | this.defaultAttrs({ 17 | selectedClass: 'selected', 18 | selectionChangedEvent: 'uiFolderSelectionChanged', 19 | 20 | //selectors 21 | itemSelector: 'li.folder-item', 22 | selectedItemSelector: 'li.folder-item.selected' 23 | }); 24 | 25 | this.fetchMailItems = function(ev, data) { 26 | this.trigger('uiMailItemsRequested', {folder: data.selectedIds[0]}); 27 | } 28 | 29 | this.after('initialize', function() { 30 | this.on('uiFolderSelectionChanged', this.fetchMailItems); 31 | }); 32 | } 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /app/component_ui/mail_controls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | [ 5 | 'flight/lib/component' 6 | ], 7 | 8 | function(defineComponent) { 9 | 10 | return defineComponent(mailControls); 11 | 12 | function mailControls() { 13 | this.defaultAttrs({ 14 | //selectors 15 | actionControlsSelector: 'button.mail-action', 16 | deleteControlSelector: '#delete_mail', 17 | moveControlSelector: '#move_mail', 18 | forwardControlSelector: '#forward', 19 | replyControlSelector: '#reply', 20 | singleItemActionSelector: 'button.single-item' 21 | }); 22 | 23 | this.disableAll = function() { 24 | this.select('actionControlsSelector').attr('disabled', 'disabled'); 25 | }; 26 | 27 | this.restyleOnSelectionChange = function(ev, data) { 28 | if (data.selectedIds.length > 1) { 29 | this.select('actionControlsSelector').not('button.single-item').removeAttr('disabled'); 30 | this.select('singleItemActionSelector').attr('disabled', 'disabled'); 31 | } else if (data.selectedIds.length == 1) { 32 | this.select('actionControlsSelector').removeAttr('disabled'); 33 | } else { 34 | this.disableAll(); 35 | } 36 | }; 37 | 38 | this.deleteMail = function(ev, data) { 39 | this.trigger('uiDeleteMail'); 40 | }; 41 | 42 | this.moveMail = function(ev, data) { 43 | this.trigger('uiMoveMail'); 44 | }; 45 | 46 | this.forwardMail = function(ev, data) { 47 | this.trigger('uiForwardMail'); 48 | }; 49 | 50 | this.replyToMail = function(ev, data) { 51 | this.trigger('uiReplyToMail'); 52 | }; 53 | 54 | this.after('initialize', function() { 55 | this.on('.mail-action', 'click', { 56 | 'deleteControlSelector': this.deleteMail, 57 | 'moveControlSelector': this.moveMail, 58 | 'forwardControlSelector': this.forwardMail, 59 | 'replyControlSelector': this.replyToMail 60 | }); 61 | this.on(document, 'uiMailItemSelectionChanged', this.restyleOnSelectionChange); 62 | this.on(document, 'uiFolderSelectionChanged', this.disableAll); 63 | }); 64 | } 65 | } 66 | ); 67 | 68 | -------------------------------------------------------------------------------- /app/component_ui/mail_items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'flight/lib/component', 7 | './with_select' 8 | ], 9 | 10 | function(defineComponent, withSelect) { 11 | 12 | return defineComponent(mailItems, withSelect); 13 | 14 | function mailItems() { 15 | 16 | this.defaultAttrs({ 17 | deleteFolder: 'trash', 18 | selectedClass: 'selected', 19 | allowMultiSelect: true, 20 | selectionChangedEvent: 'uiMailItemSelectionChanged', 21 | selectedMailItems: [], 22 | selectedFolders: [], 23 | //selectors 24 | itemContainerSelector: null, 25 | itemSelector: 'tr.mail-item', 26 | selectedItemSelector: 'tr.mail-item.selected' 27 | }); 28 | 29 | this.renderItems = function(ev, data) { 30 | this.select('itemContainerSelector').html(data.markup); 31 | //new items, so no selections 32 | this.trigger('uiMailItemSelectionChanged', {selectedIds: []}); 33 | } 34 | 35 | this.updateMailItemSelections = function(ev, data) { 36 | this.attr.selectedMailItems = data.selectedIds; 37 | } 38 | 39 | this.updateFolderSelections = function(ev, data) { 40 | this.attr.selectedFolders = data.selectedIds; 41 | } 42 | 43 | this.requestDeletion = function() { 44 | this.trigger('uiMoveItemsRequested', { 45 | itemIds: this.attr.selectedMailItems, 46 | fromFolder: this.attr.selectedFolders[0], 47 | toFolder: this.attr.deleteFolder 48 | }); 49 | }; 50 | 51 | this.after('initialize', function() { 52 | this.on(document, 'dataMailItemsServed', this.renderItems); 53 | this.on(document, 'uiDeleteMail', this.requestDeletion); 54 | 55 | this.on('uiMailItemSelectionChanged', this.updateMailItemSelections); 56 | this.on(document, 'uiFolderSelectionChanged', this.updateFolderSelections); 57 | 58 | this.trigger('uiMailItemsRequested'); 59 | }); 60 | } 61 | } 62 | ); 63 | -------------------------------------------------------------------------------- /app/component_ui/move_to_selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | [ 6 | 'flight/lib/component', 7 | './with_select' 8 | ], 9 | 10 | function(defineComponent, withSelect) { 11 | 12 | return defineComponent(moveToSelector, withSelect); 13 | 14 | function moveToSelector() { 15 | 16 | this.defaultAttrs({ 17 | selectionChangedEvent: 'uiMoveToSelectionChanged', 18 | selectedMailItems: [], 19 | selectedFolders: [], 20 | //selectors 21 | itemSelector: 'li.move-to-item', 22 | selectedItemSelector: 'li.move-to-item.selected' 23 | }); 24 | 25 | this.requestSelectorWidget = function(ev, data) { 26 | this.trigger('uiAvailableFoldersRequested', { 27 | folder: this.attr.selectedFolders[0] 28 | }) 29 | }; 30 | 31 | this.launchSelector = function(ev, data) { 32 | var controlPosition = $(this.attr.moveActionSelector).offset(); 33 | this.$node.html(data.markup).show().css({ 34 | left: controlPosition.left, 35 | top: controlPosition.top + $(this.attr.moveActionSelector).outerHeight(), 36 | width: $(this.attr.moveActionSelector).outerWidth() 37 | }); 38 | window.setTimeout( 39 | (function() { 40 | this.on(document, 'click', this.hideSelector); 41 | }).bind(this), 0); 42 | }; 43 | 44 | this.hideSelector = function() { 45 | this.off(document, 'click', this.hideSelector); 46 | this.$node.hide(); 47 | } 48 | 49 | this.updateMailItemSelections = function(ev, data) { 50 | this.attr.selectedMailItems = data.selectedIds; 51 | } 52 | 53 | this.updateFolderSelections = function(ev, data) { 54 | this.attr.selectedFolders = data.selectedIds; 55 | } 56 | 57 | this.requestMoveTo = function(ev, data) { 58 | this.trigger('uiMoveItemsRequested', { 59 | itemIds: this.attr.selectedMailItems, 60 | fromFolder: this.attr.selectedFolders[0], 61 | toFolder: data.selectedIds[0] 62 | }); 63 | this.$node.hide(); 64 | }; 65 | 66 | this.after('initialize', function() { 67 | //show selector widget 68 | this.on(document, 'uiMoveMail', this.requestSelectorWidget); 69 | this.on(document, 'dataMoveToItemsServed', this.launchSelector); 70 | //listen for other selections 71 | this.on(document, 'uiMailItemSelectionChanged', this.updateMailItemSelections); 72 | this.on(document, 'uiFolderSelectionChanged', this.updateFolderSelections); 73 | //move items 74 | this.on('uiMoveToSelectionChanged', this.requestMoveTo); 75 | 76 | }); 77 | } 78 | } 79 | ); 80 | -------------------------------------------------------------------------------- /app/component_ui/with_select.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | 5 | function() { 6 | 7 | return withSelect; 8 | 9 | function withSelect() { 10 | 11 | this.defaultAttrs({ 12 | selectedIds: [] 13 | }); 14 | 15 | this.initializeSelections = function() { 16 | this.select('selectedItemSelector').toArray().forEach(function(el) { 17 | this.attr.selectedIds.push(el.getAttribute('id')); 18 | }, this); 19 | }; 20 | 21 | this.getSelectedIds = function() { 22 | return this.attr.selectedIds; 23 | }; 24 | 25 | this.toggleItemSelect = function(ev, data) { 26 | var $item = $(data.el), append; 27 | 28 | if ($item.hasClass(this.attr.selectedClass)) { 29 | this.unselectItem($item); 30 | } else { 31 | append = this.attr.allowMultiSelect && (ev.metaKey || ev.ctrlKey || ev.shiftKey); 32 | this.selectItem($item, append); 33 | } 34 | }; 35 | 36 | this.selectItem = function($item, append) { 37 | if (!append) { 38 | this.select('selectedItemSelector').removeClass(this.attr.selectedClass); 39 | this.attr.selectedIds = []; 40 | } 41 | $item.addClass(this.attr.selectedClass); 42 | 43 | this.attr.selectedIds.push($item.attr('id')); 44 | this.trigger(this.attr.selectionChangedEvent, {selectedIds: this.attr.selectedIds}); 45 | }; 46 | 47 | this.unselectItem = function($item) { 48 | $item.removeClass(this.attr.selectedClass); 49 | 50 | var thisIdIndex = this.attr.selectedIds.indexOf($item.attr('id')); 51 | this.attr.selectedIds.splice(thisIdIndex, 1); 52 | this.trigger(this.attr.selectionChangedEvent, {selectedIds: this.attr.selectedIds}); 53 | }; 54 | 55 | this.after("initialize", function() { 56 | this.on('click', { 57 | 'itemSelector': this.toggleItemSelect 58 | }); 59 | 60 | this.initializeSelections(); 61 | }); 62 | } 63 | } 64 | ); 65 | -------------------------------------------------------------------------------- /app/css/custom.css: -------------------------------------------------------------------------------- 1 | .modal.fade.in { 2 | left: 5%; 3 | top: 10%; 4 | margin: auto auto auto auto; 5 | } 6 | 7 | table { 8 | -webkit-touch-callout: none; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | } 14 | 15 | td.mailContact, 16 | span.mailSubject, 17 | span.mailMessage { 18 | font-size: 15px; 19 | } 20 | 21 | td.mailContact, 22 | span.mailSubject { 23 | font-weight: bold; 24 | } 25 | 26 | tr.mail-item.selected td, 27 | li.folder-item.selected, 28 | li.move-to-item.selected { 29 | background-color: #D9EDF7; 30 | } 31 | 32 | tr.mail-item.selected:hover td, 33 | li.folder-item.selected:hover, 34 | li.move-to-item.selected:hover { 35 | background-color: #C1E1FF; 36 | } 37 | 38 | li.folder-item:hover, 39 | li.move-to-item:hover { 40 | background-color: #EDEDED; 41 | } 42 | 43 | li.folder-item, 44 | li.move-to-item { 45 | padding: 3px 0; 46 | margin-left: -15px; 47 | font-size: 15px; 48 | cursor:pointer; 49 | } 50 | 51 | li.move-to-item { 52 | text-align: center; 53 | margin-right: -15px; 54 | } 55 | 56 | #new_mail { 57 | width: 100px; 58 | } 59 | 60 | div.compose-box { 61 | position: absolute; 62 | z-index: 10; 63 | background-color: #FFFFFF; 64 | width: 350px; 65 | border: 1px solid; 66 | } 67 | 68 | div.compose-body { 69 | padding: 0; 70 | } 71 | 72 | div.compose-header { 73 | font-size: 15px; 74 | } 75 | 76 | #recipient_select { 77 | width: 90%; 78 | margin-bottom: 0; 79 | font-weight: bold; 80 | } 81 | 82 | div.hint { 83 | border: 1px solid #EAEAEA; 84 | color:#CACACA; 85 | } 86 | 87 | #compose_message { 88 | height: 180px; 89 | } 90 | 91 | #compose_subject, 92 | #compose_message { 93 | font-size: 15px; 94 | padding: 15px; 95 | } 96 | 97 | #move_to_selector { 98 | position: absolute; 99 | z-index: 10; 100 | background-color: #FFFFFF; 101 | border: 1px solid; 102 | } -------------------------------------------------------------------------------- /app/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | function() { 5 | return { 6 | folders: ["inbox", "later", "sent", "trash"], 7 | contacts: [ 8 | { 9 | "id": "contact_342", 10 | "firstName": "Michael", 11 | "lastName": "Smith", 12 | "email": "ms@proxyweb.com" 13 | }, 14 | { 15 | "id": "contact_377", 16 | "firstName": "Mary", 17 | "lastName": "Jones", 18 | "email": "mary@jones.net" 19 | }, 20 | { 21 | "id": "contact_398", 22 | "firstName": "Billy", 23 | "lastName": "Idiot", 24 | "email": "william_idiot@gmail.com" 25 | } 26 | ], 27 | mail: [ 28 | { 29 | "id": "mail_2139", 30 | "contact_id": "contact_342", 31 | "folders": ["inbox"], 32 | "time": 1334891976104, 33 | "subject": "Consectetur adipiscing elit", 34 | "message": "Vestibulum vestibulum varius diam in iaculis. Praesent ultrices dui vitae nibh malesuada non iaculis ante vulputate. Suspendisse feugiat ultricies egestas. Aenean a odio libero. Quisque mollis leo et est euismod sit amet dignissim sapien venenatis. Morbi interdum adipiscing massa" 35 | }, 36 | { 37 | "id": "mail_2143", 38 | "contact_id": "contact_377", 39 | "folders": ["inbox", "later"], 40 | "important": "true", 41 | "time": 1334884976104, 42 | "subject": "Neque porro quisquam velit!!", 43 | "message": "Curabitur sollicitudin mi eget sapien posuere semper. Fusce at neque et lacus luctus vulputate vehicula ac enim" 44 | }, 45 | { 46 | "id": "mail_2154", 47 | "contact_id": "contact_398", 48 | "folders": ["inbox"], 49 | "important": "true", 50 | "unread": "true", 51 | "time": 1334874976199, 52 | "subject": "Proin egestas aliquam :)", 53 | "message": "Aenean nec erat id ipsum faucibus tristique. Nam blandit est lacinia turpis consectetur elementum. Nulla in risus ut sapien dignissim feugiat. Proin ultrices sodales imperdiet. Vestibulum vehicula blandit tincidunt. Vivamus posuere rhoncus orci, porta commodo mauris aliquam nec" 54 | }, 55 | { 56 | "id": "mail_2176", 57 | "contact_id": "contact_377", 58 | "folders": ["inbox"], 59 | "time": 1334884976104, 60 | "subject": "Sed ut perspiciatis unde omnis?", 61 | "message": "laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem." 62 | }, 63 | { 64 | "id": "mail_2191", 65 | "contact_id": "contact_398", 66 | "folders": ["inbox"], 67 | "unread": "true", 68 | "time": 1334874976199, 69 | "subject": "At vero eos et accusamus!", 70 | "message": "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat" 71 | }, 72 | { 73 | "id": "mail_2203", 74 | "contact_id": "contact_377", 75 | "folders": ["later"], 76 | "important": "true", 77 | "time": 1334874576199, 78 | "subject": "Mi netus convallis", 79 | "message": "Egestas morbi at. Curabitur aliquet et commodo nonummy, aliquam quis arcu, sed pellentesque vitae molestie mattis magna, in eget, risus nulla vivamus vulputate" 80 | }, 81 | { 82 | "id": "mail_2212", 83 | "contact_id": "contact_398", 84 | "folders": ["sent"], 85 | "time": 1334874579867, 86 | "subject": "Fusce tristique pretium eros a gravida", 87 | "message": "Proin malesuada" 88 | }, 89 | { 90 | "id": "mail_2021", 91 | "contact_id": "contact_342", 92 | "folders": ["trash"], 93 | "time": 1134874579824, 94 | "subject": "Phasellus vitae interdum nulla.", 95 | "message": "Pellentesque quam eros, mollis quis vulputate eget, pellentesque nec ipsum. Cras dignissim fringilla ligula, ac ullamcorper dui convallis blandit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam id nunc ac orci hendrerit faucibus vel in ante. Mauris nec est turpis, ut fringilla mi. Suspendisse vel tortor at nulla facilisis venenatis in sit amet ligula." 96 | }, 97 | { 98 | "id": "mail_1976", 99 | "contact_id": "contact_377", 100 | "folders": ["trash"], 101 | "time": 1034874579813, 102 | "subject": "Fusce tristique pretium :(", 103 | "message": "aliquam quis arcu." 104 | } 105 | ] 106 | }; 107 | return data; 108 | } 109 | ); 110 | 111 | -------------------------------------------------------------------------------- /app/templates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define( 4 | function() { 5 | var mailItem = 6 | '{{#mailItems}}\ 7 |