├── .gitignore ├── LICENSE.txt ├── README.markdown ├── annotator-screenshot.gif ├── bower.json ├── dist ├── annotator.css ├── annotator.js ├── annotator.min.css └── annotator.min.js ├── example ├── css │ ├── annotator.css │ ├── style.css │ └── vendor │ │ └── awesomplete.css ├── index.html └── scripts │ ├── annotation.js │ ├── annotator.js │ ├── app.js │ ├── editor.js │ └── vendor │ └── awesomplete.min.js ├── gulpfile.js ├── package.json └── src ├── index.html ├── scripts ├── annotation.js ├── annotator.js ├── editor.js └── vendor │ └── awesomplete.min.js └── styles ├── annotator.scss ├── components ├── _annotator.scss └── _marker.scss ├── config └── _variables.scss ├── pages └── _layout.scss └── style.scss /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | .DS_Store 11 | 12 | pids 13 | logs 14 | results 15 | 16 | node_modules 17 | bower_components -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2015 DVNC 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.markdown: -------------------------------------------------------------------------------- 1 | # Annotator 2 | 3 | Popover that lets you highlight, share, add notes and tags to any selected text on a page 4 | 5 | *Requires jQuery to be loaded on the page. Autocompletion (for tags) requires awesomplete.js* 6 | 7 | ##### [Demo](http://dvnc.github.io/annotator) 8 | 9 | ![Annotator Screenshot](./annotator-screenshot.gif?raw=true) 10 | 11 | ##### Getting started 12 | 13 | ``` 14 | var annotator = Object.create(Annotator); 15 | annotator.init({ 16 | containerElement: "#book", 17 | annotations: annotations, // Serialized annotations 18 | existingTags: tags, // Array of tags 19 | }); 20 | annotator.startListening(); 21 | 22 | ``` 23 | 24 | --------------- 25 | 26 | ##### TODO 27 | - AJAX call to save annotation to server 28 | - Write tests 29 | 30 | --------------- 31 | 32 | 33 | ##### Development 34 | ``` 35 | npm install 36 | bowser install 37 | ``` 38 | 39 | ``` 40 | gulp 41 | ``` 42 | 43 | Visit http://localhost:8080/ 44 | 45 | 46 | --------------- 47 | **[bharanim](https://twitter.com/bharani91) / [dvnc](http://www.designventures.com/)** 48 | -------------------------------------------------------------------------------- /annotator-screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvnc/annotator/c3ba6e2c42f9f211a8b6790cdcc96d202dbefcc0/annotator-screenshot.gif -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "annotator", 3 | "version": "0.0.1", 4 | "authors": [ 5 | "Bharani " 6 | ], 7 | "description": "Highlight and annotate selected text on a page", 8 | "keywords": [ 9 | "selection", 10 | "tagging", 11 | "annotation" 12 | ], 13 | "license": "MIT", 14 | "private": true, 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "jquery": "~2.1.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dist/annotator.css: -------------------------------------------------------------------------------- 1 | ::-moz-selection { 2 | background: #babaff; } 3 | 4 | ::selection { 5 | background: #babaff; } 6 | 7 | .annotation { 8 | background: rgba(255, 255, 0, 0.05); } 9 | .annotation.shown { 10 | cursor: pointer; } 11 | 12 | #annotation-editor .colors .color.yellow { 13 | background: #FFE79D; } 14 | #annotation-editor .colors .color.green { 15 | background: #BCEFAA; } 16 | #annotation-editor .colors .color.pink { 17 | background: #FD99BB; } 18 | #annotation-editor .colors .color.blue { 19 | background: #A4D7FC; } 20 | 21 | .annotation.yellow { 22 | background: rgba(255, 231, 157, 0.7); } 23 | .annotation.green { 24 | background: rgba(188, 239, 170, 0.7); } 25 | .annotation.pink { 26 | background: rgba(253, 153, 187, 0.7); } 27 | .annotation.blue { 28 | background: rgba(164, 215, 252, 0.7); } 29 | 30 | .temporary { 31 | background: #babaff; } 32 | 33 | #annotation-editor { 34 | margin-top: 50px; 35 | position: relative; 36 | font-family: "Helvetica Neue", Helvetica, sans-serif; 37 | display: none; 38 | position: absolute; 39 | box-shadow: 0 3px 35px rgba(0, 0, 0, 0.21); } 40 | #annotation-editor:after { 41 | content: ''; 42 | position: absolute; 43 | left: 80px; 44 | top: -9px; 45 | width: 0; 46 | height: 0; 47 | border-style: solid; 48 | border-width: 0 10px 10px 10px; 49 | border-color: transparent transparent #ffffff transparent; } 50 | #annotation-editor:before { 51 | content: ''; 52 | position: absolute; 53 | left: 80px; 54 | top: -10px; 55 | width: 0; 56 | height: 0; 57 | border-style: solid; 58 | border-width: 0 10px 10px 10px; 59 | border-color: transparent transparent #bbbbbb transparent; } 60 | #annotation-editor div.awesomplete > ul > li { 61 | position: relative; 62 | padding: .2em .5em; 63 | cursor: pointer; } 64 | #annotation-editor .dropdown-list { 65 | background: #ffffff; 66 | display: inline-block; 67 | width: 180px; 68 | border-radius: 3px; 69 | border: 1px solid #bbbbbb; 70 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 3px 15px rgba(0, 0, 0, 0.15); 71 | margin: 0; 72 | padding: 0; 73 | list-style: none; 74 | z-index: 10; } 75 | #annotation-editor .dropdown-list li { 76 | border-bottom: 1px solid #eee; 77 | position: relative; 78 | padding: 0px 10px; } 79 | #annotation-editor .dropdown-list li.js-remove-annotation-wrapper { 80 | display: none; } 81 | #annotation-editor .dropdown-list li a, #annotation-editor .dropdown-list li span.link { 82 | padding: 5px 0; 83 | text-decoration: none; 84 | display: block; 85 | font-size: 12px; 86 | color: #888888; 87 | line-height: 20px; 88 | font-weight: 400; 89 | text-transform: uppercase; 90 | cursor: pointer; } 91 | #annotation-editor .dropdown-list li:hover { 92 | background-color: #eee; 93 | color: #888888; } 94 | #annotation-editor .dropdown-list li:hover .sub-list { 95 | display: block; } 96 | #annotation-editor .dropdown-list li.js-facebook-share { 97 | display: none; } 98 | #annotation-editor .dropdown-list li.note-input { 99 | padding: 10px; } 100 | #annotation-editor .dropdown-list li.note-input:hover { 101 | background: transparent; } 102 | #annotation-editor .dropdown-list li.note-input form { 103 | margin: 0; } 104 | #annotation-editor .dropdown-list li.note-input textarea, #annotation-editor .dropdown-list li.note-input input[type="text"] { 105 | font-size: 12px; 106 | color: #ddd; 107 | border-width: 0; 108 | background: transparent; 109 | width: 160px; 110 | color: #333; 111 | outline: none; 112 | line-height: 20px; 113 | padding: 0 3px; 114 | border: 1px solid #999; 115 | max-width: 160px; 116 | margin-bottom: 5px; } 117 | #annotation-editor .dropdown-list li.note-input textarea:focus, #annotation-editor .dropdown-list li.note-input input[type="text"]:focus { 118 | border-color: #4d90fe; 119 | box-shadow: 0 0 1px rgba(77, 144, 254, 0.2); } 120 | #annotation-editor .dropdown-list li.note-input textarea { 121 | max-height: 100px; } 122 | #annotation-editor .dropdown-list li.note-input #add-button { 123 | height: 22px; 124 | width: 80px; 125 | font-size: 12px; 126 | color: #fff; 127 | text-align: center; 128 | line-height: 18px; 129 | border-width: 0; 130 | background-color: #4d90fe; 131 | cursor: pointer; 132 | margin-top: 10px; } 133 | #annotation-editor .dropdown-list li .sub-list { 134 | display: none; 135 | position: absolute; 136 | top: 0; 137 | right: -182px; } 138 | #annotation-editor .dropdown-list li.colors { 139 | padding: 8px 10px; } 140 | #annotation-editor .dropdown-list li.colors:hover { 141 | background: transparent; } 142 | #annotation-editor .dropdown-list li.colors .color { 143 | width: 20px; 144 | height: 20px; 145 | padding: 0; 146 | margin: 0; 147 | border-radius: 100%; 148 | display: inline-block; 149 | margin: 0 8px; 150 | margin-top: 5px; 151 | border: 2px solid #eee; 152 | cursor: pointer; } 153 | #annotation-editor .dropdown-list li.colors .color.active { 154 | border-color: #6e6e6e; } 155 | #annotation-editor .dropdown-list li.colors .color.active:hover { 156 | border-color: #6e6e6e; } 157 | #annotation-editor .dropdown-list li.colors .color:hover { 158 | border-color: #a2a2a2; } 159 | 160 | .tag-list { 161 | position: absolute; 162 | left: 0px; } 163 | .tag-list .tag { 164 | display: block; 165 | cursor: pointer; 166 | font-size: 12px; } 167 | .tag-list .tag:hover .tag-text, .tag-list .tag.shown .tag-text { 168 | opacity: 1; } 169 | .tag-list .marker { 170 | padding-right: 4px; 171 | background: #4285f4; 172 | width: 0; 173 | height: 28px; 174 | display: inline-block; 175 | float: left; 176 | background-color: #4285f4; } 177 | .tag-list .tag-text { 178 | color: #000; 179 | height: 28px; 180 | line-height: 28px; 181 | padding: 0 4px; 182 | display: inline-block; 183 | opacity: 0; 184 | overflow: hidden; 185 | background-color: #4285f4; 186 | -webkit-transition: all 0.2s ease-out; 187 | transition: all 0.2s ease-out; } 188 | -------------------------------------------------------------------------------- /dist/annotator.js: -------------------------------------------------------------------------------- 1 | var Annotation = (function Annotation() { 2 | 3 | var Annotation = { 4 | annotator: null, 5 | range: { 6 | startContainerXPath: null, 7 | endContainerXPath: null, 8 | parentContainerXPath: null, 9 | startOffset: null, 10 | endOffset: null 11 | }, 12 | id: null, 13 | selectedText: null, 14 | color: null, 15 | note: null, 16 | tags: [], 17 | 18 | init: function(obj) { 19 | if(!obj) return; 20 | 21 | if(obj.annotator) { 22 | this.annotator = obj.annotator; 23 | } 24 | if(obj.selectedRange) { 25 | this.saveSelection(obj.selectedRange); 26 | } else if(obj.savedAnnotation) { 27 | var savedAnnotation = obj.savedAnnotation; 28 | 29 | this.range = savedAnnotation.range; 30 | this.id = savedAnnotation.id; 31 | this.selectedText = savedAnnotation.selectedText; 32 | this.color = savedAnnotation.color; 33 | this.note = savedAnnotation.note; 34 | this.tags = savedAnnotation.tags; 35 | } 36 | 37 | this.setRangeElements(); 38 | 39 | }, 40 | 41 | setRangeElements: function() { 42 | this.$parentContainer = $(this.annotator.findElementByXPath(this.range.parentContainerXPath)); 43 | this.$startContainer = $(this.annotator.findElementByXPath(this.range.startContainerXPath)); 44 | this.$endContainer = $(this.annotator.findElementByXPath(this.range.endContainerXPath)); 45 | 46 | }, 47 | 48 | render: function(opts) { 49 | if(opts && opts.temporary) { 50 | this.wrapNodes(true); 51 | } else if(opts && opts.convert) { 52 | this.convertFromTemporary(); 53 | } else if(opts && opts.update) { 54 | this.updateAnnotation(); 55 | } else { 56 | this.wrapNodes(); 57 | } 58 | 59 | // remove existing selection 60 | // we don't need it anymore 61 | if (window.getSelection) { 62 | window.getSelection().removeAllRanges(); 63 | } else if (document.selection) { 64 | document.selection.empty(); 65 | } 66 | }, 67 | 68 | updateAnnotation: function() { 69 | var $parentContainer = this.$parentContainer; 70 | 71 | var renderedAnnotation = $parentContainer 72 | .find(".annotation[data-id='" + this.id + "']"); 73 | 74 | renderedAnnotation.removeClass("annotation"); 75 | 76 | renderedAnnotation 77 | .removeAttr("class") 78 | .addClass("annotation") 79 | .addClass(this.color) 80 | .removeAttr("style"); 81 | }, 82 | 83 | saveSelection: function(range) { 84 | var parentContainer = range.commonAncestorContainer; 85 | var startContainer = range.startContainer; 86 | var endContainer = range.endContainer; 87 | 88 | var startOffset = range.startOffset; 89 | var endOffset = range.endOffset; 90 | 91 | var parentNode = this.getParentNodeFor(parentContainer); 92 | var startNode = this.getParentNodeFor(startContainer); 93 | var endNode = this.getParentNodeFor(endContainer); 94 | 95 | var nodesBetweenStart = this.getNodesToWrap(parentNode, startNode.firstChild, startContainer); 96 | var nodesBetweenEnd = this.getNodesToWrap(parentNode, endNode.firstChild, endContainer); 97 | 98 | if(nodesBetweenStart.length) { 99 | for(var i = 0; i < nodesBetweenStart.length; i++) { 100 | var characterLength = nodesBetweenStart[i].nodeValue.length; 101 | startOffset += characterLength; 102 | } 103 | } 104 | 105 | if(nodesBetweenEnd.length) { 106 | for(var i = 0; i < nodesBetweenEnd.length; i++) { 107 | endOffset += nodesBetweenEnd[i].nodeValue.length; 108 | } 109 | } 110 | 111 | var selectedContent = this.getSelectionContent(range); 112 | 113 | this.range = { 114 | startContainerXPath: this.annotator.createXPathFromElement(startNode), 115 | endContainerXPath: this.annotator.createXPathFromElement(endNode), 116 | parentContainerXPath: this.annotator.createXPathFromElement(parentNode), 117 | startOffset: startOffset, 118 | endOffset: endOffset, 119 | }; 120 | 121 | this.id = this.generateRandomID(); 122 | this.selectedText = selectedContent; 123 | }, 124 | 125 | getSelectionContent: function(range) { 126 | var container = document.createElement("div"); 127 | container.appendChild(range.cloneContents()); 128 | var text = container.textContent; 129 | return text; 130 | }, 131 | 132 | getParentNodeFor: function(node) { 133 | 134 | while(node.nodeType != 1 || (node.nodeType == 1 && node.classList.contains("js-no-select")) ) { 135 | node = node.parentNode; 136 | } 137 | 138 | return node; 139 | }, 140 | 141 | generateRandomID: function() { 142 | return 'annotation-' + new Date().getTime(); 143 | }, 144 | 145 | save: function(obj) { 146 | if(!obj) return; 147 | 148 | this.color = obj.color; 149 | 150 | if(obj.note) 151 | this.note = obj.note; 152 | 153 | if(obj.tags && obj.tags.length) { 154 | this.tags = obj.tags 155 | } 156 | 157 | var data = this.serialize(); 158 | 159 | if(obj && obj.debug) { 160 | void 0; 161 | } else { 162 | this.postToRemote(); 163 | } 164 | 165 | if(obj && obj.cbk) obj.cbk(this); 166 | 167 | }, 168 | 169 | // TODO 170 | destroy: function(cbk) { 171 | if(cbk) cbk(); 172 | }, 173 | 174 | // TODO 175 | postToRemote: function() { 176 | void 0; 177 | }, 178 | 179 | serialize: function() { 180 | var range = this.range; 181 | return { 182 | range: { 183 | startOffset: range.startOffset, 184 | endOffset: range.endOffset, 185 | startContainerXPath: range.startContainerXPath, 186 | endContainerXPath: range.endContainerXPath, 187 | parentContainerXPath: range.parentContainerXPath 188 | }, 189 | id: this.id, 190 | selectedText: this.selectedText, 191 | color: this.color, 192 | note: this.note, 193 | tags: this.tags 194 | } 195 | }, 196 | 197 | getContainedNodes: function() { 198 | var range, startContainer, endContainer, parentContainer; 199 | var nodes = []; 200 | 201 | range = this.range; 202 | 203 | parentContainer = this.$parentContainer.get(0); 204 | startContainer = this.$startContainer.get(0); 205 | endContainer = this.$endContainer.get(0); 206 | 207 | var startTextNodeParams = this.getTextNodeAtOffset(startContainer, range.startOffset); 208 | endTextNodeParams = this.getTextNodeAtOffset(endContainer, range.endOffset); 209 | 210 | var startTextNode = startTextNodeParams[0], 211 | startOffset = startTextNodeParams[1], 212 | endTextNode = endTextNodeParams[0], 213 | endOffset = endTextNodeParams[1]; 214 | 215 | 216 | if(startTextNode == endTextNode) { 217 | var startTextNodeSplit = startTextNode.splitText(startOffset); 218 | var endTextNodeSplit = startTextNodeSplit.splitText(endOffset - startOffset); 219 | } else { 220 | var startTextNodeSplit = startTextNode.splitText(startOffset); 221 | var endTextNodeSplit = endTextNode.splitText(endOffset); 222 | } 223 | 224 | 225 | var innerNodes = this.getNodesToWrap(parentContainer, startTextNodeSplit, endTextNodeSplit); 226 | 227 | 228 | for(var i = 0; i < innerNodes.length; i++) { 229 | nodes.push(innerNodes[i]); 230 | } 231 | 232 | return nodes; 233 | }, 234 | 235 | 236 | getTextNodeAtOffset: function(rootNode, offset) { 237 | var textNode, 238 | count = 0, 239 | found = false; 240 | 241 | function getTextNodes(node) { 242 | if (node.nodeType == Node.TEXT_NODE && !/^\s*$/.test(node.nodeValue)) { 243 | if ( found != true) { 244 | if(count+node.nodeValue.length >= offset) { 245 | textNode = node; 246 | found = true; 247 | } else { 248 | count += node.nodeValue.length 249 | } 250 | } 251 | } else if (node.nodeType == Node.ELEMENT_NODE ) { 252 | for (var i = 0, len = node.childNodes.length; i < len; ++i) { 253 | getTextNodes(node.childNodes[i]); 254 | } 255 | } 256 | } 257 | 258 | getTextNodes(rootNode); 259 | return [textNode, (count == 0 ? offset : offset - count)]; 260 | 261 | }, 262 | 263 | getNodesToWrap: function(rootNode, startNode, endNode) { 264 | var pastStartNode = false, reachedEndNode = false, textNodes = []; 265 | 266 | function getTextNodes(node) { 267 | 268 | if (node == startNode) { 269 | pastStartNode = true; 270 | } 271 | if (node == endNode) { 272 | reachedEndNode = true; 273 | } else if (node.nodeType == Node.TEXT_NODE) { 274 | if (pastStartNode && !reachedEndNode && !/^\s*$/.test(node.nodeValue)) { 275 | textNodes.push(node); 276 | } 277 | } else if (node.nodeType == Node.ELEMENT_NODE ) { 278 | 279 | for (var i = 0, len = node.childNodes.length; !reachedEndNode && i < len; ++i) { 280 | getTextNodes(node.childNodes[i]); 281 | } 282 | } 283 | } 284 | 285 | getTextNodes(rootNode); 286 | return textNodes; 287 | }, 288 | 289 | 290 | wrapNodes: function(temporary) { 291 | var nodes = this.getContainedNodes(); 292 | var newNode = this.createWrapperElement(temporary) 293 | for(var i = 0; i < nodes.length; i++) { 294 | $(nodes[i]).wrap(newNode); 295 | } 296 | }, 297 | 298 | createWrapperElement: function(temporary) { 299 | var annotationID = this.id; 300 | 301 | var span = document.createElement("span"); 302 | 303 | if(!temporary) { 304 | span.classList.add("annotation"); 305 | span.classList.add(this.color); 306 | } else { 307 | span.classList.add("temporary"); 308 | } 309 | 310 | span.setAttribute("data-id", annotationID); 311 | 312 | return span; 313 | }, 314 | 315 | removeTemporary: function() { 316 | var temporary = this.$parentContainer.find(".temporary"); 317 | 318 | for(var i = 0; i < temporary.length; i++) { 319 | var elem = temporary[i]; 320 | $(elem.childNodes[0]).unwrap(); 321 | } 322 | }, 323 | 324 | convertFromTemporary: function() { 325 | var temporary = this.$parentContainer.find(".temporary"); 326 | 327 | temporary 328 | .removeClass("temporary") 329 | .addClass("annotation") 330 | .addClass(this.color); 331 | 332 | } 333 | 334 | 335 | }; 336 | 337 | return Annotation; 338 | })(); 339 | var Editor = (function Editor() { 340 | var Editor = { 341 | annotator: null, 342 | annotation: null, 343 | events: [ 344 | { 345 | element: ".js-note-form", 346 | event: "submit", 347 | action: "addNote" 348 | }, 349 | 350 | { 351 | selector: ".js-color-picker", 352 | event: "click", 353 | action: "setColor" 354 | }, 355 | 356 | { 357 | selector: ".js-copy", 358 | event: "click", 359 | action: "copyToClipboard" 360 | }, 361 | 362 | { 363 | selector: ".js-share", 364 | event: "click", 365 | action: "share" 366 | }, 367 | 368 | { 369 | selector: ".js-remove-annotation", 370 | event: "click", 371 | action: "removeAnnotation" 372 | } 373 | ], 374 | 375 | init: function(opts) { 376 | this.annotator = opts.annotator; 377 | var $containerElement = $("body"); 378 | this.$popoverElement = $(this.renderEditorTemplate()); 379 | 380 | $containerElement.append(this.$popoverElement); 381 | 382 | // autocomplete 383 | if(Awesomplete){ 384 | this._awesomplete = new Awesomplete(this.$popoverElement.find(".js-tags-field")[0]); 385 | } 386 | 387 | this.events.forEach(function(eventMap) { 388 | var editor = this; 389 | this.$popoverElement.on(eventMap["event"], eventMap["selector"], function(e) { 390 | e.preventDefault(); 391 | editor[eventMap["action"]].call(editor, e); 392 | }) 393 | }, this); 394 | }, 395 | 396 | renderEditorTemplate: function() { 397 | var html = '
' 398 | + '' 427 | + '
' 428 | ; 429 | 430 | return html; 431 | }, 432 | 433 | showEditor: function(opts) { 434 | var $popover = this.$popoverElement; 435 | 436 | var position = opts.position, 437 | annotation = opts.annotation, 438 | temporary = opts.temporary; 439 | 440 | var top = position.top - 30; 441 | var left = position.left - this.$popoverElement.width()/2; 442 | 443 | if(annotation) { 444 | this.annotation = annotation; 445 | this.activateAnnotationColor(); 446 | this.renderContents(); 447 | 448 | if(!temporary) { 449 | this.showRemoveBtn(); 450 | } 451 | } 452 | 453 | // FB Share 454 | if( !(window.FB) ) this.$popoverElement.find(".js-facebook-share").hide(); 455 | else { this.$popoverElement.find(".js-facebook-share").show(); } 456 | 457 | 458 | if(temporary) { 459 | this.annotation.render({ temporary: true }); 460 | } 461 | 462 | if(this._awesomplete) { 463 | this._awesomplete.list = this.annotator.tags; 464 | } 465 | 466 | $popover.removeClass("anim").css("top", top).css("left", left).show(); 467 | $popover.find("#annotation-input").focus(); 468 | }, 469 | 470 | isVisible: function() { 471 | return this.$popoverElement.is(":visible"); 472 | }, 473 | 474 | reset: function() { 475 | this.annotation.removeTemporary(); 476 | this.resetNoteForm(); 477 | this.hideRemoveBtn(); 478 | this.annotation = null; 479 | this.$popoverElement.removeAttr("style"); 480 | }, 481 | 482 | resetNoteForm: function() { 483 | this.$popoverElement.find(".js-note-field, .js-tags-field").val(""); 484 | }, 485 | 486 | activateAnnotationColor: function() { 487 | this.$popoverElement 488 | .find(".js-color-picker.active").removeClass("active"); 489 | this.$popoverElement 490 | .find(".js-color-picker." + (this.annotation.color || 'yellow')) 491 | .addClass("active"); 492 | }, 493 | 494 | renderContents: function() { 495 | this.$popoverElement.find(".js-note-field").val(this.annotation.note); 496 | 497 | if(this.annotation.tags) 498 | this.$popoverElement.find(".js-tags-field").val(this.annotation.tags.join(", ")); 499 | }, 500 | 501 | showRemoveBtn: function() { 502 | this.$popoverElement.find(".js-remove-annotation-wrapper").show(); 503 | }, 504 | 505 | hideRemoveBtn: function() { 506 | this.$popoverElement.find(".js-remove-annotation-wrapper").hide(); 507 | }, 508 | 509 | hideEditor: function(event) { 510 | this.reset(); 511 | this.$popoverElement.hide(); 512 | }, 513 | 514 | getTagsFromString: function(string) { 515 | var tags = string 516 | .split(this.annotator.tagRegex) 517 | .map(function(tag) { 518 | var t = $.trim(tag); 519 | if(t.length) return t; 520 | }); 521 | return tags; 522 | }, 523 | 524 | setColor: function(e) { 525 | var $target = $(e.target); 526 | var color = $target.data("color"); 527 | var $form = this.$popoverElement.find(".js-note-form"); 528 | 529 | var note = $form.find(".js-note-field").val(); 530 | var tags = this.getTagsFromString($form.find(".js-tags-field").val()); 531 | 532 | this.saveAndClose({ color: color, note: note, tags: tags }); 533 | }, 534 | 535 | addNote: function(e) { 536 | var $form = $(e.target); 537 | var note = $form.find(".js-note-field").val(); 538 | var tags = this.getTagsFromString($form.find(".js-tags-field").val()); 539 | var color = this.annotation.color || this.annotator.defaultColor; 540 | 541 | this.saveAndClose({ color: color, note: note, tags: tags }); 542 | }, 543 | 544 | saveAndClose: function(data) { 545 | if(!data) return; 546 | 547 | var params = { 548 | debug: this.annotator.debug, 549 | cbk: function(annotation) { 550 | if(!this.annotator.findAnnotation(annotation.id)) { 551 | this.annotation.render({ convert: true }); 552 | this.annotator.annotations.push(annotation); 553 | } else { 554 | this.annotator.updateAnnotation(annotation.id, annotation); 555 | this.annotation.render({ update: true }); 556 | } 557 | 558 | // save tags to global list 559 | this.annotator.addTags(annotation.tags); 560 | 561 | if(this.annotator.debug) 562 | this.saveToLocalStorage(); 563 | 564 | this.hideEditor(); 565 | }.bind(this) 566 | } 567 | 568 | $.extend(params, data); 569 | void 0; 570 | this.annotation.save(params); 571 | }, 572 | 573 | copyToClipboard: function() { 574 | var text = this.annotation.selectedText; 575 | 576 | var textarea = $(""); 577 | $(this.annotator.containerElement).append(textarea); 578 | textarea.val(text).select(); 579 | 580 | try { 581 | document.execCommand("copy"); 582 | } catch(e) { 583 | void 0; 584 | } 585 | 586 | this.hideEditor(); 587 | textarea.remove(); 588 | }, 589 | 590 | truncate: function(str, limit) { 591 | 592 | if(str.length <= limit) return str; 593 | 594 | while(str.length >= limit) { 595 | str = str.substr(0, str.lastIndexOf(" ")); 596 | } 597 | 598 | return str + "..."; 599 | }, 600 | 601 | removeAnnotation: function() { 602 | var annotation = this.annotation; 603 | var annotator = this.annotator; 604 | 605 | 606 | if(!annotation) return; 607 | 608 | var renderedAnnotation = $(this.annotator.containerElement) 609 | .find(".annotation[data-id='" + annotation.id + "']"); 610 | 611 | this.annotation.destroy(function() { 612 | annotator.removeAnnotation(annotation.id); 613 | renderedAnnotation.contents().unwrap(); 614 | }); 615 | 616 | if(this.annotator.debug) 617 | this.saveToLocalStorage(); 618 | this.hideEditor(); 619 | }, 620 | 621 | share: function(e) { 622 | var text = this.annotation.selectedText; 623 | var $target = $(e.target); 624 | 625 | if($target.hasClass("facebook")) { 626 | if( !(FB && FB.ui) ) return; 627 | void 0; 628 | FB.ui( 629 | { 630 | method: 'feed', 631 | link: window.location.href, 632 | description: text, 633 | display: "popup" 634 | }, 635 | function(response) { 636 | if (response && !response.error_code) { 637 | void 0; 638 | } else { 639 | void 0; 640 | } 641 | } 642 | ); 643 | 644 | } else if($target.hasClass("twitter")) { 645 | var width = 575, 646 | height = 400, 647 | left = ($(window).width() - width) / 2, 648 | top = ($(window).height() - height) / 2, 649 | opts = 'status=1' + 650 | ',width=' + width + 651 | ',height=' + height + 652 | ',top=' + top + 653 | ',left=' + left, 654 | textLimit = 140 - '...'.length - window.location.href.length, 655 | windowURL = "http://twitter.com/share?text=" + window.encodeURIComponent( this.truncate(text, textLimit) || ""); 656 | 657 | window.open(windowURL, 'Share', opts); 658 | } 659 | 660 | this.hideEditor(); 661 | 662 | }, 663 | 664 | saveToLocalStorage: function() { 665 | // save to localStorage 666 | if(window.localStorage) { 667 | var serializedAnnotations = this.annotator.annotations.map(function(annotation) { 668 | return annotation.serialize(); 669 | }); 670 | 671 | window.localStorage.setItem("annotations", JSON.stringify(serializedAnnotations)); 672 | } 673 | } 674 | 675 | 676 | } 677 | 678 | 679 | return Editor; 680 | })(); 681 | var Annotator = (function Annotator() { 682 | 683 | var Annotator = { 684 | containerElement: "body", 685 | annotations: [], 686 | editor: null, 687 | 688 | defaultColor: "yellow", 689 | colors: [ 690 | { 691 | className: "yellow", 692 | }, 693 | 694 | { 695 | className: "green", 696 | }, 697 | 698 | { 699 | className: "pink", 700 | }, 701 | 702 | { 703 | className: "blue", 704 | }, 705 | ], 706 | 707 | tags: [], 708 | 709 | 710 | init: function(opts) { 711 | this.containerElement = opts.containerElement || "body"; 712 | this.debug = opts.debug || "true"; 713 | this.remoteURL = opts.remoteURL || ""; 714 | 715 | if(opts.existingTags) { 716 | this.tags = opts.existingTags; 717 | } 718 | 719 | if(opts.colors) { 720 | this.colors = opts.colors; 721 | } 722 | 723 | if(opts.annotations) { 724 | this.renderExistingAnnotations(opts.annotations); 725 | } 726 | 727 | // Setup editor 728 | var editor = Object.create(Editor); 729 | editor.init({ annotator: this }); 730 | this.setEditor(editor); 731 | }, 732 | 733 | addTags: function(tags) { 734 | this.tags = this.tags.concat(tags); 735 | }, 736 | 737 | setEditor: function(editor) { 738 | this.editor = editor; 739 | }, 740 | 741 | findAnnotation: function(annotationID) { 742 | return this.annotations.filter(function(annotation) { 743 | return annotation.id == annotationID; 744 | })[0]; 745 | }, 746 | 747 | updateAnnotation: function(annotationID, newAnnotation) { 748 | var index = this.annotations.map(function(i) { return i.id }).indexOf(annotationID); 749 | if(index <= -1) return; 750 | this.annotations[index] = newAnnotation; 751 | }, 752 | 753 | removeAnnotation: function(annotationID) { 754 | var index = this.annotations.map(function(i) { return i.id }).indexOf(annotationID); 755 | if(index <= -1) return; 756 | 757 | this.annotations.splice(index, 1); 758 | }, 759 | 760 | renderExistingAnnotations: function(annotations) { 761 | for(var i = 0; i < annotations.length; i++) { 762 | var annotation = Object.create(Annotation); 763 | annotation.init({ savedAnnotation: annotations[i], annotator: this }); 764 | annotation.render(); 765 | this.annotations.push(annotation); 766 | } 767 | }, 768 | 769 | 770 | handleAnnotationClick: function(e) { 771 | var $target = $(e.target); 772 | var annotationID = $target.data("id"); 773 | var annotation = this.findAnnotation(annotationID); 774 | 775 | if(!annotation) return; 776 | 777 | this.editor.showEditor({ 778 | position: { 779 | top: e.pageY, 780 | left: e.pageX 781 | }, 782 | annotation: annotation 783 | }); 784 | 785 | }, 786 | 787 | handleAnnotation: function(e) { 788 | var selection = window.getSelection(); 789 | var selectedText; 790 | if(selection.text) { 791 | selectedText = selection.text; 792 | } else { 793 | selectedText = selection.toString(); 794 | }; 795 | 796 | 797 | if(selection && !selection.isCollapsed && selectedText && selectedText.length>5) { 798 | var range = selection.getRangeAt(0); 799 | 800 | var annotation = Object.create(Annotation); 801 | annotation.init({ selectedRange: range, annotator: this }); 802 | 803 | var position = { 804 | top: e.pageY, 805 | left: e.pageX 806 | }; 807 | 808 | this.editor.showEditor({ 809 | temporary: true, 810 | position: { 811 | top: e.pageY, 812 | left: e.pageX 813 | }, 814 | annotation: annotation 815 | }); 816 | } 817 | }, 818 | 819 | startListening: function() { 820 | var $element = $(this.containerElement); 821 | var self = this; 822 | 823 | 824 | $element.on("mouseup touchend", function(e) { 825 | e.preventDefault(); 826 | e.stopPropagation(); 827 | 828 | var $target = $(e.target); 829 | 830 | if(self.editor.isVisible() && !$target.parents("#annotation-editor").length && !$target.hasClass("annotation")) { 831 | // editor is open but clicked outside 832 | self.editor.hideEditor() 833 | } 834 | 835 | 836 | if(!$target.parents(".js-no-select").length) { 837 | self.handleAnnotation(e); 838 | } 839 | }); 840 | 841 | $element.on("click", ".annotation", function(e) { 842 | e.stopPropagation(); 843 | self.handleAnnotationClick(e); 844 | }); 845 | 846 | }, 847 | 848 | findElementByXPath: function(path) { 849 | var evaluator = new XPathEvaluator(); 850 | var result = evaluator.evaluate(path, document.querySelector(this.containerElement), null,XPathResult.FIRST_ORDERED_NODE_TYPE, null); 851 | return result.singleNodeValue; 852 | }, 853 | 854 | createXPathFromElement: function(elm) { 855 | var allNodes = document.getElementsByTagName('*'); 856 | 857 | for (var segs = []; elm && elm.nodeType == 1 && elm != document.querySelector(this.containerElement).parentNode; elm = elm.parentNode) { 858 | if (elm.hasAttribute('id')) { 859 | var uniqueIdCount = 0; 860 | for (var n=0;n < allNodes.length;n++) { 861 | if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++; 862 | if (uniqueIdCount > 1) break; 863 | }; 864 | 865 | if ( uniqueIdCount == 1) { 866 | segs.unshift('id("' + elm.getAttribute('id') + '")'); 867 | return segs.join('/'); 868 | } else { 869 | segs.unshift(elm.localName.toLowerCase() + '[@id="' + elm.getAttribute('id') + '"]'); 870 | } 871 | 872 | } else if (elm.hasAttribute('class')) { 873 | segs.unshift(elm.localName.toLowerCase() + '[@class="' + elm.getAttribute('class') + '"]'); 874 | } else { 875 | for (i = 1, sib = elm.previousSibling; sib; sib = sib.previousSibling) { 876 | if (sib.localName == elm.localName) i++; 877 | }; 878 | segs.unshift(elm.localName.toLowerCase() + '[' + i + ']'); 879 | }; 880 | } 881 | 882 | return segs.length ? '' + segs.join('/') : null; 883 | } 884 | 885 | }; 886 | 887 | return Annotator; 888 | })(); -------------------------------------------------------------------------------- /dist/annotator.min.css: -------------------------------------------------------------------------------- 1 | #annotation-editor:after,#annotation-editor:before{content:'';position:absolute;left:80px;width:0;height:0;border-style:solid;border-width:0 10px 10px}::-moz-selection{background:#babaff}::selection{background:#babaff}.annotation{background:rgba(255,255,0,.05)}.annotation.shown{cursor:pointer}#annotation-editor .colors .color.yellow{background:#FFE79D}#annotation-editor .colors .color.green{background:#BCEFAA}#annotation-editor .colors .color.pink{background:#FD99BB}#annotation-editor .colors .color.blue{background:#A4D7FC}.annotation.yellow{background:rgba(255,231,157,.7)}.annotation.green{background:rgba(188,239,170,.7)}.annotation.pink{background:rgba(253,153,187,.7)}.annotation.blue{background:rgba(164,215,252,.7)}.temporary{background:#babaff}#annotation-editor{margin-top:50px;font-family:"Helvetica Neue",Helvetica,sans-serif;display:none;position:absolute;box-shadow:0 3px 35px rgba(0,0,0,.21)}#annotation-editor:after{top:-9px;border-color:transparent transparent #fff}#annotation-editor:before{top:-10px;border-color:transparent transparent #bbb}#annotation-editor div.awesomplete>ul>li{position:relative;padding:.2em .5em;cursor:pointer}#annotation-editor .dropdown-list{background:#fff;display:inline-block;width:180px;border-radius:3px;border:1px solid #bbb;box-shadow:0 1px 2px rgba(0,0,0,.1),0 3px 15px rgba(0,0,0,.15);margin:0;padding:0;list-style:none;z-index:10}#annotation-editor .dropdown-list li{border-bottom:1px solid #eee;position:relative;padding:0 10px}#annotation-editor .dropdown-list li.js-remove-annotation-wrapper{display:none}#annotation-editor .dropdown-list li a,#annotation-editor .dropdown-list li span.link{padding:5px 0;text-decoration:none;display:block;font-size:12px;color:#888;line-height:20px;font-weight:400;text-transform:uppercase;cursor:pointer}#annotation-editor .dropdown-list li:hover{background-color:#eee;color:#888}#annotation-editor .dropdown-list li:hover .sub-list{display:block}#annotation-editor .dropdown-list li.js-facebook-share{display:none}#annotation-editor .dropdown-list li.note-input{padding:10px}#annotation-editor .dropdown-list li.note-input:hover{background:0 0}#annotation-editor .dropdown-list li.note-input form{margin:0}#annotation-editor .dropdown-list li.note-input input[type=text],#annotation-editor .dropdown-list li.note-input textarea{font-size:12px;background:0 0;width:160px;color:#333;outline:0;line-height:20px;padding:0 3px;border:1px solid #999;max-width:160px;margin-bottom:5px}#annotation-editor .dropdown-list li.note-input input[type=text]:focus,#annotation-editor .dropdown-list li.note-input textarea:focus{border-color:#4d90fe;box-shadow:0 0 1px rgba(77,144,254,.2)}#annotation-editor .dropdown-list li.note-input textarea{max-height:100px}#annotation-editor .dropdown-list li.note-input #add-button{height:22px;width:80px;font-size:12px;color:#fff;text-align:center;line-height:18px;border-width:0;background-color:#4d90fe;cursor:pointer;margin-top:10px}#annotation-editor .dropdown-list li .sub-list{display:none;position:absolute;top:0;right:-182px}#annotation-editor .dropdown-list li.colors{padding:8px 10px}#annotation-editor .dropdown-list li.colors:hover{background:0 0}#annotation-editor .dropdown-list li.colors .color{width:20px;height:20px;padding:0;border-radius:100%;display:inline-block;margin:5px 8px 0;border:2px solid #eee;cursor:pointer}#annotation-editor .dropdown-list li.colors .color.active,#annotation-editor .dropdown-list li.colors .color.active:hover{border-color:#6e6e6e}#annotation-editor .dropdown-list li.colors .color:hover{border-color:#a2a2a2}.tag-list{position:absolute;left:0}.tag-list .tag{display:block;cursor:pointer;font-size:12px}.tag-list .tag.shown .tag-text,.tag-list .tag:hover .tag-text{opacity:1}.tag-list .marker{padding-right:4px;background:#4285f4;width:0;height:28px;display:inline-block;float:left}.tag-list .tag-text{color:#000;height:28px;line-height:28px;padding:0 4px;display:inline-block;opacity:0;overflow:hidden;background-color:#4285f4;-webkit-transition:all .2s ease-out;transition:all .2s ease-out} -------------------------------------------------------------------------------- /dist/annotator.min.js: -------------------------------------------------------------------------------- 1 | var Annotation=function t(){var t={annotator:null,range:{startContainerXPath:null,endContainerXPath:null,parentContainerXPath:null,startOffset:null,endOffset:null},id:null,selectedText:null,color:null,note:null,tags:[],init:function(t){if(t){if(t.annotator&&(this.annotator=t.annotator),t.selectedRange)this.saveSelection(t.selectedRange);else if(t.savedAnnotation){var e=t.savedAnnotation;this.range=e.range,this.id=e.id,this.selectedText=e.selectedText,this.color=e.color,this.note=e.note,this.tags=e.tags}this.setRangeElements()}},setRangeElements:function(){this.$parentContainer=$(this.annotator.findElementByXPath(this.range.parentContainerXPath)),this.$startContainer=$(this.annotator.findElementByXPath(this.range.startContainerXPath)),this.$endContainer=$(this.annotator.findElementByXPath(this.range.endContainerXPath))},render:function(t){t&&t.temporary?this.wrapNodes(!0):t&&t.convert?this.convertFromTemporary():t&&t.update?this.updateAnnotation():this.wrapNodes(),window.getSelection?window.getSelection().removeAllRanges():document.selection&&document.selection.empty()},updateAnnotation:function(){var t=this.$parentContainer,e=t.find(".annotation[data-id='"+this.id+"']");e.removeClass("annotation"),e.removeAttr("class").addClass("annotation").addClass(this.color).removeAttr("style")},saveSelection:function(t){var e=t.commonAncestorContainer,n=t.startContainer,a=t.endContainer,o=t.startOffset,r=t.endOffset,s=this.getParentNodeFor(e),i=this.getParentNodeFor(n),d=this.getParentNodeFor(a),l=this.getNodesToWrap(s,i.firstChild,n),h=this.getNodesToWrap(s,d.firstChild,a);if(l.length)for(var f=0;fs;++s)n(t.childNodes[s])}else 1!=r&&(o+t.nodeValue.length>=e?(a=t,r=!0):o+=t.nodeValue.length)}var a,o=0,r=!1;return n(t),[a,0==o?e:e-o]},getNodesToWrap:function(t,e,n){function a(t){if(t==e&&(o=!0),t==n)r=!0;else if(t.nodeType==Node.TEXT_NODE)!o||r||/^\s*$/.test(t.nodeValue)||s.push(t);else if(t.nodeType==Node.ELEMENT_NODE)for(var i=0,d=t.childNodes.length;!r&&d>i;++i)a(t.childNodes[i])}var o=!1,r=!1,s=[];return a(t),s},wrapNodes:function(t){for(var e=this.getContainedNodes(),n=this.createWrapperElement(t),a=0;a'}),t+='
  • Copy
  • Share
  • Remove Highlight
  • '},showEditor:function(t){var o=this.$popoverElement,e=t.position,n=t.annotation,i=t.temporary,a=e.top-30,s=e.left-this.$popoverElement.width()/2;n&&(this.annotation=n,this.activateAnnotationColor(),this.renderContents(),i||this.showRemoveBtn()),window.FB?this.$popoverElement.find(".js-facebook-share").show():this.$popoverElement.find(".js-facebook-share").hide(),i&&this.annotation.render({temporary:!0}),this._awesomplete&&(this._awesomplete.list=this.annotator.tags),o.removeClass("anim").css("top",a).css("left",s).show(),o.find("#annotation-input").focus()},isVisible:function(){return this.$popoverElement.is(":visible")},reset:function(){this.annotation.removeTemporary(),this.resetNoteForm(),this.hideRemoveBtn(),this.annotation=null,this.$popoverElement.removeAttr("style")},resetNoteForm:function(){this.$popoverElement.find(".js-note-field, .js-tags-field").val("")},activateAnnotationColor:function(){this.$popoverElement.find(".js-color-picker.active").removeClass("active"),this.$popoverElement.find(".js-color-picker."+(this.annotation.color||"yellow")).addClass("active")},renderContents:function(){this.$popoverElement.find(".js-note-field").val(this.annotation.note),this.annotation.tags&&this.$popoverElement.find(".js-tags-field").val(this.annotation.tags.join(", "))},showRemoveBtn:function(){this.$popoverElement.find(".js-remove-annotation-wrapper").show()},hideRemoveBtn:function(){this.$popoverElement.find(".js-remove-annotation-wrapper").hide()},hideEditor:function(t){this.reset(),this.$popoverElement.hide()},getTagsFromString:function(t){var o=t.split(this.annotator.tagRegex).map(function(t){var o=$.trim(t);return o.length?o:void 0});return o},setColor:function(t){var o=$(t.target),e=o.data("color"),n=this.$popoverElement.find(".js-note-form"),i=n.find(".js-note-field").val(),a=this.getTagsFromString(n.find(".js-tags-field").val());this.saveAndClose({color:e,note:i,tags:a})},addNote:function(t){var o=$(t.target),e=o.find(".js-note-field").val(),n=this.getTagsFromString(o.find(".js-tags-field").val()),i=this.annotation.color||this.annotator.defaultColor;this.saveAndClose({color:i,note:e,tags:n})},saveAndClose:function(t){if(t){var o={debug:this.annotator.debug,cbk:function(t){this.annotator.findAnnotation(t.id)?(this.annotator.updateAnnotation(t.id,t),this.annotation.render({update:!0})):(this.annotation.render({convert:!0}),this.annotator.annotations.push(t)),this.annotator.addTags(t.tags),this.annotator.debug&&this.saveToLocalStorage(),this.hideEditor()}.bind(this)};$.extend(o,t),this.annotation.save(o)}},copyToClipboard:function(){var t=this.annotation.selectedText,o=$("");$(this.annotator.containerElement).append(o),o.val(t).select();try{document.execCommand("copy")}catch(e){}this.hideEditor(),o.remove()},truncate:function(t,o){if(t.length<=o)return t;for(;t.length>=o;)t=t.substr(0,t.lastIndexOf(" "));return t+"..."},removeAnnotation:function(){var t=this.annotation,o=this.annotator;if(t){var e=$(this.annotator.containerElement).find(".annotation[data-id='"+t.id+"']");this.annotation.destroy(function(){o.removeAnnotation(t.id),e.contents().unwrap()}),this.annotator.debug&&this.saveToLocalStorage(),this.hideEditor()}},share:function(t){var o=this.annotation.selectedText,e=$(t.target);if(e.hasClass("facebook")){if(!FB||!FB.ui)return;FB.ui({method:"feed",link:window.location.href,description:o,display:"popup"},function(t){t&&!t.error_code})}else if(e.hasClass("twitter")){var n=575,i=400,a=($(window).width()-n)/2,s=($(window).height()-i)/2,r="status=1,width="+n+",height="+i+",top="+s+",left="+a,l=140-"...".length-window.location.href.length,h="http://twitter.com/share?text="+window.encodeURIComponent(this.truncate(o,l)||"");window.open(h,"Share",r)}this.hideEditor()},saveToLocalStorage:function(){if(window.localStorage){var t=this.annotator.annotations.map(function(t){return t.serialize()});window.localStorage.setItem("annotations",JSON.stringify(t))}}};return t}(); 3 | var Annotator=function t(){var t={containerElement:"body",annotations:[],editor:null,defaultColor:"yellow",colors:[{className:"yellow"},{className:"green"},{className:"pink"},{className:"blue"}],tags:[],init:function(t){this.containerElement=t.containerElement||"body",this.debug=t.debug||"true",this.remoteURL=t.remoteURL||"",t.existingTags&&(this.tags=t.existingTags),t.colors&&(this.colors=t.colors),t.annotations&&this.renderExistingAnnotations(t.annotations);var n=Object.create(Editor);n.init({annotator:this}),this.setEditor(n)},addTags:function(t){this.tags=this.tags.concat(t)},setEditor:function(t){this.editor=t},findAnnotation:function(t){return this.annotations.filter(function(n){return n.id==t})[0]},updateAnnotation:function(t,n){var e=this.annotations.map(function(t){return t.id}).indexOf(t);-1>=e||(this.annotations[e]=n)},removeAnnotation:function(t){var n=this.annotations.map(function(t){return t.id}).indexOf(t);-1>=n||this.annotations.splice(n,1)},renderExistingAnnotations:function(t){for(var n=0;n5){var i=e.getRangeAt(0),o=Object.create(Annotation);o.init({selectedRange:i,annotator:this});{({top:t.pageY,left:t.pageX})}this.editor.showEditor({temporary:!0,position:{top:t.pageY,left:t.pageX},annotation:o})}},startListening:function(){var t=$(this.containerElement),n=this;t.on("mouseup touchend",function(t){t.preventDefault(),t.stopPropagation();var e=$(t.target);!n.editor.isVisible()||e.parents("#annotation-editor").length||e.hasClass("annotation")||n.editor.hideEditor(),e.parents(".js-no-select").length||n.handleAnnotation(t)}),t.on("click",".annotation",function(t){t.stopPropagation(),n.handleAnnotationClick(t)})},findElementByXPath:function(t){var n=new XPathEvaluator,e=n.evaluate(t,document.querySelector(this.containerElement),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null);return e.singleNodeValue},createXPathFromElement:function(t){for(var n=document.getElementsByTagName("*"),e=[];t&&1==t.nodeType&&t!=document.querySelector(this.containerElement).parentNode;t=t.parentNode)if(t.hasAttribute("id")){for(var o=0,a=0;a1));a++);if(1==o)return e.unshift('id("'+t.getAttribute("id")+'")'),e.join("/");e.unshift(t.localName.toLowerCase()+'[@id="'+t.getAttribute("id")+'"]')}else if(t.hasAttribute("class"))e.unshift(t.localName.toLowerCase()+'[@class="'+t.getAttribute("class")+'"]');else{for(i=1,sib=t.previousSibling;sib;sib=sib.previousSibling)sib.localName==t.localName&&i++;e.unshift(t.localName.toLowerCase()+"["+i+"]")}return e.length?""+e.join("/"):null}};return t}(); -------------------------------------------------------------------------------- /example/css/annotator.css: -------------------------------------------------------------------------------- 1 | ::-moz-selection { 2 | background: #babaff; } 3 | 4 | ::selection { 5 | background: #babaff; } 6 | 7 | .annotation { 8 | background: rgba(255, 255, 0, 0.05); } 9 | .annotation.shown { 10 | cursor: pointer; } 11 | 12 | #annotation-editor .colors .color.yellow { 13 | background: #FFE79D; } 14 | #annotation-editor .colors .color.green { 15 | background: #BCEFAA; } 16 | #annotation-editor .colors .color.pink { 17 | background: #FD99BB; } 18 | #annotation-editor .colors .color.blue { 19 | background: #A4D7FC; } 20 | 21 | .annotation.yellow { 22 | background: rgba(255, 231, 157, 0.7); } 23 | .annotation.green { 24 | background: rgba(188, 239, 170, 0.7); } 25 | .annotation.pink { 26 | background: rgba(253, 153, 187, 0.7); } 27 | .annotation.blue { 28 | background: rgba(164, 215, 252, 0.7); } 29 | 30 | .temporary { 31 | background: #babaff; } 32 | 33 | #annotation-editor { 34 | margin-top: 50px; 35 | position: relative; 36 | font-family: "Helvetica Neue", Helvetica, sans-serif; 37 | display: none; 38 | position: absolute; 39 | box-shadow: 0 3px 35px rgba(0, 0, 0, 0.21); } 40 | #annotation-editor:after { 41 | content: ''; 42 | position: absolute; 43 | left: 80px; 44 | top: -9px; 45 | width: 0; 46 | height: 0; 47 | border-style: solid; 48 | border-width: 0 10px 10px 10px; 49 | border-color: transparent transparent #ffffff transparent; } 50 | #annotation-editor:before { 51 | content: ''; 52 | position: absolute; 53 | left: 80px; 54 | top: -10px; 55 | width: 0; 56 | height: 0; 57 | border-style: solid; 58 | border-width: 0 10px 10px 10px; 59 | border-color: transparent transparent #bbbbbb transparent; } 60 | #annotation-editor div.awesomplete > ul > li { 61 | position: relative; 62 | padding: .2em .5em; 63 | cursor: pointer; } 64 | #annotation-editor .dropdown-list { 65 | background: #ffffff; 66 | display: inline-block; 67 | width: 180px; 68 | border-radius: 3px; 69 | border: 1px solid #bbbbbb; 70 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 3px 15px rgba(0, 0, 0, 0.15); 71 | margin: 0; 72 | padding: 0; 73 | list-style: none; 74 | z-index: 10; } 75 | #annotation-editor .dropdown-list li { 76 | border-bottom: 1px solid #eee; 77 | position: relative; 78 | padding: 0px 10px; } 79 | #annotation-editor .dropdown-list li.js-remove-annotation-wrapper { 80 | display: none; } 81 | #annotation-editor .dropdown-list li a, #annotation-editor .dropdown-list li span.link { 82 | padding: 5px 0; 83 | text-decoration: none; 84 | display: block; 85 | font-size: 12px; 86 | color: #888888; 87 | line-height: 20px; 88 | font-weight: 400; 89 | text-transform: uppercase; 90 | cursor: pointer; } 91 | #annotation-editor .dropdown-list li:hover { 92 | background-color: #eee; 93 | color: #888888; } 94 | #annotation-editor .dropdown-list li:hover .sub-list { 95 | display: block; } 96 | #annotation-editor .dropdown-list li.js-facebook-share { 97 | display: none; } 98 | #annotation-editor .dropdown-list li.note-input { 99 | padding: 10px; } 100 | #annotation-editor .dropdown-list li.note-input:hover { 101 | background: transparent; } 102 | #annotation-editor .dropdown-list li.note-input form { 103 | margin: 0; } 104 | #annotation-editor .dropdown-list li.note-input textarea, #annotation-editor .dropdown-list li.note-input input[type="text"] { 105 | font-size: 12px; 106 | color: #ddd; 107 | border-width: 0; 108 | background: transparent; 109 | width: 160px; 110 | color: #333; 111 | outline: none; 112 | line-height: 20px; 113 | padding: 0 3px; 114 | border: 1px solid #999; 115 | max-width: 160px; 116 | margin-bottom: 5px; } 117 | #annotation-editor .dropdown-list li.note-input textarea:focus, #annotation-editor .dropdown-list li.note-input input[type="text"]:focus { 118 | border-color: #4d90fe; 119 | box-shadow: 0 0 1px rgba(77, 144, 254, 0.2); } 120 | #annotation-editor .dropdown-list li.note-input textarea { 121 | max-height: 100px; } 122 | #annotation-editor .dropdown-list li.note-input #add-button { 123 | height: 22px; 124 | width: 80px; 125 | font-size: 12px; 126 | color: #fff; 127 | text-align: center; 128 | line-height: 18px; 129 | border-width: 0; 130 | background-color: #4d90fe; 131 | cursor: pointer; 132 | margin-top: 10px; } 133 | #annotation-editor .dropdown-list li .sub-list { 134 | display: none; 135 | position: absolute; 136 | top: 0; 137 | right: -182px; } 138 | #annotation-editor .dropdown-list li.colors { 139 | padding: 8px 10px; } 140 | #annotation-editor .dropdown-list li.colors:hover { 141 | background: transparent; } 142 | #annotation-editor .dropdown-list li.colors .color { 143 | width: 20px; 144 | height: 20px; 145 | padding: 0; 146 | margin: 0; 147 | border-radius: 100%; 148 | display: inline-block; 149 | margin: 0 8px; 150 | margin-top: 5px; 151 | border: 2px solid #eee; 152 | cursor: pointer; } 153 | #annotation-editor .dropdown-list li.colors .color.active { 154 | border-color: #6e6e6e; } 155 | #annotation-editor .dropdown-list li.colors .color.active:hover { 156 | border-color: #6e6e6e; } 157 | #annotation-editor .dropdown-list li.colors .color:hover { 158 | border-color: #a2a2a2; } 159 | 160 | .tag-list { 161 | position: absolute; 162 | left: 0px; } 163 | .tag-list .tag { 164 | display: block; 165 | cursor: pointer; 166 | font-size: 12px; } 167 | .tag-list .tag:hover .tag-text, .tag-list .tag.shown .tag-text { 168 | opacity: 1; } 169 | .tag-list .marker { 170 | padding-right: 4px; 171 | background: #4285f4; 172 | width: 0; 173 | height: 28px; 174 | display: inline-block; 175 | float: left; 176 | background-color: #4285f4; } 177 | .tag-list .tag-text { 178 | color: #000; 179 | height: 28px; 180 | line-height: 28px; 181 | padding: 0 4px; 182 | display: inline-block; 183 | opacity: 0; 184 | overflow: hidden; 185 | background-color: #4285f4; 186 | -webkit-transition: all 0.2s ease-out; 187 | transition: all 0.2s ease-out; } 188 | -------------------------------------------------------------------------------- /example/css/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | font-size: 100%; 10 | font: inherit; 11 | vertical-align: baseline; } 12 | 13 | /* HTML5 display-role reset for older browsers */ 14 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 15 | display: block; } 16 | 17 | body { 18 | line-height: 1; } 19 | 20 | ol, ul { 21 | list-style: none; } 22 | 23 | blockquote, q { 24 | quotes: none; } 25 | 26 | blockquote:before, blockquote:after, q:before, q:after { 27 | content: ''; 28 | content: none; } 29 | 30 | table { 31 | border-collapse: collapse; 32 | border-spacing: 0; } 33 | 34 | /*===================================== 35 | = CUSTOM STYLES = 36 | =====================================*/ 37 | body { 38 | background: #f1f1f1; 39 | font: 16px/1.5 Helvetica, Arial, sans-serif; } 40 | 41 | .container { 42 | overflow: hidden; 43 | width: 100%; 44 | max-width: 1140px; 45 | margin: 0 auto; } 46 | 47 | h1, h2, h3, h4, h5, h6, strong { 48 | font-weight: 700; } 49 | 50 | h1 { 51 | font-size: 36px; } 52 | 53 | h2 { 54 | font-size: 28px; } 55 | 56 | h3 { 57 | font-size: 22px; } 58 | 59 | h4 { 60 | font-size: 18px; } 61 | 62 | h5 { 63 | font-size: 16px; } 64 | 65 | h6 { 66 | font-size: 14px; } 67 | 68 | h1, h2, h3, h4, h5, h6, p { 69 | margin: 10px 0; } 70 | 71 | ul, ol { 72 | margin-left: 20px; } 73 | 74 | ul { 75 | list-style: disc; } 76 | 77 | .content { 78 | margin: 20px auto; 79 | background: #FFFFFF; 80 | border: 1px solid #e6e6e6; } 81 | .content #book { 82 | margin: 0 auto; 83 | padding: 20px; 84 | box-sizing: border-box; 85 | position: relative; } 86 | .content .header { 87 | background: #607D8B; 88 | padding: 20px; 89 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.24); 90 | color: #ffffff; 91 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.12); } 92 | .content .header h1 { 93 | margin: 0; 94 | padding: 0; } 95 | .content .header p { 96 | margin: 0; 97 | padding: 0; 98 | color: #b7c6cd; } 99 | 100 | footer.main { 101 | text-align: center; 102 | margin: 20px auto; } 103 | footer.main a { 104 | color: #607D8B; 105 | text-decoration: none; 106 | text-shadow: 0 0 3px transparent; 107 | -webkit-transition: color 0.2s ease-in, text-shadow 0.2s ease-in; 108 | transition: color 0.2s ease-in, text-shadow 0.2s ease-in; } 109 | footer.main a:hover { 110 | color: #4d90fe; 111 | text-shadow: 0 0 3px rgba(77, 144, 254, 0.2); } 112 | 113 | /*===== End of CUSTOM STYLES ======*/ -------------------------------------------------------------------------------- /example/css/vendor/awesomplete.css: -------------------------------------------------------------------------------- 1 | [hidden] { display: none; } 2 | 3 | .visually-hidden { 4 | position: absolute; 5 | clip: rect(0, 0, 0, 0); 6 | } 7 | 8 | div.awesomplete { 9 | display: inline-block; 10 | position: relative; 11 | } 12 | 13 | div.awesomplete > input { 14 | display: block; 15 | } 16 | 17 | div.awesomplete > ul { 18 | position: absolute; 19 | left: 0; 20 | z-index: 1; 21 | min-width: 100%; 22 | box-sizing: border-box; 23 | list-style: none; 24 | padding: 0; 25 | border-radius: .3em; 26 | margin: .2em 0 0; 27 | background: hsla(0,0%,100%,.9); 28 | background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8)); 29 | border: 1px solid rgba(0,0,0,.3); 30 | box-shadow: .05em .2em .6em rgba(0,0,0,.2); 31 | text-shadow: none; 32 | } 33 | 34 | div.awesomplete > ul[hidden], 35 | div.awesomplete > ul:empty { 36 | display: none; 37 | } 38 | 39 | @supports (transform: scale(0)) { 40 | div.awesomplete > ul { 41 | transition: .3s cubic-bezier(.4,.2,.5,1.4); 42 | transform-origin: 1.43em -.43em; 43 | } 44 | 45 | div.awesomplete > ul[hidden], 46 | div.awesomplete > ul:empty { 47 | opacity: 0; 48 | transform: scale(0); 49 | display: block; 50 | transition-timing-function: ease; 51 | } 52 | } 53 | 54 | /* Pointer */ 55 | div.awesomplete > ul:before { 56 | content: ""; 57 | position: absolute; 58 | top: -.43em; 59 | left: 1em; 60 | width: 0; height: 0; 61 | padding: .4em; 62 | background: white; 63 | border: inherit; 64 | border-right: 0; 65 | border-bottom: 0; 66 | -webkit-transform: rotate(45deg); 67 | transform: rotate(45deg); 68 | } 69 | 70 | div.awesomplete > ul > li { 71 | position: relative; 72 | padding: .2em .5em; 73 | cursor: pointer; 74 | } 75 | 76 | div.awesomplete > ul > li:hover { 77 | background: hsl(200, 40%, 80%); 78 | color: black; 79 | } 80 | 81 | div.awesomplete > ul > li[aria-selected="true"] { 82 | background: hsl(205, 40%, 40%); 83 | color: white; 84 | } 85 | 86 | div.awesomplete mark { 87 | background: hsl(65, 100%, 50%); 88 | } 89 | 90 | div.awesomplete li:hover mark { 91 | background: hsl(68, 101%, 41%); 92 | } 93 | 94 | div.awesomplete li[aria-selected="true"] mark { 95 | background: hsl(86, 102%, 21%); 96 | color: inherit; 97 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Annotator 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 28 | 29 |
    30 |
    31 |
    32 |

    Annotator

    33 |

    34 | Highlight, share, add notes and tags to any selected text on a page 35 |

    36 |
    37 | 38 |
    39 | 40 |

    7.1 Deforestation and Its Causes

    41 |

    42 | A great variety of plants and animals exists on earth. They are essential for the wellbeing and survival of mankind. Today, a major threat to survival of these organisms is deforestation. We know that deforestation means clearing of forests and using that land for other purposes. 43 |

    44 | 45 |

    46 | Trees in the forest are cut for some of the purposes mentioned below: 47 |

    48 | 49 |
      50 |
    • Procuring land for cultivation.
    • 51 |
    • Building houses and factories.
    • 52 |
    • Making furniture or using wood as fuel.
    • 53 |
    54 | 55 |

    56 | Some natural causes of deforestation are forest fires and severe droughts. 57 |

    58 | 59 | 60 |

    Activity 7.1

    61 |

    62 | Add more causes of deforestation to your list and classify them into natural and man-made. 63 |

    64 | 65 | 66 |

    7.2 Consequences of Deforestation

    67 |

    68 | Paheli and Boojho recalled the consequences of deforestation. They remembered that deforestation increases the temperature and pollution level on the earth. It increases the level of carbon dioxide in the atmosphere. Ground water level also gets lowered. They know that deforestation disturbs the balance in nature. They were told by Prof. Ahmad that if cutting of trees continues, rainfall and the fertility of the soil will decrease. 69 |

    70 | 71 |

    72 | How does deforestation reduce rainfall on the one hand and lead to floods on the other? 73 |

    74 | 75 |

    76 | Moreover, there will be increased chances of natural calamities such as floods and droughts. Recall that plants need carbon dioxide for photosynthesis. Fewer trees would mean that less carbon dioxide will be used up resulting in its increased amount in the atmosphere. This will lead to global warming as carbon dioxide traps the heat rays reflected by the earth. The increase in temperature on the earth disturbs the water cycle and may reduce rainfall. This could cause droughts. 77 |

    78 | 79 |

    80 | Deforestation is a major cause which leads to the change in soil properties. Physical properties of the soil get affected by plantation and vegetation. Recall from Class VII how trees prevent soil erosion. Fewer trees result in more soil erosion. Removal of the top layer of the soil exposes the lower, hard and rocky layers. This soil has less humus and is less fertile. Gradually the fertile land gets converted into deserts. It is called desertification. 81 |

    82 | 83 |

    84 | Deforestation also leads to a decrease in the water holding capacity of the soil. The movement of water from the soil surface into the ground (infiltration rate) is reduced. So, there are floods. The other properties of the soil like nutrient content, texture, etc., also change because of deforestation. 85 |

    86 | 87 | 88 |

    Activity 7.2

    89 |

    90 | Animal life is also affected by deforestation. How? List the points and discuss them in your class. 91 |

    92 | 93 | 94 |

    7.3 Conservation of Forest and Wildlife

    95 |

    96 | Having become aware of the effects of deforestation, Paheli and Boojho are worried. They go to Prof. Ahmad and ask him how forests and wildlife can be saved. 97 |

    98 |

    99 | Prof. Ahmad organises a visit to a biosphere reserve for Paheli, Boojho and their classmates. He selects a place named Pachmarhi Biosphere Reserve. He knows that the plants and animals found here are similar to those of the upper Himalayan peaks and to those belonging to the lower western ghats. Prof. Ahmad believes that the biodiversity found here is unique. He requests Madhavji, a forest employee, to guide the children inside the biosphere reserve. He explains that preserving areas of such biological importance make them a part of our national heritage. 100 |

    101 | 102 |

    103 | Biosphere is that part of the earth in which living organisms exist or which supports life. Biological diversity or biodiversity, refers to the variety of organisms existing on the earth, their interrelationships and their relationship with the environment. 104 |

    105 | 106 |

    107 | Madhavji explains to the children that apart from our personal efforts and efforts of the society, government agencies also take care of the forests and animals. The government lays down rules, methods and policies to protect and conserve them. Wildlife sanctuaries, national parks, biosphere reserves, etc., are protected areas for conservation of plants and animals present in that area. 108 |

    109 | 110 |

    111 | To protect our flora and fauna and their habitats, protected areas called sanctuaries, national parks and biosphere reserves have been earmarked. Plantation, cultivation, grazing, felling trees, hunting and poaching are prohibited there. Sanctuary : Areas where animals are protected from any disturbance to them and their habitat. 112 |

    113 | 114 |

    115 | National Park : Areas reserved for wild life where they can freely use the habitats and natural resources. Biosphere Reserve : Large areas of protected land for conservation of wild life, plant and animal resources and traditional life of the tribals living in the area. 116 |

    117 |
    118 |
    119 | 120 | 125 |
    126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /example/scripts/annotation.js: -------------------------------------------------------------------------------- 1 | var Annotation = (function Annotation() { 2 | 3 | var Annotation = { 4 | annotator: null, 5 | range: { 6 | startContainerXPath: null, 7 | endContainerXPath: null, 8 | parentContainerXPath: null, 9 | startOffset: null, 10 | endOffset: null 11 | }, 12 | id: null, 13 | selectedText: null, 14 | color: null, 15 | note: null, 16 | tags: [], 17 | 18 | init: function(obj) { 19 | if(!obj) return; 20 | 21 | if(obj.annotator) { 22 | this.annotator = obj.annotator; 23 | } 24 | if(obj.selectedRange) { 25 | this.saveSelection(obj.selectedRange); 26 | } else if(obj.savedAnnotation) { 27 | var savedAnnotation = obj.savedAnnotation; 28 | 29 | this.range = savedAnnotation.range; 30 | this.id = savedAnnotation.id; 31 | this.selectedText = savedAnnotation.selectedText; 32 | this.color = savedAnnotation.color; 33 | this.note = savedAnnotation.note; 34 | this.tags = savedAnnotation.tags; 35 | } 36 | 37 | this.setRangeElements(); 38 | 39 | }, 40 | 41 | setRangeElements: function() { 42 | this.$parentContainer = $(this.annotator.findElementByXPath(this.range.parentContainerXPath)); 43 | this.$startContainer = $(this.annotator.findElementByXPath(this.range.startContainerXPath)); 44 | this.$endContainer = $(this.annotator.findElementByXPath(this.range.endContainerXPath)); 45 | 46 | }, 47 | 48 | render: function(opts) { 49 | if(opts && opts.temporary) { 50 | this.wrapNodes(true); 51 | } else if(opts && opts.convert) { 52 | this.convertFromTemporary(); 53 | } else if(opts && opts.update) { 54 | this.updateAnnotation(); 55 | } else { 56 | this.wrapNodes(); 57 | } 58 | 59 | // remove existing selection 60 | // we don't need it anymore 61 | if (window.getSelection) { 62 | window.getSelection().removeAllRanges(); 63 | } else if (document.selection) { 64 | document.selection.empty(); 65 | } 66 | }, 67 | 68 | updateAnnotation: function() { 69 | var $parentContainer = this.$parentContainer; 70 | 71 | var renderedAnnotation = $parentContainer 72 | .find(".annotation[data-id='" + this.id + "']"); 73 | 74 | renderedAnnotation.removeClass("annotation"); 75 | 76 | renderedAnnotation 77 | .removeAttr("class") 78 | .addClass("annotation") 79 | .addClass(this.color) 80 | .removeAttr("style"); 81 | }, 82 | 83 | saveSelection: function(range) { 84 | var parentContainer = range.commonAncestorContainer; 85 | var startContainer = range.startContainer; 86 | var endContainer = range.endContainer; 87 | 88 | var startOffset = range.startOffset; 89 | var endOffset = range.endOffset; 90 | 91 | var parentNode = this.getParentNodeFor(parentContainer); 92 | var startNode = this.getParentNodeFor(startContainer); 93 | var endNode = this.getParentNodeFor(endContainer); 94 | 95 | var nodesBetweenStart = this.getNodesToWrap(parentNode, startNode.firstChild, startContainer); 96 | var nodesBetweenEnd = this.getNodesToWrap(parentNode, endNode.firstChild, endContainer); 97 | 98 | if(nodesBetweenStart.length) { 99 | for(var i = 0; i < nodesBetweenStart.length; i++) { 100 | var characterLength = nodesBetweenStart[i].nodeValue.length; 101 | startOffset += characterLength; 102 | } 103 | } 104 | 105 | if(nodesBetweenEnd.length) { 106 | for(var i = 0; i < nodesBetweenEnd.length; i++) { 107 | endOffset += nodesBetweenEnd[i].nodeValue.length; 108 | } 109 | } 110 | 111 | var selectedContent = this.getSelectionContent(range); 112 | 113 | this.range = { 114 | startContainerXPath: this.annotator.createXPathFromElement(startNode), 115 | endContainerXPath: this.annotator.createXPathFromElement(endNode), 116 | parentContainerXPath: this.annotator.createXPathFromElement(parentNode), 117 | startOffset: startOffset, 118 | endOffset: endOffset, 119 | }; 120 | 121 | this.id = this.generateRandomID(); 122 | this.selectedText = selectedContent; 123 | }, 124 | 125 | getSelectionContent: function(range) { 126 | var container = document.createElement("div"); 127 | container.appendChild(range.cloneContents()); 128 | var text = container.textContent; 129 | return text; 130 | }, 131 | 132 | getParentNodeFor: function(node) { 133 | 134 | while(node.nodeType != 1 || (node.nodeType == 1 && node.classList.contains("js-no-select")) ) { 135 | node = node.parentNode; 136 | } 137 | 138 | return node; 139 | }, 140 | 141 | generateRandomID: function() { 142 | return 'annotation-' + new Date().getTime(); 143 | }, 144 | 145 | save: function(obj) { 146 | if(!obj) return; 147 | 148 | this.color = obj.color; 149 | 150 | if(obj.note) 151 | this.note = obj.note; 152 | 153 | if(obj.tags && obj.tags.length) { 154 | this.tags = obj.tags 155 | } 156 | 157 | var data = this.serialize(); 158 | 159 | if(obj && obj.debug) { 160 | console.log(JSON.stringify(data)); 161 | } else { 162 | this.postToRemote(); 163 | } 164 | 165 | if(obj && obj.cbk) obj.cbk(this); 166 | 167 | }, 168 | 169 | // TODO 170 | destroy: function(cbk) { 171 | if(cbk) cbk(); 172 | }, 173 | 174 | // TODO 175 | postToRemote: function() { 176 | console.log("AJAX"); 177 | }, 178 | 179 | serialize: function() { 180 | var range = this.range; 181 | return { 182 | range: { 183 | startOffset: range.startOffset, 184 | endOffset: range.endOffset, 185 | startContainerXPath: range.startContainerXPath, 186 | endContainerXPath: range.endContainerXPath, 187 | parentContainerXPath: range.parentContainerXPath 188 | }, 189 | id: this.id, 190 | selectedText: this.selectedText, 191 | color: this.color, 192 | note: this.note, 193 | tags: this.tags 194 | } 195 | }, 196 | 197 | getContainedNodes: function() { 198 | var range, startContainer, endContainer, parentContainer; 199 | var nodes = []; 200 | 201 | range = this.range; 202 | 203 | parentContainer = this.$parentContainer.get(0); 204 | startContainer = this.$startContainer.get(0); 205 | endContainer = this.$endContainer.get(0); 206 | 207 | var startTextNodeParams = this.getTextNodeAtOffset(startContainer, range.startOffset); 208 | endTextNodeParams = this.getTextNodeAtOffset(endContainer, range.endOffset); 209 | 210 | var startTextNode = startTextNodeParams[0], 211 | startOffset = startTextNodeParams[1], 212 | endTextNode = endTextNodeParams[0], 213 | endOffset = endTextNodeParams[1]; 214 | 215 | 216 | if(startTextNode == endTextNode) { 217 | var startTextNodeSplit = startTextNode.splitText(startOffset); 218 | var endTextNodeSplit = startTextNodeSplit.splitText(endOffset - startOffset); 219 | } else { 220 | var startTextNodeSplit = startTextNode.splitText(startOffset); 221 | var endTextNodeSplit = endTextNode.splitText(endOffset); 222 | } 223 | 224 | 225 | var innerNodes = this.getNodesToWrap(parentContainer, startTextNodeSplit, endTextNodeSplit); 226 | 227 | 228 | for(var i = 0; i < innerNodes.length; i++) { 229 | nodes.push(innerNodes[i]); 230 | } 231 | 232 | return nodes; 233 | }, 234 | 235 | 236 | getTextNodeAtOffset: function(rootNode, offset) { 237 | var textNode, 238 | count = 0, 239 | found = false; 240 | 241 | function getTextNodes(node) { 242 | if (node.nodeType == Node.TEXT_NODE && !/^\s*$/.test(node.nodeValue)) { 243 | if ( found != true) { 244 | if(count+node.nodeValue.length >= offset) { 245 | textNode = node; 246 | found = true; 247 | } else { 248 | count += node.nodeValue.length 249 | } 250 | } 251 | } else if (node.nodeType == Node.ELEMENT_NODE ) { 252 | for (var i = 0, len = node.childNodes.length; i < len; ++i) { 253 | getTextNodes(node.childNodes[i]); 254 | } 255 | } 256 | } 257 | 258 | getTextNodes(rootNode); 259 | return [textNode, (count == 0 ? offset : offset - count)]; 260 | 261 | }, 262 | 263 | getNodesToWrap: function(rootNode, startNode, endNode) { 264 | var pastStartNode = false, reachedEndNode = false, textNodes = []; 265 | 266 | function getTextNodes(node) { 267 | 268 | if (node == startNode) { 269 | pastStartNode = true; 270 | } 271 | if (node == endNode) { 272 | reachedEndNode = true; 273 | } else if (node.nodeType == Node.TEXT_NODE) { 274 | if (pastStartNode && !reachedEndNode && !/^\s*$/.test(node.nodeValue)) { 275 | textNodes.push(node); 276 | } 277 | } else if (node.nodeType == Node.ELEMENT_NODE ) { 278 | 279 | for (var i = 0, len = node.childNodes.length; !reachedEndNode && i < len; ++i) { 280 | getTextNodes(node.childNodes[i]); 281 | } 282 | } 283 | } 284 | 285 | getTextNodes(rootNode); 286 | return textNodes; 287 | }, 288 | 289 | 290 | wrapNodes: function(temporary) { 291 | var nodes = this.getContainedNodes(); 292 | var newNode = this.createWrapperElement(temporary) 293 | for(var i = 0; i < nodes.length; i++) { 294 | $(nodes[i]).wrap(newNode); 295 | } 296 | }, 297 | 298 | createWrapperElement: function(temporary) { 299 | var annotationID = this.id; 300 | 301 | var span = document.createElement("span"); 302 | 303 | if(!temporary) { 304 | span.classList.add("annotation"); 305 | span.classList.add(this.color); 306 | } else { 307 | span.classList.add("temporary"); 308 | } 309 | 310 | span.setAttribute("data-id", annotationID); 311 | 312 | return span; 313 | }, 314 | 315 | removeTemporary: function() { 316 | var temporary = this.$parentContainer.find(".temporary"); 317 | 318 | for(var i = 0; i < temporary.length; i++) { 319 | var elem = temporary[i]; 320 | $(elem.childNodes[0]).unwrap(); 321 | } 322 | }, 323 | 324 | convertFromTemporary: function() { 325 | var temporary = this.$parentContainer.find(".temporary"); 326 | 327 | temporary 328 | .removeClass("temporary") 329 | .addClass("annotation") 330 | .addClass(this.color); 331 | 332 | } 333 | 334 | 335 | }; 336 | 337 | return Annotation; 338 | })(); -------------------------------------------------------------------------------- /example/scripts/annotator.js: -------------------------------------------------------------------------------- 1 | var Annotator = (function Annotator() { 2 | 3 | var Annotator = { 4 | containerElement: "body", 5 | annotations: [], 6 | editor: null, 7 | 8 | defaultColor: "yellow", 9 | colors: [ 10 | { 11 | className: "yellow", 12 | }, 13 | 14 | { 15 | className: "green", 16 | }, 17 | 18 | { 19 | className: "pink", 20 | }, 21 | 22 | { 23 | className: "blue", 24 | }, 25 | ], 26 | 27 | tags: [], 28 | 29 | 30 | init: function(opts) { 31 | this.containerElement = opts.containerElement || "body"; 32 | this.debug = opts.debug || "true"; 33 | this.remoteURL = opts.remoteURL || ""; 34 | 35 | if(opts.existingTags) { 36 | this.tags = opts.existingTags; 37 | } 38 | 39 | if(opts.colors) { 40 | this.colors = opts.colors; 41 | } 42 | 43 | if(opts.annotations) { 44 | this.renderExistingAnnotations(opts.annotations); 45 | } 46 | 47 | // Setup editor 48 | var editor = Object.create(Editor); 49 | editor.init({ annotator: this }); 50 | this.setEditor(editor); 51 | }, 52 | 53 | addTags: function(tags) { 54 | this.tags = this.tags.concat(tags); 55 | }, 56 | 57 | setEditor: function(editor) { 58 | this.editor = editor; 59 | }, 60 | 61 | findAnnotation: function(annotationID) { 62 | return this.annotations.filter(function(annotation) { 63 | return annotation.id == annotationID; 64 | })[0]; 65 | }, 66 | 67 | updateAnnotation: function(annotationID, newAnnotation) { 68 | var index = this.annotations.map(function(i) { return i.id }).indexOf(annotationID); 69 | if(index <= -1) return; 70 | this.annotations[index] = newAnnotation; 71 | }, 72 | 73 | removeAnnotation: function(annotationID) { 74 | var index = this.annotations.map(function(i) { return i.id }).indexOf(annotationID); 75 | if(index <= -1) return; 76 | 77 | this.annotations.splice(index, 1); 78 | }, 79 | 80 | renderExistingAnnotations: function(annotations) { 81 | for(var i = 0; i < annotations.length; i++) { 82 | var annotation = Object.create(Annotation); 83 | annotation.init({ savedAnnotation: annotations[i], annotator: this }); 84 | annotation.render(); 85 | this.annotations.push(annotation); 86 | } 87 | }, 88 | 89 | 90 | handleAnnotationClick: function(e) { 91 | var $target = $(e.target); 92 | var annotationID = $target.data("id"); 93 | var annotation = this.findAnnotation(annotationID); 94 | 95 | if(!annotation) return; 96 | 97 | this.editor.showEditor({ 98 | position: { 99 | top: e.pageY, 100 | left: e.pageX 101 | }, 102 | annotation: annotation 103 | }); 104 | 105 | }, 106 | 107 | handleAnnotation: function(e) { 108 | var selection = window.getSelection(); 109 | var selectedText; 110 | if(selection.text) { 111 | selectedText = selection.text; 112 | } else { 113 | selectedText = selection.toString(); 114 | }; 115 | 116 | 117 | if(selection && !selection.isCollapsed && selectedText && selectedText.length>5) { 118 | var range = selection.getRangeAt(0); 119 | 120 | var annotation = Object.create(Annotation); 121 | annotation.init({ selectedRange: range, annotator: this }); 122 | 123 | var position = { 124 | top: e.pageY, 125 | left: e.pageX 126 | }; 127 | 128 | this.editor.showEditor({ 129 | temporary: true, 130 | position: { 131 | top: e.pageY, 132 | left: e.pageX 133 | }, 134 | annotation: annotation 135 | }); 136 | } 137 | }, 138 | 139 | startListening: function() { 140 | var $element = $(this.containerElement); 141 | var self = this; 142 | 143 | 144 | $element.on("mouseup touchend", function(e) { 145 | e.preventDefault(); 146 | e.stopPropagation(); 147 | 148 | var $target = $(e.target); 149 | 150 | if(self.editor.isVisible() && !$target.parents("#annotation-editor").length && !$target.hasClass("annotation")) { 151 | // editor is open but clicked outside 152 | self.editor.hideEditor() 153 | } 154 | 155 | 156 | if(!$target.parents(".js-no-select").length) { 157 | self.handleAnnotation(e); 158 | } 159 | }); 160 | 161 | $element.on("click", ".annotation", function(e) { 162 | e.stopPropagation(); 163 | self.handleAnnotationClick(e); 164 | }); 165 | 166 | }, 167 | 168 | findElementByXPath: function(path) { 169 | var evaluator = new XPathEvaluator(); 170 | var result = evaluator.evaluate(path, document.querySelector(this.containerElement), null,XPathResult.FIRST_ORDERED_NODE_TYPE, null); 171 | return result.singleNodeValue; 172 | }, 173 | 174 | createXPathFromElement: function(elm) { 175 | var allNodes = document.getElementsByTagName('*'); 176 | 177 | for (var segs = []; elm && elm.nodeType == 1 && elm != document.querySelector(this.containerElement).parentNode; elm = elm.parentNode) { 178 | if (elm.hasAttribute('id')) { 179 | var uniqueIdCount = 0; 180 | for (var n=0;n < allNodes.length;n++) { 181 | if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++; 182 | if (uniqueIdCount > 1) break; 183 | }; 184 | 185 | if ( uniqueIdCount == 1) { 186 | segs.unshift('id("' + elm.getAttribute('id') + '")'); 187 | return segs.join('/'); 188 | } else { 189 | segs.unshift(elm.localName.toLowerCase() + '[@id="' + elm.getAttribute('id') + '"]'); 190 | } 191 | 192 | } else if (elm.hasAttribute('class')) { 193 | segs.unshift(elm.localName.toLowerCase() + '[@class="' + elm.getAttribute('class') + '"]'); 194 | } else { 195 | for (i = 1, sib = elm.previousSibling; sib; sib = sib.previousSibling) { 196 | if (sib.localName == elm.localName) i++; 197 | }; 198 | segs.unshift(elm.localName.toLowerCase() + '[' + i + ']'); 199 | }; 200 | } 201 | 202 | return segs.length ? '' + segs.join('/') : null; 203 | } 204 | 205 | }; 206 | 207 | return Annotator; 208 | })(); -------------------------------------------------------------------------------- /example/scripts/app.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function($) { 2 | 3 | // Existing tags for the user. 4 | // Ideally it should come from the server 5 | var tags = [ 6 | "revision", 7 | "group study", 8 | "assignment", 9 | "later", 10 | "exam" 11 | ]; 12 | 13 | var defaultAnnotations = [ 14 | { 15 | range: { 16 | startOffset: 28, 17 | endOffset: 77, 18 | startContainerXPath: "id(\"book\")/p[5]", 19 | endContainerXPath: "id(\"book\")/p[5]", 20 | parentContainerXPath: "id(\"book\")/p[5]" 21 | }, 22 | id: "annotation-1435820385354", 23 | selectedText: "Boojho recalled the consequences of deforestation", 24 | color: "pink", 25 | note: "This is my note" 26 | 27 | }, 28 | 29 | 30 | { 31 | range: { 32 | startOffset: 41, 33 | endOffset: 379, 34 | startContainerXPath: "id(\"book\")/p[7]", 35 | endContainerXPath: "id(\"book\")/p[7]", 36 | parentContainerXPath: "id(\"book\")/p[7]" 37 | }, 38 | selectedText: "increased chances of natural calamities such as floods and droughts. Recall that plants need carbon dioxide for photosynthesis. Fewer trees would mean that less carbon dioxide will be used up resulting in its increased amount in the atmosphere. This will lead to global warming as carbon dioxide traps the heat rays reflected by the earth", 39 | color: "green", 40 | note: "This is another note" 41 | }, 42 | 43 | { 44 | range: { 45 | startOffset: 274, 46 | endOffset: 497, 47 | startContainerXPath: "id(\"book\")/p[7]", 48 | endContainerXPath: "id(\"book\")/p[7]", 49 | parentContainerXPath: "id(\"book\")/p[7]" 50 | }, 51 | id: "annotation-1435824292078", 52 | selectedText: "atmosphere. This will lead to global warming as carbon dioxide traps the heat rays reflected by the earth. The increase in temperature on the earth disturbs the water cycle and may reduce rainfall. This could cause droughts", 53 | color: "yellow" 54 | } 55 | ]; 56 | 57 | var annotations = (function() { 58 | if(window.localStorage) { 59 | var annotations = JSON.parse(localStorage.getItem("annotations")); 60 | 61 | if( !annotations || annotations.length == 0 ) { 62 | window.localStorage.setItem("annotations", JSON.stringify(defaultAnnotations)); 63 | return defaultAnnotations; 64 | } 65 | return annotations; 66 | } else { 67 | return []; 68 | } 69 | })(); 70 | 71 | var colors = [ 72 | { 73 | className: "yellow", 74 | }, 75 | 76 | { 77 | className: "green", 78 | }, 79 | 80 | { 81 | className: "pink", 82 | }, 83 | 84 | { 85 | className: "blue", 86 | }, 87 | ] 88 | 89 | var annotator = Object.create(Annotator); 90 | annotator.init({ 91 | containerElement: "#book", 92 | annotations: annotations, 93 | existingTags: tags, 94 | colors: colors 95 | }); 96 | annotator.startListening(); 97 | 98 | }); -------------------------------------------------------------------------------- /example/scripts/editor.js: -------------------------------------------------------------------------------- 1 | var Editor = (function Editor() { 2 | var Editor = { 3 | annotator: null, 4 | annotation: null, 5 | events: [ 6 | { 7 | element: ".js-note-form", 8 | event: "submit", 9 | action: "addNote" 10 | }, 11 | 12 | { 13 | selector: ".js-color-picker", 14 | event: "click", 15 | action: "setColor" 16 | }, 17 | 18 | { 19 | selector: ".js-copy", 20 | event: "click", 21 | action: "copyToClipboard" 22 | }, 23 | 24 | { 25 | selector: ".js-share", 26 | event: "click", 27 | action: "share" 28 | }, 29 | 30 | { 31 | selector: ".js-remove-annotation", 32 | event: "click", 33 | action: "removeAnnotation" 34 | } 35 | ], 36 | 37 | init: function(opts) { 38 | this.annotator = opts.annotator; 39 | var $containerElement = $("body"); 40 | this.$popoverElement = $(this.renderEditorTemplate()); 41 | 42 | $containerElement.append(this.$popoverElement); 43 | 44 | // autocomplete 45 | if(Awesomplete){ 46 | this._awesomplete = new Awesomplete(this.$popoverElement.find(".js-tags-field")[0]); 47 | } 48 | 49 | this.events.forEach(function(eventMap) { 50 | var editor = this; 51 | this.$popoverElement.on(eventMap["event"], eventMap["selector"], function(e) { 52 | e.preventDefault(); 53 | editor[eventMap["action"]].call(editor, e); 54 | }) 55 | }, this); 56 | }, 57 | 58 | renderEditorTemplate: function() { 59 | var html = '
    ' 60 | + '' 89 | + '
    ' 90 | ; 91 | 92 | return html; 93 | }, 94 | 95 | showEditor: function(opts) { 96 | var $popover = this.$popoverElement; 97 | 98 | var position = opts.position, 99 | annotation = opts.annotation, 100 | temporary = opts.temporary; 101 | 102 | var top = position.top - 30; 103 | var left = position.left - this.$popoverElement.width()/2; 104 | 105 | if(annotation) { 106 | this.annotation = annotation; 107 | this.activateAnnotationColor(); 108 | this.renderContents(); 109 | 110 | if(!temporary) { 111 | this.showRemoveBtn(); 112 | } 113 | } 114 | 115 | // FB Share 116 | if( !(window.FB) ) this.$popoverElement.find(".js-facebook-share").hide(); 117 | else { this.$popoverElement.find(".js-facebook-share").show(); } 118 | 119 | 120 | if(temporary) { 121 | this.annotation.render({ temporary: true }); 122 | } 123 | 124 | if(this._awesomplete) { 125 | this._awesomplete.list = this.annotator.tags; 126 | } 127 | 128 | $popover.removeClass("anim").css("top", top).css("left", left).show(); 129 | $popover.find("#annotation-input").focus(); 130 | }, 131 | 132 | isVisible: function() { 133 | return this.$popoverElement.is(":visible"); 134 | }, 135 | 136 | reset: function() { 137 | this.annotation.removeTemporary(); 138 | this.resetNoteForm(); 139 | this.hideRemoveBtn(); 140 | this.annotation = null; 141 | this.$popoverElement.removeAttr("style"); 142 | }, 143 | 144 | resetNoteForm: function() { 145 | this.$popoverElement.find(".js-note-field, .js-tags-field").val(""); 146 | }, 147 | 148 | activateAnnotationColor: function() { 149 | this.$popoverElement 150 | .find(".js-color-picker.active").removeClass("active"); 151 | this.$popoverElement 152 | .find(".js-color-picker." + (this.annotation.color || 'yellow')) 153 | .addClass("active"); 154 | }, 155 | 156 | renderContents: function() { 157 | this.$popoverElement.find(".js-note-field").val(this.annotation.note); 158 | 159 | if(this.annotation.tags) 160 | this.$popoverElement.find(".js-tags-field").val(this.annotation.tags.join(", ")); 161 | }, 162 | 163 | showRemoveBtn: function() { 164 | this.$popoverElement.find(".js-remove-annotation-wrapper").show(); 165 | }, 166 | 167 | hideRemoveBtn: function() { 168 | this.$popoverElement.find(".js-remove-annotation-wrapper").hide(); 169 | }, 170 | 171 | hideEditor: function(event) { 172 | this.reset(); 173 | this.$popoverElement.hide(); 174 | }, 175 | 176 | getTagsFromString: function(string) { 177 | var tags = string 178 | .split(this.annotator.tagRegex) 179 | .map(function(tag) { 180 | var t = $.trim(tag); 181 | if(t.length) return t; 182 | }); 183 | return tags; 184 | }, 185 | 186 | setColor: function(e) { 187 | var $target = $(e.target); 188 | var color = $target.data("color"); 189 | var $form = this.$popoverElement.find(".js-note-form"); 190 | 191 | var note = $form.find(".js-note-field").val(); 192 | var tags = this.getTagsFromString($form.find(".js-tags-field").val()); 193 | 194 | this.saveAndClose({ color: color, note: note, tags: tags }); 195 | }, 196 | 197 | addNote: function(e) { 198 | var $form = $(e.target); 199 | var note = $form.find(".js-note-field").val(); 200 | var tags = this.getTagsFromString($form.find(".js-tags-field").val()); 201 | var color = this.annotation.color || this.annotator.defaultColor; 202 | 203 | this.saveAndClose({ color: color, note: note, tags: tags }); 204 | }, 205 | 206 | saveAndClose: function(data) { 207 | if(!data) return; 208 | 209 | var params = { 210 | debug: this.annotator.debug, 211 | cbk: function(annotation) { 212 | if(!this.annotator.findAnnotation(annotation.id)) { 213 | this.annotation.render({ convert: true }); 214 | this.annotator.annotations.push(annotation); 215 | } else { 216 | this.annotator.updateAnnotation(annotation.id, annotation); 217 | this.annotation.render({ update: true }); 218 | } 219 | 220 | // save tags to global list 221 | this.annotator.addTags(annotation.tags); 222 | 223 | if(this.annotator.debug) 224 | this.saveToLocalStorage(); 225 | 226 | this.hideEditor(); 227 | }.bind(this) 228 | } 229 | 230 | $.extend(params, data); 231 | console.log(params); 232 | this.annotation.save(params); 233 | }, 234 | 235 | copyToClipboard: function() { 236 | var text = this.annotation.selectedText; 237 | 238 | var textarea = $(""); 239 | $(this.annotator.containerElement).append(textarea); 240 | textarea.val(text).select(); 241 | 242 | try { 243 | document.execCommand("copy"); 244 | } catch(e) { 245 | alert("Hit Ctrl/Cmd + C to copy"); 246 | } 247 | 248 | this.hideEditor(); 249 | textarea.remove(); 250 | }, 251 | 252 | truncate: function(str, limit) { 253 | 254 | if(str.length <= limit) return str; 255 | 256 | while(str.length >= limit) { 257 | str = str.substr(0, str.lastIndexOf(" ")); 258 | } 259 | 260 | return str + "..."; 261 | }, 262 | 263 | removeAnnotation: function() { 264 | var annotation = this.annotation; 265 | var annotator = this.annotator; 266 | 267 | 268 | if(!annotation) return; 269 | 270 | var renderedAnnotation = $(this.annotator.containerElement) 271 | .find(".annotation[data-id='" + annotation.id + "']"); 272 | 273 | this.annotation.destroy(function() { 274 | annotator.removeAnnotation(annotation.id); 275 | renderedAnnotation.contents().unwrap(); 276 | }); 277 | 278 | if(this.annotator.debug) 279 | this.saveToLocalStorage(); 280 | this.hideEditor(); 281 | }, 282 | 283 | share: function(e) { 284 | var text = this.annotation.selectedText; 285 | var $target = $(e.target); 286 | 287 | if($target.hasClass("facebook")) { 288 | if( !(FB && FB.ui) ) return; 289 | console.log(window.location.href); 290 | FB.ui( 291 | { 292 | method: 'feed', 293 | link: window.location.href, 294 | description: text, 295 | display: "popup" 296 | }, 297 | function(response) { 298 | if (response && !response.error_code) { 299 | console.log('Posting completed.'); 300 | } else { 301 | console.log('Error while posting.'); 302 | } 303 | } 304 | ); 305 | 306 | } else if($target.hasClass("twitter")) { 307 | var width = 575, 308 | height = 400, 309 | left = ($(window).width() - width) / 2, 310 | top = ($(window).height() - height) / 2, 311 | opts = 'status=1' + 312 | ',width=' + width + 313 | ',height=' + height + 314 | ',top=' + top + 315 | ',left=' + left, 316 | textLimit = 140 - '...'.length - window.location.href.length, 317 | windowURL = "http://twitter.com/share?text=" + window.encodeURIComponent( this.truncate(text, textLimit) || ""); 318 | 319 | window.open(windowURL, 'Share', opts); 320 | } 321 | 322 | this.hideEditor(); 323 | 324 | }, 325 | 326 | saveToLocalStorage: function() { 327 | // save to localStorage 328 | if(window.localStorage) { 329 | var serializedAnnotations = this.annotator.annotations.map(function(annotation) { 330 | return annotation.serialize(); 331 | }); 332 | 333 | window.localStorage.setItem("annotations", JSON.stringify(serializedAnnotations)); 334 | } 335 | } 336 | 337 | 338 | } 339 | 340 | 341 | return Editor; 342 | })(); -------------------------------------------------------------------------------- /example/scripts/vendor/awesomplete.min.js: -------------------------------------------------------------------------------- 1 | // Awesomplete - Lea Verou - MIT license 2 | (function(){function m(a,b){for(var c in a){var g=a[c],e=this.input.getAttribute("data-"+c.toLowerCase());this[c]="number"===typeof g?parseInt(e):!1===g?null!==e:g instanceof Function?null:e;this[c]||0===this[c]||(this[c]=c in b?b[c]:g)}}function d(a,b){return"string"===typeof a?(b||document).querySelector(a):a||null}function h(a,b){return k.call((b||document).querySelectorAll(a))}function l(){h("input.awesomplete").forEach(function(a){new Awesomplete(a)})}var f=function(a,b){var c=this;this.input= 3 | d(a);this.input.setAttribute("autocomplete","off");this.input.setAttribute("aria-autocomplete","list");b=b||{};m.call(this,{minChars:2,maxItems:10,autoFirst:!1,filter:f.FILTER_CONTAINS,sort:f.SORT_BYLENGTH,item:function(a,b){return d.create("li",{innerHTML:a.replace(RegExp(d.regExpEscape(b.trim()),"gi"),"$&"),"aria-selected":"false"})},replace:function(a){this.input.value=a}},b);this.index=-1;this.container=d.create("div",{className:"awesomplete",around:a});this.ul=d.create("ul",{hidden:"", 4 | inside:this.container});this.status=d.create("span",{className:"visually-hidden",role:"status","aria-live":"assertive","aria-relevant":"additions",inside:this.container});d.bind(this.input,{input:this.evaluate.bind(this),blur:this.close.bind(this),keydown:function(a){var b=a.keyCode;if(c.opened)if(13===b&&c.selected)a.preventDefault(),c.select();else if(27===b)c.close();else if(38===b||40===b)a.preventDefault(),c[38===b?"previous":"next"]()}});d.bind(this.input.form,{submit:this.close.bind(this)}); 5 | d.bind(this.ul,{mousedown:function(a){a=a.target;if(a!==this){for(;a&&!/li/i.test(a.nodeName);)a=a.parentNode;a&&c.select(a)}}});this.input.hasAttribute("list")?(this.list="#"+a.getAttribute("list"),a.removeAttribute("list")):this.list=this.input.getAttribute("data-list")||b.list||[];f.all.push(this)};f.prototype={set list(a){Array.isArray(a)?this._list=a:"string"===typeof a&&-1=this.minChars&&05%', 'ie >= 9'])) 45 | .pipe(cssmin()) 46 | .pipe(rename({suffix: '.min'})) 47 | .pipe(gulp.dest('./dist')) 48 | 49 | gulp.src([ 50 | './src/styles/annotator.scss' 51 | ]) 52 | .pipe(sass()).on('error', errorHandler) 53 | .pipe(autoprefixer(['>5%', 'ie >= 9'])) 54 | .pipe(gulp.dest('./dist')) 55 | 56 | }); 57 | 58 | 59 | // Sass 60 | gulp.task('sass', function() { 61 | return gulp.src('./src/styles/**/*.scss') 62 | .pipe(sass()).on('error', errorHandler) 63 | .pipe(autoprefixer(['>5%', 'ie >= 9'])) 64 | .pipe(gulp.dest('./example/css')) 65 | .pipe(connect.reload()); 66 | }); 67 | 68 | 69 | gulp.task('copy', function(){ 70 | gulp.src(['./src/scripts/**/*.js']) 71 | .pipe(gulp.dest('./example/scripts')).on('error', errorHandler); 72 | gulp.src('./src/*.html') 73 | .pipe(gulp.dest('./example')); 74 | }); 75 | 76 | 77 | 78 | // Server 79 | gulp.task('webserver', function() { 80 | connect.server({ 81 | root: ['example', './bower_components'], 82 | livereload: true 83 | }); 84 | }); 85 | 86 | 87 | gulp.task('htmlreload', function () { 88 | gulp.src('./src/*.html') 89 | .pipe(connect.reload()); 90 | }); 91 | 92 | 93 | function errorHandler (error) { 94 | console.log(error.toString()); 95 | this.emit('end'); 96 | } 97 | 98 | 99 | // watch 100 | gulp.task('watch', function() { 101 | // gulp.watch('./src/*.html', ['htmlreload', 'copy']); 102 | gulp.watch('./src/styles/**/*.scss', ['sass']); 103 | gulp.watch('./src/scripts/**/*.js', ['copy']); 104 | gulp.watch('./src/**/*.html', ['copy']); 105 | }); 106 | 107 | gulp.task('default', ['htmlreload', 'copy', 'sass', 'webserver', 'watch']); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "annotator", 3 | "version": "0.0.1", 4 | "description": "Popover that lets you add tags and highlight selected text on a page", 5 | "main": "src/scripts/annotator.js", 6 | "devDependencies": { 7 | "bower": "^1.3.12", 8 | "gulp": "^3.8.11", 9 | "gulp-autoprefixer": "^2.1.0", 10 | "gulp-changed": "^1.1.1", 11 | "gulp-concat": "^2.5.2", 12 | "gulp-connect": "^2.2.0", 13 | "gulp-copy": "0.0.2", 14 | "gulp-cssmin": "^0.1.7", 15 | "gulp-imagemin": "^2.2.1", 16 | "gulp-jshint": "^1.9.2", 17 | "gulp-minify-html": "^1.0.0", 18 | "gulp-rename": "^1.2.2", 19 | "gulp-sass": "^1.3.3", 20 | "gulp-sourcemaps": "^1.5.0", 21 | "gulp-strip-debug": "^1.0.2", 22 | "gulp-uglify": "^1.1.0" 23 | }, 24 | "scripts": { 25 | "test": "echo \"Error: no test specified\" && exit 1", 26 | "postinstall": "npm run bower", 27 | "bower": "./node_modules/bower/bin/bower install" 28 | }, 29 | "keywords": [ 30 | "selection", 31 | "tagging", 32 | "annotation", 33 | "highlight" 34 | ], 35 | "author": "Bharani M", 36 | "license": "ISC" 37 | } 38 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Annotator 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 28 | 29 |
    30 |
    31 |
    32 |

    Annotator

    33 |

    34 | Highlight, share, add notes and tags to any selected text on a page 35 |

    36 |
    37 | 38 |
    39 | 40 |

    7.1 Deforestation and Its Causes

    41 |

    42 | A great variety of plants and animals exists on earth. They are essential for the wellbeing and survival of mankind. Today, a major threat to survival of these organisms is deforestation. We know that deforestation means clearing of forests and using that land for other purposes. 43 |

    44 | 45 |

    46 | Trees in the forest are cut for some of the purposes mentioned below: 47 |

    48 | 49 |
      50 |
    • Procuring land for cultivation.
    • 51 |
    • Building houses and factories.
    • 52 |
    • Making furniture or using wood as fuel.
    • 53 |
    54 | 55 |

    56 | Some natural causes of deforestation are forest fires and severe droughts. 57 |

    58 | 59 | 60 |

    Activity 7.1

    61 |

    62 | Add more causes of deforestation to your list and classify them into natural and man-made. 63 |

    64 | 65 | 66 |

    7.2 Consequences of Deforestation

    67 |

    68 | Paheli and Boojho recalled the consequences of deforestation. They remembered that deforestation increases the temperature and pollution level on the earth. It increases the level of carbon dioxide in the atmosphere. Ground water level also gets lowered. They know that deforestation disturbs the balance in nature. They were told by Prof. Ahmad that if cutting of trees continues, rainfall and the fertility of the soil will decrease. 69 |

    70 | 71 |

    72 | How does deforestation reduce rainfall on the one hand and lead to floods on the other? 73 |

    74 | 75 |

    76 | Moreover, there will be increased chances of natural calamities such as floods and droughts. Recall that plants need carbon dioxide for photosynthesis. Fewer trees would mean that less carbon dioxide will be used up resulting in its increased amount in the atmosphere. This will lead to global warming as carbon dioxide traps the heat rays reflected by the earth. The increase in temperature on the earth disturbs the water cycle and may reduce rainfall. This could cause droughts. 77 |

    78 | 79 |

    80 | Deforestation is a major cause which leads to the change in soil properties. Physical properties of the soil get affected by plantation and vegetation. Recall from Class VII how trees prevent soil erosion. Fewer trees result in more soil erosion. Removal of the top layer of the soil exposes the lower, hard and rocky layers. This soil has less humus and is less fertile. Gradually the fertile land gets converted into deserts. It is called desertification. 81 |

    82 | 83 |

    84 | Deforestation also leads to a decrease in the water holding capacity of the soil. The movement of water from the soil surface into the ground (infiltration rate) is reduced. So, there are floods. The other properties of the soil like nutrient content, texture, etc., also change because of deforestation. 85 |

    86 | 87 | 88 |

    Activity 7.2

    89 |

    90 | Animal life is also affected by deforestation. How? List the points and discuss them in your class. 91 |

    92 | 93 | 94 |

    7.3 Conservation of Forest and Wildlife

    95 |

    96 | Having become aware of the effects of deforestation, Paheli and Boojho are worried. They go to Prof. Ahmad and ask him how forests and wildlife can be saved. 97 |

    98 |

    99 | Prof. Ahmad organises a visit to a biosphere reserve for Paheli, Boojho and their classmates. He selects a place named Pachmarhi Biosphere Reserve. He knows that the plants and animals found here are similar to those of the upper Himalayan peaks and to those belonging to the lower western ghats. Prof. Ahmad believes that the biodiversity found here is unique. He requests Madhavji, a forest employee, to guide the children inside the biosphere reserve. He explains that preserving areas of such biological importance make them a part of our national heritage. 100 |

    101 | 102 |

    103 | Biosphere is that part of the earth in which living organisms exist or which supports life. Biological diversity or biodiversity, refers to the variety of organisms existing on the earth, their interrelationships and their relationship with the environment. 104 |

    105 | 106 |

    107 | Madhavji explains to the children that apart from our personal efforts and efforts of the society, government agencies also take care of the forests and animals. The government lays down rules, methods and policies to protect and conserve them. Wildlife sanctuaries, national parks, biosphere reserves, etc., are protected areas for conservation of plants and animals present in that area. 108 |

    109 | 110 |

    111 | To protect our flora and fauna and their habitats, protected areas called sanctuaries, national parks and biosphere reserves have been earmarked. Plantation, cultivation, grazing, felling trees, hunting and poaching are prohibited there. Sanctuary : Areas where animals are protected from any disturbance to them and their habitat. 112 |

    113 | 114 |

    115 | National Park : Areas reserved for wild life where they can freely use the habitats and natural resources. Biosphere Reserve : Large areas of protected land for conservation of wild life, plant and animal resources and traditional life of the tribals living in the area. 116 |

    117 |
    118 |
    119 | 120 | 125 |
    126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/scripts/annotation.js: -------------------------------------------------------------------------------- 1 | var Annotation = (function Annotation() { 2 | 3 | var Annotation = { 4 | annotator: null, 5 | range: { 6 | startContainerXPath: null, 7 | endContainerXPath: null, 8 | parentContainerXPath: null, 9 | startOffset: null, 10 | endOffset: null 11 | }, 12 | id: null, 13 | selectedText: null, 14 | color: null, 15 | note: null, 16 | tags: [], 17 | 18 | init: function(obj) { 19 | if(!obj) return; 20 | 21 | if(obj.annotator) { 22 | this.annotator = obj.annotator; 23 | } 24 | if(obj.selectedRange) { 25 | this.saveSelection(obj.selectedRange); 26 | } else if(obj.savedAnnotation) { 27 | var savedAnnotation = obj.savedAnnotation; 28 | 29 | this.range = savedAnnotation.range; 30 | this.id = savedAnnotation.id; 31 | this.selectedText = savedAnnotation.selectedText; 32 | this.color = savedAnnotation.color; 33 | this.note = savedAnnotation.note; 34 | this.tags = savedAnnotation.tags; 35 | } 36 | 37 | this.setRangeElements(); 38 | 39 | }, 40 | 41 | setRangeElements: function() { 42 | this.$parentContainer = $(this.annotator.findElementByXPath(this.range.parentContainerXPath)); 43 | this.$startContainer = $(this.annotator.findElementByXPath(this.range.startContainerXPath)); 44 | this.$endContainer = $(this.annotator.findElementByXPath(this.range.endContainerXPath)); 45 | 46 | }, 47 | 48 | render: function(opts) { 49 | if(opts && opts.temporary) { 50 | this.wrapNodes(true); 51 | } else if(opts && opts.convert) { 52 | this.convertFromTemporary(); 53 | } else if(opts && opts.update) { 54 | this.updateAnnotation(); 55 | } else { 56 | this.wrapNodes(); 57 | } 58 | 59 | // remove existing selection 60 | // we don't need it anymore 61 | if (window.getSelection) { 62 | window.getSelection().removeAllRanges(); 63 | } else if (document.selection) { 64 | document.selection.empty(); 65 | } 66 | }, 67 | 68 | updateAnnotation: function() { 69 | var $parentContainer = this.$parentContainer; 70 | 71 | var renderedAnnotation = $parentContainer 72 | .find(".annotation[data-id='" + this.id + "']"); 73 | 74 | renderedAnnotation.removeClass("annotation"); 75 | 76 | renderedAnnotation 77 | .removeAttr("class") 78 | .addClass("annotation") 79 | .addClass(this.color) 80 | .removeAttr("style"); 81 | }, 82 | 83 | saveSelection: function(range) { 84 | var parentContainer = range.commonAncestorContainer; 85 | var startContainer = range.startContainer; 86 | var endContainer = range.endContainer; 87 | 88 | var startOffset = range.startOffset; 89 | var endOffset = range.endOffset; 90 | 91 | var parentNode = this.getParentNodeFor(parentContainer); 92 | var startNode = this.getParentNodeFor(startContainer); 93 | var endNode = this.getParentNodeFor(endContainer); 94 | 95 | var nodesBetweenStart = this.getNodesToWrap(parentNode, startNode.firstChild, startContainer); 96 | var nodesBetweenEnd = this.getNodesToWrap(parentNode, endNode.firstChild, endContainer); 97 | 98 | if(nodesBetweenStart.length) { 99 | for(var i = 0; i < nodesBetweenStart.length; i++) { 100 | var characterLength = nodesBetweenStart[i].nodeValue.length; 101 | startOffset += characterLength; 102 | } 103 | } 104 | 105 | if(nodesBetweenEnd.length) { 106 | for(var i = 0; i < nodesBetweenEnd.length; i++) { 107 | endOffset += nodesBetweenEnd[i].nodeValue.length; 108 | } 109 | } 110 | 111 | var selectedContent = this.getSelectionContent(range); 112 | 113 | this.range = { 114 | startContainerXPath: this.annotator.createXPathFromElement(startNode), 115 | endContainerXPath: this.annotator.createXPathFromElement(endNode), 116 | parentContainerXPath: this.annotator.createXPathFromElement(parentNode), 117 | startOffset: startOffset, 118 | endOffset: endOffset, 119 | }; 120 | 121 | this.id = this.generateRandomID(); 122 | this.selectedText = selectedContent; 123 | }, 124 | 125 | getSelectionContent: function(range) { 126 | var container = document.createElement("div"); 127 | container.appendChild(range.cloneContents()); 128 | var text = container.textContent; 129 | return text; 130 | }, 131 | 132 | getParentNodeFor: function(node) { 133 | 134 | while(node.nodeType != 1 || (node.nodeType == 1 && node.classList.contains("js-no-select")) ) { 135 | node = node.parentNode; 136 | } 137 | 138 | return node; 139 | }, 140 | 141 | generateRandomID: function() { 142 | return 'annotation-' + new Date().getTime(); 143 | }, 144 | 145 | save: function(obj) { 146 | if(!obj) return; 147 | 148 | this.color = obj.color; 149 | 150 | if(obj.note) 151 | this.note = obj.note; 152 | 153 | if(obj.tags && obj.tags.length) { 154 | this.tags = obj.tags 155 | } 156 | 157 | var data = this.serialize(); 158 | 159 | if(obj && obj.debug) { 160 | console.log(JSON.stringify(data)); 161 | } else { 162 | this.postToRemote(); 163 | } 164 | 165 | if(obj && obj.cbk) obj.cbk(this); 166 | 167 | }, 168 | 169 | // TODO 170 | destroy: function(cbk) { 171 | if(cbk) cbk(); 172 | }, 173 | 174 | // TODO 175 | postToRemote: function() { 176 | console.log("AJAX"); 177 | }, 178 | 179 | serialize: function() { 180 | var range = this.range; 181 | return { 182 | range: { 183 | startOffset: range.startOffset, 184 | endOffset: range.endOffset, 185 | startContainerXPath: range.startContainerXPath, 186 | endContainerXPath: range.endContainerXPath, 187 | parentContainerXPath: range.parentContainerXPath 188 | }, 189 | id: this.id, 190 | selectedText: this.selectedText, 191 | color: this.color, 192 | note: this.note, 193 | tags: this.tags 194 | } 195 | }, 196 | 197 | getContainedNodes: function() { 198 | var range, startContainer, endContainer, parentContainer; 199 | var nodes = []; 200 | 201 | range = this.range; 202 | 203 | parentContainer = this.$parentContainer.get(0); 204 | startContainer = this.$startContainer.get(0); 205 | endContainer = this.$endContainer.get(0); 206 | 207 | var startTextNodeParams = this.getTextNodeAtOffset(startContainer, range.startOffset); 208 | endTextNodeParams = this.getTextNodeAtOffset(endContainer, range.endOffset); 209 | 210 | var startTextNode = startTextNodeParams[0], 211 | startOffset = startTextNodeParams[1], 212 | endTextNode = endTextNodeParams[0], 213 | endOffset = endTextNodeParams[1]; 214 | 215 | 216 | if(startTextNode == endTextNode) { 217 | var startTextNodeSplit = startTextNode.splitText(startOffset); 218 | var endTextNodeSplit = startTextNodeSplit.splitText(endOffset - startOffset); 219 | } else { 220 | var startTextNodeSplit = startTextNode.splitText(startOffset); 221 | var endTextNodeSplit = endTextNode.splitText(endOffset); 222 | } 223 | 224 | 225 | var innerNodes = this.getNodesToWrap(parentContainer, startTextNodeSplit, endTextNodeSplit); 226 | 227 | 228 | for(var i = 0; i < innerNodes.length; i++) { 229 | nodes.push(innerNodes[i]); 230 | } 231 | 232 | return nodes; 233 | }, 234 | 235 | 236 | getTextNodeAtOffset: function(rootNode, offset) { 237 | var textNode, 238 | count = 0, 239 | found = false; 240 | 241 | function getTextNodes(node) { 242 | if (node.nodeType == Node.TEXT_NODE && !/^\s*$/.test(node.nodeValue)) { 243 | if ( found != true) { 244 | if(count+node.nodeValue.length >= offset) { 245 | textNode = node; 246 | found = true; 247 | } else { 248 | count += node.nodeValue.length 249 | } 250 | } 251 | } else if (node.nodeType == Node.ELEMENT_NODE ) { 252 | for (var i = 0, len = node.childNodes.length; i < len; ++i) { 253 | getTextNodes(node.childNodes[i]); 254 | } 255 | } 256 | } 257 | 258 | getTextNodes(rootNode); 259 | return [textNode, (count == 0 ? offset : offset - count)]; 260 | 261 | }, 262 | 263 | getNodesToWrap: function(rootNode, startNode, endNode) { 264 | var pastStartNode = false, reachedEndNode = false, textNodes = []; 265 | 266 | function getTextNodes(node) { 267 | 268 | if (node == startNode) { 269 | pastStartNode = true; 270 | } 271 | if (node == endNode) { 272 | reachedEndNode = true; 273 | } else if (node.nodeType == Node.TEXT_NODE) { 274 | if (pastStartNode && !reachedEndNode && !/^\s*$/.test(node.nodeValue)) { 275 | textNodes.push(node); 276 | } 277 | } else if (node.nodeType == Node.ELEMENT_NODE ) { 278 | 279 | for (var i = 0, len = node.childNodes.length; !reachedEndNode && i < len; ++i) { 280 | getTextNodes(node.childNodes[i]); 281 | } 282 | } 283 | } 284 | 285 | getTextNodes(rootNode); 286 | return textNodes; 287 | }, 288 | 289 | 290 | wrapNodes: function(temporary) { 291 | var nodes = this.getContainedNodes(); 292 | var newNode = this.createWrapperElement(temporary) 293 | for(var i = 0; i < nodes.length; i++) { 294 | $(nodes[i]).wrap(newNode); 295 | } 296 | }, 297 | 298 | createWrapperElement: function(temporary) { 299 | var annotationID = this.id; 300 | 301 | var span = document.createElement("span"); 302 | 303 | if(!temporary) { 304 | span.classList.add("annotation"); 305 | span.classList.add(this.color); 306 | } else { 307 | span.classList.add("temporary"); 308 | } 309 | 310 | span.setAttribute("data-id", annotationID); 311 | 312 | return span; 313 | }, 314 | 315 | removeTemporary: function() { 316 | var temporary = this.$parentContainer.find(".temporary"); 317 | 318 | for(var i = 0; i < temporary.length; i++) { 319 | var elem = temporary[i]; 320 | $(elem.childNodes[0]).unwrap(); 321 | } 322 | }, 323 | 324 | convertFromTemporary: function() { 325 | var temporary = this.$parentContainer.find(".temporary"); 326 | 327 | temporary 328 | .removeClass("temporary") 329 | .addClass("annotation") 330 | .addClass(this.color); 331 | 332 | } 333 | 334 | 335 | }; 336 | 337 | return Annotation; 338 | })(); -------------------------------------------------------------------------------- /src/scripts/annotator.js: -------------------------------------------------------------------------------- 1 | var Annotator = (function Annotator() { 2 | 3 | var Annotator = { 4 | containerElement: "body", 5 | annotations: [], 6 | editor: null, 7 | 8 | defaultColor: "yellow", 9 | colors: [ 10 | { 11 | className: "yellow", 12 | }, 13 | 14 | { 15 | className: "green", 16 | }, 17 | 18 | { 19 | className: "pink", 20 | }, 21 | 22 | { 23 | className: "blue", 24 | }, 25 | ], 26 | 27 | tags: [], 28 | 29 | 30 | init: function(opts) { 31 | this.containerElement = opts.containerElement || "body"; 32 | this.debug = opts.debug || "true"; 33 | this.remoteURL = opts.remoteURL || ""; 34 | 35 | if(opts.existingTags) { 36 | this.tags = opts.existingTags; 37 | } 38 | 39 | if(opts.colors) { 40 | this.colors = opts.colors; 41 | } 42 | 43 | if(opts.annotations) { 44 | this.renderExistingAnnotations(opts.annotations); 45 | } 46 | 47 | // Setup editor 48 | var editor = Object.create(Editor); 49 | editor.init({ annotator: this }); 50 | this.setEditor(editor); 51 | }, 52 | 53 | addTags: function(tags) { 54 | this.tags = this.tags.concat(tags); 55 | }, 56 | 57 | setEditor: function(editor) { 58 | this.editor = editor; 59 | }, 60 | 61 | findAnnotation: function(annotationID) { 62 | return this.annotations.filter(function(annotation) { 63 | return annotation.id == annotationID; 64 | })[0]; 65 | }, 66 | 67 | updateAnnotation: function(annotationID, newAnnotation) { 68 | var index = this.annotations.map(function(i) { return i.id }).indexOf(annotationID); 69 | if(index <= -1) return; 70 | this.annotations[index] = newAnnotation; 71 | }, 72 | 73 | removeAnnotation: function(annotationID) { 74 | var index = this.annotations.map(function(i) { return i.id }).indexOf(annotationID); 75 | if(index <= -1) return; 76 | 77 | this.annotations.splice(index, 1); 78 | }, 79 | 80 | renderExistingAnnotations: function(annotations) { 81 | for(var i = 0; i < annotations.length; i++) { 82 | var annotation = Object.create(Annotation); 83 | annotation.init({ savedAnnotation: annotations[i], annotator: this }); 84 | annotation.render(); 85 | this.annotations.push(annotation); 86 | } 87 | }, 88 | 89 | 90 | handleAnnotationClick: function(e) { 91 | var $target = $(e.target); 92 | var annotationID = $target.data("id"); 93 | var annotation = this.findAnnotation(annotationID); 94 | 95 | if(!annotation) return; 96 | 97 | this.editor.showEditor({ 98 | position: { 99 | top: e.pageY, 100 | left: e.pageX 101 | }, 102 | annotation: annotation 103 | }); 104 | 105 | }, 106 | 107 | handleAnnotation: function(e) { 108 | var selection = window.getSelection(); 109 | var selectedText; 110 | if(selection.text) { 111 | selectedText = selection.text; 112 | } else { 113 | selectedText = selection.toString(); 114 | }; 115 | 116 | 117 | if(selection && !selection.isCollapsed && selectedText && selectedText.length>5) { 118 | var range = selection.getRangeAt(0); 119 | 120 | var annotation = Object.create(Annotation); 121 | annotation.init({ selectedRange: range, annotator: this }); 122 | 123 | var position = { 124 | top: e.pageY, 125 | left: e.pageX 126 | }; 127 | 128 | this.editor.showEditor({ 129 | temporary: true, 130 | position: { 131 | top: e.pageY, 132 | left: e.pageX 133 | }, 134 | annotation: annotation 135 | }); 136 | } 137 | }, 138 | 139 | startListening: function() { 140 | var $element = $(this.containerElement); 141 | var self = this; 142 | 143 | 144 | $element.on("mouseup touchend", function(e) { 145 | e.preventDefault(); 146 | e.stopPropagation(); 147 | 148 | var $target = $(e.target); 149 | 150 | if(self.editor.isVisible() && !$target.parents("#annotation-editor").length && !$target.hasClass("annotation")) { 151 | // editor is open but clicked outside 152 | self.editor.hideEditor() 153 | } 154 | 155 | 156 | if(!$target.parents(".js-no-select").length) { 157 | self.handleAnnotation(e); 158 | } 159 | }); 160 | 161 | $element.on("click", ".annotation", function(e) { 162 | e.stopPropagation(); 163 | self.handleAnnotationClick(e); 164 | }); 165 | 166 | }, 167 | 168 | findElementByXPath: function(path) { 169 | var evaluator = new XPathEvaluator(); 170 | var result = evaluator.evaluate(path, document.querySelector(this.containerElement), null,XPathResult.FIRST_ORDERED_NODE_TYPE, null); 171 | return result.singleNodeValue; 172 | }, 173 | 174 | createXPathFromElement: function(elm) { 175 | var allNodes = document.getElementsByTagName('*'); 176 | 177 | for (var segs = []; elm && elm.nodeType == 1 && elm != document.querySelector(this.containerElement).parentNode; elm = elm.parentNode) { 178 | if (elm.hasAttribute('id')) { 179 | var uniqueIdCount = 0; 180 | for (var n=0;n < allNodes.length;n++) { 181 | if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++; 182 | if (uniqueIdCount > 1) break; 183 | }; 184 | 185 | if ( uniqueIdCount == 1) { 186 | segs.unshift('id("' + elm.getAttribute('id') + '")'); 187 | return segs.join('/'); 188 | } else { 189 | segs.unshift(elm.localName.toLowerCase() + '[@id="' + elm.getAttribute('id') + '"]'); 190 | } 191 | 192 | } else if (elm.hasAttribute('class')) { 193 | segs.unshift(elm.localName.toLowerCase() + '[@class="' + elm.getAttribute('class') + '"]'); 194 | } else { 195 | for (i = 1, sib = elm.previousSibling; sib; sib = sib.previousSibling) { 196 | if (sib.localName == elm.localName) i++; 197 | }; 198 | segs.unshift(elm.localName.toLowerCase() + '[' + i + ']'); 199 | }; 200 | } 201 | 202 | return segs.length ? '' + segs.join('/') : null; 203 | } 204 | 205 | }; 206 | 207 | return Annotator; 208 | })(); -------------------------------------------------------------------------------- /src/scripts/editor.js: -------------------------------------------------------------------------------- 1 | var Editor = (function Editor() { 2 | var Editor = { 3 | annotator: null, 4 | annotation: null, 5 | events: [ 6 | { 7 | element: ".js-note-form", 8 | event: "submit", 9 | action: "addNote" 10 | }, 11 | 12 | { 13 | selector: ".js-color-picker", 14 | event: "click", 15 | action: "setColor" 16 | }, 17 | 18 | { 19 | selector: ".js-copy", 20 | event: "click", 21 | action: "copyToClipboard" 22 | }, 23 | 24 | { 25 | selector: ".js-share", 26 | event: "click", 27 | action: "share" 28 | }, 29 | 30 | { 31 | selector: ".js-remove-annotation", 32 | event: "click", 33 | action: "removeAnnotation" 34 | } 35 | ], 36 | 37 | init: function(opts) { 38 | this.annotator = opts.annotator; 39 | var $containerElement = $("body"); 40 | this.$popoverElement = $(this.renderEditorTemplate()); 41 | 42 | $containerElement.append(this.$popoverElement); 43 | 44 | // autocomplete 45 | if(Awesomplete){ 46 | this._awesomplete = new Awesomplete(this.$popoverElement.find(".js-tags-field")[0]); 47 | } 48 | 49 | this.events.forEach(function(eventMap) { 50 | var editor = this; 51 | this.$popoverElement.on(eventMap["event"], eventMap["selector"], function(e) { 52 | e.preventDefault(); 53 | editor[eventMap["action"]].call(editor, e); 54 | }) 55 | }, this); 56 | }, 57 | 58 | renderEditorTemplate: function() { 59 | var html = '
    ' 60 | + '' 89 | + '
    ' 90 | ; 91 | 92 | return html; 93 | }, 94 | 95 | showEditor: function(opts) { 96 | var $popover = this.$popoverElement; 97 | 98 | var position = opts.position, 99 | annotation = opts.annotation, 100 | temporary = opts.temporary; 101 | 102 | var top = position.top - 30; 103 | var left = position.left - this.$popoverElement.width()/2; 104 | 105 | if(annotation) { 106 | this.annotation = annotation; 107 | this.activateAnnotationColor(); 108 | this.renderContents(); 109 | 110 | if(!temporary) { 111 | this.showRemoveBtn(); 112 | } 113 | } 114 | 115 | // FB Share 116 | if( !(window.FB) ) this.$popoverElement.find(".js-facebook-share").hide(); 117 | else { this.$popoverElement.find(".js-facebook-share").show(); } 118 | 119 | 120 | if(temporary) { 121 | this.annotation.render({ temporary: true }); 122 | } 123 | 124 | if(this._awesomplete) { 125 | this._awesomplete.list = this.annotator.tags; 126 | } 127 | 128 | $popover.removeClass("anim").css("top", top).css("left", left).show(); 129 | $popover.find("#annotation-input").focus(); 130 | }, 131 | 132 | isVisible: function() { 133 | return this.$popoverElement.is(":visible"); 134 | }, 135 | 136 | reset: function() { 137 | this.annotation.removeTemporary(); 138 | this.resetNoteForm(); 139 | this.hideRemoveBtn(); 140 | this.annotation = null; 141 | this.$popoverElement.removeAttr("style"); 142 | }, 143 | 144 | resetNoteForm: function() { 145 | this.$popoverElement.find(".js-note-field, .js-tags-field").val(""); 146 | }, 147 | 148 | activateAnnotationColor: function() { 149 | this.$popoverElement 150 | .find(".js-color-picker.active").removeClass("active"); 151 | this.$popoverElement 152 | .find(".js-color-picker." + (this.annotation.color || 'yellow')) 153 | .addClass("active"); 154 | }, 155 | 156 | renderContents: function() { 157 | this.$popoverElement.find(".js-note-field").val(this.annotation.note); 158 | 159 | if(this.annotation.tags) 160 | this.$popoverElement.find(".js-tags-field").val(this.annotation.tags.join(", ")); 161 | }, 162 | 163 | showRemoveBtn: function() { 164 | this.$popoverElement.find(".js-remove-annotation-wrapper").show(); 165 | }, 166 | 167 | hideRemoveBtn: function() { 168 | this.$popoverElement.find(".js-remove-annotation-wrapper").hide(); 169 | }, 170 | 171 | hideEditor: function(event) { 172 | this.reset(); 173 | this.$popoverElement.hide(); 174 | }, 175 | 176 | getTagsFromString: function(string) { 177 | var tags = string 178 | .split(this.annotator.tagRegex) 179 | .map(function(tag) { 180 | var t = $.trim(tag); 181 | if(t.length) return t; 182 | }); 183 | return tags; 184 | }, 185 | 186 | setColor: function(e) { 187 | var $target = $(e.target); 188 | var color = $target.data("color"); 189 | var $form = this.$popoverElement.find(".js-note-form"); 190 | 191 | var note = $form.find(".js-note-field").val(); 192 | var tags = this.getTagsFromString($form.find(".js-tags-field").val()); 193 | 194 | this.saveAndClose({ color: color, note: note, tags: tags }); 195 | }, 196 | 197 | addNote: function(e) { 198 | var $form = $(e.target); 199 | var note = $form.find(".js-note-field").val(); 200 | var tags = this.getTagsFromString($form.find(".js-tags-field").val()); 201 | var color = this.annotation.color || this.annotator.defaultColor; 202 | 203 | this.saveAndClose({ color: color, note: note, tags: tags }); 204 | }, 205 | 206 | saveAndClose: function(data) { 207 | if(!data) return; 208 | 209 | var params = { 210 | debug: this.annotator.debug, 211 | cbk: function(annotation) { 212 | if(!this.annotator.findAnnotation(annotation.id)) { 213 | this.annotation.render({ convert: true }); 214 | this.annotator.annotations.push(annotation); 215 | } else { 216 | this.annotator.updateAnnotation(annotation.id, annotation); 217 | this.annotation.render({ update: true }); 218 | } 219 | 220 | // save tags to global list 221 | this.annotator.addTags(annotation.tags); 222 | 223 | if(this.annotator.debug) 224 | this.saveToLocalStorage(); 225 | 226 | this.hideEditor(); 227 | }.bind(this) 228 | } 229 | 230 | $.extend(params, data); 231 | console.log(params); 232 | this.annotation.save(params); 233 | }, 234 | 235 | copyToClipboard: function() { 236 | var text = this.annotation.selectedText; 237 | 238 | var textarea = $(""); 239 | $(this.annotator.containerElement).append(textarea); 240 | textarea.val(text).select(); 241 | 242 | try { 243 | document.execCommand("copy"); 244 | } catch(e) { 245 | alert("Hit Ctrl/Cmd + C to copy"); 246 | } 247 | 248 | this.hideEditor(); 249 | textarea.remove(); 250 | }, 251 | 252 | truncate: function(str, limit) { 253 | 254 | if(str.length <= limit) return str; 255 | 256 | while(str.length >= limit) { 257 | str = str.substr(0, str.lastIndexOf(" ")); 258 | } 259 | 260 | return str + "..."; 261 | }, 262 | 263 | removeAnnotation: function() { 264 | var annotation = this.annotation; 265 | var annotator = this.annotator; 266 | 267 | 268 | if(!annotation) return; 269 | 270 | var renderedAnnotation = $(this.annotator.containerElement) 271 | .find(".annotation[data-id='" + annotation.id + "']"); 272 | 273 | this.annotation.destroy(function() { 274 | annotator.removeAnnotation(annotation.id); 275 | renderedAnnotation.contents().unwrap(); 276 | }); 277 | 278 | if(this.annotator.debug) 279 | this.saveToLocalStorage(); 280 | this.hideEditor(); 281 | }, 282 | 283 | share: function(e) { 284 | var text = this.annotation.selectedText; 285 | var $target = $(e.target); 286 | 287 | if($target.hasClass("facebook")) { 288 | if( !(FB && FB.ui) ) return; 289 | console.log(window.location.href); 290 | FB.ui( 291 | { 292 | method: 'feed', 293 | link: window.location.href, 294 | description: text, 295 | display: "popup" 296 | }, 297 | function(response) { 298 | if (response && !response.error_code) { 299 | console.log('Posting completed.'); 300 | } else { 301 | console.log('Error while posting.'); 302 | } 303 | } 304 | ); 305 | 306 | } else if($target.hasClass("twitter")) { 307 | var width = 575, 308 | height = 400, 309 | left = ($(window).width() - width) / 2, 310 | top = ($(window).height() - height) / 2, 311 | opts = 'status=1' + 312 | ',width=' + width + 313 | ',height=' + height + 314 | ',top=' + top + 315 | ',left=' + left, 316 | textLimit = 140 - '...'.length - window.location.href.length, 317 | windowURL = "http://twitter.com/share?text=" + window.encodeURIComponent( this.truncate(text, textLimit) || ""); 318 | 319 | window.open(windowURL, 'Share', opts); 320 | } 321 | 322 | this.hideEditor(); 323 | 324 | }, 325 | 326 | saveToLocalStorage: function() { 327 | // save to localStorage 328 | if(window.localStorage) { 329 | var serializedAnnotations = this.annotator.annotations.map(function(annotation) { 330 | return annotation.serialize(); 331 | }); 332 | 333 | window.localStorage.setItem("annotations", JSON.stringify(serializedAnnotations)); 334 | } 335 | } 336 | 337 | 338 | } 339 | 340 | 341 | return Editor; 342 | })(); -------------------------------------------------------------------------------- /src/scripts/vendor/awesomplete.min.js: -------------------------------------------------------------------------------- 1 | // Awesomplete - Lea Verou - MIT license 2 | (function(){function m(a,b){for(var c in a){var g=a[c],e=this.input.getAttribute("data-"+c.toLowerCase());this[c]="number"===typeof g?parseInt(e):!1===g?null!==e:g instanceof Function?null:e;this[c]||0===this[c]||(this[c]=c in b?b[c]:g)}}function d(a,b){return"string"===typeof a?(b||document).querySelector(a):a||null}function h(a,b){return k.call((b||document).querySelectorAll(a))}function l(){h("input.awesomplete").forEach(function(a){new Awesomplete(a)})}var f=function(a,b){var c=this;this.input= 3 | d(a);this.input.setAttribute("autocomplete","off");this.input.setAttribute("aria-autocomplete","list");b=b||{};m.call(this,{minChars:2,maxItems:10,autoFirst:!1,filter:f.FILTER_CONTAINS,sort:f.SORT_BYLENGTH,item:function(a,b){return d.create("li",{innerHTML:a.replace(RegExp(d.regExpEscape(b.trim()),"gi"),"$&"),"aria-selected":"false"})},replace:function(a){this.input.value=a}},b);this.index=-1;this.container=d.create("div",{className:"awesomplete",around:a});this.ul=d.create("ul",{hidden:"", 4 | inside:this.container});this.status=d.create("span",{className:"visually-hidden",role:"status","aria-live":"assertive","aria-relevant":"additions",inside:this.container});d.bind(this.input,{input:this.evaluate.bind(this),blur:this.close.bind(this),keydown:function(a){var b=a.keyCode;if(c.opened)if(13===b&&c.selected)a.preventDefault(),c.select();else if(27===b)c.close();else if(38===b||40===b)a.preventDefault(),c[38===b?"previous":"next"]()}});d.bind(this.input.form,{submit:this.close.bind(this)}); 5 | d.bind(this.ul,{mousedown:function(a){a=a.target;if(a!==this){for(;a&&!/li/i.test(a.nodeName);)a=a.parentNode;a&&c.select(a)}}});this.input.hasAttribute("list")?(this.list="#"+a.getAttribute("list"),a.removeAttribute("list")):this.list=this.input.getAttribute("data-list")||b.list||[];f.all.push(this)};f.prototype={set list(a){Array.isArray(a)?this._list=a:"string"===typeof a&&-1=this.minChars&&0 ul > li { 85 | position: relative; 86 | padding: .2em .5em; 87 | cursor: pointer; 88 | } 89 | 90 | .dropdown-list { 91 | background: $annotator-bg; 92 | display: inline-block; 93 | width: 180px; 94 | border-radius: 3px; 95 | border: 1px solid $annotator-dark-border; 96 | box-shadow: 0 1px 2px $annotator-shadow, 0 3px 15px rgba(0,0,0,0.15); 97 | margin: 0; 98 | padding: 0; 99 | list-style: none; 100 | z-index: 10; 101 | 102 | li { 103 | border-bottom: 1px solid $annotator-light-border; 104 | position: relative; 105 | padding: 0px 10px; 106 | 107 | &.js-remove-annotation-wrapper { 108 | display: none; 109 | } 110 | 111 | 112 | a, span.link { 113 | padding: 5px 0; 114 | text-decoration: none; 115 | display: block; 116 | font-size: 12px; 117 | color: $annotator-text-color; 118 | line-height: 20px; 119 | font-weight: 400; 120 | text-transform: uppercase; 121 | cursor: pointer; 122 | 123 | } 124 | 125 | 126 | &:hover { 127 | background-color: $annotator-light-border; 128 | color: $annotator-text-color; 129 | .sub-list { 130 | display: block; 131 | } 132 | } 133 | 134 | &.js-facebook-share { 135 | display: none; 136 | } 137 | 138 | &.note-input { 139 | 140 | padding: 10px; 141 | &:hover { 142 | background: transparent; 143 | } 144 | 145 | form { 146 | margin: 0; 147 | } 148 | 149 | textarea, input[type="text"] { 150 | font-size: 12px; 151 | color: #ddd; 152 | border-width: 0; 153 | background: transparent; 154 | width: 160px; 155 | color: #333; 156 | outline: none; 157 | line-height: 20px; 158 | padding: 0 3px; 159 | border: 1px solid #999; 160 | max-width: 160px; 161 | margin-bottom: 5px; 162 | 163 | 164 | 165 | &:focus { 166 | border-color: $primary; 167 | box-shadow: 0 0 1px rgba($primary, 0.2); 168 | } 169 | } 170 | 171 | textarea { 172 | max-height: 100px; 173 | } 174 | #add-button { 175 | height: 22px; 176 | width: 80px; 177 | font-size: 12px; 178 | color: #fff; 179 | text-align: center; 180 | line-height: 18px; 181 | border-width: 0; 182 | background-color: #4d90fe; 183 | cursor: pointer; 184 | margin-top: 10px; 185 | } 186 | } 187 | 188 | 189 | .sub-list { 190 | display: none; 191 | position: absolute; 192 | top: 0; 193 | right: -182px; 194 | } 195 | 196 | 197 | 198 | &.colors { 199 | padding: 8px 10px; 200 | &:hover { 201 | background: transparent; 202 | } 203 | .color { 204 | width: 20px; 205 | height: 20px; 206 | padding: 0; 207 | margin: 0; 208 | border-radius: 100%; 209 | display: inline-block; 210 | margin: 0 8px; 211 | margin-top: 5px; 212 | border: 2px solid $annotator-light-border; 213 | cursor: pointer; 214 | 215 | &.active { 216 | border-color: darken($annotator-dark-border, 30%); 217 | &:hover { 218 | border-color: darken($annotator-dark-border, 30%); 219 | } 220 | } 221 | 222 | &:hover { 223 | border-color: darken($annotator-dark-border, 10%); 224 | } 225 | 226 | } 227 | } 228 | } 229 | } 230 | } 231 | 232 | -------------------------------------------------------------------------------- /src/styles/components/_marker.scss: -------------------------------------------------------------------------------- 1 | .tag-list { 2 | position: absolute; 3 | left: 0px; 4 | .tag { 5 | display: block; 6 | cursor: pointer; 7 | font-size: 12px; 8 | &:hover .tag-text, &.shown .tag-text { 9 | opacity: 1; 10 | } 11 | } 12 | .marker { 13 | padding-right: 4px; 14 | background: rgba(66, 133, 244, 1); 15 | width: 0; 16 | height: 28px; 17 | display: inline-block; 18 | float: left; 19 | background-color: rgba(66, 133, 244, 1) 20 | } 21 | .tag-text { 22 | color:#000; 23 | height: 28px; 24 | line-height: 28px; 25 | padding:0 4px; 26 | display: inline-block; 27 | opacity: 0; 28 | overflow: hidden; 29 | background-color: rgba(66, 133, 244, 1); 30 | transition: all 0.2s ease-out; 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/config/_variables.scss: -------------------------------------------------------------------------------- 1 | $background: #f1f1f1; 2 | $lightBackground: #FFFFFF; 3 | 4 | $headerBackground: #607D8B; 5 | $primary: #4d90fe; 6 | $selection: rgb(186, 186, 255); 7 | 8 | $white: #ffffff; 9 | $black: #000000; 10 | $annotator-bg: #ffffff; 11 | $annotator-dark-border: darken(#eee, 20%); 12 | $annotator-light-border: #eee; 13 | $annotator-text-color: darken(#eee, 40%); 14 | $annotator-shadow: rgba(0,0,0,0.1); 15 | $yellow: #FFE79D; 16 | $green: #BCEFAA; 17 | $pink: #FD99BB; 18 | $blue: #A4D7FC; -------------------------------------------------------------------------------- /src/styles/pages/_layout.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | } 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; 29 | } 30 | body { 31 | line-height: 1; 32 | } 33 | ol, ul { 34 | list-style: none; 35 | } 36 | blockquote, q { 37 | quotes: none; 38 | } 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ''; 42 | content: none; 43 | } 44 | table { 45 | border-collapse: collapse; 46 | border-spacing: 0; 47 | } 48 | 49 | 50 | 51 | /*===================================== 52 | = CUSTOM STYLES = 53 | =====================================*/ 54 | body { 55 | background: $background; 56 | font: 16px/1.5 Helvetica, Arial, sans-serif; 57 | } 58 | 59 | .container { 60 | overflow: hidden; 61 | width: 100%; 62 | max-width: 1140px; 63 | margin: 0 auto; 64 | } 65 | 66 | h1, h2, h3, h4, h5, h6, strong { 67 | font-weight: 700; 68 | } 69 | 70 | h1 { 71 | font-size: 36px; 72 | } 73 | 74 | h2 { 75 | font-size: 28px; 76 | } 77 | 78 | h3 { 79 | font-size: 22px; 80 | } 81 | 82 | h4 { 83 | font-size: 18px; 84 | } 85 | 86 | h5 { 87 | font-size: 16px; 88 | } 89 | 90 | h6 { 91 | font-size: 14px; 92 | } 93 | 94 | h1, h2, h3, h4, h5, h6, p { 95 | margin: 10px 0; 96 | } 97 | 98 | ul, ol { 99 | margin-left: 20px; 100 | } 101 | 102 | ul { 103 | list-style: disc; 104 | } 105 | 106 | .content { 107 | margin: 20px auto; 108 | background: $lightBackground; 109 | border: 1px solid darken($lightBackground, 10%); 110 | 111 | #book { 112 | margin: 0 auto; 113 | padding: 20px; 114 | box-sizing: border-box; 115 | position: relative; 116 | } 117 | 118 | .header { 119 | background: $headerBackground; 120 | padding: 20px; 121 | box-shadow: 0 2px 4px rgba(0,0,0,0.24); 122 | color: $white; 123 | text-shadow: 0 1px 1px rgba($black, 0.12); 124 | h1 { 125 | margin: 0; 126 | padding: 0; 127 | } 128 | p { 129 | margin: 0; 130 | padding: 0; 131 | color: lighten($headerBackground, 30%); 132 | } 133 | } 134 | } 135 | 136 | footer.main { 137 | text-align: center; 138 | margin: 20px auto; 139 | a { 140 | color: $headerBackground; 141 | text-decoration: none; 142 | text-shadow: 0 0 3px transparent; 143 | transition: color 0.2s ease-in, text-shadow 0.2s ease-in; 144 | &:hover { 145 | color: $primary; 146 | text-shadow: 0 0 3px rgba($primary, 0.2); 147 | } 148 | } 149 | } 150 | 151 | 152 | 153 | /*===== End of CUSTOM STYLES ======*/ 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /src/styles/style.scss: -------------------------------------------------------------------------------- 1 | // config 2 | @import "config/variables"; 3 | @import "pages/layout"; --------------------------------------------------------------------------------