diff --git a/README.md b/README.md index 927240a53..b67e84b07 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ This is an 'evergreen' application, each time it launches the application code i # Development -The application is built using AngularJS and ES2016, transpiled via Babel. The charts are rendered using [d3fc](https://d3fc.io/), a Scott Logic open source project which provides a number of components that allow the creation of bespoke interactive charts. The bulk of the charting code is adapted from [BitFlux](http://scottlogic.github.io/BitFlux/), which showcases the capabilities of d3fc. +The application is built using AngularJS and ES2015, transpiled via Babel. The charts are rendered using [d3fc](https://d3fc.io/), a Scott Logic open source project which provides a number of components that allow the creation of bespoke interactive charts. The bulk of the charting code is adapted from [BitFlux](http://scottlogic.github.io/BitFlux/), which showcases the capabilities of d3fc. + +The displayed data is real and provided by [Quandl](https://www.quandl.com). The application uses separate Quandl API keys for development and release to mitigate chances of crossing Quandl's [rate limits](https://www.quandl.com/docs/api?json#rate-limits). ### Initial Setup @@ -49,12 +51,16 @@ grunt serve The project is also accessible at http://localhost:5000 +#### Testing + +There is a [test plan](docs/TEST_PLAN.md) that covers the main features and behaviour. This should be used as a basis for testing before releasing and also the main features covered on testing PR changes. + #### Releasing To release, run the Grunt task: `grunt release` for a major release, or `grunt bump:minor` for a minor. This updates all the version references to a new version. Then, submit a PR -with this new version in to `dev`, and then merge it to `master`. Finally, run -`grunt deploy:upstream` or `grunt deploy`, depending on how your remotes are set up. +with this new version in to `dev`, and then merge it to `master`. Merging to master will +trigger the update of the deployed gh-pages version. ## License diff --git a/deploy.sh b/deploy.sh index 40e53fafd..a3b723f75 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,28 +1,54 @@ #!/usr/bin/env bash set -eo pipefail -if ([ $TRAVIS_PULL_REQUEST == "false" ] && [ "${TRAVIS_REPO_SLUG}" == "ScottLogic/StockFlux" ] && ([ $TRAVIS_BRANCH == "dev" ] || [ $TRAVIS_BRANCH == "master" ])) + +# Check for release branch - not using grep as set -e means it fails script +RELEASE_BRANCH=$(echo "$TRAVIS_BRANCH" | sed -n 's/^release\-/&/p') + +#Get the release type (dev/master) from the branch name +TYPE="$TRAVIS_BRANCH" + +if ([ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "${TRAVIS_REPO_SLUG}" == "ScottLogic/StockFlux" ] && ([ "$TYPE" == "dev" ] || [ "$TYPE" == "master" ] || [ -n "$RELEASE_BRANCH" ])) then #Clone the latest gh-pages git clone https://github.com/ScottLogic/StockFlux.git --branch gh-pages gh-pages #Get line with version from the file -> get the second word -> remove quotes around the value VERSION=$(grep "version" package.json | awk -v N=$2 '{print $2}' | cut -d \" -f2) - echo "Version is: $VERSION" - #Get line with the release type (develop/master) from the file -> get the second word -> remove quotes around the value - TYPE=$(grep "type" package.json | awk -v N=$2 '{print $2}' | cut -d \" -f2) echo "Type is: $TYPE" + echo "Version is: $VERSION" - if ([ -z "$TYPE" ] || [ -z "$VERSION" ]) + if ([ $TYPE == "master" ] || [ $TYPE == "dev" ]) then - echo "Version or Type not set in package.json" - exit 1 + echo "Preparing to build version $TYPE" + grunt ci --build-target=$TYPE + + rm -rf "./gh-pages/$TYPE" + cp -r "./public" "./gh-pages/$TYPE" + fi + + if ([ $TYPE == "master" ] || [ -n "$RELEASE_BRANCH" ]) + then + echo "On $TYPE - building versioned build" + if ([ -z "$VERSION" ]) + then + echo "Unable to determine version from package.json." + exit 1 + fi + if [ -n "$RELEASE_BRANCH" ] + then + #For release branches add rc postfix + VERSION="$VERSION-rc" + echo "Release branch - updating version to $VERSION" + fi + # Rebuild everything to do $VERSION + echo "Cleaning build. Targetting $VERSION" + grunt ci --build-target=$VERSION + + rm -rf "./gh-pages/$VERSION" + cp -r "./public" "./gh-pages/$VERSION" fi - rm -rf "./gh-pages/$TYPE" - cp -r "./public" "./gh-pages/$TYPE" - rm -rf "./gh-pages/$VERSION" - cp -r "./public" "./gh-pages/$VERSION" cd gh-pages #Removing git history @@ -42,6 +68,11 @@ then # repo's gh-pages branch. (All previous history on the gh-pages branch # will be lost, since we are overwriting it.) We redirect any output to # /dev/null to hide any sensitive credential data that might otherwise be exposed. - echo "Pushing to: https://${GH_TOKEN}@${GH_REF}" + echo "Pushing to Github..." git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1 + + echo "Cleaning residual gh-pages folder" + rm -rf ./gh-pages +else + echo "Nothing needs deploying" fi diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md new file mode 100644 index 000000000..8f5ebc535 --- /dev/null +++ b/docs/TEST_PLAN.md @@ -0,0 +1,80 @@ +# Test Plan +## Assumptions/Luxuries +You should really run full view tests using the full screen of the computer being tested AND also at a reduced size of perhaps 50% of the screen size to improve the quality of the results +The assumption is that right click is disabled and in any instance should always produce no effect. +Hover on means to move mouse cursor over an element. +Hover off means to move mouse cursor off an element. + + +## Task bar functionality +| |Action |Favourites (FV) | Search (FV) | Favourites (CV) | Search (CV) | +|--- |--- |--- | --- | --- | --- | +| Close Button | Hover in | Close button is highlighted | "" | "" | "" | +| | Hover out | Close button highlight removed | "" | "" | "" | +| | Left click | Application Closes | "" | "" | "" | +| Minimize Button | Hover in | Minimize button is highlighted | "" | "" | "" | +| | Hover out | Minimize button highlight removed | "" | "" | "" | +| | Left click | Application Minimizes | "" | "" | "" | +| Maximize Button | Hover in | Maximize button is highlighted | "" | "" | "" | +| | Hover out | Maximize button highlight removed | "" | "" | "" | +| | Left click | Application maximizes | "" | "" | "" | +| History Button (No history) | Observe | Button should not be present | "" | "" | "" | +| History Button (unviewed history) | Observe | Button should be present highlighted | "" | "" | "" | +| | Hover in | History button is highlighted | "" | "" | "" | +| | Hover out | History button is highlighted | "" | "" | "" | +| | Left Click | List of history is displayed to user, most recent first. | "" | "" | "" | +| | Exiting history | Button should lose highlight. | "" | "" | "" | +| History Button (viewed history) | Observe | Button should be present and not highlighted | "" | "" | "" | +| | hover in | History button is highlighted | "" | "" | "" | +| | hover out | History button highlight is removed | "" | "" | "" | +| | Left Click | List of history is displayed to user, most recent first. | "" | "" | "" | +| | Exiting history | Button remain unhighlighted. | "" | "" | "" | +| Compact View Button | Hover in | CW button is highlighted | "" | N/A | "" | +| | Hover out | CW button highlight removed | "" | N/A | "" | +| | Left click | Changes view to CV | "" | N/A | "" | +| Full View Button | Hover in | N/A | "" | FW button is highlighted | "" | +| | Hover out | N/A | "" | FW button highlight is removed | "" | +| | Left Click | N/A | "" | Changes view to FV | "" | + + +### Favourites +|Action |Favourites (FV) | Search (FV) | Favourites (CV) | Search (CV) | +|---|---|---|---|---| +| Before First favourite is added | Should contain no stocks and display the text "You have no favourites to display. Use the search tab to add new stocks to the list" | Should display nothing if the search field is empty | Should contain no stocks and display the text "You have no favourites to display. Use the search tab to add new stocks to the list" | Should display nothing if the search field is empty | +| Add *first* favourite | The favourite should be displayed in favourites list, the stock should be automatically selected and the chart and navigation bar for it opened. | The newly added favourite should be displayed in favourites list at the bottom if search field is empty | The newly added favourite should be displayed in favourites list | The newly added favourite should be displayed in favourites list at the bottom if search field is empty | +| Add *another* favourite | The newly added favourite should be displayed in favourites list at the bottom | The newly added favourite should be displayed in favourites list at the bottom if search field is empty | The newly added favourite should be displayed in favourites list at the bottom | The newly added favourite should be displayed in favourites list at the bottom if search field is empty | +| Left click on a favourite stock star symbol | Should display a confirmation dialog asking the user to confirm they wish to remove the stock | The stock should be unfavourited and should not be displayed in the favourites list | Should display a confirmation dialog asking the user to confirm they wish to remove the stock | The stock should be unfavourited and should not be displayed in the favourites list | +| Left click on confirm button when removing stock | The stock should be unfavourited and should not be displayed in the favourites list | N/A | The stock should be unfavourited and should not be displayed in the favourites list | N/A | +| Left click anywhere on page other than confirm button when removing stock | The stock should not be unfavourited and should still be displayed in the favourites list | N/A | The stock should not be unfavourited and should still be displayed in the favourites list | N/A| +| Hover on a favourite stock | Should change the background colour of the hovered favourite to dark grey | "" | "" | "" | +| Hover off a favourite stock | Should change the background colour of the hovered out favourite back to light grey | "" | "" | "" | +| Hover on a favourite stock with long list of favourites | Should change the background colour of the hovered favourite to dark grey. The favourite stock labels should shift right allowing a scroll bar to appear without colliding with the text. Scrolling up/down the list should allow navigating favourites list. | "" | Should change the background colour of the hovered favourite to dark grey. Scroll bar should appear to allow scrolling up/down the list. | "" | +| Hover off a favourite stock with long list of favourites | Should change the background colour of the hovered out favourite back to light grey, the scroll bar should disappear. The text labels should return to their default location | "" | Should change the background colour of the hovered out favourite back to light grey, the scroll bar should disappear. | "" | +| Left click a favourite stock | Favourite should become "selected". Dark grey background should remain even after hover out. Chart and Navi bar should be displayed of the selected favourite. | "" | Stock should not be selected | "" | +| Double left click a favourite stock | Favourite should become "selected". Dark grey background should remain even after hover out. Chart and Navi bar should be displayed of the selected favourite. | "" | Full view should be opened. The clicked favourite should become "selected". Dark grey background should remain even after hover out. Chart and Navi bar should be displayed of the selected favourite. | Stock should not be selected | +| Hover on another favourite whilst one is selected | Selected favourite should maintain its dark grey state. Hovered favourite should also be displayed with dark grey background. | "" | Selected stock should not be dark grey in compact view | "" | +| Hover off another favourite whilst one is selected | Should maintain the dark background on selected favourite. Should change the background colour of the hovered out favourite back to light grey. | "" | N/A | "" | +| Reposition favourites by holding down left mouse button and dragging | Should move to its new position in the list. | Drag to reposition should not work in search tab | Should move to its new position in the list. | Drag to reposition should not work in search tab | +| Position refreshed | New positions should be immediately visible | "" | "" | "" | +| Maintain position | New positions should be retained if window is closed and reopened | "" | "" | "" | +| Drag a favourite out and drop else where on desktop | Favourite is removed from original window and new full window opens with favourite | "" | Favourite is removed from original window and new collapsed window opens with the favourite | "" | +| Drag a favourite out and drop on to another windows favourites | Favourite is removed from original window and moved to the other window | "" | Favourite is removed from original window and moved to the other window | "" | +| Drag the last favourite out and keep mouse down | Window should fade out whilst mouse is moving and reappear if mouse is still for a short while | "" | Window should fade out whilst mouse is moving and reappear if mouse is still for a short while | "" | +| Drag the last favourite out and drop else where on desktop | Window should move to drop location | "" | Window should move to drop location | "" | +| Drag the last favourite out and drop on to another windows favourites | Favourite should move to other window and original should stay closed and not appear in closed window list | "" | Favourite should move to other window and original should stay closed and not appear in closed window list | "" | + +### Transition between Favourites FV and Search FV +| Element | Action |Result | +|---|---|---| +| Search Tab | From the favourites FV, left click the search tab | favourites tab should hide and search tab become visible. | +| Favourites Tab | From the Search FV, left click favourites tab | Search tab should hide and favourites become visible| + +### Searching +| Action |Result | +|---|---|---| +| View empty search field | Should contain text "Enter stock name or symbol" | +| Enter "A" character into search field | Should remove the "Enter stock name or symbol" text and replace with the entered search term. The search results field should display "loading search results". Search results which match criteria should be displayed| +| Entering "A" character into search field and hovering on search results | A scroll bar should become visible down the left side of the search results to allow scrolling down the long list. Dragging it should scroll down the list. The stock hovered over should receive dark grey highlighting. | +| Entering "complicated" into search field | No search results should be found. The search results box should display "Oops! looks like no matches were found."| +| Entering "two" into search field and hovering on results | Should display less results than are needed for a scroll bar to appear on hover. The stock hovered over should receive dark grey highlighting.| +| Entering a search query, closing the search tab and then reopening it| The previous search results should remain on the screen. Long search result lists will be displayed from the first element (not to where you scrolled in the list)| diff --git a/gruntfile.js b/gruntfile.js index 10dce9c9f..43e289a25 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -3,27 +3,9 @@ module.exports = function(grunt) { var target = grunt.option('target') || 'http://localhost:5000', port = process.env.PORT || 5000, - version = grunt.file.readJSON('package.json').version, - type = grunt.file.readJSON('package.json').type; + buildTarget = grunt.option('build-target') || 'dev'; grunt.initConfig({ - 'gh-pages': { - origin: { - options: { - base: 'public', - message: 'Deploy to GitHub Pages' - }, - src: ['**/*'] - }, - upstream: { - options: { - base: 'public', - message: 'Deploy to GitHub Pages', - repo: 'git@github.com:ScottLogic/StockFlux.git' - }, - src: ['**/*'] - } - }, connect: { options: { @@ -53,7 +35,8 @@ module.exports = function(grunt) { }, shortcut: { icon: target + '/favicon.ico' - } + }, + splashScreenImage: target + '/assets/png/splashscreen.png' } } }, @@ -88,19 +71,6 @@ module.exports = function(grunt) { } }, - download: { - //One zip for release type (development/master) and one for the version are created here - openfinZip: { - src: ['https://dl.openfin.co/services/download?fileName=StockFlux-' + version + - '&config=http://scottlogic.github.io/StockFlux/' + version + '/app.json'], - dest: './public/StockFlux-' + version + '.zip' - }, - openfinTypeZip: { - src: ['https://dl.openfin.co/services/download?fileName=StockFlux-' + type + '&config=http://scottlogic.github.io/StockFlux/' + type + '/app.json'], - dest: './public/StockFlux-' + type + '.zip' - } - }, - eslint: { target: ['src/**/*.js'] }, @@ -231,6 +201,20 @@ module.exports = function(grunt) { replacements: [{ pattern: 'const allowContextMenu = true;', replacement: 'const allowContextMenu = false;' + }, { + pattern: 'API_KEY = \'kM9Z9aEULVDD7svZ4A8B\'', + replacement: 'API_KEY = \'SmMCEZxMRoNizToppows\'' + }] + } + }, + 'gh-pages': { + files: { + 'public/app.json': 'public/app.json' + }, + options: { + replacements: [{ + pattern: new RegExp('http://scottlogic.github.io/StockFlux/([A-z]+)/', 'g'), + replacement: 'http://scottlogic.github.io/StockFlux/' + buildTarget + '/' }] } } @@ -247,10 +231,8 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-eslint'); - grunt.loadNpmTasks('grunt-http-download'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-gh-pages'); grunt.loadNpmTasks('grunt-babel'); grunt.loadNpmTasks('grunt-bump'); grunt.loadNpmTasks('grunt-string-replace'); @@ -273,17 +255,13 @@ module.exports = function(grunt) { grunt.registerTask('transpile', ['babel', 'less:development']); grunt.registerTask('build:dev', ['concatenate', 'transpile', 'connect:livereload']); - grunt.registerTask('build:release', ['concatenate', 'string-replace', 'transpile', 'uglify', 'connect:livereload']); + grunt.registerTask('build:release', ['concatenate', 'string-replace', 'transpile', 'uglify']); grunt.registerTask('serve', ['build:dev', 'openfin:serve']); grunt.registerTask('default', ['serve']); - grunt.registerTask('createZip', ['build:release', 'download']); - grunt.registerTask('deploy', ['createZip', 'gh-pages:origin']); - grunt.registerTask('deploy:upstream', ['ci', 'gh-pages:upstream']); - grunt.registerTask('release', ['bump:major']); - grunt.registerTask('ci', ['build:release', 'download']); + grunt.registerTask('ci', ['build:release']); }; diff --git a/package.json b/package.json index ca5c62281..8fe4cf794 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bitflux-openfin", - "version": "10.0.1", - "type": "master", + "version": "10.1.0", + "type": "develop", "scripts": { "test": "grunt ci", "postinstall": "cd node_modules/BitFlux && npm i" @@ -22,8 +22,6 @@ "grunt-contrib-less": "^1.1.0", "grunt-contrib-uglify": "^0.11.1", "grunt-eslint": "^17.3.1", - "grunt-gh-pages": "^1.0.0", - "grunt-http-download": "^0.1.0", "grunt-jsbeautifier": "^0.2.7", "grunt-openfin": ">=1.0.2", "grunt-string-replace": "^1.2.1", @@ -34,7 +32,7 @@ "angular-animate": "^1.4.8", "angular-resource": "^1.4.8", "d3fc": "^5.2.0", - "BitFlux": "ScottLogic/BitFlux#dc6c552014d6d578152967a2930b9b73d7b5a687", + "BitFlux": "ScottLogic/BitFlux#37e9a2d632a0ff06e5fc3932bcbd9400bcfabb5d", "jquery": "^1.11.1", "malihu-custom-scrollbar-plugin": "^3.1.3", "moment": "^2.10.6" diff --git a/src/.eslintrc b/src/.eslintrc index d78223dae..079a8d3ef 100644 --- a/src/.eslintrc +++ b/src/.eslintrc @@ -12,6 +12,7 @@ "confirm": true, "Event": true, "localStorage": true, - "$q": true + "$q": true, + "reportAction": true } } diff --git a/src/app.js b/src/app.js index f844ba583..d07ff6099 100644 --- a/src/app.js +++ b/src/app.js @@ -23,7 +23,7 @@ angular.module('stockflux.main', []); angular.module('stockflux.showcase', ['stockflux.selection', 'stockflux.quandl', 'stockflux.config']); - angular.module('stockflux.toolbar', ['stockflux.currentWindow', 'stockflux.closedWindows']); + angular.module('stockflux.toolbar', ['stockflux.currentWindow', 'stockflux.closedWindows', 'stockflux.config']); angular.module('stockflux.icon', []); angular.module('stockflux.search', ['stockflux.quandl', 'stockflux.selection', 'stockflux.currentWindow']); angular.module('stockflux.favourites', ['stockflux.quandl', 'stockflux.selection', 'stockflux.currentWindow']); diff --git a/src/app.json b/src/app.json index bc06745f9..96ce2bfd8 100644 --- a/src/app.json +++ b/src/app.json @@ -21,5 +21,6 @@ "description": "Hosts BitFlux as an OpenFin application.", "icon": "http://scottlogic.github.io/StockFlux/master/favicon.ico", "name": "StockFlux" - } + }, + "splashScreenImage": "http://scottlogic.github.io/StockFlux/master/assets/png/splashscreen.png" } diff --git a/src/assets/png/drag_handle.png b/src/assets/png/drag_handle.png deleted file mode 100644 index 62710786f..000000000 Binary files a/src/assets/png/drag_handle.png and /dev/null differ diff --git a/src/assets/png/splashscreen.png b/src/assets/png/splashscreen.png new file mode 100644 index 000000000..d2e67e61f Binary files /dev/null and b/src/assets/png/splashscreen.png differ diff --git a/src/assets/styles/mixins.less b/src/assets/styles/mixins.less index 0d30552e9..65a408ea8 100644 --- a/src/assets/styles/mixins.less +++ b/src/assets/styles/mixins.less @@ -1,8 +1,4 @@ -.right-alignment(@icon-order) { - right: (15 + (@icon-order * 30)); -} - -.transition(@duration: 0.2s, @delay: 0.1s) { +.transition(@duration: 0.2s, @delay: 0.1s) { transition: all @duration linear @delay; } @@ -14,6 +10,11 @@ -webkit-app-region: no-drag; } +.size(@width: auto, @height: auto) { + width: @width; + height: @height; +} + // 400 is 'normal' .font(@size, @weight: 400, @line-height: 1.5) { font-size: @size; @@ -31,30 +32,29 @@ white-space: nowrap; } -.bubble-head(@top: auto, @bottom: auto, @left: auto, @right: auto) { - position: absolute; - width: 10px; - height: 10px; +.bubble-head(@top: auto, @right: auto, @bottom: auto, @left: auto) { + .position(absolute, @top: @top, @right: @right, @bottom: @bottom, @left: @left); + .size(@width: 10px, @height: 10px); transform: rotate(45deg); background-color: @light-background; - top: @top; - bottom: @bottom; - left: @left; - right: @right; } .box-shadow(@shadow-position, @colour) { box-shadow: @shadow-position 2px 0px @colour; } -.closed-window(@shadow-position, @top: auto, @bottom: auto, @left: auto, @right: auto) { +.closed-window(@shadow-position, @top: auto, @right: auto, @bottom: auto, @left: auto) { .box-shadow(@shadow-position, @dark-background); + .position(fixed, @top: @top, @right: @right, @bottom: @bottom, @left: @left); background: @light-background; - position: fixed; - width: 180px; + .size(@width: 180px); max-height: 353px; +} + +.position(@position, @top: auto, @right: auto, @bottom: auto, @left: auto) { + position: @position; top: @top; + right: @right; bottom: @bottom; left: @left; - right: @right; } diff --git a/src/assets/styles/style.less b/src/assets/styles/style.less index 0d592a87e..490d03a91 100644 --- a/src/assets/styles/style.less +++ b/src/assets/styles/style.less @@ -15,11 +15,12 @@ @import "../../sidebars/favourites/minichart/minichart.less"; @import "../../sidebars/star/star.less"; @import "../../showcase/showcase-changes.less"; -@import "custom-scroll.less"; +@import "../../sidebars/custom-scroll.less"; // Compact styles @import "../../main/main-compact.less"; @import "../../toolbar/toolbar-compact.less"; +@import "../../sidebars/star/star-compact.less"; @import "../../sidebars/favourites/favourite-compact.less"; @import "../../sidebars/sidebar-compact.less"; @import "../../sidebars/search/search-compact.less"; @@ -32,19 +33,12 @@ body { .main { background-color: @dark-background; padding: 0px; - position: fixed; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; + .position(fixed, @top: 0px, @right: 0px, @bottom: 0px, @left: 0px); + border: 1px solid @light-background; } .main-content { - bottom: -12px; - position: absolute; - top: @toolbar-height; - left: (@sidebar-width + 20); - right: -2px; + .position(absolute, @top: @toolbar-height, @right: 0px, @bottom: 0px, @left: (@sidebar-width + 20)); } .disable-transitions { @@ -61,10 +55,8 @@ body { #showcase-title { color: @chart-teal; - position: absolute; - right: 75px; + .position(absolute, @top: -10px, @right: 75px); z-index: 10; - top: -10px; .code, .name { display: inline-block; diff --git a/src/assets/styles/variables.less b/src/assets/styles/variables.less index fc71e4abd..4992e0193 100644 --- a/src/assets/styles/variables.less +++ b/src/assets/styles/variables.less @@ -2,9 +2,9 @@ @text-grey: #AEB1B5; @light-background: #464E58; @middle-background: #28313D; -@dark-background: #1A1F25; +@dark-background: #1A212A; @separator-colour: #5B626B; -@chart-teal: #42D8BD; +@chart-teal: #37D8BC; @chart-fade: #307A71; @font-thin: 100; @font-light: 300; @@ -16,6 +16,9 @@ // Dimensions @sidebar-width: 280px; @expanded-width: 230px; -@chart-width: 155px; -@chart-height: 40px; @toolbar-height: 50px; +@shadow-depth: 2px; +@star-width: 16px; +@bubble-width: 10px; +// Declared here as used outside of the favourite card styling. +@favourite-padding-right: 15px; diff --git a/src/closedWindows/closedCard/closedCard-controller.js b/src/closedWindows/closedCard/closedCard-controller.js index 59b22937d..6edab5e44 100644 --- a/src/closedWindows/closedCard/closedCard-controller.js +++ b/src/closedWindows/closedCard/closedCard-controller.js @@ -3,6 +3,7 @@ class ClosedCardCtrl { openClosedWindow(name) { + reportAction('Restore window', name); var store = window.storeService.open(name); window.windowService.createMainWindow(name, store.isCompact()); } diff --git a/src/closedWindows/closedCard/closedCard.less b/src/closedWindows/closedCard/closedCard.less index c24ebd76f..3ada59af3 100644 --- a/src/closedWindows/closedCard/closedCard.less +++ b/src/closedWindows/closedCard/closedCard.less @@ -8,11 +8,8 @@ } .closed-card { - height: 70px; - padding-left: 12px; - padding-right: 12px; - padding-top: 15px; - padding-bottom: 15px; + .size(@height: 70px); + padding: 15px 12px 15px 12px; cursor: default; .no-overflow; @@ -21,8 +18,6 @@ } } -.closed-selection { - .closed-card-container:not(:last-child) { - border-bottom: 1px solid @dark-background; - } +.closed-selection .closed-card-container:not(:last-child) { + border-bottom: 1px solid @dark-background; } diff --git a/src/config-service.js b/src/config-service.js index 3111d834e..fdba418b9 100644 --- a/src/config-service.js +++ b/src/config-service.js @@ -1,10 +1,16 @@ (function() { - const RESIZE_NO_LIMIT = 50000; - const BITFLUX_STOCK_AMOUNT = 1200; - const BITFLUX_INITIAL_PROPORTION = 16 * 7 / BITFLUX_STOCK_AMOUNT; // ~4 months - const TEAROUT_WINDOW_OFFSET = [268, 65]; - const TEAROUT_WINDOW_OFFSET_COMPACT = [218, 47]; + const RESIZE_NO_LIMIT = 50000, + BITFLUX_STOCK_AMOUNT = 1200, + BITFLUX_INITIAL_PROPORTION = 16 * 7 / BITFLUX_STOCK_AMOUNT, // ~4 months + CLOSED_SIDEBAR_WIDTH = 50, + SIDETAB_TOP_HEIGHT = 50, + TEAROUT_WINDOW_OFFSET = [CLOSED_SIDEBAR_WIDTH, SIDETAB_TOP_HEIGHT], + TEAROUT_WINDOW_OFFSET_COMPACT = [0, 34], + TEAROUT_CARD_WIDTH = 230, + TEAROUT_CARD_DIMENSIONS = [TEAROUT_CARD_WIDTH, 110], + COMPACT_WINDOW_DIMENSIONS = [TEAROUT_CARD_WIDTH, 500], + DEFAULT_WINDOW_DIMENSIONS = [1280, 720]; // Be very careful changing the line below. It is replaced with a string.replace in the grunt build // to disable the right click menu in release. @@ -18,24 +24,13 @@ return 'window' + Math.floor(Math.random() * 1000) + Math.ceil(Math.random() * 999); } - getWindowConfig(name) { - return { + _getConfig(name, overrides) { + var sharedConfig = { name: name || this.createName(), contextMenu: allowContextMenu, autoShow: false, frame: false, - showTaskbarIcon: true, - saveWindowState: true, - url: 'index.html', - resizable: true, - maximizable: true, - minWidth: 918, - minHeight: 510, - maxWidth: RESIZE_NO_LIMIT, - maxHeight: RESIZE_NO_LIMIT, - defaultWidth: 1280, - defaultHeight: 720, - shadow: true, + shadow: false, resizeRegion: { size: 7, topLeftCorner: 14, @@ -44,44 +39,52 @@ bottomLeftCorner: 14 } }; + + Object.keys(sharedConfig).forEach((key) => { + if (overrides[key] === undefined) { + overrides[key] = sharedConfig[key]; + } + }); + + return overrides; } - getCompactConfig(name) { - return { - name: name || this.createName(), - contextMenu: allowContextMenu, - autoShow: false, - frame: false, + getWindowConfig(name) { + return this._getConfig(name, { showTaskbarIcon: true, saveWindowState: true, url: 'index.html', - resizable: false, - maximizable: false, - minWidth: 230, - minHeight: 500, - maxWidth: 230, - maxHeight: 500, - defaultWidth: 230, - defaultHeight: 500, - shadow: true - }; + resizable: true, + maximizable: true, + minWidth: 918, + minHeight: 510, + defaultWidth: DEFAULT_WINDOW_DIMENSIONS[0], + defaultHeight: DEFAULT_WINDOW_DIMENSIONS[1] + }); } getTearoutConfig(name) { - return { - name: name || this.createName(), - contextMenu: allowContextMenu, - autoShow: false, - frame: false, + return this._getConfig(name, { maximizable: false, resizable: false, showTaskbarIcon: false, saveWindowState: false, - maxWidth: 230, - maxHeight: 100, - url: 'tearout.html', - shadow: true - }; + maxWidth: TEAROUT_CARD_DIMENSIONS[0], + maxHeight: TEAROUT_CARD_DIMENSIONS[1], + url: 'tearout.html' + }); + } + + getTearoutCardDimensions() { + return TEAROUT_CARD_DIMENSIONS; + } + + getCompactWindowDimensions() { + return COMPACT_WINDOW_DIMENSIONS; + } + + getDefaultWindowDimensions() { + return DEFAULT_WINDOW_DIMENSIONS; } getTopCardOffset(compact) { diff --git a/src/favicon.ico b/src/favicon.ico index d641c09c5..4073d2b15 100644 Binary files a/src/favicon.ico and b/src/favicon.ico differ diff --git a/src/index.html b/src/index.html index bc8a1f71d..ede4d24df 100644 --- a/src/index.html +++ b/src/index.html @@ -26,5 +26,21 @@ + diff --git a/src/install.html b/src/install.html new file mode 100644 index 000000000..e0b12fb6e --- /dev/null +++ b/src/install.html @@ -0,0 +1,5 @@ + diff --git a/src/main/main-compact.less b/src/main/main-compact.less index 50eaebe50..f32275d02 100644 --- a/src/main/main-compact.less +++ b/src/main/main-compact.less @@ -1,5 +1,3 @@ -.compact { - .right-content { - display: none; - } +.compact .right-content { + display: none; } diff --git a/src/main/version/version.less b/src/main/version/version.less index c08519b89..9fa904e4b 100644 --- a/src/main/version/version.less +++ b/src/main/version/version.less @@ -1,10 +1,7 @@ .version { - bottom: 5px; - right: 15px; - position: absolute; + .position(absolute, @bottom: 5px, @right: 15px); color: @text-grey; cursor: pointer; - .font(11px, @font-light); &:hover { diff --git a/src/services/quandl-service.js b/src/services/quandl-service.js index 83b574c4b..c25538030 100644 --- a/src/services/quandl-service.js +++ b/src/services/quandl-service.js @@ -1,6 +1,8 @@ (function() { 'use strict'; + // Be very careful changing the line below. It is replaced with a string.replace in the grunt build + // to swap out the API key for release. const API_KEY = 'kM9Z9aEULVDD7svZ4A8B', API_KEY_VALUE = 'api_key=' + API_KEY, DATE_INDEX = 0, @@ -17,16 +19,13 @@ return moment().subtract(28, 'days'); } - function processDataset(dataset, query, cb) { - var code = dataset.dataset_code; - var stock = { + function processDataset(dataset, query) { + return { name: dataset.name, - code: code, + code: dataset.dataset_code, favourite: false, query: query }; - - cb(stock); } function isValidResponse(json) { @@ -85,11 +84,13 @@ search(query, cb, noResultsCb, errorCb, usefallback = false) { this.stockSearch(usefallback).get({ query: query }, (result) => { - result.datasets.map((dataset) => { - processDataset(dataset, query, cb); + var processedDataset = result.datasets.map((dataset) => { + return processDataset(dataset, query); }); - if (result.datasets.length === 0) { + if (processedDataset.length > 0) { + cb(processedDataset); + } else { noResultsCb(); } }, (result) => { @@ -107,7 +108,7 @@ getMeta(stockCode, cb) { this.stockMetadata().get({ 'stock_code': stockCode }, (result) => { - processDataset(result.dataset, stockCode, cb); + cb(processDataset(result.dataset, stockCode)); }); } diff --git a/src/showcase/showcase-changes.less b/src/showcase/showcase-changes.less index 38c18876a..b3ce366c7 100644 --- a/src/showcase/showcase-changes.less +++ b/src/showcase/showcase-changes.less @@ -50,23 +50,22 @@ padding-top: 2px; padding-left: 8px; padding-right: 8px; - .font(@size: 14px, @line-height: 0); } - .selector-dropdown { &.open { .dropdown-toggle { - border-bottom: none; - position: relative; - z-index: 2; + border-bottom: none; + .position(relative); + z-index: 2; } .dropdown-menu { - z-index: 1; + z-index: 1; } } + .dropdown-menu { top: 25px; border-radius: 0; @@ -99,11 +98,11 @@ } .y-axis { - .tick { - &.orient-right text { - .font(10px); - } + .tick { + &.orient-right text { + .font(10px); } + } } } @@ -173,14 +172,14 @@ } #x-axis-container .tick, #navbar-row #navbar-container { - text { - .font(14px); - fill: @text-grey; - } + text { + .font(14px); + fill: @text-grey; + } } .multi.rsi.indicator { - stroke: @tertiary-purple; + stroke: @tertiary-purple; } .multi.rsi.annotations .annotation.horizontal line { @@ -242,26 +241,26 @@ } #product-dropdown { - display: @hide; + display: @hide; } #mobile-product-dropdown { - .dropdown-toggle { - display: none; - } + .dropdown-toggle { + display: none; + } } #overlay { - .edit-indicator { - .icon { - border-radius: 0; + .edit-indicator { + .icon { + border-radius: 0; - &.bf-icon-delete { - background-image: url('../png/close_indicator.png'); - } + &.bf-icon-delete { + background-image: url('../png/close_indicator.png'); } } + } } // OVERRIDES diff --git a/src/assets/styles/custom-scroll.less b/src/sidebars/custom-scroll.less similarity index 66% rename from src/assets/styles/custom-scroll.less rename to src/sidebars/custom-scroll.less index d3dfe419c..6cf65803f 100644 --- a/src/assets/styles/custom-scroll.less +++ b/src/sidebars/custom-scroll.less @@ -1,10 +1,11 @@ .compact .side-scroll { - position: fixed; + .position(fixed); } .side-scroll { - padding-bottom: 30px; padding-bottom: 0px; + border-top: 1px solid @dark-background; + transition: border 0.7s; .mCSB_scrollTools { left: 2px; @@ -13,19 +14,21 @@ } &#favourite-scroll { - top: @toolbar-height; - bottom: @bottom-tab-height; - position: absolute; + .position(absolute, @top: @toolbar-height, @bottom: @bottom-tab-height); } } +.contracted .side-scroll { + border-top: 1px solid transparent; +} + .side-scroll:hover .mCSB_dragger_bar { opacity: 0.80; } .mCSB_dragger_bar { opacity: 0; - width: 10px !important; + .size(@width: 10px, @height: 100%) !important; } #favourite-scroll .mCSB_dragger_bar { diff --git a/src/sidebars/favourites/favourite-compact.less b/src/sidebars/favourites/favourite-compact.less index 25d713bbf..2d5898cc9 100644 --- a/src/sidebars/favourites/favourite-compact.less +++ b/src/sidebars/favourites/favourite-compact.less @@ -10,8 +10,4 @@ } } } - - .favourite .top { - transition: none; - } } diff --git a/src/sidebars/favourites/favourite-controller.js b/src/sidebars/favourites/favourite-controller.js index 1bb2865c1..95798a193 100644 --- a/src/sidebars/favourites/favourite-controller.js +++ b/src/sidebars/favourites/favourite-controller.js @@ -35,12 +35,14 @@ } singleClick(stock) { + reportAction('Select favourite', stock.code); if (!window.storeService.open(window.name).isCompact()) { this.select(stock); } } doubleClick(stock) { + reportAction('Select favourite', stock.code); var store = window.storeService.open(window.name); if (store.isCompact()) { this.select(stock); @@ -59,6 +61,11 @@ update(updatedStock) { this.currentWindowService.ready(() => { if (!window.storeService) { + var cb = ((e) => { + this.update(updatedStock); + window.removeEventListener('onStoreServiceReady', cb); + }); + window.addEventListener('onStoreServiceReady', cb); return; } @@ -101,7 +108,8 @@ if (updatedStock) { if (this.stocks.length === 0) { // If there aren't any stocks, we could be adding one... - if (updatedStock.favourite) { + // But only if there's no selection already + if (updatedStock.favourite && this.selection() === '') { this.selectionService.select(updatedStock); } else if (oldSelectedStock.code === updatedStock.code) { // If there's no stocks and it's not a favourite any more, but also @@ -131,9 +139,9 @@ // Repeat the check as in the mean time a stock for this favourite could have been added. if (this.stocks.map((stock1) => { return stock1.code; }).indexOf(favourite) === -1) { - var data = stock && stock.data && stock.data[0], - delta = data.close - data.open; + var data = stock && stock.data && stock.data[0]; if (data) { + var delta = data.close - data.open; this.stocks.push({ favourite: true, name: stock.name, diff --git a/src/sidebars/favourites/favourite.html b/src/sidebars/favourites/favourite.html index 61f75f5bf..cf294abb2 100644 --- a/src/sidebars/favourites/favourite.html +++ b/src/sidebars/favourites/favourite.html @@ -1,22 +1,17 @@ 
-
- -
- -
-
{{stock.name | truncate}}
+ +
{{stock.name | truncate}}
{{stock.code | uppercase}}
-
- -
+
{{stock.price | number:2 }}
{{stock.delta < 0 ? '' : '+'}}{{stock.delta | number:2 }}
-
{{stock.percentage | number:2 }}%
+
{{stock.percentage | number:2 }}%
diff --git a/src/sidebars/favourites/favourites.less b/src/sidebars/favourites/favourites.less index b959ee3b6..3accc3079 100644 --- a/src/sidebars/favourites/favourites.less +++ b/src/sidebars/favourites/favourites.less @@ -1,15 +1,19 @@ -@favourite-height: 100px; +@favourite-height: 110px; +@favourite-padding-top: 10px; +// Padding right is declared in the variables file: +// it's used to determine the placement of other elements, while the other +// padding variables are not. +@favourite-padding-bottom: 5px; @favourite-padding-left: 0px; -@favourite-padding-top: 5px; -@favourite-padding-right: 4px; -@favourite-padding-bottom: 10px; @top-height: 30px; +@top-padding-left: 10px; @fav-split-color: #323A48; @highlight-colour: #447272; +@scrollbar-padding: 20px; .favourites { float: right; - height: 100%; + .size(@width: @expanded-width, @height: 100%); background-color: @favourites-background; .transition(); @@ -30,88 +34,72 @@ .hover-area { background-color: @fav-split-color; - height: 1px; + .size(@width: @expanded-width, @height: 1px); } .drop-target { - height: @favourite-height; + .size(@width: @expanded-width, @height: @favourite-height); background-color: #2E5356; } // Don't nest this as it is the root element of the injected HTML for tearout windows .favourite { - padding-top: @favourite-padding-top; - padding-bottom: @favourite-padding-bottom; - padding-right: @favourite-padding-right; - padding-left: @favourite-padding-left; + padding: @favourite-padding-top @favourite-padding-right @favourite-padding-bottom @favourite-padding-left; background-color: @middle-background; - height: @favourite-height; - width: @expanded-width; + .size(@width: @expanded-width, @height: @favourite-height); + cursor: pointer; .top { - padding-left: 10px; + padding-left: @top-padding-left; .transition(); - } - - .bottom { - padding-top: 1px; - } - - .star, .tearable { - float: right; - } - .tearable { - margin-left: 5px; - cursor: move; - } - - .single { - cursor: default; - } - - .name { - .font(12px, @font-thin, 1.4); - } - - .details { - float: right; - text-align: right; - padding-top: 1px; - - .price { - .font(18px, @font-medium, 1.0); - } - - .delta { - padding-top: 4px; - .font(14px, @font-light, 1.0); - } - - .delta, .percentage { - padding-right: 1px; - } - - .stock-arrow { - height: 9px; - padding-bottom: 1px; - padding-right: 3px; + .name { + .font(12px, @font-light, 1.4); + .size(@width: (@expanded-width - @top-padding-left - @scrollbar-padding - 5 - @star-width - @favourite-padding-right)); // padding width + .wrap-ellipsis; } - .percentage { - display: inherit; - .font(12px, @font-light, 1.2); + .code { + padding-top: 1px; + .font(20px, @font-medium, 1.0); } } - .code { - padding-top: 1px; - .font(20px, @font-medium, 1.0); + .bottom { + .details { + float: right; + text-align: right; + padding-top: 5px; + + .price { + .font(18px, @font-medium, 1.0); + } + + .delta { + padding-top: 5px; + .font(14px, @font-light, 1.0); + } + + .delta, .percentage { + padding-right: 1px; + } + + .stock-arrow { + .size(@height: 9px); + padding-bottom: 1px; + padding-right: 3px; + } + + .percentage { + padding-top: 1px; + .font(12px, @font-light, 1.0); + } + } } } .scroll-padding:hover .top { - padding-left: 20px !important; + padding-left: @scrollbar-padding !important; } .dark { diff --git a/src/sidebars/favourites/geometry-service.js b/src/sidebars/favourites/geometry-service.js index 5f7281d30..a67b2cd51 100644 --- a/src/sidebars/favourites/geometry-service.js +++ b/src/sidebars/favourites/geometry-service.js @@ -45,6 +45,18 @@ otherOrigin.x < this.corner().x && otherOrigin.y < this.corner().y; } + + areaOfIntersection(otherRectangle) { + if (this.intersects(otherRectangle)) { + var left = Math.max(this.left(), otherRectangle.left()); + var right = Math.min(this.right(), otherRectangle.right()); + var top = Math.max(this.top(), otherRectangle.top()); + var bottom = Math.min(this.bottom(), otherRectangle.bottom()); + return (right - left) * (bottom - top); + } else { + return 0; + } + } } // Helper function to retrieve the height, width, top, and left from a window object @@ -69,20 +81,27 @@ }; } - function intersectHelper(bounds1, bounds2) { - var rectangle1 = new Rectangle(bounds1), - rectangle2 = new Rectangle(bounds2); - - return rectangle1.intersects(rectangle2); + function rectangles(bounds1, bounds2) { + return [new Rectangle(bounds1), new Rectangle(bounds2)]; } class GeometryService { elementIntersect(openFinWindow, _window, element) { var nativeWindow = openFinWindow.getNativeWindow(); + var rects = rectangles( + getWindowPosition(nativeWindow), + elementScreenPosition(getWindowPosition(_window), element)); - return intersectHelper( + return rects[0].intersects(rects[1]); + } + + elementIntersectArea(openFinWindow, _window, element) { + var nativeWindow = openFinWindow.getNativeWindow(); + var rects = rectangles( getWindowPosition(nativeWindow), elementScreenPosition(getWindowPosition(_window), element)); + + return rects[0].areaOfIntersection(rects[1]); } } diff --git a/src/sidebars/favourites/minichart/minichart-controller.js b/src/sidebars/favourites/minichart/minichart-controller.js index 610dadfbb..5f283a79c 100644 --- a/src/sidebars/favourites/minichart/minichart-controller.js +++ b/src/sidebars/favourites/minichart/minichart-controller.js @@ -3,6 +3,7 @@ class MinichartCtrl { constructor(quandlService, $timeout) { + this.showMinichart = true; this.quandlService = quandlService; this.$timeout = $timeout; } @@ -21,6 +22,13 @@ return; } + // There needs to be 2 data points to draw the minichart + // if there's not enough, show an error. + if (data.length < 2) { + this.showMinichart = false; + return; + } + data = data.map((d) => { var date = moment(d.date); d.date = date.toDate(); diff --git a/src/sidebars/favourites/minichart/minichart-directive.js b/src/sidebars/favourites/minichart/minichart-directive.js index e1e7d379d..82dc374e2 100644 --- a/src/sidebars/favourites/minichart/minichart-directive.js +++ b/src/sidebars/favourites/minichart/minichart-directive.js @@ -7,9 +7,10 @@ restrict: 'E', templateUrl: 'sidebars/favourites/minichart/minichart.html', scope: { - renderChart: '&', stock: '=' - } + }, + controller: 'MinichartCtrl', + controllerAs: 'minichartCtrl' }; }]); }()); diff --git a/src/sidebars/favourites/minichart/minichart.html b/src/sidebars/favourites/minichart/minichart.html index d00188213..f396eef92 100644 --- a/src/sidebars/favourites/minichart/minichart.html +++ b/src/sidebars/favourites/minichart/minichart.html @@ -1,4 +1,4 @@ - + @@ -6,3 +6,6 @@ +
+ Not enough data to show minichart +
diff --git a/src/sidebars/favourites/minichart/minichart.less b/src/sidebars/favourites/minichart/minichart.less index 85472c616..a6d6c5551 100644 --- a/src/sidebars/favourites/minichart/minichart.less +++ b/src/sidebars/favourites/minichart/minichart.less @@ -1,17 +1,23 @@ -minichart { - position: absolute; - padding-top: 10px; - width: 100%; +@minichart-error-colour: white; +@minichart-width: 145px; +@minichart-height: 40px; + +.overflow { overflow: visible; } +minichart { + .position(absolute); + padding-top: 19px; + .overflow; +} + .minichart { - width: @chart-width; - height: @chart-height; - overflow: visible !important; + .size(@width: @minichart-width, @height: @minichart-height); + .overflow !important; svg { - overflow: visible; + .overflow; } g { @@ -31,8 +37,16 @@ stop-color: @chart-teal; stop-opacity: 0.5; } + &.bottom { stop-color: @chart-teal; stop-opacity: 0; } } + +.minichart-error { + padding: 0px 3px; + text-align: center; + .font(12px, @font-thin); + color: @minichart-error-colour; +} diff --git a/src/sidebars/favourites/tearout-directive.js b/src/sidebars/favourites/tearout-directive.js index f1fff656d..84d7ecb08 100644 --- a/src/sidebars/favourites/tearout-directive.js +++ b/src/sidebars/favourites/tearout-directive.js @@ -3,27 +3,37 @@ const TEAR_IN_SELECTOR = '.favourites'; angular.module('stockflux.tearout') - .directive('tearable', ['geometryService', 'hoverService', 'currentWindowService', 'configService', '$rootScope', - (geometryService, hoverService, currentWindowService, configService, $rootScope) => { + .directive('tearable', ['geometryService', 'hoverService', 'currentWindowService', 'configService', '$rootScope', '$timeout', '$interval', + (geometryService, hoverService, currentWindowService, configService, $rootScope, $timeout, $interval) => { return { restrict: 'C', link: (scope, element, attrs) => { - // Finding the tear element is tightly coupled to the HTML layout. - var dragElement = element[0], - tearElement = dragElement.parentNode.parentNode, - tileWidth = tearElement.clientWidth || 230, - tileHeight = tearElement.clientHeight || 100, - store; - - var windowService = window.windowService; - var tearoutWindow = windowService.createTearoutWindow(window.name); + const ANIMATION_TIME = 400; + var tearoutCardDimensions = configService.getTearoutCardDimensions(); - var myDropTarget = tearElement.parentNode, + // Finding the tear element is tightly coupled to the HTML layout. + var tearElement = element[0], + tileWidth = tearElement.clientWidth || tearoutCardDimensions[0], + tileHeight = tearElement.clientHeight || tearoutCardDimensions[1], + store, + mouseDown = false, + emptyWindow = false, + dimmed = false, + timer = 0, + incrementPromise, + dimPromise, + currentMousePosition = {}, + windowService = window.windowService, + tearoutWindow = windowService.createTearoutWindow(window.name), + myDropTarget = tearElement.parentNode, parent = myDropTarget.parentNode, - myHoverArea = parent.getElementsByClassName('hover-area')[0], - offset = { x: 0, y: 0 }, + myHoverArea = parent.getElementsByClassName('drop-target')[0], + mouseOffset = { x: 0, y: 0 }, + elementOffset = { x: 0, y: 0 }, + previousOffset = { x: 0, y: 0 }, currentlyDragging = false, - dragService; + dragService, + dragTimeout; hoverService.add(myHoverArea, scope.stock.code); @@ -33,19 +43,43 @@ tearoutWindow, window, document.getElementsByClassName('favourites')[0]); } - function setOffset(x, y) { - offset.x = x; - offset.y = y; + function setMouseOffset(e) { + mouseOffset.x = -e.pageX; + mouseOffset.y = -e.pageY; } - function moveTearoutWindow(x, y) { - var tileTopPadding = 5, - tileRightPadding = 5, - tearElementWidth = 16; + function setElementOffset() { + var el = tearElement, + currentLeft = 0, + currentTop = 0; + + if (el.offsetParent) { + do { + currentLeft += el.offsetLeft; + currentTop += el.offsetTop; + el = el.offsetParent; + } while (el); + } + + elementOffset.x = currentLeft; + elementOffset.y = currentTop; + } - tearoutWindow.moveTo( - x - tileWidth + (tearElementWidth - offset.x + tileRightPadding), - y - (tileTopPadding + offset.y)); + function moveWindow(_window, x, y, showFunction) { + var offset = { + x: x + mouseOffset.x + elementOffset.x, + y: y + mouseOffset.y + elementOffset.y + }; + //hard sets the width and height to stop windows pixel ratio from triggering resize + if (offset.x !== previousOffset.x || offset.y !== previousOffset.y) { + _window.setBounds( + offset.x, + offset.y, + tearoutCardDimensions[0], + tearoutCardDimensions[1] + ); + previousOffset = offset; + } } function displayTearoutWindow() { @@ -58,8 +92,10 @@ } function returnFromTearout() { + reportAction('Tearout', 'Return to same'); myDropTarget.appendChild(tearElement); tearoutWindow.hide(); + dragService.destroy(); } function clearIncomingTearoutWindow() { @@ -67,31 +103,147 @@ document.body = document.createElement('body'); } + function tearout(mouseEvent) { + $rootScope.$broadcast('tearoutStart'); + moveWindow(tearoutWindow, mouseEvent.screenX, mouseEvent.screenY); + clearIncomingTearoutWindow(); + appendToOpenfinWindow(tearElement, tearoutWindow); + displayTearoutWindow(); + + tearoutWindow.addEventListener('blurred', onBlur); + + // If there's is only one favourite card; flag the window for closing + emptyWindow = tearElement.classList.contains('single'); + + currentlyDragging = true; + } + + function startTimer() { + resetTimer(); + if (!incrementPromise) { + incrementPromise = $interval(() => { + timer++; + if (timer > 100) { + dragService.overAnotherInstance(TEAR_IN_SELECTOR, (overAnotherInstance) => { + if (!overAnotherInstance) { + stopTimer(); + undim(); + } + }); + } + }, + 10); + } + } + + function stopTimer() { + $interval.cancel(incrementPromise); + incrementPromise = null; + $timeout.cancel(dimPromise); + dimPromise = null; + resetTimer(); + } + + function resetTimer() { + timer = 0; + } + + function dim() { + var _window = currentWindowService.getCurrentWindow(); + dimmed = true; + + dimPromise = $timeout(() => { + stopTimer(); + _window.animate({ + opacity: { + opacity: 0, + duration: ANIMATION_TIME + } + }); + _window.updateOptions({ + shadow: false + }); + dimPromise = null; + startTimer(); + }, + 400); + } + + function undim() { + var _window = currentWindowService.getCurrentWindow(); + + var newCardOffset = configService.getTopCardOffset(store.isCompact()); + + newCardOffset = [ + currentMousePosition.x - newCardOffset[0] + mouseOffset.x + elementOffset.x - 2, + currentMousePosition.y - newCardOffset[1] + mouseOffset.y + elementOffset.y - 1 + ]; + + _window.setBounds( + newCardOffset[0], + newCardOffset[1], + window.outerWidth, + window.outerHeight + ); + + _window.animate({ + opacity: { + opacity: 1, + duration: ANIMATION_TIME + } + }, + { + interrupt: true + }); + + dimmed = false; + } + function handleMouseDown(e) { if (e.button !== 0) { // Only process left clicks return false; } - if (dragElement.classList.contains('single')) { - // There is only one favourite card so don't allow tearing out - return false; - } - - $rootScope.$broadcast('tearoutStart'); + mouseDown = true; + setMouseOffset(e); + setElementOffset(); dragService = windowService.registerDrag(tearoutWindow, currentWindowService.getCurrentWindow()); - currentlyDragging = true; - setOffset(e.offsetX, e.offsetY); - moveTearoutWindow(e.screenX, e.screenY); - clearIncomingTearoutWindow(); - appendToOpenfinWindow(tearElement, tearoutWindow); - displayTearoutWindow(); + if (dragTimeout) { + $timeout.cancel(dragTimeout); + } + dragTimeout = $timeout(() => { + dragTimeout = null; + if (mouseDown) { + tearout(e); + } else { + return false; + } + }, + 250); } function handleMouseMove(e) { + if (mouseDown && dragTimeout) { + if (Math.abs(e.pageX + mouseOffset.x) > 50 || Math.abs(e.pageY + mouseOffset.y) > 50) { + $timeout.cancel(dragTimeout); + dragTimeout = null; + tearout(e); + } + } + if (currentlyDragging) { - moveTearoutWindow(e.screenX, e.screenY); + moveWindow(tearoutWindow, e.screenX, e.screenY); + currentMousePosition.x = e.screenX; + currentMousePosition.y = e.screenY; + if (emptyWindow) { + resetTimer(); + if (!insideFavouritesPane() && !dimmed) { + dim(); + startTimer(); + } + } } } @@ -100,12 +252,19 @@ // Only process left clicks return false; } + + mouseDown = false; $rootScope.$broadcast('tearoutEnd'); + tearoutWindow.removeEventListener('blurred', onBlur); if (currentlyDragging) { currentlyDragging = false; if (dragService.overThisInstance(TEAR_IN_SELECTOR)) { returnFromTearout(); + if (emptyWindow) { + stopTimer(); + undim(); + } } else { if (!store) { store = window.storeService.open(window.name); @@ -113,50 +272,84 @@ dragService.overAnotherInstance(TEAR_IN_SELECTOR, (overAnotherInstance) => { if (overAnotherInstance) { + reportAction('Tearout', 'Moved ' + scope.stock.code); dragService.moveToOtherInstance(scope.stock); dragService.destroy(); store.remove(scope.stock); + + if (emptyWindow) { + tidy(); + currentWindowService.getCurrentWindow().close(); + return; + } + tidy(); + } else if (emptyWindow) { + returnFromTearout(); + stopTimer(); + undim(); } else { // Create new window instance var compact = store.isCompact(); + var indicators = store.indicators(); + windowService.createMainWindow(null, compact, (newWindow, showFunction) => { - newWindow.resizeTo(window.outerWidth, window.outerHeight, 'top-left'); + reportAction('Tearout', 'Created ' + scope.stock.code); var newCardOffset = configService.getTopCardOffset(compact); - newWindow.moveTo(e.screenX - newCardOffset[0], e.screenY - newCardOffset[1], showFunction); + newCardOffset = [ + e.screenX - newCardOffset[0] + mouseOffset.x + elementOffset.x - 2, + e.screenY - newCardOffset[1] + mouseOffset.y + elementOffset.y - 1 + ]; + newWindow.setBounds( + newCardOffset[0], + newCardOffset[1], + window.outerWidth, + window.outerHeight, + showFunction + ); var newStore = window.storeService.open(newWindow.name); + newStore.indicators(indicators); newStore.add(scope.stock); store.remove(scope.stock); newStore.toggleCompact(compact); }); + tidy(); } - // Remove drop-target from original instance - parent.removeChild(myHoverArea); - parent.removeChild(myDropTarget); - dispose(); - - // Destroy myself. - tearoutWindow.close(); }); } } } + function tidy() { + // Remove drop-target from original instance + parent.removeChild(myDropTarget); + dispose(); + + // Destroy myself. + tearoutWindow.close(); + } + function reorderFavourites() { var hoverTargets = hoverService.get(); + var largestIntersectionArea = -1; + var largestIntersector = hoverTargets[0]; for (var i = 0, max = hoverTargets.length; i < max; i++) { - var overDropTarget = geometryService.elementIntersect( + var areaOfIntersection = geometryService.elementIntersectArea( tearoutWindow, window, hoverTargets[i].hoverArea); - if (overDropTarget) { - if (!store) { - store = window.storeService.open(window.name); - } + if (areaOfIntersection > largestIntersectionArea) { + largestIntersectionArea = areaOfIntersection; + largestIntersector = hoverTargets[i]; + } + } - store.reorder(scope.stock.code, hoverTargets[i].code); - break; + if (largestIntersectionArea > 0 && largestIntersector) { + if (!store) { + store = window.storeService.open(window.name); } + + store.reorder(scope.stock.code, largestIntersector.code); } } @@ -169,16 +362,26 @@ } } + function onBlur() { + if (currentlyDragging) { + $rootScope.$broadcast('tearoutEnd'); + returnFromTearout(); + currentlyDragging = false; + tearoutWindow.removeEventListener('blurred', onBlur); + } + } + tearoutWindow.addEventListener('bounds-changing', boundsChangingEvent); - dragElement.addEventListener('mousedown', handleMouseDown); + tearElement.addEventListener('mousedown', handleMouseDown); document.addEventListener('mousemove', handleMouseMove, true); document.addEventListener('mouseup', handleMouseUp, true); function dispose() { hoverService.remove(scope.stock.code); tearoutWindow.removeEventListener('bounds-changing', boundsChangingEvent); - dragElement.removeEventListener('mousedown', handleMouseDown); + tearoutWindow.removeEventListener('blurred', onBlur); + tearElement.removeEventListener('mousedown', handleMouseDown); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } diff --git a/src/sidebars/search/search-compact.less b/src/sidebars/search/search-compact.less index c7e8ab091..fd7cec4f2 100644 --- a/src/sidebars/search/search-compact.less +++ b/src/sidebars/search/search-compact.less @@ -4,24 +4,26 @@ .compact { #search-scroll { - top: 35px; - bottom: 0px; - position: absolute; + .position(absolute, @top: 35px, @bottom: 0px); } .compact-search { display: block; - &.expanded { - icon { - display: block; + &.expanded icon { + display: block; - .close-button { - position: relative; - float: right; - top: -16px; - right: 7px; - } + .close-button { + .position(relative, @top: -16px, @right: 7px); + float: right; + } + } + + &.search .search-result.selected.dark { + background-color: transparent; + + &.darkens:hover { + background-color: @dark-background; } } } diff --git a/src/sidebars/search/search-controller.js b/src/sidebars/search/search-controller.js index a9b1e88c2..701c4f535 100644 --- a/src/sidebars/search/search-controller.js +++ b/src/sidebars/search/search-controller.js @@ -19,15 +19,31 @@ this._watch(); } + getStore() { + if (!window.storeService) { + return null; + } + + if (!this.store) { + this.store = window.storeService.open(window.name); + } + return this.store; + } + + getFavourites() { + if (this.getStore()) { + return this.store.get(); + } + return null; + } + selection() { return this.selectionService.selectedStock().code; } select(stock) { - if (!this.store) { - this.store = window.storeService.open(window.name); - } - if (!this.store.isCompact()) { + reportAction('Select search', stock.code); + if (this.getStore() && !this.store.isCompact()) { this.selectionService.select(stock); } } @@ -74,73 +90,90 @@ submit() { + if (this.query !== '') { + reportAction('Search', this.query); + } + this.stocks = []; this.noResults = false; this.currentWindowService.ready(() => { - if (!this.store) { - this.store = window.storeService.open(window.name); - } - - var favourites = this.store.get(); - if (this.query) { - var length = favourites.length; - this.isLoading = true; - this.errors = []; - this.quandlService.search(this.query, (stock) => { - this.isLoading = false; - var i; - - // removing stocks found with old query - this.stocks = this.stocks.filter((result, j) => { - return result.query === this.query; - }); - - // not adding old stocks - if (stock.query !== this.query) { - return; - } - - // Due to the asynchronicity of the search, if multiple searches - // are fired off in a small amount of time, with an intermediate one - // returning no results it's possible to have both the noResults flag - // set to true, while some stocks have been retrieved by a later search. - // - // Here we re-set the flag to keep it up-to-date. - this.noResults = false; - - var stockAdded = false; - for (i = 0; i < length; i++) { - if (stock.code === favourites[i]) { - stock.favourite = true; - this.stocks.unshift(stock); - stockAdded = true; + if (this.getStore()) { + if (this.query) { + this.isLoading = true; + this.errors = []; + this.quandlService.search(this.query, (stocks) => { + this.isLoading = false; + + // removing stocks found with old query + this.stocks = this.stocks.filter((result) => { + return result.query === this.query; + }); + + // Only show the favourites if the query is empty + if (this.query.trim() === '') { + this.displayFavourites(); + return; } - } - - if (!stockAdded) { - this.stocks.push(stock); - } - }, - () => {this.noResults = true;}, - (error) => { - this.isLoading = false; - this._addError({ - code: (error && error.code) || 'No code received', - message: (error && error.message) || 'No message' - }); - }); - } else { - favourites.map((favourite) => { - this.quandlService.getMeta(favourite, (stock) => { - stock.favourite = true; - this.stocks.push(stock); + + // Due to the asynchronicity of the search, if multiple searches + // are fired off in a small amount of time, with an intermediate one + // returning no results it's possible to have both the noResults flag + // set to true, while some stocks have been retrieved by a later search. + // + // Here we re-set the flag to keep it up-to-date. + this.noResults = false; + + stocks.forEach((stock) => { + // not adding old stocks + if (stock.query !== this.query) { + return; + } + + var stockAdded = false; + this.getFavourites().forEach((favourite) => { + if (stock.code === favourite) { + stock.favourite = true; + this.stocks.unshift(stock); + stockAdded = true; + } + }); + + if (!stockAdded) { + this.stocks.push(stock); + } + }); + }, + () => { + this.noResults = true; + this.isLoading = false; + }, + (error) => { + this.isLoading = false; + this._addError({ + code: (error && error.code) || 'No code received', + message: (error && error.message) || 'No message' + }); }); - }); + } else { + this.displayFavourites(); + } } }); } + displayFavourites() { + var favourites = this.getFavourites(); + if (favourites) { + favourites.map((favourite) => { + this.quandlService.getMeta(favourite, (stock) => { + stock.favourite = true; + this.stocks.push(stock); + }); + }); + } + } + _addError(newError) { var errors = this.errors, max = errors.length; var newCode = newError.code; @@ -172,23 +205,46 @@ } var index = this.stocks.map((stock) => { return stock.code; }).indexOf(data.code); if (index > -1) { - if (!this.query) { - // There are no search results, so remove the favourite. - this.stocks.splice(index, 1); - } else { - // Update the stock's favourite - this.stocks[index].favourite = data.favourite; - } - // The stock doesn't exist, push it on if it's a favourite. - } else if (data.favourite) { + // If the stock in question doesn't exist on the list, check + // the favourites and update accordingly + this.updateFavouriteStates(); + } else if (data.favourite && !this.query) { + // If there's no query, and the new stock is a favourite, + // add it to the list. this.stocks.push(data); } }); }); } + /* + * Updates the favourite states depending on whether or not they're in + * the window's store (this is the best way to ensure that they are + * favourites, as the updateFavourites event is broadcast after the store + * has been updated). + */ + updateFavouriteStates() { + var favourites = this.getFavourites(); + if (this.getFavourites()) { + if (this.query) { + // If there's a query, check to see if each stock in the query + // is a favourite or not. + this.stocks.forEach((stock) => { + stock.favourite = favourites.indexOf(stock.code) > -1; + }); + } else { + // If there's no query, remove any non-favourites. + this.stocks = this.stocks.filter((stock) => favourites.indexOf(stock.code) > -1); + } + } + } + darkenClass(stock) { - return (this.selection() === stock.code || stock.isHovered) ? 'dark' : ''; + return (this.selection() === stock.code || stock.isHovered); + } + + selectedClass(stock) { + return this.selection() === stock.code; } } SearchCtrl.$inject = ['$scope', '$timeout', 'quandlService', 'selectionService', 'currentWindowService']; diff --git a/src/sidebars/search/search.html b/src/sidebars/search/search.html index 36e6b3970..fbe2a191d 100644 --- a/src/sidebars/search/search.html +++ b/src/sidebars/search/search.html @@ -1,6 +1,6 @@
- + @@ -14,9 +14,10 @@ Loading search results...
-
+
-
{{stock.name | truncate}}
+
{{stock.name | truncate}}
{{stock.code | uppercase}}
diff --git a/src/sidebars/search/search.less b/src/sidebars/search/search.less index 1319cbac3..1f59f3a55 100644 --- a/src/sidebars/search/search.less +++ b/src/sidebars/search/search.less @@ -3,22 +3,26 @@ .search { float: left; - height: 100%; + height: 100%; // Not using this mixin as this depends on contracted/expanded background-color: @search-background; .transition(); - position: absolute; - .box-shadow(2px 0px, @dark-background); + .position(absolute); + .box-shadow(@shadow-depth 0px, @dark-background); icon { display: none; } + .side-scroll { + border-top: 1px solid @middle-background; + } + input { border: none; background: transparent; .font(12px); color: @text-grey; - width: 184px; + .size(@width: 184px); padding-left: 15px; padding-top: 16px; @@ -33,22 +37,18 @@ .font(14px); } - .sidetab > div:first-of-type search .search-result { - border-top: @search-splitter-style; - } - .errorDetailsWrapper { - margin: 12px 0; + margin: 12px 0; } .search-result { - height: @search-result-height; - width: @expanded-width; + .size(@width: @expanded-width, @height: @search-result-height); border-bottom: @search-splitter-style; - .name { .font(12px, @font-thin); + .size(@width: 155px); + .wrap-ellipsis; } .code { @@ -56,7 +56,7 @@ } .details { - position: relative; + .position(relative); float: left; padding-left: 20px; padding-top: 8px; @@ -65,13 +65,10 @@ .star-icon { padding-right: 15px; padding-top: 21px; - float: right; } } } .search #search-scroll { - top: @toolbar-height; - bottom: 0px; - position: absolute; + .position(absolute, @top: @toolbar-height, @bottom: 0px); } diff --git a/src/sidebars/sidebar-compact.less b/src/sidebars/sidebar-compact.less index 4e9a1750f..6d51e6a5f 100644 --- a/src/sidebars/sidebar-compact.less +++ b/src/sidebars/sidebar-compact.less @@ -2,13 +2,14 @@ .compact { .sidetab-top { - right: 125px; - left: 0px; - top: 0px; - position: absolute; + .position(absolute, @top: 0px, @right: 125px, @left: 0px); .drag; } + .sidebars { + .size(@width: (@expanded-width - 2px), @height: 100%); + } + #search-scroll, #favourite-scroll { bottom: @compact-top-bottom-margin; } @@ -28,8 +29,7 @@ .search { .transition(); bottom: 0px; - width: 100%; - height: @compact-top-bottom-margin; + .size(@width: 100%, @height: @compact-top-bottom-margin); .top-icon { margin-top: 7px; @@ -38,14 +38,17 @@ input { padding-top: 7px; } - } - .search.expanded { - height: calc(100% - @compact-top-bottom-margin); + &.expanded { + .size(@width: 100%, @height: calc(100% - @compact-top-bottom-margin)); - #searchScroll { - top: 68px; - bottom: 0px; + #searchScroll { + top: 68px; + bottom: 0px; + } + } + &.contracted { + box-shadow: none; } } diff --git a/src/sidebars/sidebar-controller.js b/src/sidebars/sidebar-controller.js index 13f6553d7..3ee54d3c7 100644 --- a/src/sidebars/sidebar-controller.js +++ b/src/sidebars/sidebar-controller.js @@ -8,20 +8,15 @@ class SidebarCtrl { constructor() { - this._favouritesClass = classes.expanded; - this._searchClass = classes.contracted; - this._showSearches = false; - this._showFavourites = true; - this._searchSmall = true; } searchClass() { - return this._searchClass; + return this.showSearches() ? classes.expanded : classes.contracted; } favouritesClass() { - return this._favouritesClass; + return this.showFavourites() ? classes.expanded : classes.contracted; } showSearches() { @@ -29,27 +24,26 @@ } showFavourites() { - return this._showFavourites; + return !this._showSearches; } searchClick() { - if (this._searchSmall) { - this._searchSmall = false; - this._showFavourites = false; - this._searchClass = classes.expanded; - this._favouritesClass = classes.contracted; - this._showSearches = true; + if (!this._showSearches) { + reportAction('Show', 'Search'); } + this._showSearches = true; } favouritesClick() { - if (!this._searchSmall) { - this._searchSmall = true; - this._searchClass = classes.contracted; - this._favouritesClass = classes.expanded; - this._showSearches = false; - this._showFavourites = true; + if (this._showSearches) { + reportAction('Show', 'Favourites'); } + this._showSearches = false; + } + + toggleClick() { + reportAction('Toggle', 'Sidebar'); + this._showSearches = !this._showSearches; } } SidebarCtrl.$inject = []; diff --git a/src/sidebars/sidebar.less b/src/sidebars/sidebar.less index 6b00307a7..93ca71e14 100644 --- a/src/sidebars/sidebar.less +++ b/src/sidebars/sidebar.less @@ -2,28 +2,26 @@ @flash-colour: #447272; @bottom-tab-height: 40px; -@-webkit-keyframes search { +.click-flash(@final-colour) { 0% { background-color: @flash-colour; } 100% { - background-color: @search-background; + background-color: @final-colour; } } -@-webkit-keyframes favourites { - 0% { - background-color: @flash-colour; - } +@-webkit-keyframes search { + .click-flash(@search-background); +} - 100% { - background-color: @favourites-background; - } +@-webkit-keyframes favourites { + .click-flash(@favourites-background); } .expanded { - width: @expanded-width; + .size(@width: @expanded-width); -webkit-animation-duration: 0.4s; -webkit-animation-timing-function: ease-in; } @@ -37,34 +35,24 @@ } .closed-window-selection { - position: absolute; - bottom: 0; - left: 50px; - height: @bottom-tab-height; + .position(absolute, @bottom: 0px, @left: 50px); background-color: @middle-background; - width: @expanded-width; + .size(@width: @expanded-width, @height: @bottom-tab-height); border-top: 1px solid @dark-background; } .favourite-closed { - bottom: 10px; - right: 15px; - position: absolute; - height: 20px; - width: 20px; + .position(absolute, @right: 15px, @bottom: 10px); + .size(@width: 20px, @height: 20px); } .favourite-closed-cover { - position: fixed; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; + .position(fixed, @top: 0px, @right: 0px, @bottom: 0px, @left: 0px); z-index: 1000; } .contracted { - width: (@sidebar-width - @expanded-width); + .size(@width: (@sidebar-width - @expanded-width)); .sidetab-top { .no-drag; @@ -77,6 +65,7 @@ .sidetab { opacity: 1; + width: @expanded-width; &.ng-hide-remove { .transition(@duration: 0.1s, @delay: 0.3s); @@ -92,17 +81,12 @@ } .sidebars { - width: @sidebar-width !important; - height: 100%; + .size(@width: @sidebar-width, @height: 100%); float: left; - - .closed_tabs-button { - width: 21px; - } } .sidetab-top { - height: @toolbar-height; + .size(@height: @toolbar-height); .drag; } @@ -116,18 +100,13 @@ margin-top: 15px; } -.stock-name { - width: 155px; - .wrap-ellipsis; -} - .no-favourites { - width: @expanded-width; + .size(@width: @expanded-width); padding: 12px 15px 12px 20px; color: @text-grey; .font(14px, @font-medium); } -.errorWrapper{ - padding-top: 0; +.errorWrapper { + padding-top: 0; } diff --git a/src/sidebars/star/star-compact.less b/src/sidebars/star/star-compact.less new file mode 100644 index 000000000..7f8206bfd --- /dev/null +++ b/src/sidebars/star/star-compact.less @@ -0,0 +1,9 @@ +.compact { + .bubble-head { + .bubble-head(@left: (@expanded-width - ((@bubble-width + @star-width) / 2)- @favourite-padding-right)); + } + + .confirmation-modal { + left: 13px; + } +} \ No newline at end of file diff --git a/src/sidebars/star/star-controller.js b/src/sidebars/star/star-controller.js index 06fabdabd..943375d55 100644 --- a/src/sidebars/star/star-controller.js +++ b/src/sidebars/star/star-controller.js @@ -24,10 +24,10 @@ } favouriteUrl() { - if (this.stock.favourite) { - return starUrls.on; - } else if (this.starHovered) { + if (this.starHovered) { return starUrls.onHover; + } else if (this.stock.favourite) { + return starUrls.on; } else if (this.stock.isHovered || this.selectionService.selectedStock() === this.stock) { return starUrls.offHover; } else { @@ -62,6 +62,11 @@ } click(event) { + if (event.button !== 0) { + // Only process left clicks + return false; + } + this.mouseY = event.currentTarget.y; this.viewHeight = event.view.innerHeight; if (this.check) { @@ -76,6 +81,7 @@ if (this.stock.favourite) { this.deselect(); } else { + reportAction('Add Favourite', this.stock.code); this.stock.favourite = true; this.store.add(this.stock); } @@ -86,6 +92,7 @@ if (!this.store) { this.store = window.storeService.open(window.name); } + reportAction('Remove Favourite', + this.stock.code); this.stock.favourite = false; this.store.remove(this.stock); this.hideModal(); diff --git a/src/sidebars/star/star.html b/src/sidebars/star/star.html index eace6144a..b6dbc2be8 100644 --- a/src/sidebars/star/star.html +++ b/src/sidebars/star/star.html @@ -1,8 +1,10 @@  -
+
-
+
Are you sure you wish to remove this stock from your favourites? diff --git a/src/sidebars/star/star.less b/src/sidebars/star/star.less index f0567ff7a..60e93b047 100644 --- a/src/sidebars/star/star.less +++ b/src/sidebars/star/star.less @@ -1,58 +1,49 @@ .star { cursor: pointer; + float: right; } .bubble-head { - .bubble-head(@left: 242px, @top: 75px); + .bubble-head(@left: (@sidebar-width - ((@bubble-width + @star-width) / 2) - @favourite-padding-right), @top: 75px); } .confirmation-backdrop { cursor: default; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; + .position(fixed, @top: 0px, @right: 0px, @bottom: 0px, @left: 0px); z-index: 1050; - display: none; .no-overflow; outline: 0; display: block; + .confirmation-modal { - &.flipped{ - .box-shadow(-2px -2px, @dark-background); + &.flipped { + .box-shadow(-@shadow-depth -@shadow-depth, @dark-background); } - .box-shadow(-2px 2px, @dark-background); - position: relative; - width: 204px; - left: 200px; - top: 80px; + + .box-shadow(-@shadow-depth @shadow-depth, @dark-background); + .position(relative, @top: 80px, @left: 200px); + .size(@width: 204px); + .confirmation-content { .message { .font(12px, @font-thin, 1.4); text-align: left; } + .buttons { text-align: center; + button { color: inherit; margin: 10px 0 6px 0; - width: 90px; + .size(@width: 90px); border-radius: 0; border: none; } } + background-color: @light-background; padding: 6px; } } } - -.compact { - .bubble-head { - .bubble-head(@left: 192px); - } - .confirmation-modal { - left: 13px; - } -} diff --git a/src/store-service.js b/src/store-service.js index 057bca75a..8ffa40eb4 100644 --- a/src/store-service.js +++ b/src/store-service.js @@ -92,17 +92,38 @@ closeWindow() { this.store.closed = Date.now(); this.save(); - this.$rootScope.$broadcast('closeWindow'); + + if (this.store.stocks.length > 0) { + this.$rootScope.$broadcast('closeWindow'); + } + + var restorableWindows = [], + cleanableWindows = []; + + storage + .filter((store) => store.closed !== 0) + .forEach((store) => { + if (store.stocks.length > 0) { + restorableWindows.push(store); + } else { + cleanableWindows.push(store); + } + }); + + // Trim any stores without stocks + cleanableWindows.forEach((cleanableWindow) => { + var index = storage.indexOf(cleanableWindow); + storage.splice(index, 1); + }); // Trim the oldest closed store - var closedArray = storage.filter((store) => store.closed !== 0); - if (closedArray.length > closedCacheSize) { - closedArray.sort((a, b) => { + if (restorableWindows.length > closedCacheSize) { + restorableWindows.sort((a, b) => { return b.closed - a.closed; }); - for (var i = closedCacheSize, max = closedArray.length; i < max; i++) { - var storageIndex = storage.indexOf(closedArray[i]); + for (var i = closedCacheSize, max = restorableWindows.length; i < max; i++) { + var storageIndex = storage.indexOf(restorableWindows[i]); storage.splice(storageIndex, 1); } } diff --git a/src/toolbar/toolbar-compact.less b/src/toolbar/toolbar-compact.less index 84995c396..4ed34bbd0 100644 --- a/src/toolbar/toolbar-compact.less +++ b/src/toolbar/toolbar-compact.less @@ -1,19 +1,19 @@ .compact #toolbar { - .full_view-button { - .right-alignment(1); - .toolbar-offset-top(); + .full_view-button, .close-button { + .toolbar-offset-top-compact(); } - .minimise-button { - .right-alignment(2); + .close-button { + .right-alignment(0); } - .full_view-button, .close-button { - .toolbar-offset-top-compact(); + .full_view-button { + .right-alignment(1); } .minimise-button { .toolbar-offset-top(2px); + .right-alignment(2); } .closed_tabs_small-button { @@ -21,4 +21,24 @@ display: inline-block; .right-alignment(3); } + + .drag-left { + .drag-position(@height: @compact-top-bottom-margin, @right: @toolbar-compact-icon-width, @left: 0px); + } + + .drag-right { + .drag-position(@height: @compact-top-bottom-margin, @right: 0px, @width: @toolbar-icons-margin); + } + + .drag-rec-closed { + .drag-position(@height: @compact-top-bottom-margin, @right: 90px, @width: 30px); + } + + .drag-top { + .drag-position(@height: @toolbar-compact-top-margin, @right: 0px, @width: 100%); + } + + .drag-bottom { + .drag-position(@height: @toolbar-compact-top-margin, @right: 0px, @top: (@compact-top-bottom-margin - @toolbar-compact-top-margin), @width: 100%); + } } diff --git a/src/toolbar/toolbar-controller.js b/src/toolbar/toolbar-controller.js index bf15fd849..c9f220b26 100644 --- a/src/toolbar/toolbar-controller.js +++ b/src/toolbar/toolbar-controller.js @@ -1,20 +1,24 @@ (function() { 'use strict'; - const defaultWidth = 1280, - defaultHeight = 720, - compactWidth = 230, - compactHeight = 500; - class ToolbarCtrl { - constructor($scope, $timeout, currentWindowService) { + constructor($scope, $timeout, currentWindowService, configService) { this.$scope = $scope; this.$timeout = $timeout; this.currentWindowService = currentWindowService; + this.configService = configService; this.store = null; this.window = null; + this.compactWindowDimensions = null; this.maximised = false; + this.defaultWindowDimensions = this.configService.getDefaultWindowDimensions(); this.oldSize = null; + this.oldBounds = { + width: this.defaultWindowDimensions[0], + height: this.defaultWindowDimensions[1], + top: 100, + left: 100 + }; this.maximisedEvent = () => { this.$timeout(() => { @@ -47,25 +51,37 @@ this.window = this.currentWindowService.getCurrentWindow(); this.window.addEventListener('maximized', this.maximisedEvent); this.window.addEventListener('restored', this.restoredEvent); + this.compactWindowDimensions = this.configService.getCompactWindowDimensions(); } minimiseClick() { + reportAction('Window change', 'Minimised'); this.window.minimize(); } maximiseClick() { + if (window.outerWidth !== this.compactWindowDimensions[0]) { + this.oldBounds = { + height: window.outerHeight, + left: window.screenLeft, + top: window.screenTop, + width: window.outerWidth + }; + } + reportAction('Window change', 'Maximised'); this.window.maximize(); } normalSizeClick() { this.window.restore(); - this.window.resizeTo(defaultWidth, defaultHeight, 'top-right'); + this.window.setBounds(this.oldBounds.left, this.oldBounds.top, this.oldBounds.width, this.oldBounds.height); } _compactChanged() { var becomingCompact = this.isCompact(); - if (window.outerWidth !== compactWidth) { + if (window.outerWidth !== this.compactWindowDimensions[0]) { this.oldSize = [window.outerWidth, window.outerHeight]; + this.wasMaximised = this.maximised; } if (window.windowService) { @@ -73,14 +89,17 @@ } if (becomingCompact) { - this.window.resizeTo(compactWidth, compactHeight, 'top-right'); + reportAction('Window change', 'Compact'); + this.window.resizeTo(this.compactWindowDimensions[0], this.compactWindowDimensions[1], 'top-right'); } - else if (this.maximised) { + else if (this.wasMaximised) { + reportAction('Window change', 'Maximised'); this.window.maximize(); } else { - var width = defaultWidth, - height = defaultHeight; + reportAction('Window change', 'Standard'); + var width = this.defaultWindowDimensions[0], + height = this.defaultWindowDimensions[1]; if (this.oldSize) { width = this.oldSize[0]; height = this.oldSize[1]; @@ -111,7 +130,7 @@ () => this._compactChanged()); } } - ToolbarCtrl.$inject = ['$scope', '$timeout', 'currentWindowService']; + ToolbarCtrl.$inject = ['$scope', '$timeout', 'currentWindowService', 'configService']; angular.module('stockflux.toolbar') .controller('ToolbarCtrl', ToolbarCtrl); diff --git a/src/toolbar/toolbar-mixins.less b/src/toolbar/toolbar-mixins.less index db54809b1..ff54a6fd0 100644 --- a/src/toolbar/toolbar-mixins.less +++ b/src/toolbar/toolbar-mixins.less @@ -1,18 +1,16 @@ +.right-alignment(@icon-order) { + right: (15 + (@icon-order * 30)); +} + .toolbar-offset-top(@additional-offset: 0px) { - position: fixed; - top: (15px + @additional-offset); + .position(fixed, @top: (15px + @additional-offset)); } .toolbar-offset-top-compact(@additional-offset: 0px) { - position: fixed; - top: (@toolbar-compact-top-margin + @additional-offset); + .position(fixed, @top: (@toolbar-compact-top-margin + @additional-offset)); } .drag-position(@left: auto, @right: auto, @top: auto, @height: auto, @width: auto) { - position: fixed; - width: @width; - left: @left; - right: @right; - height: @height; - top: @top; -} \ No newline at end of file + .position(fixed, @top: @top, @right: @right, @left: @left); + .size(@width: @width, @height: @height); +} diff --git a/src/toolbar/toolbar.less b/src/toolbar/toolbar.less index 69ee69096..d6fa0002d 100644 --- a/src/toolbar/toolbar.less +++ b/src/toolbar/toolbar.less @@ -4,10 +4,8 @@ @toolbar-compact-top-margin: 7px; #toolbar { - height: @toolbar-height; - right: 0px; - left: @sidebar-width; - position: absolute; + .size(@height: @toolbar-height); + .position(absolute, @right: 0px, @left: @sidebar-width); .drag-left { .drag-position(@height: @toolbar-height, @right: @toolbar-icons-width, @left: @sidebar-width); @@ -59,25 +57,3 @@ } } -.compact #toolbar { - .drag-left { - .drag-position(@height: @compact-top-bottom-margin, @right: @toolbar-compact-icon-width, @left: 0px); - } - - .drag-right { - .drag-position(@height: @compact-top-bottom-margin, @right: 0px, @width: @toolbar-icons-margin); - } - - .drag-rec-closed { - .drag-position(@height: @compact-top-bottom-margin, @right: 90px, @width: 30px); - } - - .drag-top { - .drag-position(@height: @toolbar-compact-top-margin, @right: 0px, @width: 100%); - } - - .drag-bottom { - .drag-position(@height: @toolbar-compact-top-margin, @right: 0px, - @top: (@compact-top-bottom-margin - @toolbar-compact-top-margin), @width: 100%); - } -} diff --git a/src/version-value.js b/src/version-value.js index 26ce9661d..b34bf5198 100644 --- a/src/version-value.js +++ b/src/version-value.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - const VERSION = { version: '10.0.1' }; + const VERSION = { version: '10.1.0' }; angular.module('stockflux.version') .value('Version', VERSION.version); diff --git a/src/window-service.js b/src/window-service.js index 3497176a8..262c80813 100644 --- a/src/window-service.js +++ b/src/window-service.js @@ -246,8 +246,10 @@ createMainWindow(name, isCompact, successCb) { var windowCreatedCb = (newWindow) => { - newWindow.getNativeWindow().windowService = this; - newWindow.getNativeWindow().storeService = this.storeService; + var nativeWindow = newWindow.getNativeWindow(); + nativeWindow.windowService = this; + nativeWindow.storeService = this.storeService; + nativeWindow.dispatchEvent(new Event('onStoreServiceReady')); this.windowTracker.add(newWindow); @@ -265,17 +267,22 @@ showFunction(); } - this.storeService.open(newWindow.name).openWindow(); this.snapToScreenBounds(newWindow); }; var mainWindow; if (name) { + // Notify the store service that the window has opened. + this.storeService.open(name).openWindow(); + mainWindow = new fin.desktop.Window( - isCompact ? - this.configService.getCompactConfig(name) : - this.configService.getWindowConfig(name), + this.configService.getWindowConfig(name), () => { + if (isCompact) { + var compactSize = this.configService.getCompactWindowDimensions(); + this.updateOptions(mainWindow, true); + mainWindow.resizeTo(compactSize[0], compactSize[1], 'top-left'); + } windowCreatedCb(mainWindow); } ); @@ -283,8 +290,9 @@ var poolWindow = this.pool.fetch(); mainWindow = poolWindow.window; if (isCompact) { + var compactWindowDimensions = this.configService.getCompactWindowDimensions(); this.updateOptions(poolWindow.window, true); - poolWindow.window.resizeTo(230, 500, 'top-right'); + poolWindow.window.resizeTo(compactWindowDimensions[0], compactWindowDimensions[1], 'top-right'); } poolWindow.promise.then(() => { @@ -383,10 +391,11 @@ updateOptions(_window, isCompact) { if (isCompact) { + var compactWindowDimensions = this.configService.getCompactWindowDimensions(); _window.updateOptions({ resizable: false, - minHeight: 500, - minWidth: 230, + minWidth: compactWindowDimensions[0], + minHeight: compactWindowDimensions[1], maximizable: false }); } else {