├── .gitignore ├── CITATION.cff ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── _config.yaml ├── deploy.sh ├── lib ├── element.js ├── graph.js ├── mapper.js ├── reasons.js ├── ui.js ├── utils.js └── view.js ├── package-lock.json ├── package.json ├── paper ├── paper.bib └── paper.md ├── reasons.js ├── reasons.min.js ├── test ├── animate-map.html ├── big-text-map.html ├── dual-maps.html ├── element.js ├── graph.js ├── map.html ├── mapper.js ├── mock-canvas.js ├── remote-map.html ├── should.js ├── small-map.html ├── text-size.html ├── ui.js ├── utils.js └── view.js └── web ├── about.md ├── css ├── font-awesome.css ├── font-awesome.min.css └── styles.css ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── index.md └── template.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/* 3 | npm-debug.log 4 | deprecated/* 5 | _site/* 6 | test.html -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Kinkead" 5 | given-names: "Dave" 6 | orcid: "https://orcid.org/0000-0001-5396-8099" 7 | - family-names: "Brown" 8 | given-names: "Deborah" 9 | orcid: "http://orcid.org/0000-0001-5707-7605" 10 | - family-names: "Ellerton" 11 | given-names: "Peter" 12 | orcid: "http://orcid.org/0000-0002-6588-376X" 13 | - family-names: "Mazzola" 14 | given-names: "Claudio" 15 | orcid: "http://orcid.org/0000-0001-6117-7465" 16 | title: "Reasons: A digital argument mapping library for modern 17 | browsers" 18 | version: 1.1.5 19 | doi: 10.21105/joss.01044 20 | date-released: 2019-05-12 21 | url: "https://github.com/davekinkead/reasons" 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 4 | 5 | Please note we have a code of conduct, please follow it in all your interactions with the project. 6 | 7 | ## Pull Request Process 8 | 9 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 10 | 2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 11 | 3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 12 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. 13 | 14 | ## Code of Conduct 15 | 16 | ### Our Pledge 17 | 18 | In the interest of fostering an open and welcoming environment, we as 19 | contributors and maintainers pledge to making participation in our project and 20 | our community a harassment-free experience for everyone, regardless of age, body 21 | size, disability, ethnicity, gender identity and expression, level of experience, 22 | nationality, personal appearance, race, religion, or sexual identity and 23 | orientation. 24 | 25 | ### Our Standards 26 | 27 | Examples of behavior that contributes to creating a positive environment 28 | include: 29 | 30 | * Using welcoming and inclusive language 31 | * Being respectful of differing viewpoints and experiences 32 | * Gracefully accepting constructive criticism 33 | * Focusing on what is best for the community 34 | * Showing empathy towards other community members 35 | 36 | Examples of unacceptable behavior by participants include: 37 | 38 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 39 | * Trolling, insulting/derogatory comments, and personal or political attacks 40 | * Public or private harassment 41 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 42 | * Other conduct which could reasonably be considered inappropriate in a professional setting 43 | 44 | ### Our Responsibilities 45 | 46 | Project maintainers are responsible for clarifying the standards of acceptable 47 | behavior and are expected to take appropriate and fair corrective action in 48 | response to any instances of unacceptable behavior. 49 | 50 | Project maintainers have the right and responsibility to remove, edit, or 51 | reject comments, commits, code, wiki edits, issues, and other contributions 52 | that are not aligned to this Code of Conduct, or to ban temporarily or 53 | permanently any contributor for other behaviors that they deem inappropriate, 54 | threatening, offensive, or harmful. 55 | 56 | ### Scope 57 | 58 | This Code of Conduct applies both within project spaces and in public spaces 59 | when an individual is representing the project or its community. Examples of 60 | representing a project or community include using an official project e-mail 61 | address, posting via an official social media account, or acting as an appointed 62 | representative at an online or offline event. Representation of a project may be 63 | further defined and clarified by project maintainers. 64 | 65 | ### Enforcement 66 | 67 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 68 | reported by contacting the project team at [Dave Kinkead](mailto:d.kinkead@uq.edu.au). All 69 | complaints will be reviewed and investigated and will result in a response that 70 | is deemed necessary and appropriate to the circumstances. The project team is 71 | obligated to maintain confidentiality with regard to the reporter of an incident. 72 | Further details of specific enforcement policies may be posted separately. 73 | 74 | Project maintainers who do not follow or enforce the Code of Conduct in good 75 | faith may face temporary or permanent repercussions as determined by other 76 | members of the project's leadership. 77 | 78 | ### Attribution 79 | 80 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 81 | available at [http://contributor-covenant.org/version/1/4][version] 82 | 83 | [homepage]: http://contributor-covenant.org 84 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Dave Kinkead, University of Queensland 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reasons 2 | 3 | `Reasons` is a digital argument mapping library designed for modern web browsers. 4 | 5 | Argument mapping is the process of visually representating the logical structure of arguments. Argument maps are an important pedagogical tool in the analysis of argumentation and have been [associated with substantial increases in student cognative gains](https://www.pdcnet.org/teachphil/content/teachphil_2004_0027_0002_0095_0116). 6 | 7 | Argument mapping forms the middle of the three stages of informal logical analysis - identification of truth claims within arguments, the analysis of logical structure, and synthesis of logcial structure into writen form. `Reasons` is designed to seemlessly integrate these stages into existing teaching pedagogies. 8 | 9 | [![DOI](http://joss.theoj.org/papers/10.21105/joss.01044/status.svg)](https://doi.org/10.21105/joss.01044) 10 | 11 | ## Embedding Reasons 12 | 13 | Download the library and add a reference to it and any initial data just before the `` tag of a HTML page. 14 | 15 | 16 | ```html 17 | 18 | 19 |
20 | 21 | ... 22 | 23 | 24 | 33 | 34 | 35 | ``` 36 | 37 | 38 | `Reasons` needs to know the target `#element` of the DOM in which to render the argument map. You could use `body` to append the map to the HTML body tag or `#id` to append it to a specific element id. The optional `graph` is just an array of nodes and edges representing the logical structure of an argument. `Nodes` and `edges` are plain javascript objects representing propositional claims and inferential support. 39 | 40 | Saving an argument map is left as an implementation detail. `Mapper.export()` will return a `Graph` array from the map's current state. 41 | 42 | 43 | ## Using Reasons 44 | 45 | Reasons relies on a few simple commands to edit an argument map: 46 | 47 | - Double clicking/touching on an empty canvas will create a new reason node. 48 | 49 | - Double clicking/touching on an element will open the edit dialog. `Return` or `Esc` will close the dialog. 50 | 51 | - Dragging one reason onto another creates an inferential connection. Dragging reasons that share an inferential connection onto one another will create a conjoined reason. 52 | 53 | - A single click or `Tab` will select an element. `Delete` or `Backspace` will remove that element. 54 | 55 | - `Ctrl z` or `⌘ z` or device shake will undo an action. `Ctrl y` or `⌘ y` will redo it. 56 | 57 | - `Ctrl +` or `⌘ +` or wheel or expand will zoom in. `Ctrl -` or `⌘ -` or wheel or pinch will zoom out. 58 | 59 | 60 | [Click for a demonstration](http://reasons.io/) 61 | 62 | 63 | ## Development & Testing 64 | 65 | 66 | Clone the repo and install the dependencies. 67 | 68 | 69 | $ git clone https://github.com/davekinkead/reasons.git 70 | $ npm install 71 | 72 | 73 | Running the tests requires additional dev dependencies listed in the [package.json](/package.json) file under dev-dependencies including [JSDOM](https://github.com/jsdom/jsdom) and [Canvas](https://github.com/node-gfx/node-canvas-prebuilt). 74 | . You will need to install all these using the command: 75 | 76 | 77 | $ npm install mocha --save-dev 78 | $ npm install jsdom --save-dev 79 | ... 80 | 81 | Then: 82 | 83 | $ npm test 84 | 85 | To get autobuilding to work, install [workman](https://facebook.github.io/watchman/) 86 | 87 | $ watchman-make --make "npm run" -p "lib/*.js" -t build 88 | 89 | 90 | ## Browser Compatibility 91 | 92 | `Reasons` relies on the HTML5 Canvas library - as such, it will not work on legacy browsers. Touch events will be added in future releases. It has been tested and works on the following browsers: 93 | 94 | 95 | - [X] Chrome on OSX (60+) 96 | - [X] Firefox on OSX (54+) 97 | - [X] Safari on OSX (11+) 98 | - [X] Chrome on Windows 99 | - [X] Firefox on Windows 100 | - [X] Safari iOS (View only) 101 | - [ ] Safari iOS (Touch) 102 | - [ ] Internet Explorer on Windows 103 | - [ ] Chrome on Android 104 | 105 | 106 | ## Contributing & Support 107 | 108 | We gladly accept feature requests and bug fixes. If you have questions or problems, please [open an issue](https://github.com/davekinkead/reasons/issues). 109 | 110 | Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us. 111 | 112 | ## How to Cite 113 | 114 | If you are using this software in an academic capacity, please cite as: 115 | 116 | > Kinkead et al., (2019). Reasons: A digital argument mapping library for modern browsers. Journal of Open Source Software, 4(37), 1044, https://doi.org/10.21105/joss.01044 117 | 118 | [![DOI](http://joss.theoj.org/papers/10.21105/joss.01044/status.svg)](https://doi.org/10.21105/joss.01044) 119 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Feature Roadmap 2 | 3 | ## Future / Maybe 4 | 5 | - [X] UX: Improve sharpness of canvas elements 6 | - [X] UX: Fix resolution of text 7 | 8 | - [] UX: Fix text variable padding bug within node 9 | 10 | - [x] UX: Add zoom in/out functionality map 11 | - [x] UX: Add pan from single click & drag 12 | 13 | - [] Compatibility: Touch UI for iOS 14 | - [] Compatibility: Touch UI for Android 15 | - [] UX: Add delete & save icons to the edit dialog 16 | - [] UX: Merge with delete, escape, & return key events 17 | - [] UX: Add params option for line, overlay, background colour etc 18 | 19 | - [] UX: Automatically scale to view port 20 | - [] UX: Automatically scale for PNG export 21 | 22 | - [] UX: Node line type selector (solid/dotted) 23 | - [] UX: Increase edge text target area 24 | 25 | - [] UX: Add automatic padding when dropping node 26 | - [] UX: Automatic layout options 27 | - [] UX: Automatically assign layout 28 | 29 | - [] UX: Add multiple node selection for dragging?? 30 | - [] UX: Curved edges when overlap is present?? 31 | 32 | - [] Highlighter: Specifiy target URL 33 | - [] Highlighter: Better icons 34 | - [] Highlighter: Remember selections 35 | - [] Highlighter: Delete selections 36 | - [] Highlighter: Remember selections between sessions 37 | 38 | - [] Scaffolder: Export graph structure to text/html for copy & paste into word 39 | 40 | ## 1.0.1 41 | 42 | - [] Bug: Fix undo bug on Windows 43 | - [X] Bug: Prevent edges being added to other edges 44 | - [X] Bug: Fix backspace preventDefault issue 45 | 46 | - [X] UX: Clicking outside text area closes edit modal 47 | - [X] UX: Prohibit duplication connections 48 | - [X] UX: Tab wont add to history 49 | - [X] UX: Multi reason intersection moved closer to conclusion 50 | 51 | ## 1.0.0 52 | 53 | - [X] UX: Double click canvas to add node 54 | - [X] UX: Automatically highlight reason text on node creation 55 | - [X] UX: Double click node to edit text 56 | - [X] UX: Double click edge to edit text 57 | - [X] UX: Drag node to move position 58 | - [X] UX: Dragging node updates edges 59 | - [X] UX: Drop node on node to create edge 60 | - [X] UX: Automatically adjust node size from text 61 | - [X] UX: Focused element should be on top 62 | - [X] UX: Focused element visual affordances 63 | - [X] UX: Hover over edge text should tigger focus 64 | 65 | - [X] Hot Keys: Add backspace and delete key codes for Windows 66 | - [X] Hot Keys: Tab to different elements 67 | - [X] Hot Keys: Return to edit selected element 68 | - [X] Hot Keys: Add undo / redo hot keys 69 | - [X] Hot Keys: Escape to cancel overlay 70 | 71 | - [X] Edges: Add divergent reasons 72 | - [X] Edges: Conjoining reasons should only join on dropped reasons 73 | - [X] Edges: Add independent conjoined argument chains 74 | - [X] Edges: Permit multiple relations between nodes 75 | 76 | - [X] Compatibility: Chrome on OSX 77 | - [X] Compatibility: Firefox on OSX 78 | - [X] Compatibility: Safari on OSX 79 | - [X] Compatibility: Firefox on Windows 80 | - [X] Compatibility: Chrome on Windows 81 | 82 | - [X] Data: Changes to canvas events should update graph object 83 | - [X] Data: Render map from existing x,y coordinates -------------------------------------------------------------------------------- /_config.yaml: -------------------------------------------------------------------------------- 1 | # Config for Github Pages 2 | 3 | title: Reasons.js 4 | author: Dave Kinkead 5 | description: Web based argument mapping using HTML5 Canvas and Javascript 6 | baseurl: /reasons 7 | permalink: /:title/ 8 | layouts_dir: /web/ -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git checkout development 4 | npm run test 5 | npm run minify 6 | git commit -am "Autobuild and minify" 7 | git push development 8 | git checkout master 9 | git merge development 10 | git push github master 11 | git checkout gh-pages 12 | git merge master 13 | git push github gh-pages 14 | git checkout development -------------------------------------------------------------------------------- /lib/element.js: -------------------------------------------------------------------------------- 1 | // Reasons.js by Dave Kinkead 2 | // Copyright 2017-2019 University of Queensland 3 | // Available under the MIT license 4 | 5 | 'use strict' 6 | 7 | const Utils = require('./Utils') 8 | const maxWidth = 200 9 | const padding = 10 10 | const fontSize = 16 11 | 12 | 13 | module.exports = { 14 | mixin, isEdge, isNode, save 15 | } 16 | 17 | 18 | /** 19 | * Mixes in specific behaviour of an Element to an Object 20 | */ 21 | function mixin(element) { 22 | element.isEdge = isEdge 23 | element.isNode = isNode 24 | element.export = save 25 | element.collides = collides 26 | element.move = move 27 | 28 | return init(element) 29 | } 30 | 31 | 32 | /** 33 | * Initialize an Element 34 | * @params element Node or Edge 35 | */ 36 | function init (element) { 37 | element.id = element.id || Math.random().toString(36).slice(-5) 38 | 39 | if (element.isEdge()) { 40 | 41 | /** 42 | * Default Edge values: 43 | * From should return ['node_id', 'node_id'] 44 | * To should return 'node_id' 45 | * Path should be an empty array to be set in the View/UI 46 | */ 47 | element.from = Utils.flatten([element.from]).map((from) => { return from.id || from }) 48 | element.to = element.to.id || element.to 49 | element.type = element.type || 'supports' 50 | element.paths = [] 51 | } else { 52 | 53 | // Default Node values 54 | element.text = element.text || 'A reason' 55 | element.width = maxWidth 56 | element.height = fontSize * 3.5 57 | locate(element, {x: element.x || 0, y: element.y || 0}) 58 | } 59 | 60 | return element 61 | } 62 | 63 | 64 | /** 65 | * Returns true if an element is an Edge 66 | */ 67 | function isEdge () { 68 | return (this.to && this.from) ? true : false 69 | } 70 | 71 | 72 | /** 73 | * Returns true if an element is a Node 74 | */ 75 | function isNode () { 76 | return (this.isEdge()) ? false : true 77 | } 78 | 79 | /** 80 | * Determines if an point is withing the boundaries of an element 81 | */ 82 | function collides (el) { 83 | if (this.isEdge()) { 84 | 85 | // Determine a hit for each of the paths 86 | let hit = false 87 | this.paths.forEach((path) => { 88 | if (differenceOfVectors(el, path) < 0.05) 89 | hit = true 90 | }) 91 | 92 | if (!this.center) return false 93 | 94 | // Estimate collision of the label box 95 | let width = this.type.length * 5 96 | hit = (el.x < this.center.x - width || el.x > this.center.x + width || 97 | el.y < this.center.y - 10 || el.y > this.center.y + 10) ? false : true 98 | 99 | // otherwise 100 | return hit 101 | } else { 102 | 103 | // is the element a node or x,y coordinate 104 | if (el.isNode && el.isNode()) 105 | return (this.x2 < el.x1 || this.x1 > el.x2 || this.y1 > el.y2 || this.y2 < el.y1) ? false : true 106 | else 107 | return (el.x > this.x1 && el.x < this.x2 && el.y > this.y1 && el.y < this.y2) ? true : false 108 | } 109 | } 110 | 111 | /** 112 | * Increases the x & y values of an element 113 | */ 114 | function move (position) { 115 | if (this.isNode()) { 116 | this.x = position.x 117 | this.y = position.y 118 | locate(this, position) 119 | } 120 | } 121 | 122 | /** 123 | * Exports an element's data 124 | */ 125 | function save () { 126 | if (this.isEdge()) { 127 | 128 | // Export an Edge 129 | return { 130 | id: this.id, 131 | type: this.type, 132 | from: convertObjectsToIds(this.from), 133 | to: convertObjectsToIds(this.to) 134 | } 135 | } else { 136 | 137 | // Export a Node 138 | return { 139 | id: this.id, 140 | text: this.text, 141 | x: parseInt(this.x1 + this.width/2), 142 | y: parseInt(this.y1 + this.height/2), 143 | lineType: (this.lineType) ? this.lineType : 'solid' 144 | } 145 | } 146 | } 147 | 148 | 149 | /** 150 | * Helper function to set position values 151 | */ 152 | function locate (element, position) { 153 | if (element.isNode()) { 154 | element.x1 = parseInt(position.x - element.width/2) 155 | element.x2 = parseInt(position.x + element.width/2) 156 | element.y1 = parseInt(position.y - element.height/2) 157 | element.y2 = parseInt(position.y + element.height/2) 158 | } 159 | } 160 | 161 | 162 | /** 163 | * Helper function to ensure permit edge references to both nodes and node.ids 164 | */ 165 | function convertObjectsToIds (obj) { 166 | if (obj instanceof Array) { 167 | return obj.map(el => el.id || el) 168 | } else { 169 | return obj.id || obj 170 | } 171 | } 172 | 173 | /** 174 | * Helper function to calculate the difference between 2 vectors el -> x1,y1 and el -> x2,y2 175 | */ 176 | function differenceOfVectors (point, path) { 177 | return Math.abs((Math.atan2(point.y-path.y1, point.x-path.x1)) 178 | -(Math.atan2(path.y2-point.y, path.x2-point.x))) 179 | } -------------------------------------------------------------------------------- /lib/graph.js: -------------------------------------------------------------------------------- 1 | // Reasons.js by Dave Kinkead 2 | // Copyright 2017-2019 University of Queensland 3 | // Available under the MIT license 4 | 5 | 'use strict' 6 | 7 | const Utils = require('./utils') 8 | const Element = require('./element') 9 | 10 | module.exports = Graph 11 | 12 | 13 | /** 14 | * A Graph is simply an extended array containing node and edge objects. 15 | * It is an abstract data structure with no DOM form. 16 | * Edges will contain references to node objects. 17 | * 18 | * @param elements the elements (nodes & edges) to consitute the graph 19 | */ 20 | function Graph(elements) { 21 | // sort the elements so nodes are added before edges 22 | if (elements instanceof Array) { 23 | elements.sort((a,b) => { 24 | return a.to ? 1: -1 25 | }).forEach(el => this.add(el)) 26 | } 27 | } 28 | 29 | 30 | /** 31 | * Use Array as the prototype 32 | */ 33 | Graph.prototype = Object.create(Array.prototype) 34 | 35 | 36 | /** 37 | * Adds a new element to the Graph. 38 | * Nodes should be added before edges as the latter referrence the former 39 | * 40 | * @param element an element to add 41 | */ 42 | Graph.prototype.add = function (element) { 43 | 44 | // Mixin Element behaviour 45 | Element.mixin(element) 46 | 47 | if (element.isNode()) { 48 | this.push(element) 49 | } else { 50 | 51 | // Sanity check to ensure that edges only join nodes 52 | if (this.hasDuplicate(element) || this.isFromEdge(element) || this.isToEdge(element)) { 53 | return false 54 | } 55 | 56 | // Edges can connect independent or conjoined reasons. 57 | // If A B & C both already support D 58 | // and a new edge is added from A to B or vice versa 59 | // then the relationships should be merged [A,B] -> D 60 | // and C -> D kept unchanged 61 | let commonChildren = Utils.intersection( 62 | Utils.flatten(element.from.map(e => this.children(e))), 63 | this.children(element.to) 64 | ).map(el => el.id) 65 | 66 | if (commonChildren.length > 0) { 67 | let commonParents = Utils.flatten([element.from, element.to]).map(el => el.id || el) 68 | 69 | // remove the edge that contains element.from to common children 70 | this.edges().forEach((edge) => { 71 | if (Utils.intersection(element.from, edge.from).length > 0) 72 | this.remove(edge) 73 | 74 | if (edge.from.includes(element.to)) { 75 | this.push(Element.mixin({ 76 | from: Utils.flatten([edge.from, element.from]), 77 | to: edge.to, 78 | type: edge.type 79 | })) 80 | this.remove(edge) 81 | } 82 | }) 83 | } else { 84 | this.push(element) 85 | } 86 | } 87 | } 88 | 89 | 90 | /** 91 | * Removes an existing element from the Graph. 92 | * If a node is removed, it should also remove relevent edges 93 | * 94 | * @param el the element to remove 95 | */ 96 | Graph.prototype.remove = function (el) { 97 | let i = this.indexOf(el) 98 | 99 | if (i > -1) { 100 | if (el.isNode()) { 101 | 102 | // find associated edges first 103 | let edgesTo = this.edges().filter(edge => edge.to == el.id) 104 | let edgesFrom = this.edges().filter(edge => edge.from.includes(el.id)) 105 | 106 | // determine if any associated edge is conjoined 107 | let conjoined = edgesFrom.filter(edge => edge.from.length > 1) 108 | 109 | // remove the node 110 | this.splice(i, 1) 111 | 112 | // and then the associated edges 113 | edgesTo.forEach((edge) => { 114 | if (this.indexOf(edge) > -1) 115 | this.splice(this.indexOf(edge), 1) 116 | }) 117 | 118 | edgesFrom.forEach((edge) => { 119 | if (this.indexOf(edge) > -1) 120 | this.splice(this.indexOf(edge), 1) 121 | }) 122 | 123 | // also remove node from any complex relations 124 | edgesFrom.filter(e => e.from instanceof Array).map((e) => { 125 | if (e.from.indexOf(el) > -1) 126 | e.from.splice(e.from.indexOf(el), 1) 127 | if (e.from.length === 1) 128 | e.from = e.from[0] 129 | }) 130 | 131 | // and now modify the conjoined edges and add them back 132 | conjoined.forEach((edge) => { 133 | edge.from.splice(edge.from.indexOf(el.id), 1) 134 | this.push(edge) 135 | }) 136 | } else { 137 | this.splice(i, 1) 138 | } 139 | } 140 | 141 | // permit chaining during tests 142 | return this 143 | } 144 | 145 | 146 | /** 147 | * Moves an element to the front of the Graph and sets the focused flag. 148 | * Useful for assiting with layouts. 149 | * 150 | * @param el an element to focus 151 | */ 152 | Graph.prototype.focus = function (el) { 153 | let index = this.indexOf(el) 154 | 155 | if (index > -1) { 156 | this.push(this.splice(index, 1)[0]) 157 | } 158 | 159 | this.forEach(function (e) { 160 | e.focused = (e === el) ? true : false 161 | }) 162 | 163 | // permit chaining during tests 164 | return el 165 | } 166 | 167 | Graph.prototype.undoFocus = function () { 168 | const last = this.pop(); 169 | this.unshift(last); 170 | 171 | this.forEach(function (e) { 172 | e.focused = (e === last) ? true : false 173 | }) 174 | 175 | // permit chaining during tests 176 | return last 177 | } 178 | 179 | 180 | /** 181 | * Unsets the focused flag of an element 182 | */ 183 | Graph.prototype.unfocus = function () { 184 | this.forEach(function (el) { 185 | el.focused = false 186 | }) 187 | 188 | // permit chaining during tests 189 | return this 190 | } 191 | 192 | 193 | /** 194 | * Returns the last element of the array 195 | */ 196 | Graph.prototype.last = function () { 197 | return this[this.length - 1] 198 | } 199 | 200 | /** 201 | * Returns an array of all the Graph's edges 202 | */ 203 | Graph.prototype.edges = function () { 204 | return this.filter(el => el.from && el.to) 205 | } 206 | 207 | 208 | /** 209 | * Returns an array of all the Graph's nodes 210 | */ 211 | Graph.prototype.nodes = function () { 212 | return this.filter(el => !el.from || !el.to ) 213 | } 214 | 215 | 216 | /** 217 | * Returns an array of all the Graph's elements 218 | */ 219 | Graph.prototype.elements = function () { 220 | return this 221 | } 222 | 223 | 224 | /** 225 | * Find all the parents for a given node or id 226 | * Returns an array of objects 227 | * 228 | * @params id a Node or String id of a Node 229 | */ 230 | Graph.prototype.parents = function (id) { 231 | if (id instanceof Object) id = id.id 232 | 233 | return Utils.flatten( 234 | this.edges().filter(el => el.to == id).map(el => el.from) 235 | ).map(el => this.find(i => i.id == el)) 236 | } 237 | 238 | 239 | /** 240 | * Find all the children for a given node or id 241 | * Returns an array of objects 242 | * 243 | * @params id a Node or String id of a Node 244 | */ 245 | Graph.prototype.children = function (id) { 246 | if (id instanceof Object) id = id.id 247 | 248 | return Utils.unique(this.edges().filter((el) => { 249 | return Utils.flatten([el.from]).map(el => el.id || el).indexOf(id) > -1 250 | }).map(el => el.to) 251 | .map(el => this.find(i => i == el || i.id == el))) 252 | } 253 | 254 | 255 | /** 256 | * Determine if the proposed element is a dupliucate 257 | * Returns boolean 258 | * 259 | * @params edge object 260 | */ 261 | Graph.prototype.hasDuplicate = function (el) { 262 | Element.mixin(el) 263 | let dupe = false 264 | 265 | if (el.isEdge()) { 266 | this.edges().forEach((edge) => { 267 | if (el.to === edge.to && el.from.toString() === edge.from.toString()) { 268 | dupe = true 269 | } 270 | }) 271 | } 272 | return dupe 273 | } 274 | 275 | 276 | /** 277 | * Determine if the proposed Edge is from an Edge 278 | * Returns boolean 279 | * 280 | * @params edge object 281 | */ 282 | Graph.prototype.isFromEdge = function (element) { 283 | var fromEdge = false 284 | element.from.forEach((el) => { 285 | var match = this.edges().find((e) => e.id == el) 286 | if (match) fromEdge = true 287 | }) 288 | 289 | return fromEdge 290 | } 291 | 292 | 293 | /** 294 | * Determine if the proposed Edge is to an Edge 295 | * Returns boolean 296 | * 297 | * @params edge object 298 | */ 299 | Graph.prototype.isToEdge = function (element) { 300 | let to = this.find((el) => el.id == element.to) 301 | if (to && to.isEdge()) { 302 | return true 303 | } 304 | } -------------------------------------------------------------------------------- /lib/mapper.js: -------------------------------------------------------------------------------- 1 | // Reasons.js by Dave Kinkead 2 | // Copyright 2017-2019 University of Queensland 3 | // Available under the MIT license 4 | 5 | 'use strict' 6 | 7 | const Graph = require('./graph') 8 | const UI = require('./ui') 9 | const View = require('./view') 10 | 11 | 12 | /** 13 | * This module wraps the DOM UI, Canvas renderer, and Graph data 14 | */ 15 | module.exports = Mapper 16 | 17 | 18 | /** 19 | * The Mapper acts as the interface between the Graph data object and the browser DOM. 20 | * It contains references to @graph (the data) and @DOM (the DOM object) 21 | * 22 | * @params elementID the element id to append the map canvas to 23 | */ 24 | function Mapper (elementID) { 25 | 26 | // get the DOM element 27 | this.DOM = document.querySelector(elementID) 28 | 29 | // attach the canvas and event listeners to the HTML if the reference was valid 30 | if (this.DOM) { 31 | View.init(this) 32 | UI.setup(this) 33 | UI.addEventListeners(this) 34 | } 35 | } 36 | 37 | 38 | /** 39 | * Populates a Graph with nodes and edges. 40 | * 41 | * @params elements the elements to render 42 | */ 43 | Mapper.prototype.render = function (elements) { 44 | this.graph = new Graph(elements) 45 | View.draw(this) // this is repeated to generate node heights 46 | View.resize(this) 47 | View.zero(this) 48 | View.draw(this) 49 | return this 50 | } 51 | 52 | 53 | /** 54 | * Exports a Graph's data structure as an Array 55 | */ 56 | Mapper.prototype.export = function () { 57 | return this.graph.map(element => element.export()) 58 | } -------------------------------------------------------------------------------- /lib/reasons.js: -------------------------------------------------------------------------------- 1 | // Reasons.js by Dave Kinkead 2 | // Copyright 2017-2019 University of Queensland 3 | // Available under the MIT license 4 | 5 | /** 6 | * The Reasons.js API. This module forms the top level wrapper 7 | */ 8 | const Mapper = require('./mapper') 9 | 10 | module.exports = { 11 | mapper: function (dom) { 12 | return new Mapper(dom) 13 | } 14 | } -------------------------------------------------------------------------------- /lib/ui.js: -------------------------------------------------------------------------------- 1 | // Reasons.js by Dave Kinkead 2 | // Copyright 2017-2019 University of Queensland 3 | // Available under the MIT license 4 | 5 | 'use strict' 6 | 7 | const View = require('./view') 8 | const Utils = require('./utils') 9 | const Graph = require('./graph') 10 | const Keycode = require('keycode') 11 | const Hammer = require('hammerjs') 12 | 13 | 14 | module.exports = { 15 | addEventListeners, 16 | setup 17 | } 18 | 19 | function setup(mapper) { 20 | setupSharedStyles() 21 | const styleTag = Utils.buildNode('style') 22 | styleTag.innerHTML = ` 23 | #${mapper.DOM.id} { 24 | min-height: 100px; 25 | } 26 | ` 27 | document.head.appendChild(styleTag) 28 | View.resize(mapper) 29 | 30 | mapper.History = [] 31 | mapper.Future = [] 32 | } 33 | 34 | function setupSharedStyles() { 35 | if (!document.head.querySelector('style[data-reasons-shared]')) { 36 | const sharedStyles = Utils.buildNode('style', undefined, { 'data-reasons-shared': true }) 37 | sharedStyles.innerHTML = ` 38 | body.modal { 39 | overflow-y: hidden; 40 | } 41 | #reason-overlay { 42 | font-size: 18px; 43 | position: fixed; 44 | top: 0; left: 0; right: 0; 45 | height: 100vh; 46 | background: rgba(0,0,0,0.75); 47 | touch-action: none; 48 | } 49 | #edit-reason-input { 50 | font-size: 18px; 51 | padding: 1rem 1rem 0 1rem; 52 | margin-top: 10vh; 53 | box-sizing: border-box; 54 | } 55 | #reason-overlay__wrapper { 56 | margin: auto; 57 | margin-top: 10vh; 58 | width:50%; 59 | padding: 1rem; 60 | flex-direction: column; 61 | display: flex; 62 | } 63 | #reasons-overlay-toolbar { 64 | margin: 0.75rem -0.5rem; 65 | } 66 | .reason-overlay__button { 67 | font-size: inherit; 68 | background-color: white; 69 | padding: 0.5rem 1rem; 70 | border: 1px solid grey; 71 | border-radius: 4px; 72 | margin: 0 0.5rem; 73 | } 74 | [data-reasons-layout="inline"] { 75 | touch-action: pinch-zoom; 76 | } 77 | .show-touch, .show-pointer { 78 | display: none; 79 | } 80 | ` 81 | document.head.appendChild(sharedStyles) 82 | } 83 | } 84 | 85 | function addEventListeners (mapper) { 86 | 87 | const hammer = new Hammer(mapper.DOM, {}) 88 | if (!mapper.inline) { 89 | hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL }) 90 | } 91 | hammer.get('pinch').set({ enable: true }) 92 | 93 | // 3 finger swipes for undo/redo 94 | hammer.add(new Hammer.Swipe({ pointers: 3 })) 95 | 96 | // encapuslate event state in the argumentMap 97 | mapper.altered = true 98 | mapper.editMode = false 99 | mapper.dirty = false // for when changes shouldnt be added to history 100 | let mouseDown = false 101 | let selected = null 102 | let dragging = null 103 | let clickPos = null 104 | let clickOffset = null 105 | let metaKeyPressed = false 106 | 107 | // const elPosition = function( _el ) { 108 | 109 | // var target = _el, 110 | // target_width = target.offsetWidth, 111 | // target_height = target.offsetHeight, 112 | // target_left = target.offsetLeft, 113 | // target_top = target.offsetTop, 114 | // gleft = 0, 115 | // gtop = 0, 116 | // rect = {}; 117 | 118 | // // what does moonwalk do here? 119 | // var moonwalk = function( _parent ) { 120 | // if (!!_parent) { 121 | // gleft += _parent.offsetLeft; 122 | // gtop += _parent.offsetTop; 123 | // moonwalk( _parent.offsetParent ); 124 | // } else { 125 | // return rect = { 126 | // top: target.offsetTop + gtop, 127 | // left: target.offsetLeft + gleft, 128 | // bottom: (target.offsetTop + gtop) + target_height, 129 | // right: (target.offsetLeft + gleft) + target_width 130 | // }; 131 | // } 132 | // }; 133 | // moonwalk( target.offsetParent ); 134 | // return rect; 135 | // } 136 | 137 | const localPosition = (event) => { 138 | const {x,y} = mapper.offset 139 | const parent = event.target.getClientRects()[0] 140 | 141 | return { 142 | x: (parseInt((event.x || event.pageX) - parseInt(parent.left)) / mapper.scale) - x, 143 | y: (parseInt((event.y || event.pageY) - parseInt(parent.top)) / mapper.scale) - y 144 | } 145 | } 146 | 147 | /** 148 | * Private: Returns mouse event and hovered element 149 | */ 150 | function detect (event) { 151 | const local = localPosition(event) 152 | return { 153 | position: local, 154 | collision: mapper.graph.elements().find(el => el.collides(local)) 155 | } 156 | } 157 | 158 | // For testing 159 | // const click = (event) => { 160 | // console.log(mapper.scale) 161 | // // console.log(mapper.graph) 162 | // } 163 | // mapper.DOM.addEventListener('click', click) 164 | 165 | // Double click creates or edits element 166 | const doubleClick = (event) => { 167 | 168 | const {position, collision} = detect(event) 169 | 170 | if (collision) { 171 | // Double click on nodes or edges trigger edit mode 172 | addOverlay(mapper, collision) 173 | } else { 174 | // Double clicks on a bare map creates a new node 175 | mapper.graph.add({x: position.x, y: position.y}) 176 | selected = mapper.graph.last() 177 | mapper.graph.focus(selected) 178 | mapper.altered = true 179 | addOverlay(mapper, selected, true) 180 | } 181 | 182 | redraw(mapper) 183 | } 184 | hammer.on('doubletap', (hammerEvent) => { 185 | const event = hammerEvent.srcEvent 186 | event.preventDefault() 187 | doubleClick(event) 188 | }) 189 | 190 | function triggerRedo() { 191 | // Store for undo 192 | save(mapper.History, mapper) 193 | const next = mapper.Future.pop() 194 | if (next) { 195 | mapper.graph = new Graph(JSON.parse(next)) 196 | mapper.dirty = true 197 | } 198 | } 199 | 200 | function triggerUndo() { 201 | // Store for redo 202 | save(mapper.Future, mapper) 203 | const last = mapper.History.pop() 204 | if (last) { 205 | mapper.graph = new Graph(JSON.parse(last)) 206 | mapper.dirty = true 207 | } 208 | } 209 | 210 | hammer.on('swipe', (hammerEvent) => { 211 | if (hammerEvent.direction & Hammer.DIRECTION_HORIZONTAL) { 212 | mapper._isSwipping = true 213 | if (hammerEvent.direction === Hammer.DIRECTION_LEFT) { 214 | triggerUndo() 215 | } else { 216 | triggerRedo() 217 | } 218 | setTimeout(() => { 219 | mapper._isSwipping = false 220 | }, 250) 221 | } 222 | }) 223 | 224 | hammer.on('panstart', function(hammerEvent) { 225 | const event = hammerEvent.srcEvent 226 | const {collision} = detect(event) 227 | if (mapper._isSwipping) { return } 228 | if (collision) { 229 | dragStart(event) 230 | } else { 231 | mapper._startPan = { ...mapper.offset } 232 | } 233 | }) 234 | 235 | const panMove = function (hammerEvent) { 236 | const event = hammerEvent.srcEvent 237 | const {collision} = detect(event) 238 | if (collision) { return dragMove(event) } 239 | if (dragging || mapper._isSwipping) { return } 240 | 241 | mapper.offset = { 242 | x: mapper._startPan.x + (hammerEvent.deltaX / mapper.scale), 243 | y: mapper._startPan.y + (hammerEvent.deltaY / mapper.scale), 244 | } 245 | mapper.dirty = true 246 | View.zero(mapper) 247 | redraw(mapper) 248 | }; 249 | hammer.on('panmove', panMove) 250 | 251 | // Draging an element selects and moves it 252 | // Selecting nothing unfocuses the graph 253 | const dragStart = (event) => { 254 | 255 | const {position, collision} = detect(event) 256 | if (dragging) { return } 257 | 258 | if (collision) { 259 | selected = collision 260 | mapper.graph.focus(selected) 261 | mapper.dirty = true 262 | clickPos = position 263 | clickOffset = { 264 | x: (selected.x1 + (selected.width / 2)) - position.x, 265 | y: (selected.y1 + (selected.height /2)) - position.y 266 | } 267 | dragging = selected 268 | } 269 | 270 | redraw(mapper) 271 | } 272 | mapper.DOM.addEventListener('mousedown', dragStart) 273 | 274 | // Move a selected element on drag 275 | // Highlight a hovered element 276 | const dragMove = (event) => { 277 | 278 | window.currentMapper = mapper; 279 | 280 | // Set element hover flag on mouseover 281 | const mouse = localPosition(event) 282 | 283 | mapper.graph.forEach((el) => { 284 | if (el.collides(mouse)) { 285 | if (!el.hovering) mapper.dirty = true 286 | el.hovering = true 287 | } else { 288 | if (el.hovering) mapper.dirty = true 289 | el.hovering = false 290 | } 291 | }) 292 | 293 | // Specify a node as the drag target when clicked 294 | if (dragging) { 295 | const localPos = localPosition(event) 296 | dragging.move({ 297 | x: localPos.x + clickOffset.x, 298 | y: localPos.y + clickOffset.y 299 | }) 300 | mapper.dirty = true 301 | } 302 | 303 | redraw(mapper) 304 | } 305 | mapper.DOM.addEventListener('mousemove', dragMove) 306 | 307 | 308 | // Release a drag action and add an edge if needed 309 | const dragEnd = (event) => { 310 | 311 | const {collision} = detect(event) 312 | 313 | // Check for node drop and add a new edge to the graph if required 314 | if (dragging) { 315 | const target = mapper.graph.nodes().find(el => dragging.collides(el) && dragging.id !== el.id) 316 | if (target) { 317 | mapper.graph.add({from: dragging, to: target}) 318 | dragging.move(clickPos) 319 | } 320 | 321 | mapper.altered = true 322 | dragging = null 323 | } else if (!collision) { 324 | selected = null 325 | mapper.graph.unfocus() 326 | mapper.dirty = true 327 | } 328 | 329 | redraw(mapper) 330 | } 331 | mapper.DOM.addEventListener('mouseup', dragEnd) 332 | mapper.DOM.addEventListener('touchend', dragEnd) 333 | 334 | 335 | // Close modal if click occurs outside text box 336 | window.addEventListener('click', (event) => { 337 | // if (mapper.inline) { 338 | // event.preventDefault(); 339 | // } 340 | if (mapper.editMode && event.target.id === 'reason-overlay') { 341 | removeOverlay(mapper) 342 | } 343 | }) 344 | 345 | 346 | window.addEventListener('keydown', (event) => { 347 | 348 | if (window.currentMapper !== mapper) { 349 | return; // Only respond if we are the last mapper to have a mouse move event. 350 | } 351 | 352 | if (mapper.editMode) { 353 | // Escape key 354 | if (Keycode.isEventKey(event, 'Escape')) removeOverlay(mapper) 355 | 356 | // Return key 357 | if (Keycode.isEventKey(event, 'Enter')) submitOverlay(mapper) 358 | 359 | } else { 360 | // this is a hack to get multiple presses working on windows 361 | // if removing it, ensure you remove it from the keyup event too 362 | if (isMetaKey(event)) metaKeyPressed = true 363 | 364 | // Focus on `Tab` 365 | if (!isMetaKey(event) && Keycode.isEventKey(event, 'tab')) { 366 | event.preventDefault() 367 | if (event.shiftKey) { 368 | mapper.graph.undoFocus() 369 | } else { 370 | selected = mapper.graph[0] 371 | mapper.graph.focus(selected) 372 | } 373 | console.log(mapper.graph.map(g => g.id)) 374 | mapper.dirty = true 375 | } 376 | 377 | 378 | // Undo `⌘-z` 379 | if (metaKeyPressed && Keycode.isEventKey(event, 'z')) { 380 | event.preventDefault() 381 | triggerUndo() 382 | } 383 | 384 | // Redo `⌘-y` 385 | if (metaKeyPressed && Keycode.isEventKey(event, 'y')) { 386 | event.preventDefault() 387 | triggerRedo() 388 | } 389 | 390 | // Edit selected element on `enter` 391 | if (selected && Keycode.isEventKey(event, 'Enter')) { 392 | addOverlay(mapper, selected) 393 | } 394 | 395 | // Delete a selected element on `backspace` or `delete` 396 | if (Keycode.isEventKey(event, 'Delete') || Keycode.isEventKey(event, 'Backspace')) { 397 | if (document.activeElement.tagName !== 'INPUT') { 398 | event.preventDefault() 399 | } 400 | 401 | if (selected) { 402 | deleteElement(mapper, selected) 403 | } 404 | } 405 | } 406 | 407 | redraw(mapper) 408 | }) 409 | 410 | window.addEventListener('keyup', (event) => { 411 | 412 | // remove the metakey flag 413 | if (isMetaKey(event)) metaKeyPressed = false 414 | }) 415 | 416 | 417 | window.addEventListener('resize', (event) => { 418 | mapper.dirty = true 419 | View.resize(mapper) 420 | View.zero(mapper) 421 | redraw(mapper) 422 | }) 423 | 424 | const zoomAction = (event) => { 425 | // need a hack for firefox event mismatch 426 | if ((event.target.id === mapper.DOM.firstElementChild.id) || 427 | (event.type === 'wheel' && event.target.id === mapper.DOM.id)) { 428 | 429 | event.preventDefault() 430 | 431 | mapper.dirty = true 432 | View.setScale(mapper, event.deltaY) 433 | View.zero(mapper) 434 | redraw(mapper) 435 | } else { 436 | if (mapper.inline && !event.metaKey) 437 | metaWarning(mapper) 438 | 439 | return 440 | } 441 | } 442 | window.addEventListener('wheel', zoomAction, { passive: false }) 443 | 444 | // Use _lastScale to help calculate the diff of the event's movement 445 | let _lastScale = 1 446 | 447 | hammer.on('pinch', (hammerEvent) => { 448 | if (mapper._isSwipping) { return } 449 | if (mapper.inline) { 450 | panMove(hammerEvent) 451 | } 452 | hammerEvent.preventDefault() 453 | let tmpScale = hammerEvent.scale - _lastScale 454 | 455 | mapper.dirty = true 456 | View.setScale(mapper, tmpScale * 1000, true) 457 | View.zero(mapper) 458 | redraw(mapper) 459 | _lastScale = hammerEvent.scale 460 | }) 461 | 462 | hammer.on('pinchend', () => { 463 | _lastScale = 1 464 | }) 465 | 466 | } 467 | 468 | let timeout 469 | 470 | function deleteElement(mapper, selected) { 471 | mapper.graph.remove(selected) 472 | mapper.dirty = true 473 | } 474 | 475 | function metaWarning(mapper) { 476 | // mapper.DOM.querySelector('') 477 | console.log("Please hold CMD while scrolling to zoom"); 478 | } 479 | 480 | function redraw(mapper) { 481 | if (mapper.altered || mapper.dirty) { 482 | // If there's a timer, cancel it 483 | if (timeout) { 484 | window.cancelAnimationFrame(timeout) 485 | } 486 | 487 | // Setup the new requestAnimationFrame() 488 | timeout = window.requestAnimationFrame(function () { 489 | _redraw(mapper) 490 | }) 491 | } 492 | } 493 | 494 | /** 495 | * Private: Redraws the canvas if changes have occured 496 | */ 497 | function _redraw (mapper) { 498 | if (mapper.altered || mapper.dirty) { 499 | if (mapper.altered) { 500 | save(mapper.History, mapper) 501 | mapper.Future.length = 0 // Reset the redo buffer 502 | } 503 | 504 | View.draw(mapper) 505 | mapper.altered = false 506 | mapper.dirty = false 507 | } 508 | } 509 | 510 | 511 | /** 512 | * Private: Saves a serialized copy of the graph 513 | */ 514 | function save (store, mapper) { 515 | const last = (store.length == 0) ? JSON.stringify([]) : store[store.length-1] 516 | const current = JSON.stringify( 517 | mapper.graph.map(function (element) { 518 | return element.export(mapper.offset) 519 | }) 520 | ) 521 | 522 | if (current !== last) store.push(current) 523 | } 524 | 525 | function changeLine(mapper, element, type) { 526 | element.lineType = type 527 | mapper.altered = true 528 | redraw(mapper) 529 | } 530 | 531 | /** 532 | * Private: Creates the html for the overlaytoolbar 533 | */ 534 | function toolbarNode(mapper, element) { 535 | const node = Utils.buildNode('div', {id: 'reasons-overlay-toolbar'}) 536 | node.setAttribute('style', 'display: flex; flex-direction: row;') 537 | node.appendChild(Utils.buildNode('div', { style: 'flex-grow: 1;' })) 538 | 539 | node.appendChild(toolButton({ 540 | name: '', 541 | onclick:() => changeLine(mapper, element, 'solid') 542 | })) 543 | node.appendChild(toolButton({ 544 | name: '- -', 545 | onclick:() => changeLine(mapper, element, 'dashed') 546 | })) 547 | node.appendChild(toolButton({ 548 | name: 'Delete', 549 | onclick: () => { 550 | if (confirm("Really remove this?")) { 551 | deleteElement(mapper, element) 552 | removeOverlay(mapper) 553 | redraw(mapper) 554 | } 555 | } 556 | })) 557 | node.appendChild(toolButton({ 558 | name: 'OK', 559 | onclick: () => { 560 | submitOverlay(mapper) 561 | redraw(mapper) 562 | } 563 | })) 564 | return node 565 | } 566 | 567 | function toolButton(opts) { 568 | const {name} = opts 569 | delete opts.name 570 | const button = Utils.buildNode('button', opts, {class: 'reason-overlay__button'}) 571 | button.innerHTML = name 572 | return button 573 | } 574 | 575 | /** 576 | * Private: Overlays a text box to edit a node or edge 577 | */ 578 | function addOverlay (mapper, element, highlightAll = false) { 579 | 580 | // set the 581 | mapper.editMode = true 582 | 583 | // Create background layer 584 | let overlay = Utils.buildNode('div', {id: 'reason-overlay'}) 585 | 586 | // create modal content wrapper 587 | const wrapper = Utils.buildNode('div', {id: 'reason-overlay__wrapper'}) 588 | 589 | // Create text input field 590 | let input = Utils.buildNode('textarea', {id: 'edit-reason-input', value: element.text || element.type}) 591 | input.setAttribute('data-element', element.id) 592 | 593 | // Append to the DOM 594 | overlay.appendChild(wrapper) 595 | wrapper.appendChild(input) 596 | wrapper.appendChild(toolbarNode(mapper, element)) 597 | document.body.appendChild(overlay) 598 | document.body.classList.add('modal') 599 | 600 | // Highlight text on element creation 601 | if (highlightAll) { 602 | input.select() 603 | input.setSelectionRange(0, input.value.length) 604 | } 605 | input.scrollIntoView() 606 | } 607 | 608 | 609 | /** 610 | * Private: Updates the graph from the overlay and removes it 611 | */ 612 | function submitOverlay (mapper) { 613 | let input = document.querySelector('#edit-reason-input') 614 | let el = mapper.graph.elements().find(el => el.id == input.getAttribute('data-element') ) 615 | 616 | if (el.isNode()) { 617 | el.text = input.value 618 | } else { 619 | el.type = input.value 620 | } 621 | removeOverlay(mapper) 622 | } 623 | 624 | 625 | /** 626 | * Private: Removes the overlay 627 | */ 628 | function removeOverlay (argumentMap) { 629 | argumentMap.editMode = false 630 | argumentMap.altered = true 631 | document.querySelector('#reason-overlay').remove() 632 | document.body.classList.remove('modal') 633 | } 634 | 635 | function isMetaKey (event) { 636 | return ( 637 | event.metaKey || 638 | Keycode.isEventKey(event, 'Alt') || 639 | Keycode.isEventKey(event, 'Meta') || 640 | Keycode.isEventKey(event, 'Command') || 641 | Keycode.isEventKey(event, 'Control') || 642 | Keycode.isEventKey(event, 'Win') || 643 | Keycode.isEventKey(event, 'ControlLeft') || 644 | Keycode.isEventKey(event, 'ControlRight') 645 | ) ? true : false 646 | } -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // Reasons.js by Dave Kinkead 2 | // Copyright 2017-2019 University of Queensland 3 | // Available under the MIT license 4 | 5 | module.exports = { 6 | 7 | // build a DOM element 8 | buildNode: function (type, options, attributes) { 9 | const node = document.createElement(type) 10 | for (var key in options) { 11 | node[key] = options[key] 12 | } 13 | for (var key in attributes) { 14 | node.setAttribute(key, attributes[key]) 15 | } 16 | return node 17 | }, 18 | 19 | intersection: function (array1, array2) { 20 | return array1.filter(function(n) { 21 | return array2.indexOf(n) !== -1; 22 | }) 23 | }, 24 | 25 | unique: require('array-unique'), 26 | flatten: require('array-flatten'), 27 | diff: require('array-difference') 28 | } -------------------------------------------------------------------------------- /lib/view.js: -------------------------------------------------------------------------------- 1 | // Reasons.js by Dave Kinkead 2 | // Copyright 2017-2019 University of Queensland 3 | // Available under the MIT license 4 | 5 | 'use strict' 6 | 7 | const Element = require('./Element') 8 | const Utils = require('./utils') 9 | 10 | // Display Settings 11 | const maxWidth = 200 12 | const padding = 10 13 | const fontSize = 16 14 | const cornerRadius = 4 15 | const rgbFocused = '81,36,122' 16 | const rgbDefault = '0,0,0' 17 | 18 | let dpr = 1 19 | let graph = {} 20 | 21 | function getLocal(point) { 22 | return point //* dpr 23 | } 24 | 25 | function getGlobal(point) { 26 | return point / dpr 27 | } 28 | 29 | /** 30 | * Singleton View module to render a canvas. 31 | */ 32 | module.exports = (function () { 33 | 34 | /** 35 | * Initialise the view for this mapper map instance 36 | * by appending a HTML canvas element. 37 | * 38 | * @params mapper The mapper map to provide a view for 39 | */ 40 | function init (mapper) { 41 | dpr = window.devicePixelRatio || 1 42 | mapper.scale = 1 43 | mapper.offset = { x: 0, y: 0 } 44 | 45 | mapper.inline = mapper.DOM.getAttribute('data-reasons-layout') == 'inline' 46 | 47 | let domBB = mapper.DOM.getBoundingClientRect() 48 | let canvas = Utils.buildNode( 49 | 'canvas', 50 | {id: 'reasons-'+mapper.DOM.id}, 51 | {width: domBB.width, height: domBB.height || window.innerHeight } 52 | ) 53 | mapper.context = canvas.getContext('2d', {alpha: true}) 54 | 55 | mapper.DOM.style['min-height'] = "100px"; 56 | mapper.DOM.style['min-width'] = "100px"; 57 | mapper.DOM.appendChild(canvas) 58 | 59 | resize(mapper) 60 | } 61 | 62 | 63 | /** 64 | * Render an mapper map instance 65 | */ 66 | function draw (mapper) { 67 | clear(mapper) 68 | 69 | // draw edges before nodes 70 | graph = mapper.graph 71 | graph.edges().forEach(el => draw_edge(el, mapper)) 72 | graph.nodes().forEach(el => draw_node(el, mapper)) 73 | } 74 | 75 | function zero (mapper) { 76 | // find a bounded box of nodes and DOM 77 | let nodeBB = mapper.graph.nodes().map((node) => { 78 | return { x1: node.x1, x2: node.x2, y1: node.y1, y2: node.y2 } 79 | }).reduce( (acc, cur) => { 80 | return { 81 | x1: Math.min(acc.x1, cur.x1), 82 | x2: Math.max(acc.x2, cur.x2), 83 | y1: Math.min(acc.y1, cur.y1), 84 | y2: Math.max(acc.y2, cur.y2), 85 | } 86 | }) 87 | 88 | let mid = { 89 | x: ((mapper.DOM.clientWidth-mapper.DOM.clientLeft)/2/mapper.scale + mapper.DOM.clientLeft) 90 | - ((nodeBB.x2-nodeBB.x1)/2 + nodeBB.x1), 91 | y: ((mapper.DOM.clientHeight-mapper.DOM.clientTop)/2/mapper.scale + mapper.DOM.clientTop) 92 | - ((nodeBB.y2-nodeBB.y1)/2 + nodeBB.y1) 93 | } 94 | 95 | // translate node position to centre of DOM 96 | mapper.graph.nodes().forEach((node) => { 97 | node.x1 += mid.x 98 | node.x2 += mid.x 99 | node.y1 += mid.y 100 | node.y2 += mid.y 101 | }) 102 | } 103 | 104 | function resize (mapper) { 105 | 106 | mapper.DOM.width = (mapper.DOM.clientWidth - mapper.DOM.clientLeft) 107 | mapper.DOM.height = (mapper.DOM.clientHeight - mapper.DOM.clientTop) 108 | const canvas = mapper.DOM.querySelector('canvas') 109 | canvas.width = mapper.DOM.width * dpr 110 | canvas.height = mapper.DOM.height * dpr 111 | 112 | mapper.DOM.style.overflow = 'hidden' 113 | canvas.style['transform-origin'] = "top left" 114 | canvas.style.transform = 'scale(' + 1/dpr + ')' 115 | mapper.context.scale(dpr * mapper.scale, dpr * mapper.scale) 116 | } 117 | 118 | function setScale(mapper, newScale, transform=true) { 119 | const relativeScale = transform ? (1+ newScale/1000) : (newScale) 120 | // mapper.scale = Math.max(mapper.scale * relativeScale, 3) 121 | const updatedScale = mapper.scale * relativeScale 122 | if (updatedScale < 10 && updatedScale > 0.4) { 123 | mapper.scale = mapper.scale * relativeScale 124 | mapper.context.scale(relativeScale, relativeScale) 125 | } 126 | } 127 | 128 | return { 129 | init, 130 | draw, 131 | zero, 132 | setScale, 133 | resize, 134 | } 135 | })() 136 | 137 | 138 | /** 139 | * Private: Clear the canvas before drawing 140 | */ 141 | function clear (mapper) { 142 | let domBB = mapper.DOM.getBoundingClientRect() 143 | mapper.context.clearRect(0, 0, domBB.width / mapper.scale, domBB.height / mapper.scale) 144 | } 145 | 146 | 147 | /** 148 | * Private: Draws a node on the canvas 149 | */ 150 | function draw_node (node, {context, offset}) { 151 | // Set font size before calculating text widths 152 | context.font = fontSize + 'px sans-serif' 153 | 154 | // word wrap the text 155 | const text = wordWrap(node.text, context) 156 | const rgb = (node.hovering) ? rgbFocused : rgbDefault 157 | const opacity = (node.focused) ? 0.9 : (node.hovering) ? 0.75 : 0.5 158 | const ox = offset.x 159 | const oy = offset.y 160 | 161 | // recalculate the height with extra padding when multi-line 162 | node.height = (text.length * fontSize * 1.22) + fontSize * ((text.length > 1 ) ? 2 : 1.75) 163 | resizeNode(node) 164 | 165 | // clear a white rectangle for background 166 | context.clearRect(node.x1+ox, node.y1+oy, node.width, node.height) 167 | 168 | context.strokeStyle = 'rgba('+rgb+','+opacity+')' 169 | context.lineJoin = "round" 170 | context.lineWidth = cornerRadius 171 | if (node.lineType == 'dashed') { 172 | context.setLineDash([10, 10]) 173 | context.lineWidth *= 0.75 174 | } 175 | context.strokeRect( 176 | node.x1+cornerRadius/2+ox, node.y1+cornerRadius/2+oy, 177 | node.width-cornerRadius, node.height-cornerRadius 178 | ) 179 | context.setLineDash([]) 180 | 181 | // set text box styles 182 | context.fillStyle = 'rgba('+rgb+',0.8)' 183 | context.textAlign = 'center' 184 | 185 | const lineHeight = fontSize * 1.25; 186 | const textX = node.x1 + ox + node.width/2 187 | const textY = node.y1 + oy + cornerRadius * 2 188 | 189 | text.forEach((line, i) => { 190 | context.fillText(line, textX, textY + ((i+1) * lineHeight), node.width) 191 | }) 192 | } 193 | 194 | 195 | /** 196 | * Private: Draws an edge on the canvas 197 | */ 198 | function draw_edge (edge, {context, offset}) { 199 | locate(edge) 200 | const ox = offset.x 201 | const oy = offset.y 202 | 203 | // stroke style 204 | const rgb = (edge.hovering) ? rgbFocused : rgbDefault 205 | const opacity = (edge.focused) ? 0.9 : (edge.hovering) ? 0.75 : 0.5 206 | context.strokeStyle = 'rgba('+rgb+','+opacity+')' 207 | context.lineWidth = 4 208 | 209 | // stroke position 210 | context.beginPath() 211 | edge.paths.forEach((path) => { 212 | context.moveTo(path.x1+ox, path.y1+oy) 213 | context.lineTo(path.x2+ox, path.y2+oy) 214 | }) 215 | 216 | // arrow tip 217 | let last = edge.paths[edge.paths.length-1] 218 | let arrow = arrowify(last) 219 | context.lineTo(arrow.x1+ox, arrow.y1+oy) 220 | context.moveTo(last.x2+ox, last.y2+oy) 221 | context.lineTo(arrow.x2+ox, arrow.y2+oy) 222 | context.stroke() 223 | 224 | // text stroke 225 | let textWidth = context.measureText(edge.type).width + padding 226 | context.clearRect(edge.center.x+ox-textWidth/2, edge.center.y+oy-15, textWidth, 25) 227 | 228 | // label 229 | context.fillStyle = 'rgba('+rgb+',0.8)' 230 | context.font = 14 + 'px sans-serif' 231 | context.textAlign = 'center' 232 | context.fillText(edge.type, edge.center.x+ox, edge.center.y+oy) 233 | 234 | if (edge.intersection) 235 | context.fillRect(edge.intersection.x+ox, edge.intersection.y+oy, 10, 10) 236 | } 237 | 238 | 239 | /** 240 | * Private: Returns a list of `paths` between nodes for this relation 241 | * Requires reference to @graph from outside of function 242 | */ 243 | function locate (edge) { 244 | 245 | // collect all the nodes involved 246 | let ids = Utils.flatten([edge.from, edge.to]) 247 | let elements = graph.filter((el) => { 248 | return ids.includes(el.id) 249 | }) 250 | 251 | // find the mid point between the connected nodes 252 | let coords = elements.map((el) => { 253 | return {x: getLocal((el.x1+(el.width)/2)), y: getLocal((el.y1+(el.height )/2))} 254 | }) 255 | 256 | let xs = coords.map(el => el.x) 257 | let ys = coords.map(el => el.y) 258 | edge.center = { 259 | x: getLocal(Math.max(...xs) - (Math.max(...xs) - Math.min(...xs)) / 2), 260 | y: getLocal(Math.max(...ys) - (Math.max(...ys) - Math.min(...ys)) / 2) 261 | } 262 | 263 | // find the weighted center point of those nodes 264 | // edge.center = elements.map((el) => { 265 | // return {x: (el.x1+(el.width)/2), y: (el.y1+(el.height )/2)} 266 | // }).reduce((acc, el) => { 267 | // return {x: acc.x + el.x, y: acc.y + el.y} 268 | // }) 269 | // edge.center.x = edge.center.x/(elements.length) 270 | // edge.center.y = edge.center.y/(elements.length) 271 | 272 | // create pairs from from-points to center to to-point 273 | edge.paths = edge.from.map((node) => { 274 | let el = elements.find(e => e.id == node) 275 | return { 276 | x1: el.x1+(el.x2-el.x1)/2, 277 | y1: el.y1+(el.y2-el.y1)/2, 278 | x2: edge.center.x, 279 | y2: edge.center.y 280 | } 281 | }) 282 | 283 | // move the 'to' point back down the path to just outside the node. 284 | let to = elements.find(e => e.id == edge.to) 285 | let offset = pointOfIntersection(edge.center, to, 5) 286 | 287 | // get offset x,y from rectangle intersect 288 | edge.paths.push({ 289 | x1: edge.center.x, 290 | y1: edge.center.y, 291 | x2: (to.x1 + (to.x2 - to.x1)/2) - offset.x, 292 | y2: (to.y1 + (to.y2 - to.y1)/2) + offset.y 293 | }) 294 | } 295 | 296 | 297 | function resizeNode (node) { 298 | node.x2 = node.x1 + node.width 299 | node.y2 = node.y1 + node.height 300 | } 301 | 302 | 303 | function wordWrap(text, context) { 304 | let words = text.split(' ') 305 | let lines = [] 306 | let line = '' 307 | 308 | words.forEach((word) => { 309 | let width = context.measureText(line + ' ' + word).width 310 | 311 | if (width < (maxWidth - padding * 2) ) { 312 | line += ' ' + word 313 | } else { 314 | lines.push(line) 315 | line = word 316 | } 317 | }) 318 | 319 | lines.push(line) 320 | return lines 321 | } 322 | 323 | // Helper function to make arrow tips 324 | function arrowify(path) { 325 | let angle = Math.atan2(path.y1-path.y2, path.x1-path.x2) 326 | return { 327 | x1: path.x2 + 10*Math.cos(angle+0.5), 328 | y1: path.y2 + 10*Math.sin(angle+0.5), 329 | x2: path.x2 + 10*Math.cos(angle-0.5), 330 | y2: path.y2 + 10*Math.sin(angle-0.5) 331 | } 332 | } 333 | 334 | // determines the intersection x,y from a point to center of rectangle 335 | function pointOfIntersection (from, rect, buffer) { 336 | let center = {x: rect.x1 + rect.width/2, y: rect.y1 + rect.height/2} 337 | 338 | // determine the angle of the path 339 | let angle = Math.atan2(from.y - center.y, center.x - from.x) 340 | let absCos = Math.abs(Math.cos(angle)) 341 | let absSin = Math.abs(Math.sin(angle)) 342 | 343 | let distance = (rect.width/2*absSin <= rect.height/2*absCos) ? rect.width/2/absCos : rect.height/2/absSin 344 | distance += buffer || 0 345 | 346 | return {x: distance * Math.cos(angle), y: distance * Math.sin(angle)} 347 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reasons", 3 | "version": "1.1.5", 4 | "description": "Argument Mapping in your browser", 5 | "main": "reasons.js", 6 | "dependencies": { 7 | "array-difference": "0.0.2", 8 | "array-flatten": "^2.1.2", 9 | "array-intersection": "^0.1.2", 10 | "array-unique": "^0.3.2", 11 | "hammerjs": "^2.0.8", 12 | "keycode": "^2.2.0" 13 | }, 14 | "devDependencies": { 15 | "browserify": "16.5.1", 16 | "canvas": "2.8.0", 17 | "jsdom": "11.0.0", 18 | "mocha": "8.3.0", 19 | "npm": "7.20.6", 20 | "request": "2.88.2", 21 | "should": "^11.2.1", 22 | "uglify-es": "^3.3.9", 23 | "yamljs": "^0.2.10" 24 | }, 25 | "scripts": { 26 | "build": "browserify lib/reasons.js -s Reasons -o reasons.js", 27 | "deploy": "./deploy.sh", 28 | "minify": "npm run build && uglifyjs reasons.js -o reasons.min.js", 29 | "test": "npm run build && mocha" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/davekinkead/reasons.git" 34 | }, 35 | "keywords": [ 36 | "argument mapping", 37 | "logic", 38 | "philosophy" 39 | ], 40 | "author": "Dave Kinkead", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/davekinkead/reasons/issues" 44 | }, 45 | "homepage": "https://github.com/davekinkead/reasons#readme" 46 | } 47 | -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @book{arum2011academically, 2 | title={Academically adrift: Limited learning on college campuses}, 3 | author={Arum, Richard and Roksa, Josipa}, 4 | year={2011}, 5 | publisher={University of Chicago Press}, 6 | doi={10.7208/chicago/9780226028576.001.0001} 7 | } 8 | 9 | @article{butchart2009improving, 10 | title={Improving critical thinking using web based argument mapping exercises with automated feedback}, 11 | author={Butchart, Sam and Forster, Daniella and Gold, Ian and Bigelow, John and Korb, Kevin and Oppy, Graham and Serrenti, Alexandra}, 12 | journal={Australasian Journal of Educational Technology}, 13 | volume={25}, 14 | number={2}, 15 | year={2009}, 16 | doi={10.14742/ajet.1154} 17 | } 18 | 19 | @article{davies2009computer, 20 | title={Computer-assisted argument mapping: a rationale approach}, 21 | author={Davies, W Martin}, 22 | journal={Higher Education}, 23 | volume={58}, 24 | number={6}, 25 | pages={799}, 26 | year={2009}, 27 | publisher={Springer}, 28 | doi={10.1007/s10734-009-9226-9} 29 | 30 | } 31 | 32 | @article{dwyer2012, 33 | title={An evaluation of argument mapping as a method of enhancing critical thinking performance in e-learning environments}, 34 | author={Dwyer, Christopher P and Hogan, Michael J and Stewart, Ian}, 35 | journal={Metacognition and Learning}, 36 | volume={7}, 37 | number={3}, 38 | pages={219--244}, 39 | year={2012}, 40 | publisher={Springer}, 41 | doi={10.1007/s11409-012-9092-1} 42 | } 43 | 44 | @article{govier1992good, 45 | title={What is a good argument?}, 46 | author={Govier, Trudy}, 47 | journal={Metaphilosophy}, 48 | volume={23}, 49 | number={4}, 50 | pages={393--409}, 51 | year={1992}, 52 | publisher={Wiley Online Library}, 53 | doi={10.1111/j.1467-9973.1992.tb00551.x} 54 | } 55 | 56 | @article{harrell2004improvement, 57 | title={The improvement of critical thinking skills in What Philosophy Is}, 58 | author={Harrell, Maralee}, 59 | journal={Carnegie Mellon University. Retrieved May}, 60 | volume={9}, 61 | pages={2007}, 62 | year={2004}, 63 | url={https://www.cmu.edu/dietrich/philosophy/docs/harrell/Improving_Critical_Thinking_Skills.pdf} 64 | } 65 | 66 | @article{mulnix2012thinking, 67 | title={Thinking critically about critical thinking}, 68 | author={Mulnix, Jennifer Wilson}, 69 | journal={Educational Philosophy and Theory}, 70 | volume={44}, 71 | number={5}, 72 | pages={464--479}, 73 | year={2012}, 74 | publisher={Taylor \& Francis}, 75 | doi={10.1111/j.1469-5812.2010.00673.x} 76 | } 77 | 78 | @incollection{toulmin2003uses, 79 | doi = {10.1017/cbo9780511840005.005}, 80 | url = {https://doi.org/10.1017%2Fcbo9780511840005.005}, 81 | publisher = {Cambridge University Press}, 82 | pages = {11--40}, 83 | year = {1958}, 84 | author = {Stephen E. Toulmin}, 85 | title = {Fields of Argument and Modals}, 86 | booktitle = {The Uses of Argument} 87 | } 88 | 89 | @article{twardy2004argument, 90 | title={Argument maps improve critical thinking}, 91 | author={Twardy, Charles}, 92 | journal={Teaching Philosophy}, 93 | volume={27}, 94 | number={2}, 95 | pages={95--116}, 96 | doi = {10.5840/teachphil200427213}, 97 | year={2004} 98 | } 99 | 100 | @article{van2002argument, 101 | title={Argument mapping with reason! able}, 102 | author={Van Gelder, Tim}, 103 | journal={The American Philosophical Association Newsletter on Philosophy and Computers}, 104 | volume={2}, 105 | number={1}, 106 | pages={85--90}, 107 | url={https://sites.google.com/site/timvangelder/publications-1/argument-mapping-with-reason-able/ArgumentMappingwithReasonAble-APANewsletter.pdf}, 108 | year={2002} 109 | } 110 | 111 | @article{vangelder_2005, 112 | doi = {10.3200/ctch.53.1.41-48}, 113 | url = {https://doi.org/10.3200%2Fctch.53.1.41-48}, 114 | year = 2005, 115 | month = {jan}, 116 | publisher = {Informa {UK} Limited}, 117 | volume = {53}, 118 | number = {1}, 119 | pages = {41--48}, 120 | author={Van Gelder, Tim}, 121 | title = {Teaching Critical Thinking: Some Lessons From Cognitive Science}, 122 | journal = {College Teaching} 123 | } 124 | @book{wigmore1913principles, 125 | title={The principles of judicial proof: as given by logic, psychology, and general experience, and illustrated in judicial trials}, 126 | author={Wigmore, John Henry}, 127 | volume={1}, 128 | year={1913}, 129 | publisher={Little, Brown,}, 130 | url={https://archive.org/details/principlesofjudi00wigm/} 131 | } -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Reasons: A digital argument mapping library for modern browsers' 3 | tags: [argument mapping, critical thinking, pedagogy] 4 | authors: 5 | - name: Dave Kinkead 6 | email: d.kinkead@uq.edu.au 7 | orcid: 0000-0001-5396-8099 8 | affiliation: 1 9 | - name: Deborah Brown 10 | email: deborah.brown@uq.edu.au 11 | orcid: 0000-0001-5707-7605 12 | affiliation: 1 13 | - name: Peter Ellerton 14 | email: peter.ellerton@uq.edu.au 15 | orcid: 0000-0002-6588-376X 16 | affiliation: 1 17 | - name: Claudio Mazzola 18 | email: c.mazzola@uq.edu.au 19 | orcid: 0000-0001-6117-7465 20 | affiliation: 1 21 | affiliations: 22 | - name: University of Queensland Critical Thinking Project 23 | index: 1 24 | date: 25 September 2018 25 | bibliography: paper.bib 26 | --- 27 | 28 | # Summary 29 | 30 | There is growing recognition globally of the need to teach Critical Thinking as part of formal schooling and of its importance to the “knowledge economies” of the future. Yet international research demonstrates that without explicit instruction in critical thinking, undergraduate education often results in little to no gains in critical thinking, analytic reasoning, and other "higher level" skills [@harrell2004improvement, @arum2011academically]. 31 | 32 | One very effective way to improving critical thinking is through argument mapping — the visual representation of an argument’s logical structure. Argument mapping in paper form is common in philosophy courses and has a pedagogical pedigree that can be traced back to Wigmore [-@wigmore1913principles], Toulmin [-@toulmin2003uses], and Govier [-@govier1992good]. Argument mapping can improve critical thinking skills by offering students an opportunity to engage in _metacogntive evaluation_ — evaluating the quality of their own, and others', reasoning. 33 | 34 | Digital argument mapping as an educational tool has been validated by van Gelder [-@van2002argument], Butchart et al [-@butchart2009improving], and Mulnix [-@mulnix2012thinking]. Dwyer, Hogan, & Stewart [-@dwyer2012] demonstrated that argument mapping improves concept recall compared with textual analysis; Twardy [-@twardy2004argument p2] that it produces cognitive gains three times that of other methods; and van Gelder [-@vangelder_2005 p45] that the cognitive gains from one semester of explicit argument mapping are equivalent to that of an entire undergraduate degree. 35 | 36 | Unfortunately, argument mapping is rarely used outside of philosophy classes owing either to a lack of instructor expertise or availability of tools appropriate to non-philosophical pedagogies. Current digital argument mapping tools are either desktop software, limiting their ability to be integrated into online courseware, or propriety and tighly coupled, limiting their access and extensibility. 37 | 38 | `Reasons` seeks to bridge this gap by offering an open-source, loosely-coupled, web-based argument mapping library that can be integrated into a range of online coursewares and websites. The javascript library can be embedded into any HTML page and allows users to create, edit, share, and export argument maps (see https://reasons.io for an example). The API is designed to permit the integration of the three stages of informal logical analysis — identification of truth claims within arguments, the analysis of logical structure, and synthesis of logcial structure into writen form. 39 | 40 | Development has been funded by a University of Queensland Teaching Innovation Grant and the software forms a key component of the UQ Critical Thinking Project's research program into digital and critical thinking pedagogies. The intended audience for this software includes education researchers and practitions in secondary and higher education. 41 | 42 | 43 | # References 44 | -------------------------------------------------------------------------------- /test/animate-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |
13 | 14 | 15 | 258 | 259 | -------------------------------------------------------------------------------- /test/big-text-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 37 | 38 | -------------------------------------------------------------------------------- /test/dual-maps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | 19 |

I was going to say something extremely rough to Lorem Ipsum, to its family, and I said to myself, "I can't do it. I just can't do it. It's inappropriate. It's not nice." I think the only card she has is the Lorem card.

20 | 21 |

He’s not a word hero. He’s a word hero because he was captured. I like text that wasn’t captured. Lorem Ispum is a choke artist. It chokes!

22 | 23 |

24 | 25 |
26 | 27 |

I know words. I have the best words. I don't think anybody knows it was Russia that wrote Lorem Ipsum, but I don't know, maybe it was. It could be Russia, but it could also be China. It could also be lots of other people. It also could be some wordsmith sitting on their bed that weights 400 pounds. Ok? Some people have an ability to write placeholder text... It's an art you're basically born with. You either have it or you don't. You have so many different things placeholder text has to be able to do, and I don't believe Lorem Ipsum has the stamina.

28 | 29 |

I will write some great placeholder text – and nobody writes better placeholder text than me, believe me – and I’ll write it very inexpensively. I will write some great, great text on your website’s Southern border, and I will make Google pay for that text. Mark my words. Despite the constant negative ipsum covfefe. Lorem Ipsum is FAKE TEXT! My text is long and beautiful, as, it has been well documented, are various other parts of my website. Look at these words. Are they small words? And he referred to my words - if they're small, something else must be small. I guarantee you there's no problem, I guarantee.

30 | 31 |
32 | 33 | 34 |

Lorem Ipsum's father was with Lee Harvey Oswald prior to Oswald's being, you know, shot. Does everybody know that pig named Lorem Ipsum? She's a disgusting pig, right? My text is long and beautiful, as, it has been well documented, are various other parts of my website. I think the only difference between me and the other placeholder text is that I’m more honest and my words are more beautiful.

35 | 36 |

You’re disgusting. I write the best placeholder text, and I'm the biggest developer on the web by far... While that's mock-ups and this is politics, are they really so different? We are going to make placeholder text great again. Greater than ever before.

37 | 38 | 39 | 51 | 52 | -------------------------------------------------------------------------------- /test/element.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | const Element = require('./../lib/element') 5 | 6 | const reasons = { 7 | a: {id: 'a', text: 'blah blah A', x: 0, y: 0}, 8 | b: {id: 'b', text: 'blah blah B', x: 100, y: 50, lineType: 'dashed'}, 9 | c: {id: 'c', text: 'blah blah C', x: 500, y: 500}, 10 | ab: {from: 'a', to: 'b', type: 'a relation'} 11 | } 12 | 13 | describe('Element', () => { 14 | describe('#mixin', () => { 15 | it('should compose behaviour into a plain JS Object', () => { 16 | Element.mixin(reasons.a) 17 | reasons.a.isNode().should.equal(true) 18 | }) 19 | 20 | it('should return the element it composes', () => { 21 | Element.mixin(reasons.a).should.equal(reasons.a) 22 | }) 23 | }) 24 | 25 | describe('#init', () => { 26 | it('should create a new node', () => { 27 | Element.mixin(reasons.a).isNode().should.equal(true) 28 | }) 29 | 30 | it('should create a new edge', () => { 31 | Element.mixin(reasons.ab).isEdge().should.be.equal(true) 32 | }) 33 | 34 | it('should expose an edges to: property as a string id', () => { 35 | Element.mixin(reasons.ab) 36 | reasons.ab.to.should.equal('b') 37 | 38 | const el = Element.mixin({from: reasons.a, to: reasons.b}) 39 | el.to.should.equal('b') 40 | }) 41 | 42 | it('should expose an edges from: property as an array of string ids', () => { 43 | const el = Element.mixin({from: [reasons.a, reasons.b], to: reasons.c}) 44 | el.from.should.haveTheSameItemsAs(['a', 'b']) 45 | }) 46 | 47 | it('should transform an edge from: id to from: [id]', () => { 48 | Element.mixin({from: 'a', to: 'b'}).from.should.haveTheSameItemsAs(['a']) 49 | }) 50 | 51 | it('should generate a default ID if none are supplied', () => { 52 | Element.mixin({text: 'no id'}).id.should.be.instanceOf(String) 53 | }) 54 | 55 | it('should use the existing ID if supplied',() => { 56 | Element.mixin(reasons.c).id.should.equal(reasons.c.id) 57 | }) 58 | 59 | it('should create a default type and id for edges', () => { 60 | const el = Element.mixin({from: reasons.a, to: reasons.b}) 61 | el.isEdge().should.equal(true) 62 | el.type.should.equal('supports') 63 | }) 64 | 65 | it('should create a new relation from objects', () => { 66 | Element.mixin({from: reasons.a, to: reasons.c}).isEdge().should.equal(true) 67 | }) 68 | }) 69 | 70 | describe('#save', () => { 71 | it('should export a node as {id, text, x, y} in JSON', () => { 72 | const el = Element.mixin(reasons.b) 73 | const json = JSON.parse(JSON.stringify(el.export())) 74 | json.should.be.instanceof(Object) 75 | json.id.should.equal(reasons.b.id) 76 | json.text.should.equal(reasons.b.text) 77 | json.x.should.equal(reasons.b.x) 78 | json.y.should.equal(reasons.b.y) 79 | json.lineType.should.equal('dashed') 80 | }) 81 | 82 | it('should default linetypes to solid', () => { 83 | const el = Element.mixin(reasons.c) 84 | const json = JSON.parse(JSON.stringify(el.export())) 85 | json.lineType.should.equal('solid') 86 | }) 87 | 88 | it('should export an edge as {id:, type:, from:, to:} in JSON', () => { 89 | const el = Element.mixin(reasons.ab) 90 | const json = JSON.parse(JSON.stringify(el.export())) 91 | json.id.should.be.instanceof(String) 92 | json.type.should.equal(reasons.ab.type) 93 | }) 94 | 95 | it('should export an edge from: and to: as id strings', () => { 96 | const el = Element.mixin(reasons.ab) 97 | const json = JSON.parse(JSON.stringify(el.export())) 98 | json.from[0].should.equal(reasons.a.id) 99 | json.to.should.equal(reasons.b.id) 100 | }) 101 | 102 | it('should export conjoined edges as arrays of ids', () => { 103 | const el = Element.mixin({from: [reasons.a, reasons.b], to: reasons.c}) 104 | el.export().from.should.be.instanceof(Array) 105 | el.export().from.should.haveTheSameItemsAs([reasons.a.id, reasons.b.id]) 106 | }) 107 | }) 108 | 109 | describe('#isNode', () => { 110 | it('should return true for nodes', () => { 111 | const el = Element.mixin({text: 'balh, blah'}) 112 | el.isNode().should.be.true() 113 | }) 114 | 115 | it('should return false for edges', () => { 116 | const el = Element.mixin({from: 'a', to: 'b'}) 117 | el.isNode().should.be.false() 118 | }) 119 | }) 120 | 121 | describe('#isEdge', () => { 122 | it('should return true for edges', () => { 123 | const el = Element.mixin({text: 'balh, blah'}) 124 | el.isEdge().should.be.false() 125 | }) 126 | 127 | it('should return false for nodes', () => { 128 | const el = Element.mixin({from: 'a', to: 'b'}) 129 | el.isEdge().should.be.true() 130 | }) 131 | }) 132 | 133 | describe('#collides', () => { 134 | it('returns true when x,y are inside a nodes boundaries', () => { 135 | const el = Element.mixin(reasons.b) 136 | el.collides({x: el.x, y: el.y}).should.be.true() 137 | }) 138 | 139 | it('returns false when x,y are outside a nodes boundaries', () => { 140 | const el = Element.mixin(reasons.b) 141 | el.collides({x: (el.x + el.width), y: (el.y + el.height)}).should.be.false() 142 | }) 143 | 144 | it('returns true when any part of a node is inside another nodes boundaries', () => { 145 | const el = Element.mixin({x:50, y: 50}) 146 | el.collides(reasons.a).should.be.true() 147 | }) 148 | 149 | it('returns false when all parts of a node are outside another nodes boundaries', () => { 150 | const el = Element.mixin({x:50, y: 50}) 151 | el.collides(reasons.c).should.be.false() 152 | }) 153 | 154 | it('returns true when x,y are inside an edges boundaries', () => { 155 | 156 | }) 157 | 158 | it('returns false when x,y are outside an edges boundaries', () => { 159 | 160 | }) 161 | }) 162 | 163 | describe('#move', () => { 164 | it('should move node coordinates by the corresponding amount', () => { 165 | const el = Element.mixin({}) 166 | el.move({x: 100, y: 100}) 167 | el.x1.should.be.equal(100 - el.width/2) 168 | el.y2.should.be.equal(100 + el.height/2) 169 | }) 170 | }) 171 | }) -------------------------------------------------------------------------------- /test/graph.js: -------------------------------------------------------------------------------- 1 | const should = require('./should') 2 | const Graph = require('./../lib/graph') 3 | 4 | const map = { 5 | a: {id: 'a', text: 'blah blah a'}, 6 | b: {id: 'b', text: 'blah blah b'}, 7 | c: {id: 'c', text: 'blah blah c'}, 8 | d: {id: 'd', text: 'blah blah d'}, 9 | ab: {id: 'ab', from: 'a', to: 'b'}, 10 | ac: {id: 'ac', from: 'a', to: 'c'}, 11 | bc: {id: 'bc', from: 'b', to: 'c'}, 12 | ca: {id: 'ca', from: 'c', to: 'a'}, 13 | cd: {id: 'cd', from: 'c', to: 'd'}, 14 | abc: {id: 'abc', from: ['a', 'b'], to: 'c'} 15 | } 16 | 17 | describe('Graph', () => { 18 | describe('#new', () => { 19 | it('should accept no arguments', () => { 20 | new Graph() 21 | }) 22 | 23 | it('should accept an array of objects', () => { 24 | new Graph([map.a, map.b]) 25 | }) 26 | 27 | it('should be an instance of Array', () => { 28 | new Graph([map.a, map.b]).should.be.instanceof(Array) 29 | }) 30 | 31 | it('should add nodes before edges', () => { 32 | const graph = new Graph([map.ac, map.a, map.c]) 33 | graph.elements()[2].should.equal(map.ac) 34 | }) 35 | }) 36 | 37 | describe('#add', () => { 38 | it('should add a new node to the graph', () => { 39 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 40 | graph.nodes().length.should.equal(3) 41 | }) 42 | 43 | it('should add a new edge to the graph', () => { 44 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 45 | graph.add(map.cd) 46 | graph.edges().length.should.equal(3) 47 | }) 48 | 49 | it('should add trianglar edge to the graph as 3 edges', () => { 50 | const graph = new Graph([map.a, map.b, map.c]) 51 | graph.add(map.ab) 52 | graph.add(map.bc) 53 | graph.add(map.ca) 54 | graph.edges().should.have.length(3) 55 | }) 56 | 57 | it('should add conjoined edge to the graph when new edges share the same child', () => { 58 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 59 | graph.edges().length.should.equal(2) 60 | graph.add({from: map.a, to: map.b}) 61 | graph.edges().length.should.equal(1) 62 | graph.edges()[0].from.should.haveTheSameItemsAs(['a', 'b']) 63 | }) 64 | 65 | it('should conjoin shared edge to the graph from ids', () => { 66 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 67 | graph.add({from: map.a.id, to: map.b.id}) 68 | graph.edges().length.should.equal(1) 69 | graph.edges()[0].from.should.haveTheSameItemsAs([map.a.id, map.b.id]) 70 | }) 71 | 72 | it('should prevent nodes being added to edges', () => { 73 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 74 | graph.add({from: map.a.id, to: map.ac.id}) 75 | graph.edges().length.should.equal(2) 76 | }) 77 | 78 | it('should prevent edges being added to nodes', () => { 79 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 80 | graph.add({from: [map.ac.id], to: map.a}) 81 | graph.edges().length.should.equal(2) 82 | }) 83 | 84 | it('should prevent duplicate edges', () => { 85 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 86 | graph.add({from: map.a.id, to: map.c.id}) 87 | graph.edges().length.should.equal(2) 88 | }) 89 | }) 90 | 91 | describe('#remove', () => { 92 | it('should remove an element from the graph', () => { 93 | const graph = new Graph([map.a, map.b, map.c, {from: map.a, to: map.c}]) 94 | graph.remove(map.b).should.have.length(3) 95 | }) 96 | 97 | it('should remove any dependent edges from the graph', () => { 98 | const graph = new Graph([map.a, map.b, map.c, {from: map.a, to: map.c}]) 99 | graph.remove(map.c).should.haveTheSameItemsAs([map.a, map.b]) 100 | }) 101 | 102 | it('should remove an complex element from the graph', () => { 103 | const graph = new Graph([map.a, map.b, map.c, map.abc]) 104 | graph.remove(map.a).should.have.length(3) 105 | graph.edges()[0].from.should.haveTheSameItemsAs([map.b.id]) 106 | }) 107 | 108 | it('should remove both conclusion and conjoined edges when conclusion is removed', () => { 109 | const graph = new Graph([map.a, map.b, map.c, {from: ['a', 'b'], to: 'c'}]) 110 | graph.remove(map.c) 111 | graph.elements().should.haveTheSameItemsAs([map.a, map.b]) 112 | }) 113 | 114 | it('should only remove an isolated node when that node is deleted', () => { 115 | const graph = new Graph([map.a, map.b, map.c, map.d, {from: ['a', 'b'], to: 'c'}]) 116 | graph.remove(map.d) 117 | graph.edges()[0].from.should.haveTheSameItemsAs([map.a.id, map.b.id]) 118 | }) 119 | }) 120 | 121 | describe('#focus', () => { 122 | it('should move the desired element to the front of the array', () => { 123 | const graph = new Graph([map.a, map.b, map.c]) 124 | graph.focus(map.b).should.equal(map.b) 125 | }) 126 | 127 | it('should set the focused property of the specified element', () => { 128 | const graph = new Graph([map.a, map.b, map.c]) 129 | graph.focus(map.b).focused.should.equal(true) 130 | graph.elements()[0].focused.should.equal(false) 131 | }) 132 | }) 133 | 134 | describe('#unfocus', () => { 135 | it('should unfocus all elements', () => { 136 | const graph = new Graph([map.a, map.b, map.c]) 137 | graph.unfocus().elements().forEach((el) => { 138 | el.focused.should.equal(false) 139 | }) 140 | }) 141 | }) 142 | 143 | describe('#last', () => { 144 | it('should return the last element', () => { 145 | const graph = new Graph([map.a, map.b, map.c]) 146 | graph.last().should.equal(map.a) 147 | }) 148 | }) 149 | 150 | describe('#edges', () => { 151 | it('should return all the edges', () => { 152 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 153 | graph.edges().should.be.instanceof(Array) 154 | graph.edges().should.have.length(2) 155 | }) 156 | 157 | it('should expose an edges to: propery as an id string', () => { 158 | const graph = new Graph([map.a, map.b, map.ab]) 159 | graph.edges()[0].to.should.equal(map.b.id) 160 | }) 161 | }) 162 | 163 | describe('#nodes', () => { 164 | it('should return all the nodes', () => { 165 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 166 | graph.nodes().should.be.instanceof(Array) 167 | graph.nodes().should.have.length(3) 168 | }) 169 | 170 | it('should expose an edges from: propery as an array of id strings', () => { 171 | const graph = new Graph([map.a, map.b, map.ab]) 172 | graph.edges()[0].from.should.haveTheSameItemsAs([map.a.id]) 173 | }) 174 | }) 175 | 176 | describe('#elements', () => { 177 | it('should return all the elements', () => { 178 | const graph = new Graph([map.a, map.b, map.c, map.ac, map.bc]) 179 | graph.elements().should.be.instanceof(Array) 180 | graph.elements().should.have.length(5) 181 | }) 182 | 183 | it('should reproduce the same elements from itself', () => { 184 | const elements = [map.a, map.b, map.c, map.ac, map.bc] 185 | const graph = new Graph(elements) 186 | new Graph(graph.elements()).elements().should.haveTheSameItemsAs(elements) 187 | }) 188 | }) 189 | 190 | describe('#parents', () => { 191 | it('should find all the parents of a node', () => { 192 | const graph = new Graph([map.a, map.b, map.c, map.d, map.ac, map.bc, map.cd]) 193 | graph.parents('d').should.be.instanceof(Array) 194 | graph.parents('d').should.have.length(1) 195 | graph.parents('d').should.haveTheSameItemsAs([map.c]) 196 | }) 197 | 198 | it('should find all the parents of a conjoined node', () => { 199 | const graph = new Graph([map.a, map.b, map.c, {from: ['a', 'b'], to: 'c'}]) 200 | graph.parents('c').should.have.length(2) 201 | graph.parents('c').should.haveTheSameItemsAs([map.a, map.b]) 202 | }) 203 | }) 204 | 205 | describe('#children', () => { 206 | it('should find all the children of a node id', () => { 207 | const graph = new Graph([map.a, map.b, map.c, map.d, map.ac, map.bc, map.cd]) 208 | graph.children('b').should.be.instanceof(Array) 209 | graph.children('b').should.have.length(1) 210 | graph.children('b').should.haveTheSameItemsAs([map.c]) 211 | }) 212 | 213 | it('should find all the children of a node', () => { 214 | const graph = new Graph([map.a, map.b, map.c, map.d, map.ac, map.bc, map.cd]) 215 | graph.children(map.b).should.be.instanceof(Array) 216 | graph.children(map.b).should.have.length(1) 217 | graph.children(map.a).should.haveTheSameItemsAs([map.c]) 218 | }) 219 | 220 | it('should find all the children of a conjoined node', () => { 221 | var graph = new Graph([map.a, map.b, map.c, {from: [map.a, map.b], to: map.c}]) 222 | graph.children(map.a).should.be.instanceof(Array) 223 | graph.children(map.a).should.haveTheSameItemsAs([map.c]) 224 | }) 225 | }) 226 | 227 | describe('#hasDuplicate', () => { 228 | it('should prevent duplicate edges', () => { 229 | var graph = new Graph([map.a, map.b, map.c, map.ab]) 230 | graph.hasDuplicate({from: map.a.id, to: map.b.id}).should.equal(true) 231 | graph.hasDuplicate({from: map.a.id, to: map.c.id}).should.equal(false) 232 | }) 233 | }) 234 | }) 235 | 236 | -------------------------------------------------------------------------------- /test/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 36 | 37 | -------------------------------------------------------------------------------- /test/mapper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Mock out the DOM and CANVAS 4 | const JSDOM = require('jsdom').JSDOM 5 | global.window = (new JSDOM('
')).window 6 | global.document = window.document 7 | window.HTMLCanvasElement.prototype.getContext = require('./mock-canvas') 8 | 9 | const should = require('should') 10 | const Mapper = require('./../lib/mapper') 11 | 12 | const M = new Mapper('#target') 13 | const G = [ 14 | {id: 'p1', text: "Circular arguments work"}, 15 | {id: 'c1', text: "Circular arguments work"}, 16 | {from: 'p1', to: 'c1', type: "supports"}, 17 | {from: 'c1', to: 'p1', type: "supports"} 18 | ] 19 | 20 | describe('Mapper', () => { 21 | describe('#new', () => { 22 | it('should create a map with a valid DOM reference', () => { 23 | M.should.be.instanceOf(Mapper) 24 | }) 25 | 26 | it('return null for invalid DOM references', () => { 27 | (new Mapper('#nosuchref')).should.be.null 28 | }) 29 | }) 30 | 31 | describe('#render', () => { 32 | it('should populate the graph in the argument map', () => { 33 | M.render(G) 34 | M.graph.length.should.equal(G.length) 35 | }) 36 | 37 | it('should return a reference of the graph', () => { 38 | const m = M.render(G) 39 | m.should.be.instanceOf(Mapper) 40 | }) 41 | }) 42 | 43 | describe('#export', () => { 44 | it('should export the graph as an array from the argument map', () => { 45 | M.render(G) 46 | M.export().should.be.instanceOf(Array) 47 | M.export().length.should.equal(G.length) 48 | }) 49 | }) 50 | }) -------------------------------------------------------------------------------- /test/mock-canvas.js: -------------------------------------------------------------------------------- 1 | /* Due to changes in the JSDOM library, the following patch is necessary 2 | * to mock the HTML Canvas for testing. 3 | * See https://github.com/jsdom/jsdom/issues/1782 4 | */ 5 | 6 | module.exports = function() { 7 | return { 8 | fillRect: function() {}, 9 | clearRect: function(){}, 10 | getImageData: function(x, y, w, h) { 11 | return { 12 | data: new Array(w*h*4) 13 | }; 14 | }, 15 | putImageData: function() {}, 16 | createImageData: function(){ return []}, 17 | setTransform: function(){}, 18 | drawImage: function(){}, 19 | save: function(){}, 20 | fillText: function(){}, 21 | restore: function(){}, 22 | beginPath: function(){}, 23 | moveTo: function(){}, 24 | lineTo: function(){}, 25 | closePath: function(){}, 26 | stroke: function(){}, 27 | translate: function(){}, 28 | scale: function(){}, 29 | rotate: function(){}, 30 | arc: function(){}, 31 | fill: function(){}, 32 | measureText: function (){ return {width: 5} }, 33 | strokeRect: function () {}, 34 | setLineDash: function () {}, 35 | class: 'HTMLDivElement' 36 | } 37 | } -------------------------------------------------------------------------------- /test/remote-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 |
15 | 16 | 35 | 36 | -------------------------------------------------------------------------------- /test/should.js: -------------------------------------------------------------------------------- 1 | const should = require('should') 2 | 3 | global.testing = true 4 | 5 | module.exports = should 6 | 7 | should.Assertion.add('haveTheSameItemsAs', function(other) { 8 | this.params = { operator: 'to be have same items' }; 9 | this.obj.forEach(item => { 10 | other.should.containEql(item); 11 | }) 12 | this.obj.length.should.be.equal(other.length); 13 | }) -------------------------------------------------------------------------------- /test/small-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 40 | 41 | -------------------------------------------------------------------------------- /test/text-size.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 36 | 37 | -------------------------------------------------------------------------------- /test/ui.js: -------------------------------------------------------------------------------- 1 | /** Testing of click & touch events is currently done by hand :( 2 | * Contributions for automated testing of browser events will be warmly accepted. 3 | */ -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | const JSDOM = require('jsdom').JSDOM 5 | const Utils = require('./../lib/utils') 6 | 7 | describe('Utils', () => { 8 | }) -------------------------------------------------------------------------------- /test/view.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | const Mapper = require('./../lib/mapper') 5 | const View = require('./../lib/view') 6 | 7 | // Mock out the DOM and CANVAS 8 | const JSDOM = require('jsdom').JSDOM 9 | global.window = (new JSDOM('
')).window 10 | global.document = window.document 11 | 12 | const M = new Mapper('#target') 13 | const G = [ 14 | {id: 'a', text: "Blah blah A"}, 15 | {id: 'b', text: "Blah blah B", x: 200}, 16 | {id: 'c', text: "Blah blah C", x: 100, y: 400}, 17 | {from: ['a', 'b'], to: 'c'} 18 | ] 19 | M.render(G) 20 | 21 | describe('View', () => { 22 | describe('#init', () => { 23 | it('shoud add a canvas element to #target', () => { 24 | M.DOM.should.be.class('HTMLDivElement') 25 | M.DOM.id.should.equal('target') 26 | M.DOM.childNodes[0].should.be.class('HTMLCanvasElement') 27 | }) 28 | }) 29 | 30 | describe('#draw', () => { 31 | it('shoud set x & y variables for nodes', () => { 32 | const C = M.graph.nodes()[2] 33 | C.x1.should.be.instanceOf(Number) 34 | C.y2.should.be.instanceOf(Number) 35 | }) 36 | 37 | it('shoud set x & y variables for edge center', () => { 38 | const E = M.graph.edges()[0] 39 | E.center.x.should.be.instanceOf(Number) 40 | E.center.y.should.be.instanceOf(Number) 41 | }) 42 | 43 | it('shoud set x & y variables for edge paths', () => { 44 | const E = M.graph.edges()[0] 45 | E.paths[0].x1.should.be.instanceOf(Number) 46 | E.paths[0].y2.should.be.instanceOf(Number) 47 | }) 48 | 49 | it('should set three paths for a conjoined graph', () => { 50 | M.graph.edges()[0].paths.length.should.equal(3) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /web/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: about 3 | layout: template 4 | permalink: /about/ 5 | --- 6 | 7 | # Reasons.js 8 | 9 | Reasons.js is a web based argument mapping tool built with HTML5 Canvas and Javascript. 10 | 11 | Argument mapping has been shown to have significant positive effects on student reasoning abilities (van Gelder 2004) owing to the way “[p]ictures and structured diagrams are thought to be more comprehensible than just words, and a clearer way to illustrate understanding of complex topics.” (Davies 2011) 12 | 13 | 14 | ## Usage 15 | 16 | - Double click the canvas to add a reason 17 | - Drag one reason onto another to create a premise -> conclusion relation 18 | - Double click a reason or relation to edit their text 19 | - Select a reason or relation and press backspace to delete it 20 | - To create a compound relation between two premises: 21 | - First make sure that they support a common conclusion 22 | - Then drag one premise onto another 23 | 24 | 25 | ## Feedback & Suggestions 26 | 27 | Feedback & Suggestions are very welcome. If you've got something in mind, create and [issue](https://github.com/davekinkead/reasons/issues) on the tracker 28 | 29 | ## Credits 30 | 31 | Reasons.js was built by [Dave Kinkead](http://dave.kinkead.com.au) and is released under a [MIT license](/LICENSE). Copyright 2017 University of Queensland. -------------------------------------------------------------------------------- /web/css/font-awesome.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | /* FONT PATH 6 | * -------------------------- */ 7 | @font-face { 8 | font-family: 'FontAwesome'; 9 | src: url('../fonts/fontawesome-webfont.eot?v=4.7.0'); 10 | src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | .fa { 15 | display: inline-block; 16 | font: normal normal normal 14px/1 FontAwesome; 17 | font-size: inherit; 18 | text-rendering: auto; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | /* makes the font 33% larger relative to the icon container */ 23 | .fa-lg { 24 | font-size: 1.33333333em; 25 | line-height: 0.75em; 26 | vertical-align: -15%; 27 | } 28 | .fa-2x { 29 | font-size: 2em; 30 | } 31 | .fa-3x { 32 | font-size: 3em; 33 | } 34 | .fa-4x { 35 | font-size: 4em; 36 | } 37 | .fa-5x { 38 | font-size: 5em; 39 | } 40 | .fa-fw { 41 | width: 1.28571429em; 42 | text-align: center; 43 | } 44 | .fa-ul { 45 | padding-left: 0; 46 | margin-left: 2.14285714em; 47 | list-style-type: none; 48 | } 49 | .fa-ul > li { 50 | position: relative; 51 | } 52 | .fa-li { 53 | position: absolute; 54 | left: -2.14285714em; 55 | width: 2.14285714em; 56 | top: 0.14285714em; 57 | text-align: center; 58 | } 59 | .fa-li.fa-lg { 60 | left: -1.85714286em; 61 | } 62 | .fa-border { 63 | padding: .2em .25em .15em; 64 | border: solid 0.08em #eeeeee; 65 | border-radius: .1em; 66 | } 67 | .fa-pull-left { 68 | float: left; 69 | } 70 | .fa-pull-right { 71 | float: right; 72 | } 73 | .fa.fa-pull-left { 74 | margin-right: .3em; 75 | } 76 | .fa.fa-pull-right { 77 | margin-left: .3em; 78 | } 79 | /* Deprecated as of 4.4.0 */ 80 | .pull-right { 81 | float: right; 82 | } 83 | .pull-left { 84 | float: left; 85 | } 86 | .fa.pull-left { 87 | margin-right: .3em; 88 | } 89 | .fa.pull-right { 90 | margin-left: .3em; 91 | } 92 | .fa-spin { 93 | -webkit-animation: fa-spin 2s infinite linear; 94 | animation: fa-spin 2s infinite linear; 95 | } 96 | .fa-pulse { 97 | -webkit-animation: fa-spin 1s infinite steps(8); 98 | animation: fa-spin 1s infinite steps(8); 99 | } 100 | @-webkit-keyframes fa-spin { 101 | 0% { 102 | -webkit-transform: rotate(0deg); 103 | transform: rotate(0deg); 104 | } 105 | 100% { 106 | -webkit-transform: rotate(359deg); 107 | transform: rotate(359deg); 108 | } 109 | } 110 | @keyframes fa-spin { 111 | 0% { 112 | -webkit-transform: rotate(0deg); 113 | transform: rotate(0deg); 114 | } 115 | 100% { 116 | -webkit-transform: rotate(359deg); 117 | transform: rotate(359deg); 118 | } 119 | } 120 | .fa-rotate-90 { 121 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; 122 | -webkit-transform: rotate(90deg); 123 | -ms-transform: rotate(90deg); 124 | transform: rotate(90deg); 125 | } 126 | .fa-rotate-180 { 127 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; 128 | -webkit-transform: rotate(180deg); 129 | -ms-transform: rotate(180deg); 130 | transform: rotate(180deg); 131 | } 132 | .fa-rotate-270 { 133 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; 134 | -webkit-transform: rotate(270deg); 135 | -ms-transform: rotate(270deg); 136 | transform: rotate(270deg); 137 | } 138 | .fa-flip-horizontal { 139 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; 140 | -webkit-transform: scale(-1, 1); 141 | -ms-transform: scale(-1, 1); 142 | transform: scale(-1, 1); 143 | } 144 | .fa-flip-vertical { 145 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; 146 | -webkit-transform: scale(1, -1); 147 | -ms-transform: scale(1, -1); 148 | transform: scale(1, -1); 149 | } 150 | :root .fa-rotate-90, 151 | :root .fa-rotate-180, 152 | :root .fa-rotate-270, 153 | :root .fa-flip-horizontal, 154 | :root .fa-flip-vertical { 155 | filter: none; 156 | } 157 | .fa-stack { 158 | position: relative; 159 | display: inline-block; 160 | width: 2em; 161 | height: 2em; 162 | line-height: 2em; 163 | vertical-align: middle; 164 | } 165 | .fa-stack-1x, 166 | .fa-stack-2x { 167 | position: absolute; 168 | left: 0; 169 | width: 100%; 170 | text-align: center; 171 | } 172 | .fa-stack-1x { 173 | line-height: inherit; 174 | } 175 | .fa-stack-2x { 176 | font-size: 2em; 177 | } 178 | .fa-inverse { 179 | color: #ffffff; 180 | } 181 | /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen 182 | readers do not read off random characters that represent icons */ 183 | .fa-glass:before { 184 | content: "\f000"; 185 | } 186 | .fa-music:before { 187 | content: "\f001"; 188 | } 189 | .fa-search:before { 190 | content: "\f002"; 191 | } 192 | .fa-envelope-o:before { 193 | content: "\f003"; 194 | } 195 | .fa-heart:before { 196 | content: "\f004"; 197 | } 198 | .fa-star:before { 199 | content: "\f005"; 200 | } 201 | .fa-star-o:before { 202 | content: "\f006"; 203 | } 204 | .fa-user:before { 205 | content: "\f007"; 206 | } 207 | .fa-film:before { 208 | content: "\f008"; 209 | } 210 | .fa-th-large:before { 211 | content: "\f009"; 212 | } 213 | .fa-th:before { 214 | content: "\f00a"; 215 | } 216 | .fa-th-list:before { 217 | content: "\f00b"; 218 | } 219 | .fa-check:before { 220 | content: "\f00c"; 221 | } 222 | .fa-remove:before, 223 | .fa-close:before, 224 | .fa-times:before { 225 | content: "\f00d"; 226 | } 227 | .fa-search-plus:before { 228 | content: "\f00e"; 229 | } 230 | .fa-search-minus:before { 231 | content: "\f010"; 232 | } 233 | .fa-power-off:before { 234 | content: "\f011"; 235 | } 236 | .fa-signal:before { 237 | content: "\f012"; 238 | } 239 | .fa-gear:before, 240 | .fa-cog:before { 241 | content: "\f013"; 242 | } 243 | .fa-trash-o:before { 244 | content: "\f014"; 245 | } 246 | .fa-home:before { 247 | content: "\f015"; 248 | } 249 | .fa-file-o:before { 250 | content: "\f016"; 251 | } 252 | .fa-clock-o:before { 253 | content: "\f017"; 254 | } 255 | .fa-road:before { 256 | content: "\f018"; 257 | } 258 | .fa-download:before { 259 | content: "\f019"; 260 | } 261 | .fa-arrow-circle-o-down:before { 262 | content: "\f01a"; 263 | } 264 | .fa-arrow-circle-o-up:before { 265 | content: "\f01b"; 266 | } 267 | .fa-inbox:before { 268 | content: "\f01c"; 269 | } 270 | .fa-play-circle-o:before { 271 | content: "\f01d"; 272 | } 273 | .fa-rotate-right:before, 274 | .fa-repeat:before { 275 | content: "\f01e"; 276 | } 277 | .fa-refresh:before { 278 | content: "\f021"; 279 | } 280 | .fa-list-alt:before { 281 | content: "\f022"; 282 | } 283 | .fa-lock:before { 284 | content: "\f023"; 285 | } 286 | .fa-flag:before { 287 | content: "\f024"; 288 | } 289 | .fa-headphones:before { 290 | content: "\f025"; 291 | } 292 | .fa-volume-off:before { 293 | content: "\f026"; 294 | } 295 | .fa-volume-down:before { 296 | content: "\f027"; 297 | } 298 | .fa-volume-up:before { 299 | content: "\f028"; 300 | } 301 | .fa-qrcode:before { 302 | content: "\f029"; 303 | } 304 | .fa-barcode:before { 305 | content: "\f02a"; 306 | } 307 | .fa-tag:before { 308 | content: "\f02b"; 309 | } 310 | .fa-tags:before { 311 | content: "\f02c"; 312 | } 313 | .fa-book:before { 314 | content: "\f02d"; 315 | } 316 | .fa-bookmark:before { 317 | content: "\f02e"; 318 | } 319 | .fa-print:before { 320 | content: "\f02f"; 321 | } 322 | .fa-camera:before { 323 | content: "\f030"; 324 | } 325 | .fa-font:before { 326 | content: "\f031"; 327 | } 328 | .fa-bold:before { 329 | content: "\f032"; 330 | } 331 | .fa-italic:before { 332 | content: "\f033"; 333 | } 334 | .fa-text-height:before { 335 | content: "\f034"; 336 | } 337 | .fa-text-width:before { 338 | content: "\f035"; 339 | } 340 | .fa-align-left:before { 341 | content: "\f036"; 342 | } 343 | .fa-align-center:before { 344 | content: "\f037"; 345 | } 346 | .fa-align-right:before { 347 | content: "\f038"; 348 | } 349 | .fa-align-justify:before { 350 | content: "\f039"; 351 | } 352 | .fa-list:before { 353 | content: "\f03a"; 354 | } 355 | .fa-dedent:before, 356 | .fa-outdent:before { 357 | content: "\f03b"; 358 | } 359 | .fa-indent:before { 360 | content: "\f03c"; 361 | } 362 | .fa-video-camera:before { 363 | content: "\f03d"; 364 | } 365 | .fa-photo:before, 366 | .fa-image:before, 367 | .fa-picture-o:before { 368 | content: "\f03e"; 369 | } 370 | .fa-pencil:before { 371 | content: "\f040"; 372 | } 373 | .fa-map-marker:before { 374 | content: "\f041"; 375 | } 376 | .fa-adjust:before { 377 | content: "\f042"; 378 | } 379 | .fa-tint:before { 380 | content: "\f043"; 381 | } 382 | .fa-edit:before, 383 | .fa-pencil-square-o:before { 384 | content: "\f044"; 385 | } 386 | .fa-share-square-o:before { 387 | content: "\f045"; 388 | } 389 | .fa-check-square-o:before { 390 | content: "\f046"; 391 | } 392 | .fa-arrows:before { 393 | content: "\f047"; 394 | } 395 | .fa-step-backward:before { 396 | content: "\f048"; 397 | } 398 | .fa-fast-backward:before { 399 | content: "\f049"; 400 | } 401 | .fa-backward:before { 402 | content: "\f04a"; 403 | } 404 | .fa-play:before { 405 | content: "\f04b"; 406 | } 407 | .fa-pause:before { 408 | content: "\f04c"; 409 | } 410 | .fa-stop:before { 411 | content: "\f04d"; 412 | } 413 | .fa-forward:before { 414 | content: "\f04e"; 415 | } 416 | .fa-fast-forward:before { 417 | content: "\f050"; 418 | } 419 | .fa-step-forward:before { 420 | content: "\f051"; 421 | } 422 | .fa-eject:before { 423 | content: "\f052"; 424 | } 425 | .fa-chevron-left:before { 426 | content: "\f053"; 427 | } 428 | .fa-chevron-right:before { 429 | content: "\f054"; 430 | } 431 | .fa-plus-circle:before { 432 | content: "\f055"; 433 | } 434 | .fa-minus-circle:before { 435 | content: "\f056"; 436 | } 437 | .fa-times-circle:before { 438 | content: "\f057"; 439 | } 440 | .fa-check-circle:before { 441 | content: "\f058"; 442 | } 443 | .fa-question-circle:before { 444 | content: "\f059"; 445 | } 446 | .fa-info-circle:before { 447 | content: "\f05a"; 448 | } 449 | .fa-crosshairs:before { 450 | content: "\f05b"; 451 | } 452 | .fa-times-circle-o:before { 453 | content: "\f05c"; 454 | } 455 | .fa-check-circle-o:before { 456 | content: "\f05d"; 457 | } 458 | .fa-ban:before { 459 | content: "\f05e"; 460 | } 461 | .fa-arrow-left:before { 462 | content: "\f060"; 463 | } 464 | .fa-arrow-right:before { 465 | content: "\f061"; 466 | } 467 | .fa-arrow-up:before { 468 | content: "\f062"; 469 | } 470 | .fa-arrow-down:before { 471 | content: "\f063"; 472 | } 473 | .fa-mail-forward:before, 474 | .fa-share:before { 475 | content: "\f064"; 476 | } 477 | .fa-expand:before { 478 | content: "\f065"; 479 | } 480 | .fa-compress:before { 481 | content: "\f066"; 482 | } 483 | .fa-plus:before { 484 | content: "\f067"; 485 | } 486 | .fa-minus:before { 487 | content: "\f068"; 488 | } 489 | .fa-asterisk:before { 490 | content: "\f069"; 491 | } 492 | .fa-exclamation-circle:before { 493 | content: "\f06a"; 494 | } 495 | .fa-gift:before { 496 | content: "\f06b"; 497 | } 498 | .fa-leaf:before { 499 | content: "\f06c"; 500 | } 501 | .fa-fire:before { 502 | content: "\f06d"; 503 | } 504 | .fa-eye:before { 505 | content: "\f06e"; 506 | } 507 | .fa-eye-slash:before { 508 | content: "\f070"; 509 | } 510 | .fa-warning:before, 511 | .fa-exclamation-triangle:before { 512 | content: "\f071"; 513 | } 514 | .fa-plane:before { 515 | content: "\f072"; 516 | } 517 | .fa-calendar:before { 518 | content: "\f073"; 519 | } 520 | .fa-random:before { 521 | content: "\f074"; 522 | } 523 | .fa-comment:before { 524 | content: "\f075"; 525 | } 526 | .fa-magnet:before { 527 | content: "\f076"; 528 | } 529 | .fa-chevron-up:before { 530 | content: "\f077"; 531 | } 532 | .fa-chevron-down:before { 533 | content: "\f078"; 534 | } 535 | .fa-retweet:before { 536 | content: "\f079"; 537 | } 538 | .fa-shopping-cart:before { 539 | content: "\f07a"; 540 | } 541 | .fa-folder:before { 542 | content: "\f07b"; 543 | } 544 | .fa-folder-open:before { 545 | content: "\f07c"; 546 | } 547 | .fa-arrows-v:before { 548 | content: "\f07d"; 549 | } 550 | .fa-arrows-h:before { 551 | content: "\f07e"; 552 | } 553 | .fa-bar-chart-o:before, 554 | .fa-bar-chart:before { 555 | content: "\f080"; 556 | } 557 | .fa-twitter-square:before { 558 | content: "\f081"; 559 | } 560 | .fa-facebook-square:before { 561 | content: "\f082"; 562 | } 563 | .fa-camera-retro:before { 564 | content: "\f083"; 565 | } 566 | .fa-key:before { 567 | content: "\f084"; 568 | } 569 | .fa-gears:before, 570 | .fa-cogs:before { 571 | content: "\f085"; 572 | } 573 | .fa-comments:before { 574 | content: "\f086"; 575 | } 576 | .fa-thumbs-o-up:before { 577 | content: "\f087"; 578 | } 579 | .fa-thumbs-o-down:before { 580 | content: "\f088"; 581 | } 582 | .fa-star-half:before { 583 | content: "\f089"; 584 | } 585 | .fa-heart-o:before { 586 | content: "\f08a"; 587 | } 588 | .fa-sign-out:before { 589 | content: "\f08b"; 590 | } 591 | .fa-linkedin-square:before { 592 | content: "\f08c"; 593 | } 594 | .fa-thumb-tack:before { 595 | content: "\f08d"; 596 | } 597 | .fa-external-link:before { 598 | content: "\f08e"; 599 | } 600 | .fa-sign-in:before { 601 | content: "\f090"; 602 | } 603 | .fa-trophy:before { 604 | content: "\f091"; 605 | } 606 | .fa-github-square:before { 607 | content: "\f092"; 608 | } 609 | .fa-upload:before { 610 | content: "\f093"; 611 | } 612 | .fa-lemon-o:before { 613 | content: "\f094"; 614 | } 615 | .fa-phone:before { 616 | content: "\f095"; 617 | } 618 | .fa-square-o:before { 619 | content: "\f096"; 620 | } 621 | .fa-bookmark-o:before { 622 | content: "\f097"; 623 | } 624 | .fa-phone-square:before { 625 | content: "\f098"; 626 | } 627 | .fa-twitter:before { 628 | content: "\f099"; 629 | } 630 | .fa-facebook-f:before, 631 | .fa-facebook:before { 632 | content: "\f09a"; 633 | } 634 | .fa-github:before { 635 | content: "\f09b"; 636 | } 637 | .fa-unlock:before { 638 | content: "\f09c"; 639 | } 640 | .fa-credit-card:before { 641 | content: "\f09d"; 642 | } 643 | .fa-feed:before, 644 | .fa-rss:before { 645 | content: "\f09e"; 646 | } 647 | .fa-hdd-o:before { 648 | content: "\f0a0"; 649 | } 650 | .fa-bullhorn:before { 651 | content: "\f0a1"; 652 | } 653 | .fa-bell:before { 654 | content: "\f0f3"; 655 | } 656 | .fa-certificate:before { 657 | content: "\f0a3"; 658 | } 659 | .fa-hand-o-right:before { 660 | content: "\f0a4"; 661 | } 662 | .fa-hand-o-left:before { 663 | content: "\f0a5"; 664 | } 665 | .fa-hand-o-up:before { 666 | content: "\f0a6"; 667 | } 668 | .fa-hand-o-down:before { 669 | content: "\f0a7"; 670 | } 671 | .fa-arrow-circle-left:before { 672 | content: "\f0a8"; 673 | } 674 | .fa-arrow-circle-right:before { 675 | content: "\f0a9"; 676 | } 677 | .fa-arrow-circle-up:before { 678 | content: "\f0aa"; 679 | } 680 | .fa-arrow-circle-down:before { 681 | content: "\f0ab"; 682 | } 683 | .fa-globe:before { 684 | content: "\f0ac"; 685 | } 686 | .fa-wrench:before { 687 | content: "\f0ad"; 688 | } 689 | .fa-tasks:before { 690 | content: "\f0ae"; 691 | } 692 | .fa-filter:before { 693 | content: "\f0b0"; 694 | } 695 | .fa-briefcase:before { 696 | content: "\f0b1"; 697 | } 698 | .fa-arrows-alt:before { 699 | content: "\f0b2"; 700 | } 701 | .fa-group:before, 702 | .fa-users:before { 703 | content: "\f0c0"; 704 | } 705 | .fa-chain:before, 706 | .fa-link:before { 707 | content: "\f0c1"; 708 | } 709 | .fa-cloud:before { 710 | content: "\f0c2"; 711 | } 712 | .fa-flask:before { 713 | content: "\f0c3"; 714 | } 715 | .fa-cut:before, 716 | .fa-scissors:before { 717 | content: "\f0c4"; 718 | } 719 | .fa-copy:before, 720 | .fa-files-o:before { 721 | content: "\f0c5"; 722 | } 723 | .fa-paperclip:before { 724 | content: "\f0c6"; 725 | } 726 | .fa-save:before, 727 | .fa-floppy-o:before { 728 | content: "\f0c7"; 729 | } 730 | .fa-square:before { 731 | content: "\f0c8"; 732 | } 733 | .fa-navicon:before, 734 | .fa-reorder:before, 735 | .fa-bars:before { 736 | content: "\f0c9"; 737 | } 738 | .fa-list-ul:before { 739 | content: "\f0ca"; 740 | } 741 | .fa-list-ol:before { 742 | content: "\f0cb"; 743 | } 744 | .fa-strikethrough:before { 745 | content: "\f0cc"; 746 | } 747 | .fa-underline:before { 748 | content: "\f0cd"; 749 | } 750 | .fa-table:before { 751 | content: "\f0ce"; 752 | } 753 | .fa-magic:before { 754 | content: "\f0d0"; 755 | } 756 | .fa-truck:before { 757 | content: "\f0d1"; 758 | } 759 | .fa-pinterest:before { 760 | content: "\f0d2"; 761 | } 762 | .fa-pinterest-square:before { 763 | content: "\f0d3"; 764 | } 765 | .fa-google-plus-square:before { 766 | content: "\f0d4"; 767 | } 768 | .fa-google-plus:before { 769 | content: "\f0d5"; 770 | } 771 | .fa-money:before { 772 | content: "\f0d6"; 773 | } 774 | .fa-caret-down:before { 775 | content: "\f0d7"; 776 | } 777 | .fa-caret-up:before { 778 | content: "\f0d8"; 779 | } 780 | .fa-caret-left:before { 781 | content: "\f0d9"; 782 | } 783 | .fa-caret-right:before { 784 | content: "\f0da"; 785 | } 786 | .fa-columns:before { 787 | content: "\f0db"; 788 | } 789 | .fa-unsorted:before, 790 | .fa-sort:before { 791 | content: "\f0dc"; 792 | } 793 | .fa-sort-down:before, 794 | .fa-sort-desc:before { 795 | content: "\f0dd"; 796 | } 797 | .fa-sort-up:before, 798 | .fa-sort-asc:before { 799 | content: "\f0de"; 800 | } 801 | .fa-envelope:before { 802 | content: "\f0e0"; 803 | } 804 | .fa-linkedin:before { 805 | content: "\f0e1"; 806 | } 807 | .fa-rotate-left:before, 808 | .fa-undo:before { 809 | content: "\f0e2"; 810 | } 811 | .fa-legal:before, 812 | .fa-gavel:before { 813 | content: "\f0e3"; 814 | } 815 | .fa-dashboard:before, 816 | .fa-tachometer:before { 817 | content: "\f0e4"; 818 | } 819 | .fa-comment-o:before { 820 | content: "\f0e5"; 821 | } 822 | .fa-comments-o:before { 823 | content: "\f0e6"; 824 | } 825 | .fa-flash:before, 826 | .fa-bolt:before { 827 | content: "\f0e7"; 828 | } 829 | .fa-sitemap:before { 830 | content: "\f0e8"; 831 | } 832 | .fa-umbrella:before { 833 | content: "\f0e9"; 834 | } 835 | .fa-paste:before, 836 | .fa-clipboard:before { 837 | content: "\f0ea"; 838 | } 839 | .fa-lightbulb-o:before { 840 | content: "\f0eb"; 841 | } 842 | .fa-exchange:before { 843 | content: "\f0ec"; 844 | } 845 | .fa-cloud-download:before { 846 | content: "\f0ed"; 847 | } 848 | .fa-cloud-upload:before { 849 | content: "\f0ee"; 850 | } 851 | .fa-user-md:before { 852 | content: "\f0f0"; 853 | } 854 | .fa-stethoscope:before { 855 | content: "\f0f1"; 856 | } 857 | .fa-suitcase:before { 858 | content: "\f0f2"; 859 | } 860 | .fa-bell-o:before { 861 | content: "\f0a2"; 862 | } 863 | .fa-coffee:before { 864 | content: "\f0f4"; 865 | } 866 | .fa-cutlery:before { 867 | content: "\f0f5"; 868 | } 869 | .fa-file-text-o:before { 870 | content: "\f0f6"; 871 | } 872 | .fa-building-o:before { 873 | content: "\f0f7"; 874 | } 875 | .fa-hospital-o:before { 876 | content: "\f0f8"; 877 | } 878 | .fa-ambulance:before { 879 | content: "\f0f9"; 880 | } 881 | .fa-medkit:before { 882 | content: "\f0fa"; 883 | } 884 | .fa-fighter-jet:before { 885 | content: "\f0fb"; 886 | } 887 | .fa-beer:before { 888 | content: "\f0fc"; 889 | } 890 | .fa-h-square:before { 891 | content: "\f0fd"; 892 | } 893 | .fa-plus-square:before { 894 | content: "\f0fe"; 895 | } 896 | .fa-angle-double-left:before { 897 | content: "\f100"; 898 | } 899 | .fa-angle-double-right:before { 900 | content: "\f101"; 901 | } 902 | .fa-angle-double-up:before { 903 | content: "\f102"; 904 | } 905 | .fa-angle-double-down:before { 906 | content: "\f103"; 907 | } 908 | .fa-angle-left:before { 909 | content: "\f104"; 910 | } 911 | .fa-angle-right:before { 912 | content: "\f105"; 913 | } 914 | .fa-angle-up:before { 915 | content: "\f106"; 916 | } 917 | .fa-angle-down:before { 918 | content: "\f107"; 919 | } 920 | .fa-desktop:before { 921 | content: "\f108"; 922 | } 923 | .fa-laptop:before { 924 | content: "\f109"; 925 | } 926 | .fa-tablet:before { 927 | content: "\f10a"; 928 | } 929 | .fa-mobile-phone:before, 930 | .fa-mobile:before { 931 | content: "\f10b"; 932 | } 933 | .fa-circle-o:before { 934 | content: "\f10c"; 935 | } 936 | .fa-quote-left:before { 937 | content: "\f10d"; 938 | } 939 | .fa-quote-right:before { 940 | content: "\f10e"; 941 | } 942 | .fa-spinner:before { 943 | content: "\f110"; 944 | } 945 | .fa-circle:before { 946 | content: "\f111"; 947 | } 948 | .fa-mail-reply:before, 949 | .fa-reply:before { 950 | content: "\f112"; 951 | } 952 | .fa-github-alt:before { 953 | content: "\f113"; 954 | } 955 | .fa-folder-o:before { 956 | content: "\f114"; 957 | } 958 | .fa-folder-open-o:before { 959 | content: "\f115"; 960 | } 961 | .fa-smile-o:before { 962 | content: "\f118"; 963 | } 964 | .fa-frown-o:before { 965 | content: "\f119"; 966 | } 967 | .fa-meh-o:before { 968 | content: "\f11a"; 969 | } 970 | .fa-gamepad:before { 971 | content: "\f11b"; 972 | } 973 | .fa-keyboard-o:before { 974 | content: "\f11c"; 975 | } 976 | .fa-flag-o:before { 977 | content: "\f11d"; 978 | } 979 | .fa-flag-checkered:before { 980 | content: "\f11e"; 981 | } 982 | .fa-terminal:before { 983 | content: "\f120"; 984 | } 985 | .fa-code:before { 986 | content: "\f121"; 987 | } 988 | .fa-mail-reply-all:before, 989 | .fa-reply-all:before { 990 | content: "\f122"; 991 | } 992 | .fa-star-half-empty:before, 993 | .fa-star-half-full:before, 994 | .fa-star-half-o:before { 995 | content: "\f123"; 996 | } 997 | .fa-location-arrow:before { 998 | content: "\f124"; 999 | } 1000 | .fa-crop:before { 1001 | content: "\f125"; 1002 | } 1003 | .fa-code-fork:before { 1004 | content: "\f126"; 1005 | } 1006 | .fa-unlink:before, 1007 | .fa-chain-broken:before { 1008 | content: "\f127"; 1009 | } 1010 | .fa-question:before { 1011 | content: "\f128"; 1012 | } 1013 | .fa-info:before { 1014 | content: "\f129"; 1015 | } 1016 | .fa-exclamation:before { 1017 | content: "\f12a"; 1018 | } 1019 | .fa-superscript:before { 1020 | content: "\f12b"; 1021 | } 1022 | .fa-subscript:before { 1023 | content: "\f12c"; 1024 | } 1025 | .fa-eraser:before { 1026 | content: "\f12d"; 1027 | } 1028 | .fa-puzzle-piece:before { 1029 | content: "\f12e"; 1030 | } 1031 | .fa-microphone:before { 1032 | content: "\f130"; 1033 | } 1034 | .fa-microphone-slash:before { 1035 | content: "\f131"; 1036 | } 1037 | .fa-shield:before { 1038 | content: "\f132"; 1039 | } 1040 | .fa-calendar-o:before { 1041 | content: "\f133"; 1042 | } 1043 | .fa-fire-extinguisher:before { 1044 | content: "\f134"; 1045 | } 1046 | .fa-rocket:before { 1047 | content: "\f135"; 1048 | } 1049 | .fa-maxcdn:before { 1050 | content: "\f136"; 1051 | } 1052 | .fa-chevron-circle-left:before { 1053 | content: "\f137"; 1054 | } 1055 | .fa-chevron-circle-right:before { 1056 | content: "\f138"; 1057 | } 1058 | .fa-chevron-circle-up:before { 1059 | content: "\f139"; 1060 | } 1061 | .fa-chevron-circle-down:before { 1062 | content: "\f13a"; 1063 | } 1064 | .fa-html5:before { 1065 | content: "\f13b"; 1066 | } 1067 | .fa-css3:before { 1068 | content: "\f13c"; 1069 | } 1070 | .fa-anchor:before { 1071 | content: "\f13d"; 1072 | } 1073 | .fa-unlock-alt:before { 1074 | content: "\f13e"; 1075 | } 1076 | .fa-bullseye:before { 1077 | content: "\f140"; 1078 | } 1079 | .fa-ellipsis-h:before { 1080 | content: "\f141"; 1081 | } 1082 | .fa-ellipsis-v:before { 1083 | content: "\f142"; 1084 | } 1085 | .fa-rss-square:before { 1086 | content: "\f143"; 1087 | } 1088 | .fa-play-circle:before { 1089 | content: "\f144"; 1090 | } 1091 | .fa-ticket:before { 1092 | content: "\f145"; 1093 | } 1094 | .fa-minus-square:before { 1095 | content: "\f146"; 1096 | } 1097 | .fa-minus-square-o:before { 1098 | content: "\f147"; 1099 | } 1100 | .fa-level-up:before { 1101 | content: "\f148"; 1102 | } 1103 | .fa-level-down:before { 1104 | content: "\f149"; 1105 | } 1106 | .fa-check-square:before { 1107 | content: "\f14a"; 1108 | } 1109 | .fa-pencil-square:before { 1110 | content: "\f14b"; 1111 | } 1112 | .fa-external-link-square:before { 1113 | content: "\f14c"; 1114 | } 1115 | .fa-share-square:before { 1116 | content: "\f14d"; 1117 | } 1118 | .fa-compass:before { 1119 | content: "\f14e"; 1120 | } 1121 | .fa-toggle-down:before, 1122 | .fa-caret-square-o-down:before { 1123 | content: "\f150"; 1124 | } 1125 | .fa-toggle-up:before, 1126 | .fa-caret-square-o-up:before { 1127 | content: "\f151"; 1128 | } 1129 | .fa-toggle-right:before, 1130 | .fa-caret-square-o-right:before { 1131 | content: "\f152"; 1132 | } 1133 | .fa-euro:before, 1134 | .fa-eur:before { 1135 | content: "\f153"; 1136 | } 1137 | .fa-gbp:before { 1138 | content: "\f154"; 1139 | } 1140 | .fa-dollar:before, 1141 | .fa-usd:before { 1142 | content: "\f155"; 1143 | } 1144 | .fa-rupee:before, 1145 | .fa-inr:before { 1146 | content: "\f156"; 1147 | } 1148 | .fa-cny:before, 1149 | .fa-rmb:before, 1150 | .fa-yen:before, 1151 | .fa-jpy:before { 1152 | content: "\f157"; 1153 | } 1154 | .fa-ruble:before, 1155 | .fa-rouble:before, 1156 | .fa-rub:before { 1157 | content: "\f158"; 1158 | } 1159 | .fa-won:before, 1160 | .fa-krw:before { 1161 | content: "\f159"; 1162 | } 1163 | .fa-bitcoin:before, 1164 | .fa-btc:before { 1165 | content: "\f15a"; 1166 | } 1167 | .fa-file:before { 1168 | content: "\f15b"; 1169 | } 1170 | .fa-file-text:before { 1171 | content: "\f15c"; 1172 | } 1173 | .fa-sort-alpha-asc:before { 1174 | content: "\f15d"; 1175 | } 1176 | .fa-sort-alpha-desc:before { 1177 | content: "\f15e"; 1178 | } 1179 | .fa-sort-amount-asc:before { 1180 | content: "\f160"; 1181 | } 1182 | .fa-sort-amount-desc:before { 1183 | content: "\f161"; 1184 | } 1185 | .fa-sort-numeric-asc:before { 1186 | content: "\f162"; 1187 | } 1188 | .fa-sort-numeric-desc:before { 1189 | content: "\f163"; 1190 | } 1191 | .fa-thumbs-up:before { 1192 | content: "\f164"; 1193 | } 1194 | .fa-thumbs-down:before { 1195 | content: "\f165"; 1196 | } 1197 | .fa-youtube-square:before { 1198 | content: "\f166"; 1199 | } 1200 | .fa-youtube:before { 1201 | content: "\f167"; 1202 | } 1203 | .fa-xing:before { 1204 | content: "\f168"; 1205 | } 1206 | .fa-xing-square:before { 1207 | content: "\f169"; 1208 | } 1209 | .fa-youtube-play:before { 1210 | content: "\f16a"; 1211 | } 1212 | .fa-dropbox:before { 1213 | content: "\f16b"; 1214 | } 1215 | .fa-stack-overflow:before { 1216 | content: "\f16c"; 1217 | } 1218 | .fa-instagram:before { 1219 | content: "\f16d"; 1220 | } 1221 | .fa-flickr:before { 1222 | content: "\f16e"; 1223 | } 1224 | .fa-adn:before { 1225 | content: "\f170"; 1226 | } 1227 | .fa-bitbucket:before { 1228 | content: "\f171"; 1229 | } 1230 | .fa-bitbucket-square:before { 1231 | content: "\f172"; 1232 | } 1233 | .fa-tumblr:before { 1234 | content: "\f173"; 1235 | } 1236 | .fa-tumblr-square:before { 1237 | content: "\f174"; 1238 | } 1239 | .fa-long-arrow-down:before { 1240 | content: "\f175"; 1241 | } 1242 | .fa-long-arrow-up:before { 1243 | content: "\f176"; 1244 | } 1245 | .fa-long-arrow-left:before { 1246 | content: "\f177"; 1247 | } 1248 | .fa-long-arrow-right:before { 1249 | content: "\f178"; 1250 | } 1251 | .fa-apple:before { 1252 | content: "\f179"; 1253 | } 1254 | .fa-windows:before { 1255 | content: "\f17a"; 1256 | } 1257 | .fa-android:before { 1258 | content: "\f17b"; 1259 | } 1260 | .fa-linux:before { 1261 | content: "\f17c"; 1262 | } 1263 | .fa-dribbble:before { 1264 | content: "\f17d"; 1265 | } 1266 | .fa-skype:before { 1267 | content: "\f17e"; 1268 | } 1269 | .fa-foursquare:before { 1270 | content: "\f180"; 1271 | } 1272 | .fa-trello:before { 1273 | content: "\f181"; 1274 | } 1275 | .fa-female:before { 1276 | content: "\f182"; 1277 | } 1278 | .fa-male:before { 1279 | content: "\f183"; 1280 | } 1281 | .fa-gittip:before, 1282 | .fa-gratipay:before { 1283 | content: "\f184"; 1284 | } 1285 | .fa-sun-o:before { 1286 | content: "\f185"; 1287 | } 1288 | .fa-moon-o:before { 1289 | content: "\f186"; 1290 | } 1291 | .fa-archive:before { 1292 | content: "\f187"; 1293 | } 1294 | .fa-bug:before { 1295 | content: "\f188"; 1296 | } 1297 | .fa-vk:before { 1298 | content: "\f189"; 1299 | } 1300 | .fa-weibo:before { 1301 | content: "\f18a"; 1302 | } 1303 | .fa-renren:before { 1304 | content: "\f18b"; 1305 | } 1306 | .fa-pagelines:before { 1307 | content: "\f18c"; 1308 | } 1309 | .fa-stack-exchange:before { 1310 | content: "\f18d"; 1311 | } 1312 | .fa-arrow-circle-o-right:before { 1313 | content: "\f18e"; 1314 | } 1315 | .fa-arrow-circle-o-left:before { 1316 | content: "\f190"; 1317 | } 1318 | .fa-toggle-left:before, 1319 | .fa-caret-square-o-left:before { 1320 | content: "\f191"; 1321 | } 1322 | .fa-dot-circle-o:before { 1323 | content: "\f192"; 1324 | } 1325 | .fa-wheelchair:before { 1326 | content: "\f193"; 1327 | } 1328 | .fa-vimeo-square:before { 1329 | content: "\f194"; 1330 | } 1331 | .fa-turkish-lira:before, 1332 | .fa-try:before { 1333 | content: "\f195"; 1334 | } 1335 | .fa-plus-square-o:before { 1336 | content: "\f196"; 1337 | } 1338 | .fa-space-shuttle:before { 1339 | content: "\f197"; 1340 | } 1341 | .fa-slack:before { 1342 | content: "\f198"; 1343 | } 1344 | .fa-envelope-square:before { 1345 | content: "\f199"; 1346 | } 1347 | .fa-wordpress:before { 1348 | content: "\f19a"; 1349 | } 1350 | .fa-openid:before { 1351 | content: "\f19b"; 1352 | } 1353 | .fa-institution:before, 1354 | .fa-bank:before, 1355 | .fa-university:before { 1356 | content: "\f19c"; 1357 | } 1358 | .fa-mortar-board:before, 1359 | .fa-graduation-cap:before { 1360 | content: "\f19d"; 1361 | } 1362 | .fa-yahoo:before { 1363 | content: "\f19e"; 1364 | } 1365 | .fa-google:before { 1366 | content: "\f1a0"; 1367 | } 1368 | .fa-reddit:before { 1369 | content: "\f1a1"; 1370 | } 1371 | .fa-reddit-square:before { 1372 | content: "\f1a2"; 1373 | } 1374 | .fa-stumbleupon-circle:before { 1375 | content: "\f1a3"; 1376 | } 1377 | .fa-stumbleupon:before { 1378 | content: "\f1a4"; 1379 | } 1380 | .fa-delicious:before { 1381 | content: "\f1a5"; 1382 | } 1383 | .fa-digg:before { 1384 | content: "\f1a6"; 1385 | } 1386 | .fa-pied-piper-pp:before { 1387 | content: "\f1a7"; 1388 | } 1389 | .fa-pied-piper-alt:before { 1390 | content: "\f1a8"; 1391 | } 1392 | .fa-drupal:before { 1393 | content: "\f1a9"; 1394 | } 1395 | .fa-joomla:before { 1396 | content: "\f1aa"; 1397 | } 1398 | .fa-language:before { 1399 | content: "\f1ab"; 1400 | } 1401 | .fa-fax:before { 1402 | content: "\f1ac"; 1403 | } 1404 | .fa-building:before { 1405 | content: "\f1ad"; 1406 | } 1407 | .fa-child:before { 1408 | content: "\f1ae"; 1409 | } 1410 | .fa-paw:before { 1411 | content: "\f1b0"; 1412 | } 1413 | .fa-spoon:before { 1414 | content: "\f1b1"; 1415 | } 1416 | .fa-cube:before { 1417 | content: "\f1b2"; 1418 | } 1419 | .fa-cubes:before { 1420 | content: "\f1b3"; 1421 | } 1422 | .fa-behance:before { 1423 | content: "\f1b4"; 1424 | } 1425 | .fa-behance-square:before { 1426 | content: "\f1b5"; 1427 | } 1428 | .fa-steam:before { 1429 | content: "\f1b6"; 1430 | } 1431 | .fa-steam-square:before { 1432 | content: "\f1b7"; 1433 | } 1434 | .fa-recycle:before { 1435 | content: "\f1b8"; 1436 | } 1437 | .fa-automobile:before, 1438 | .fa-car:before { 1439 | content: "\f1b9"; 1440 | } 1441 | .fa-cab:before, 1442 | .fa-taxi:before { 1443 | content: "\f1ba"; 1444 | } 1445 | .fa-tree:before { 1446 | content: "\f1bb"; 1447 | } 1448 | .fa-spotify:before { 1449 | content: "\f1bc"; 1450 | } 1451 | .fa-deviantart:before { 1452 | content: "\f1bd"; 1453 | } 1454 | .fa-soundcloud:before { 1455 | content: "\f1be"; 1456 | } 1457 | .fa-database:before { 1458 | content: "\f1c0"; 1459 | } 1460 | .fa-file-pdf-o:before { 1461 | content: "\f1c1"; 1462 | } 1463 | .fa-file-word-o:before { 1464 | content: "\f1c2"; 1465 | } 1466 | .fa-file-excel-o:before { 1467 | content: "\f1c3"; 1468 | } 1469 | .fa-file-powerpoint-o:before { 1470 | content: "\f1c4"; 1471 | } 1472 | .fa-file-photo-o:before, 1473 | .fa-file-picture-o:before, 1474 | .fa-file-image-o:before { 1475 | content: "\f1c5"; 1476 | } 1477 | .fa-file-zip-o:before, 1478 | .fa-file-archive-o:before { 1479 | content: "\f1c6"; 1480 | } 1481 | .fa-file-sound-o:before, 1482 | .fa-file-audio-o:before { 1483 | content: "\f1c7"; 1484 | } 1485 | .fa-file-movie-o:before, 1486 | .fa-file-video-o:before { 1487 | content: "\f1c8"; 1488 | } 1489 | .fa-file-code-o:before { 1490 | content: "\f1c9"; 1491 | } 1492 | .fa-vine:before { 1493 | content: "\f1ca"; 1494 | } 1495 | .fa-codepen:before { 1496 | content: "\f1cb"; 1497 | } 1498 | .fa-jsfiddle:before { 1499 | content: "\f1cc"; 1500 | } 1501 | .fa-life-bouy:before, 1502 | .fa-life-buoy:before, 1503 | .fa-life-saver:before, 1504 | .fa-support:before, 1505 | .fa-life-ring:before { 1506 | content: "\f1cd"; 1507 | } 1508 | .fa-circle-o-notch:before { 1509 | content: "\f1ce"; 1510 | } 1511 | .fa-ra:before, 1512 | .fa-resistance:before, 1513 | .fa-rebel:before { 1514 | content: "\f1d0"; 1515 | } 1516 | .fa-ge:before, 1517 | .fa-empire:before { 1518 | content: "\f1d1"; 1519 | } 1520 | .fa-git-square:before { 1521 | content: "\f1d2"; 1522 | } 1523 | .fa-git:before { 1524 | content: "\f1d3"; 1525 | } 1526 | .fa-y-combinator-square:before, 1527 | .fa-yc-square:before, 1528 | .fa-hacker-news:before { 1529 | content: "\f1d4"; 1530 | } 1531 | .fa-tencent-weibo:before { 1532 | content: "\f1d5"; 1533 | } 1534 | .fa-qq:before { 1535 | content: "\f1d6"; 1536 | } 1537 | .fa-wechat:before, 1538 | .fa-weixin:before { 1539 | content: "\f1d7"; 1540 | } 1541 | .fa-send:before, 1542 | .fa-paper-plane:before { 1543 | content: "\f1d8"; 1544 | } 1545 | .fa-send-o:before, 1546 | .fa-paper-plane-o:before { 1547 | content: "\f1d9"; 1548 | } 1549 | .fa-history:before { 1550 | content: "\f1da"; 1551 | } 1552 | .fa-circle-thin:before { 1553 | content: "\f1db"; 1554 | } 1555 | .fa-header:before { 1556 | content: "\f1dc"; 1557 | } 1558 | .fa-paragraph:before { 1559 | content: "\f1dd"; 1560 | } 1561 | .fa-sliders:before { 1562 | content: "\f1de"; 1563 | } 1564 | .fa-share-alt:before { 1565 | content: "\f1e0"; 1566 | } 1567 | .fa-share-alt-square:before { 1568 | content: "\f1e1"; 1569 | } 1570 | .fa-bomb:before { 1571 | content: "\f1e2"; 1572 | } 1573 | .fa-soccer-ball-o:before, 1574 | .fa-futbol-o:before { 1575 | content: "\f1e3"; 1576 | } 1577 | .fa-tty:before { 1578 | content: "\f1e4"; 1579 | } 1580 | .fa-binoculars:before { 1581 | content: "\f1e5"; 1582 | } 1583 | .fa-plug:before { 1584 | content: "\f1e6"; 1585 | } 1586 | .fa-slideshare:before { 1587 | content: "\f1e7"; 1588 | } 1589 | .fa-twitch:before { 1590 | content: "\f1e8"; 1591 | } 1592 | .fa-yelp:before { 1593 | content: "\f1e9"; 1594 | } 1595 | .fa-newspaper-o:before { 1596 | content: "\f1ea"; 1597 | } 1598 | .fa-wifi:before { 1599 | content: "\f1eb"; 1600 | } 1601 | .fa-calculator:before { 1602 | content: "\f1ec"; 1603 | } 1604 | .fa-paypal:before { 1605 | content: "\f1ed"; 1606 | } 1607 | .fa-google-wallet:before { 1608 | content: "\f1ee"; 1609 | } 1610 | .fa-cc-visa:before { 1611 | content: "\f1f0"; 1612 | } 1613 | .fa-cc-mastercard:before { 1614 | content: "\f1f1"; 1615 | } 1616 | .fa-cc-discover:before { 1617 | content: "\f1f2"; 1618 | } 1619 | .fa-cc-amex:before { 1620 | content: "\f1f3"; 1621 | } 1622 | .fa-cc-paypal:before { 1623 | content: "\f1f4"; 1624 | } 1625 | .fa-cc-stripe:before { 1626 | content: "\f1f5"; 1627 | } 1628 | .fa-bell-slash:before { 1629 | content: "\f1f6"; 1630 | } 1631 | .fa-bell-slash-o:before { 1632 | content: "\f1f7"; 1633 | } 1634 | .fa-trash:before { 1635 | content: "\f1f8"; 1636 | } 1637 | .fa-copyright:before { 1638 | content: "\f1f9"; 1639 | } 1640 | .fa-at:before { 1641 | content: "\f1fa"; 1642 | } 1643 | .fa-eyedropper:before { 1644 | content: "\f1fb"; 1645 | } 1646 | .fa-paint-brush:before { 1647 | content: "\f1fc"; 1648 | } 1649 | .fa-birthday-cake:before { 1650 | content: "\f1fd"; 1651 | } 1652 | .fa-area-chart:before { 1653 | content: "\f1fe"; 1654 | } 1655 | .fa-pie-chart:before { 1656 | content: "\f200"; 1657 | } 1658 | .fa-line-chart:before { 1659 | content: "\f201"; 1660 | } 1661 | .fa-lastfm:before { 1662 | content: "\f202"; 1663 | } 1664 | .fa-lastfm-square:before { 1665 | content: "\f203"; 1666 | } 1667 | .fa-toggle-off:before { 1668 | content: "\f204"; 1669 | } 1670 | .fa-toggle-on:before { 1671 | content: "\f205"; 1672 | } 1673 | .fa-bicycle:before { 1674 | content: "\f206"; 1675 | } 1676 | .fa-bus:before { 1677 | content: "\f207"; 1678 | } 1679 | .fa-ioxhost:before { 1680 | content: "\f208"; 1681 | } 1682 | .fa-angellist:before { 1683 | content: "\f209"; 1684 | } 1685 | .fa-cc:before { 1686 | content: "\f20a"; 1687 | } 1688 | .fa-shekel:before, 1689 | .fa-sheqel:before, 1690 | .fa-ils:before { 1691 | content: "\f20b"; 1692 | } 1693 | .fa-meanpath:before { 1694 | content: "\f20c"; 1695 | } 1696 | .fa-buysellads:before { 1697 | content: "\f20d"; 1698 | } 1699 | .fa-connectdevelop:before { 1700 | content: "\f20e"; 1701 | } 1702 | .fa-dashcube:before { 1703 | content: "\f210"; 1704 | } 1705 | .fa-forumbee:before { 1706 | content: "\f211"; 1707 | } 1708 | .fa-leanpub:before { 1709 | content: "\f212"; 1710 | } 1711 | .fa-sellsy:before { 1712 | content: "\f213"; 1713 | } 1714 | .fa-shirtsinbulk:before { 1715 | content: "\f214"; 1716 | } 1717 | .fa-simplybuilt:before { 1718 | content: "\f215"; 1719 | } 1720 | .fa-skyatlas:before { 1721 | content: "\f216"; 1722 | } 1723 | .fa-cart-plus:before { 1724 | content: "\f217"; 1725 | } 1726 | .fa-cart-arrow-down:before { 1727 | content: "\f218"; 1728 | } 1729 | .fa-diamond:before { 1730 | content: "\f219"; 1731 | } 1732 | .fa-ship:before { 1733 | content: "\f21a"; 1734 | } 1735 | .fa-user-secret:before { 1736 | content: "\f21b"; 1737 | } 1738 | .fa-motorcycle:before { 1739 | content: "\f21c"; 1740 | } 1741 | .fa-street-view:before { 1742 | content: "\f21d"; 1743 | } 1744 | .fa-heartbeat:before { 1745 | content: "\f21e"; 1746 | } 1747 | .fa-venus:before { 1748 | content: "\f221"; 1749 | } 1750 | .fa-mars:before { 1751 | content: "\f222"; 1752 | } 1753 | .fa-mercury:before { 1754 | content: "\f223"; 1755 | } 1756 | .fa-intersex:before, 1757 | .fa-transgender:before { 1758 | content: "\f224"; 1759 | } 1760 | .fa-transgender-alt:before { 1761 | content: "\f225"; 1762 | } 1763 | .fa-venus-double:before { 1764 | content: "\f226"; 1765 | } 1766 | .fa-mars-double:before { 1767 | content: "\f227"; 1768 | } 1769 | .fa-venus-mars:before { 1770 | content: "\f228"; 1771 | } 1772 | .fa-mars-stroke:before { 1773 | content: "\f229"; 1774 | } 1775 | .fa-mars-stroke-v:before { 1776 | content: "\f22a"; 1777 | } 1778 | .fa-mars-stroke-h:before { 1779 | content: "\f22b"; 1780 | } 1781 | .fa-neuter:before { 1782 | content: "\f22c"; 1783 | } 1784 | .fa-genderless:before { 1785 | content: "\f22d"; 1786 | } 1787 | .fa-facebook-official:before { 1788 | content: "\f230"; 1789 | } 1790 | .fa-pinterest-p:before { 1791 | content: "\f231"; 1792 | } 1793 | .fa-whatsapp:before { 1794 | content: "\f232"; 1795 | } 1796 | .fa-server:before { 1797 | content: "\f233"; 1798 | } 1799 | .fa-user-plus:before { 1800 | content: "\f234"; 1801 | } 1802 | .fa-user-times:before { 1803 | content: "\f235"; 1804 | } 1805 | .fa-hotel:before, 1806 | .fa-bed:before { 1807 | content: "\f236"; 1808 | } 1809 | .fa-viacoin:before { 1810 | content: "\f237"; 1811 | } 1812 | .fa-train:before { 1813 | content: "\f238"; 1814 | } 1815 | .fa-subway:before { 1816 | content: "\f239"; 1817 | } 1818 | .fa-medium:before { 1819 | content: "\f23a"; 1820 | } 1821 | .fa-yc:before, 1822 | .fa-y-combinator:before { 1823 | content: "\f23b"; 1824 | } 1825 | .fa-optin-monster:before { 1826 | content: "\f23c"; 1827 | } 1828 | .fa-opencart:before { 1829 | content: "\f23d"; 1830 | } 1831 | .fa-expeditedssl:before { 1832 | content: "\f23e"; 1833 | } 1834 | .fa-battery-4:before, 1835 | .fa-battery:before, 1836 | .fa-battery-full:before { 1837 | content: "\f240"; 1838 | } 1839 | .fa-battery-3:before, 1840 | .fa-battery-three-quarters:before { 1841 | content: "\f241"; 1842 | } 1843 | .fa-battery-2:before, 1844 | .fa-battery-half:before { 1845 | content: "\f242"; 1846 | } 1847 | .fa-battery-1:before, 1848 | .fa-battery-quarter:before { 1849 | content: "\f243"; 1850 | } 1851 | .fa-battery-0:before, 1852 | .fa-battery-empty:before { 1853 | content: "\f244"; 1854 | } 1855 | .fa-mouse-pointer:before { 1856 | content: "\f245"; 1857 | } 1858 | .fa-i-cursor:before { 1859 | content: "\f246"; 1860 | } 1861 | .fa-object-group:before { 1862 | content: "\f247"; 1863 | } 1864 | .fa-object-ungroup:before { 1865 | content: "\f248"; 1866 | } 1867 | .fa-sticky-note:before { 1868 | content: "\f249"; 1869 | } 1870 | .fa-sticky-note-o:before { 1871 | content: "\f24a"; 1872 | } 1873 | .fa-cc-jcb:before { 1874 | content: "\f24b"; 1875 | } 1876 | .fa-cc-diners-club:before { 1877 | content: "\f24c"; 1878 | } 1879 | .fa-clone:before { 1880 | content: "\f24d"; 1881 | } 1882 | .fa-balance-scale:before { 1883 | content: "\f24e"; 1884 | } 1885 | .fa-hourglass-o:before { 1886 | content: "\f250"; 1887 | } 1888 | .fa-hourglass-1:before, 1889 | .fa-hourglass-start:before { 1890 | content: "\f251"; 1891 | } 1892 | .fa-hourglass-2:before, 1893 | .fa-hourglass-half:before { 1894 | content: "\f252"; 1895 | } 1896 | .fa-hourglass-3:before, 1897 | .fa-hourglass-end:before { 1898 | content: "\f253"; 1899 | } 1900 | .fa-hourglass:before { 1901 | content: "\f254"; 1902 | } 1903 | .fa-hand-grab-o:before, 1904 | .fa-hand-rock-o:before { 1905 | content: "\f255"; 1906 | } 1907 | .fa-hand-stop-o:before, 1908 | .fa-hand-paper-o:before { 1909 | content: "\f256"; 1910 | } 1911 | .fa-hand-scissors-o:before { 1912 | content: "\f257"; 1913 | } 1914 | .fa-hand-lizard-o:before { 1915 | content: "\f258"; 1916 | } 1917 | .fa-hand-spock-o:before { 1918 | content: "\f259"; 1919 | } 1920 | .fa-hand-pointer-o:before { 1921 | content: "\f25a"; 1922 | } 1923 | .fa-hand-peace-o:before { 1924 | content: "\f25b"; 1925 | } 1926 | .fa-trademark:before { 1927 | content: "\f25c"; 1928 | } 1929 | .fa-registered:before { 1930 | content: "\f25d"; 1931 | } 1932 | .fa-creative-commons:before { 1933 | content: "\f25e"; 1934 | } 1935 | .fa-gg:before { 1936 | content: "\f260"; 1937 | } 1938 | .fa-gg-circle:before { 1939 | content: "\f261"; 1940 | } 1941 | .fa-tripadvisor:before { 1942 | content: "\f262"; 1943 | } 1944 | .fa-odnoklassniki:before { 1945 | content: "\f263"; 1946 | } 1947 | .fa-odnoklassniki-square:before { 1948 | content: "\f264"; 1949 | } 1950 | .fa-get-pocket:before { 1951 | content: "\f265"; 1952 | } 1953 | .fa-wikipedia-w:before { 1954 | content: "\f266"; 1955 | } 1956 | .fa-safari:before { 1957 | content: "\f267"; 1958 | } 1959 | .fa-chrome:before { 1960 | content: "\f268"; 1961 | } 1962 | .fa-firefox:before { 1963 | content: "\f269"; 1964 | } 1965 | .fa-opera:before { 1966 | content: "\f26a"; 1967 | } 1968 | .fa-internet-explorer:before { 1969 | content: "\f26b"; 1970 | } 1971 | .fa-tv:before, 1972 | .fa-television:before { 1973 | content: "\f26c"; 1974 | } 1975 | .fa-contao:before { 1976 | content: "\f26d"; 1977 | } 1978 | .fa-500px:before { 1979 | content: "\f26e"; 1980 | } 1981 | .fa-amazon:before { 1982 | content: "\f270"; 1983 | } 1984 | .fa-calendar-plus-o:before { 1985 | content: "\f271"; 1986 | } 1987 | .fa-calendar-minus-o:before { 1988 | content: "\f272"; 1989 | } 1990 | .fa-calendar-times-o:before { 1991 | content: "\f273"; 1992 | } 1993 | .fa-calendar-check-o:before { 1994 | content: "\f274"; 1995 | } 1996 | .fa-industry:before { 1997 | content: "\f275"; 1998 | } 1999 | .fa-map-pin:before { 2000 | content: "\f276"; 2001 | } 2002 | .fa-map-signs:before { 2003 | content: "\f277"; 2004 | } 2005 | .fa-map-o:before { 2006 | content: "\f278"; 2007 | } 2008 | .fa-map:before { 2009 | content: "\f279"; 2010 | } 2011 | .fa-commenting:before { 2012 | content: "\f27a"; 2013 | } 2014 | .fa-commenting-o:before { 2015 | content: "\f27b"; 2016 | } 2017 | .fa-houzz:before { 2018 | content: "\f27c"; 2019 | } 2020 | .fa-vimeo:before { 2021 | content: "\f27d"; 2022 | } 2023 | .fa-black-tie:before { 2024 | content: "\f27e"; 2025 | } 2026 | .fa-fonticons:before { 2027 | content: "\f280"; 2028 | } 2029 | .fa-reddit-alien:before { 2030 | content: "\f281"; 2031 | } 2032 | .fa-edge:before { 2033 | content: "\f282"; 2034 | } 2035 | .fa-credit-card-alt:before { 2036 | content: "\f283"; 2037 | } 2038 | .fa-codiepie:before { 2039 | content: "\f284"; 2040 | } 2041 | .fa-modx:before { 2042 | content: "\f285"; 2043 | } 2044 | .fa-fort-awesome:before { 2045 | content: "\f286"; 2046 | } 2047 | .fa-usb:before { 2048 | content: "\f287"; 2049 | } 2050 | .fa-product-hunt:before { 2051 | content: "\f288"; 2052 | } 2053 | .fa-mixcloud:before { 2054 | content: "\f289"; 2055 | } 2056 | .fa-scribd:before { 2057 | content: "\f28a"; 2058 | } 2059 | .fa-pause-circle:before { 2060 | content: "\f28b"; 2061 | } 2062 | .fa-pause-circle-o:before { 2063 | content: "\f28c"; 2064 | } 2065 | .fa-stop-circle:before { 2066 | content: "\f28d"; 2067 | } 2068 | .fa-stop-circle-o:before { 2069 | content: "\f28e"; 2070 | } 2071 | .fa-shopping-bag:before { 2072 | content: "\f290"; 2073 | } 2074 | .fa-shopping-basket:before { 2075 | content: "\f291"; 2076 | } 2077 | .fa-hashtag:before { 2078 | content: "\f292"; 2079 | } 2080 | .fa-bluetooth:before { 2081 | content: "\f293"; 2082 | } 2083 | .fa-bluetooth-b:before { 2084 | content: "\f294"; 2085 | } 2086 | .fa-percent:before { 2087 | content: "\f295"; 2088 | } 2089 | .fa-gitlab:before { 2090 | content: "\f296"; 2091 | } 2092 | .fa-wpbeginner:before { 2093 | content: "\f297"; 2094 | } 2095 | .fa-wpforms:before { 2096 | content: "\f298"; 2097 | } 2098 | .fa-envira:before { 2099 | content: "\f299"; 2100 | } 2101 | .fa-universal-access:before { 2102 | content: "\f29a"; 2103 | } 2104 | .fa-wheelchair-alt:before { 2105 | content: "\f29b"; 2106 | } 2107 | .fa-question-circle-o:before { 2108 | content: "\f29c"; 2109 | } 2110 | .fa-blind:before { 2111 | content: "\f29d"; 2112 | } 2113 | .fa-audio-description:before { 2114 | content: "\f29e"; 2115 | } 2116 | .fa-volume-control-phone:before { 2117 | content: "\f2a0"; 2118 | } 2119 | .fa-braille:before { 2120 | content: "\f2a1"; 2121 | } 2122 | .fa-assistive-listening-systems:before { 2123 | content: "\f2a2"; 2124 | } 2125 | .fa-asl-interpreting:before, 2126 | .fa-american-sign-language-interpreting:before { 2127 | content: "\f2a3"; 2128 | } 2129 | .fa-deafness:before, 2130 | .fa-hard-of-hearing:before, 2131 | .fa-deaf:before { 2132 | content: "\f2a4"; 2133 | } 2134 | .fa-glide:before { 2135 | content: "\f2a5"; 2136 | } 2137 | .fa-glide-g:before { 2138 | content: "\f2a6"; 2139 | } 2140 | .fa-signing:before, 2141 | .fa-sign-language:before { 2142 | content: "\f2a7"; 2143 | } 2144 | .fa-low-vision:before { 2145 | content: "\f2a8"; 2146 | } 2147 | .fa-viadeo:before { 2148 | content: "\f2a9"; 2149 | } 2150 | .fa-viadeo-square:before { 2151 | content: "\f2aa"; 2152 | } 2153 | .fa-snapchat:before { 2154 | content: "\f2ab"; 2155 | } 2156 | .fa-snapchat-ghost:before { 2157 | content: "\f2ac"; 2158 | } 2159 | .fa-snapchat-square:before { 2160 | content: "\f2ad"; 2161 | } 2162 | .fa-pied-piper:before { 2163 | content: "\f2ae"; 2164 | } 2165 | .fa-first-order:before { 2166 | content: "\f2b0"; 2167 | } 2168 | .fa-yoast:before { 2169 | content: "\f2b1"; 2170 | } 2171 | .fa-themeisle:before { 2172 | content: "\f2b2"; 2173 | } 2174 | .fa-google-plus-circle:before, 2175 | .fa-google-plus-official:before { 2176 | content: "\f2b3"; 2177 | } 2178 | .fa-fa:before, 2179 | .fa-font-awesome:before { 2180 | content: "\f2b4"; 2181 | } 2182 | .fa-handshake-o:before { 2183 | content: "\f2b5"; 2184 | } 2185 | .fa-envelope-open:before { 2186 | content: "\f2b6"; 2187 | } 2188 | .fa-envelope-open-o:before { 2189 | content: "\f2b7"; 2190 | } 2191 | .fa-linode:before { 2192 | content: "\f2b8"; 2193 | } 2194 | .fa-address-book:before { 2195 | content: "\f2b9"; 2196 | } 2197 | .fa-address-book-o:before { 2198 | content: "\f2ba"; 2199 | } 2200 | .fa-vcard:before, 2201 | .fa-address-card:before { 2202 | content: "\f2bb"; 2203 | } 2204 | .fa-vcard-o:before, 2205 | .fa-address-card-o:before { 2206 | content: "\f2bc"; 2207 | } 2208 | .fa-user-circle:before { 2209 | content: "\f2bd"; 2210 | } 2211 | .fa-user-circle-o:before { 2212 | content: "\f2be"; 2213 | } 2214 | .fa-user-o:before { 2215 | content: "\f2c0"; 2216 | } 2217 | .fa-id-badge:before { 2218 | content: "\f2c1"; 2219 | } 2220 | .fa-drivers-license:before, 2221 | .fa-id-card:before { 2222 | content: "\f2c2"; 2223 | } 2224 | .fa-drivers-license-o:before, 2225 | .fa-id-card-o:before { 2226 | content: "\f2c3"; 2227 | } 2228 | .fa-quora:before { 2229 | content: "\f2c4"; 2230 | } 2231 | .fa-free-code-camp:before { 2232 | content: "\f2c5"; 2233 | } 2234 | .fa-telegram:before { 2235 | content: "\f2c6"; 2236 | } 2237 | .fa-thermometer-4:before, 2238 | .fa-thermometer:before, 2239 | .fa-thermometer-full:before { 2240 | content: "\f2c7"; 2241 | } 2242 | .fa-thermometer-3:before, 2243 | .fa-thermometer-three-quarters:before { 2244 | content: "\f2c8"; 2245 | } 2246 | .fa-thermometer-2:before, 2247 | .fa-thermometer-half:before { 2248 | content: "\f2c9"; 2249 | } 2250 | .fa-thermometer-1:before, 2251 | .fa-thermometer-quarter:before { 2252 | content: "\f2ca"; 2253 | } 2254 | .fa-thermometer-0:before, 2255 | .fa-thermometer-empty:before { 2256 | content: "\f2cb"; 2257 | } 2258 | .fa-shower:before { 2259 | content: "\f2cc"; 2260 | } 2261 | .fa-bathtub:before, 2262 | .fa-s15:before, 2263 | .fa-bath:before { 2264 | content: "\f2cd"; 2265 | } 2266 | .fa-podcast:before { 2267 | content: "\f2ce"; 2268 | } 2269 | .fa-window-maximize:before { 2270 | content: "\f2d0"; 2271 | } 2272 | .fa-window-minimize:before { 2273 | content: "\f2d1"; 2274 | } 2275 | .fa-window-restore:before { 2276 | content: "\f2d2"; 2277 | } 2278 | .fa-times-rectangle:before, 2279 | .fa-window-close:before { 2280 | content: "\f2d3"; 2281 | } 2282 | .fa-times-rectangle-o:before, 2283 | .fa-window-close-o:before { 2284 | content: "\f2d4"; 2285 | } 2286 | .fa-bandcamp:before { 2287 | content: "\f2d5"; 2288 | } 2289 | .fa-grav:before { 2290 | content: "\f2d6"; 2291 | } 2292 | .fa-etsy:before { 2293 | content: "\f2d7"; 2294 | } 2295 | .fa-imdb:before { 2296 | content: "\f2d8"; 2297 | } 2298 | .fa-ravelry:before { 2299 | content: "\f2d9"; 2300 | } 2301 | .fa-eercast:before { 2302 | content: "\f2da"; 2303 | } 2304 | .fa-microchip:before { 2305 | content: "\f2db"; 2306 | } 2307 | .fa-snowflake-o:before { 2308 | content: "\f2dc"; 2309 | } 2310 | .fa-superpowers:before { 2311 | content: "\f2dd"; 2312 | } 2313 | .fa-wpexplorer:before { 2314 | content: "\f2de"; 2315 | } 2316 | .fa-meetup:before { 2317 | content: "\f2e0"; 2318 | } 2319 | .sr-only { 2320 | position: absolute; 2321 | width: 1px; 2322 | height: 1px; 2323 | padding: 0; 2324 | margin: -1px; 2325 | overflow: hidden; 2326 | clip: rect(0, 0, 0, 0); 2327 | border: 0; 2328 | } 2329 | .sr-only-focusable:active, 2330 | .sr-only-focusable:focus { 2331 | position: static; 2332 | width: auto; 2333 | height: auto; 2334 | margin: 0; 2335 | overflow: visible; 2336 | clip: auto; 2337 | } 2338 | -------------------------------------------------------------------------------- /web/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} 5 | -------------------------------------------------------------------------------- /web/css/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | color: #666; 5 | box-sizing: border-box; 6 | font-family: sans-serif; 7 | } 8 | .fa-bars { 9 | position: absolute; 10 | font-size: 2rem; 11 | top: 1.5rem; 12 | left: 2rem; 13 | z-index: 10; 14 | } 15 | nav { 16 | position:fixed; 17 | top:0; 18 | left:0; 19 | width:250px; 20 | height:100%; 21 | margin:0 0 0 -250px; 22 | -moz-transition:all 200ms ease-in; 23 | -webkit-transition:all 200ms ease-in; 24 | -o-transition:all 200ms ease-in; 25 | transition:all 200ms ease-in; 26 | background: #ddd; 27 | font-size: 2rem; 28 | } 29 | .fa-bars:hover + nav { 30 | margin: 0; 31 | } 32 | nav:hover { 33 | margin: 0; 34 | } 35 | nav ul { 36 | font-size: 1.2rem; 37 | margin: 5rem 0 0 1rem; 38 | width:250px; 39 | padding:0; 40 | list-style:none; 41 | overflow:hidden; 42 | } 43 | nav li { 44 | padding: 0.25rem 1rem; 45 | } 46 | #content { 47 | width: 100%; 48 | height: 99vh; 49 | } 50 | h1, h2, h3, h4, h5, p, #content ul, #content ol { 51 | max-width: 700px; 52 | margin: 1rem auto; 53 | } 54 | h1 { 55 | margin-top: 5rem; 56 | } 57 | .index h1 { 58 | position: absolute; 59 | top: 50px; 60 | left: 0; 61 | right: 0; 62 | margin-top: 0; 63 | margin-left: auto; 64 | margin-right: auto; 65 | width: 700px; 66 | text-align: center; 67 | color: rgba(0,0,0,0.25); 68 | } 69 | a { 70 | text-decoration: none; 71 | color: rgba(0,132,180,0.8); 72 | } 73 | a:hover { 74 | text-decoration: underline; 75 | color: rgba(0,132,180,1.0); 76 | 77 | } 78 | 79 | #content a { 80 | 81 | } 82 | #site-credits { 83 | position: absolute; 84 | left: 1rem; 85 | bottom: 1rem; 86 | font-size: 1rem; 87 | } -------------------------------------------------------------------------------- /web/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davekinkead/reasons/07ac8e99a9651fdebddc65ac489a72c6a3eaacb0/web/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /web/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davekinkead/reasons/07ac8e99a9651fdebddc65ac489a72c6a3eaacb0/web/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /web/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davekinkead/reasons/07ac8e99a9651fdebddc65ac489a72c6a3eaacb0/web/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /web/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davekinkead/reasons/07ac8e99a9651fdebddc65ac489a72c6a3eaacb0/web/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /web/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davekinkead/reasons/07ac8e99a9651fdebddc65ac489a72c6a3eaacb0/web/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /web/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: index 3 | layout: template 4 | permalink: / 5 | --- 6 | 7 | # Reasons.js 8 | 9 | -------------------------------------------------------------------------------- /web/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 |
Made with ❤️ by Dave Kinkead
23 |
24 | 25 | {{ content }} 26 | 27 |
28 | 29 | --------------------------------------------------------------------------------