├── .gitignore ├── css ├── images │ ├── btn_cancel.png │ ├── askBoxButton.png │ └── btn_confirm.png └── askbox.css ├── LICENSE ├── README.md ├── index.html └── askbox.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /css/images/btn_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AskBox/master/css/images/btn_cancel.png -------------------------------------------------------------------------------- /css/images/askBoxButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AskBox/master/css/images/askBoxButton.png -------------------------------------------------------------------------------- /css/images/btn_confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AskBox/master/css/images/btn_confirm.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Dropbox, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | AskBox is a simple web widget that enables users to ask 4 | questions about page content. AskBox presents itself as a 5 | small icon on the bottom right of the page, and when clicked 6 | opens up a panel that prompts the user for their Slack username 7 | and their message. When the user finishes their message, it 8 | will be posted on the configured Slack channel, along with a 9 | link to the page they are on. If the user had selected text 10 | before opening the panel, that information will also be added 11 | to the URL, and clicking that modified link will re-highlight 12 | the user's selected text. 13 | 14 | # Installing 15 | 16 | 1. To install AskBox, simply copy the contents into the static 17 | folder of your web application. It is important that the 18 | folder structure be preserved as is. 19 | 20 | 2. On your page template (or on each page that you want AskBox 21 | to appear), simply import the javascript `askbox.js`. Be sure 22 | to specify the following required config options are query 23 | parameters: 24 | 25 | * slackurl: the incoming webhook URL for Slack. 26 | * channel: the Slack channel or user who should get 27 | messaged. Be sure to include the `#` or `@` prefix. 28 | * restrict: (optional) a selector that describes portions 29 | that should be made selectable (ex: `.askable`). If you do 30 | not want to make the entire page selectable, set restrict to 31 | `false`. The default is to restrict marking selections 32 | to elements with the class `.askable`. 33 | 34 | ``` 35 | 5 | 6 | 7 | 22 | 23 | 24 |
25 |
26 |

Squid hammock helvetica pop-up biodiesel, 27 | 90's brunch affogato. Scenester thundercats umami kombucha. Kinfolk 28 | pitchfork echo park 29 | kickstarter chartreuse, tofu knausgaard dreamcatcher cardigan 30 | church-key 90's. Four dollar toast retro aesthetic, readymade man 31 | braid 32 | meggings thundercats. Paleo distillery sriracha blue bottle, meh 33 | whatever cred ennui marfa kale chips trust fund try-hard 34 | flexitarian 35 | put a bird on it williamsburg. Portland hashtag blog pork belly, 36 | kickstarter iPhone brooklyn hoodie trust fund williamsburg tousled 37 | blue 38 | bottle. Butcher offal put a bird on it, tofu before they sold out 39 | bespoke tilde.

40 | 41 |

Bicycle rights offal PBR&B, poutine ramps 42 | cronut twee vice tumblr 43 | gochujang fap single-origin coffee irony freegan iPhone. Beard 44 | skateboard freegan VHS etsy humblebrag. Helvetica green juice squid 45 | pitchfork. Kitsch narwhal banh mi VHS. Dreamcatcher fingerstache 46 | cray, 47 | pickled waistcoat locavore skateboard chicharrones kinfolk 48 | thundercats 49 | hella. Health goth pour-over godard celiac dreamcatcher, mixtape 50 | bushwick meditation fixie fingerstache sriracha squid next level 51 | marfa. 52 | Lumbersexual sriracha cold-pressed, whatever bespoke bushwick 53 | shabby 54 | chic.

55 |
56 |
57 | 58 |

Organic offal salvia affogato, taxidermy 59 | hammock vegan brooklyn 60 | PBR&B 61 | retro butcher lo-fi. Tofu disrupt vice, marfa fixie retro hella. 62 | Venmo 63 | small batch lumbersexual single-origin coffee letterpress, pop-up 64 | offal 65 | blog. 8-bit synth 90's venmo. Trust fund celiac cred hammock 66 | disrupt 67 | stumptown. Portland helvetica blue bottle cold-pressed, YOLO kale 68 | chips 69 | vegan salvia cray. Tote bag bicycle rights tousled direct trade 70 | keffiyeh tofu, gentrify YOLO knausgaard.

71 |
72 |
73 | 74 |

Etsy semiotics messenger bag, kinfolk mumblecore flexitarian 75 | blue 76 | bottle 77 | shabby chic chia four loko man braid cred. Salvia pork belly 78 | selvage 79 | forage, bushwick vegan tacos swag blue bottle cardigan health goth 80 | art 81 | party paleo man braid photo booth. Beard meditation health goth 82 | affogato yuccie, bushwick freegan chartreuse. Dreamcatcher 83 | pitchfork 84 | salvia, brooklyn artisan +1 schlitz authentic scenester ramps 85 | whatever 86 | 8-bit selvage keytar pug. Hoodie tacos quinoa, raw denim austin 87 | ethical 88 | cornhole brunch church-key drinking vinegar small batch vegan meh 89 | chia. 90 | Gastropub typewriter cliche chillwave craft beer blue bottle. Hella 91 | microdosing artisan tofu forage gentrify.

92 | 93 |

Helvetica plaid cliche, pour-over slow-carb 94 | banh mi selfies austin. 95 | Hammock shoreditch gentrify vice leggings. Taxidermy tumblr 96 | organic, 97 | freegan pop-up kogi farm-to-table XOXO normcore. Banh mi lo-fi 98 | vinyl, 99 | offal migas intelligentsia literally man bun salvia chambray tote 100 | bag. 101 | Ennui lo-fi affogato hella. Craft beer intelligentsia shoreditch 102 | kickstarter. Letterpress artisan viral put a bird on it 103 | kinfolk.

104 |
105 |
106 | 107 |

Roof party post-ironic art party 108 | asymmetrical, food truck kitsch 109 | taxidermy keffiyeh. Godard yuccie fap, blue bottle waistcoat 110 | mumblecore 111 | direct trade. Pork belly kombucha cornhole tilde. Offal venmo 112 | viral, 113 | waistcoat cornhole +1 truffaut migas meggings before they sold out 114 | banjo humblebrag meditation hoodie. Put a bird on it bitters 115 | pitchfork 116 | sustainable franzen green juice 8-bit. Skateboard trust fund VHS 117 | craft 118 | beer asymmetrical. YOLO blog wolf, chambray hashtag bitters 119 | kombucha 120 | skateboard whatever.

121 | 122 |

Fap biodiesel 3 wolf moon skateboard, quinoa 123 | meggings narwhal 124 | everyday 125 | carry franzen. Put a bird on it listicle pug slow-carb. Skateboard 126 | bitters bushwick art party, hammock green juice beard neutra 127 | humblebrag 128 | ramps gochujang you probably haven't heard of them bespoke direct 129 | trade 130 | man braid. Beard kickstarter pug semiotics, salvia banjo pitchfork. 131 | Humblebrag master cleanse vinyl kitsch, beard gluten-free before 132 | they 133 | sold out. Umami echo park synth venmo man braid, retro ennui chia 134 | biodiesel roof party +1. Man bun squid viral, typewriter tousled 135 | celiac 136 | tofu echo park next level occupy authentic cardigan salvia hashtag 137 | tattooed.

138 | 139 |

Plaid chambray VHS franzen mlkshk, sartorial 140 | taxidermy messenger 141 | bag. 142 | Ethical taxidermy stumptown +1 small batch you probably haven't 143 | heard 144 | of them. Humblebrag man braid fap offal. Blog affogato flannel 145 | viral 146 | godard williamsburg. Single-origin coffee keytar flannel 3 wolf 147 | moon, 148 | forage four dollar toast humblebrag chillwave hashtag meggings 149 | plaid 150 | ethical +1 readymade. Roof party cardigan seitan fingerstache, four 151 | loko quinoa ramps pabst kinfolk williamsburg iPhone everyday carry 152 | keffiyeh vice bitters. Twee polaroid church-key ramps YOLO, 153 | truffaut 154 | direct trade irony health goth.

155 | 156 |

Food truck aesthetic franzen etsy, plaid 157 | williamsburg jean shorts 158 | drinking vinegar pinterest everyday carry gochujang venmo. Narwhal 159 | franzen mumblecore poutine 90's, raw denim dreamcatcher roof party 160 | cliche yuccie occupy. Ennui health goth truffaut bicycle rights 161 | distillery banjo, lomo pop-up flannel direct trade vinyl slow-carb. 162 | Cred shabby chic migas literally poutine. Pickled vegan tumblr, 163 | pour-over polaroid lomo sriracha authentic paleo squid four loko 164 | cardigan artisan literally. Irony etsy poutine artisan vegan. Man 165 | bun 166 | cardigan meggings XOXO.

167 | 168 |

Authentic green juice YOLO kinfolk, man 169 | braid celiac raw denim. Put 170 | a 171 | bird on it kinfolk butcher, ramps plaid truffaut mumblecore photo 172 | booth 173 | try-hard. Four loko tousled pork belly, biodiesel lumbersexual tote 174 | bag 175 | jean shorts. Kitsch tacos locavore butcher offal, heirloom cliche 176 | viral 177 | tote bag leggings ethical irony chartreuse. Marfa blue bottle 178 | schlitz 179 | pour-over tote bag everyday carry typewriter portland post-ironic. 180 | Keytar bushwick poutine listicle beard master cleanse, forage 181 | mumblecore cred fashion axe neutra. Listicle post-ironic selfies 182 | tattooed, meggings quinoa helvetica offal ramps YOLO everyday 183 | carry.

184 |
185 |
186 | 187 | 188 | -------------------------------------------------------------------------------- /askbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AskBox: a simple on-page question submission form. 3 | * 4 | * AskBox provides a simple way to allow users to ask questions directly from a webpage. 5 | * Questions are posted to the configured Slack channel. 6 | * 7 | * FIXME -- make the slack integration configurable, which probably means AskBox needs to call an API to get that config from the server 8 | */ 9 | $(function () { 10 | // default config 11 | var config = { 12 | SLACK_URL: "https://slackurl", 13 | channel: "#our-channel", 14 | restrict: ".askable", 15 | }; 16 | var ROOTURL = ""; 17 | 18 | 19 | var trackingRange = null; // the text selection range, if any 20 | 21 | // because the tracking range can change when we modify the DOM, 22 | // we want to store the details here so we can properly send them 23 | // in the message 24 | var selectionDetails = null; 25 | 26 | /** 27 | * Display and error message 28 | */ 29 | function showErrorMessage(message) { 30 | var messageField = $("#message"); 31 | 32 | messageField.addClass("error"); 33 | messageField.html(message); 34 | } 35 | 36 | /** 37 | * Display a non-error message 38 | */ 39 | function showNormalMessage(message) { 40 | var messageField = $("#message"); 41 | 42 | messageField.removeClass("error"); 43 | messageField.html(message); 44 | } 45 | 46 | /** 47 | * Validate the form values are not empty and display an error if needed 48 | */ 49 | function validateForm(username, message) { 50 | if (!username) { 51 | showErrorMessage("Error: must include a valid Slack username!"); 52 | return false; 53 | } 54 | if (!message) { 55 | showErrorMessage("Error: must include a message to send!"); 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * Send a message to the slack channel. The message will include the user specified 64 | * Slack username, the message they typed, and the website's URL. If the user 65 | * has selected some text on the page, we'll add query parameters that AskBox 66 | * can use to recreate that selection. 67 | */ 68 | function sendMessage() { 69 | var slackUsername = $("#slackUsername").val(); 70 | 71 | if (slackUsername.indexOf('@') != 0) { 72 | slackUsername = '@' + slackUsername; 73 | } 74 | 75 | var slackMessage = $("#slackMessage").val(); 76 | if (validateForm(slackUsername, slackMessage)) { 77 | showNormalMessage("Sending..."); 78 | 79 | var newUrl = null; 80 | // if we have a selection, let's add those params 81 | if (selectionDetails) { 82 | var splitUrl = window.location.href.split("#"); 83 | newUrl = splitUrl[0]; 84 | if (newUrl.indexOf("?") == -1) { 85 | newUrl += "?abSN=" + encodeURI(selectionDetails.startNode); 86 | } else { 87 | newUrl += "&abSN=" + encodeURI(selectionDetails.startNode); 88 | } 89 | 90 | newUrl += "&abSO=" + selectionDetails.startOffset; 91 | newUrl += "&abEN=" + encodeURI(selectionDetails.endNode); 92 | newUrl += "&abEO=" + selectionDetails.endOffset; 93 | 94 | if (splitUrl.length == 2) { 95 | newUrl += "#" + splitUrl[1]; 96 | } 97 | } 98 | 99 | var finalMessage = slackUsername + " asks about <" + newUrl + ">:\n" + slackMessage; 100 | var json = { 101 | username: "AskBox", 102 | channel: config.channel, 103 | text: finalMessage 104 | }; 105 | 106 | $.ajax({ 107 | type: "POST", 108 | url: config.SLACK_URL, 109 | processData: false, 110 | data: JSON.stringify(json), 111 | success: function (data) { 112 | showNormalMessage("Message sent!"); 113 | toggleAskBox(); 114 | }, 115 | error: function (data) { 116 | showErrorMessage("Failed to send message."); 117 | } 118 | }); 119 | 120 | } 121 | } 122 | 123 | /** 124 | * Cancel the message; clear the fields and close the box 125 | */ 126 | function cancelMessage() { 127 | toggleAskBox(); 128 | } 129 | 130 | /** 131 | * Toggle the question box's visibility 132 | */ 133 | function toggleAskBox(event) { 134 | if (event) event.preventDefault(); 135 | 136 | var buttonDimensions = document.getElementById("askboxButton").getBoundingClientRect(); 137 | var askboxPanel = $("#askboxPanel"); 138 | askboxPanel.css({right: buttonDimensions.width + 20}); 139 | askboxPanel.slideToggle(200, function () { 140 | if (askboxPanel.is(":visible")) { 141 | // if the panel just became visible, highlight the selected text 142 | if (trackingRange) { 143 | addHighlights(); 144 | } 145 | 146 | // set focus on the first input element 147 | $("#slackUsername").focus(); 148 | 149 | } else { 150 | // if the panel just closed, clear the highlights 151 | clearHighlights(); 152 | stopTrackingSelection(); 153 | } 154 | }); 155 | } 156 | 157 | /** 158 | * Add the question mark button to the DOM and wire in the behavior 159 | */ 160 | function addAskButton() { 161 | $("body").append("
"); 162 | $("#askboxButton").click(toggleAskBox); 163 | } 164 | 165 | /** 166 | * Add the question box to the DOM and wire up any behaviors 167 | */ 168 | function addAskBox() { 169 | $("body").append("
"); 170 | var askboxPanel = $("#askboxPanel"); 171 | askboxPanel.append("
Ask the Monitoring Team
"); 172 | 173 | var boxBody = $("
"); 174 | askboxPanel.append(boxBody); 175 | boxBody.append("

Enter your Slack username and question. We'll post your question and a link to this page straight to " + config.channel + ".

"); 176 | 177 | var slackUsername = $(""); 178 | boxBody.append(slackUsername); 179 | 180 | var slackMessage = $(""); 181 | boxBody.append(slackMessage); 182 | 183 | var buttonPanel = $("
"); 184 | boxBody.append(buttonPanel); 185 | 186 | var messageField = $("
"); 187 | buttonPanel.append(messageField); 188 | 189 | var sendButton = $(""); 190 | buttonPanel.append(sendButton); 191 | sendButton.click(sendMessage); 192 | 193 | var cancelButton = $(""); 194 | buttonPanel.append(cancelButton); 195 | cancelButton.click(cancelMessage); 196 | } 197 | 198 | /** 199 | * Select text on the screen. This is called on load if we've 200 | * detected query params and so we only have nodes and offsets instead of 201 | * prebuilt range 202 | */ 203 | function selectText(startNode, startOffset, endNode, endOffset) { 204 | var selection = window.getSelection(); 205 | selection.removeAllRanges(); 206 | 207 | var newRange = document.createRange(); 208 | 209 | // the selections have to be on the text elements inside the node 210 | var foundStartNode = $("#" + startNode).contents() 211 | .filter(function () { 212 | return this.nodeType === 3; // filter for Node.TEXT_NODE 213 | })[0]; 214 | var foundEndNode = $("#" + endNode).contents() 215 | .filter(function () { 216 | return this.nodeType === 3; // filter for Node.TEXT_NODE 217 | })[0]; 218 | 219 | newRange.setStart(foundStartNode, startOffset); 220 | newRange.setEnd(foundEndNode, endOffset); 221 | 222 | trackingRange = newRange; 223 | addHighlights(); 224 | } 225 | 226 | /** 227 | * Start making the toggle button track the text selection so the button 228 | * stays near the selected text, letting the user know they can ask a 229 | * question about the part they've highlighted. 230 | */ 231 | function startTrackingSelection(range) { 232 | trackingRange = range.cloneRange(); 233 | selectionDetails = { 234 | startNode: trackingRange.startContainer.parentNode.id, 235 | endNode: trackingRange.endContainer.parentNode.id, 236 | startOffset: trackingRange.startOffset, 237 | endOffset: trackingRange.endOffset 238 | }; 239 | adjustButtonPosition(); 240 | } 241 | 242 | /** 243 | * Stop making the toggle button track the text selection 244 | */ 245 | function stopTrackingSelection() { 246 | $("#askboxButton").css("bottom", "15px"); 247 | trackingRange = null; 248 | selectionDetails = null; 249 | } 250 | 251 | /** 252 | * Called when page scrolled so we can adjust the button position, but only 253 | * if we are having the button track the position of selected text. 254 | */ 255 | function adjustButtonPosition() { 256 | if (trackingRange) { 257 | var selectionRect = trackingRange.getClientRects(); 258 | if (!selectionRect || selectionRect.length == 0) { 259 | return; 260 | } 261 | var askboxButton = $("#askboxButton"); 262 | var trackedBottom = selectionRect[selectionRect.length - 1].bottom; 263 | var newBottom = window.innerHeight - trackedBottom; 264 | askboxButton.css("bottom", newBottom); 265 | } 266 | } 267 | 268 | /** 269 | * A selection range often spans across elements. In order to highlight 270 | * properly, we want to break this down into safe ranges that do not cross 271 | * element boundaries. 272 | */ 273 | function getSafeRanges(range) { 274 | var ancestor = range.commonAncestorContainer; 275 | 276 | // work our way out from the starting container up to the common ancestor 277 | var startSegments = []; 278 | if (range.startContainer != ancestor) 279 | for (var i = range.startContainer; i != ancestor; i = i.parentNode) 280 | startSegments.push(i); 281 | 282 | var rangeStarts = []; 283 | if (startSegments.length > 0) for (var i = 0; i < startSegments.length; i++) { 284 | var segmentChunk = document.createRange(); 285 | if (i) { 286 | segmentChunk.setStartAfter(startSegments[i - 1]); 287 | segmentChunk.setEndAfter(startSegments[i].lastChild); 288 | } 289 | else { 290 | segmentChunk.setStart(startSegments[i], range.startOffset); 291 | segmentChunk.setEndAfter( 292 | (startSegments[i].nodeType == Node.TEXT_NODE) 293 | ? startSegments[i] : startSegments[i].lastChild 294 | ); 295 | } 296 | rangeStarts.push(segmentChunk); 297 | } 298 | 299 | // work our way out from the ending container up to the common ancestor 300 | var endSegments = []; 301 | if (range.endContainer != ancestor) 302 | for (var i = range.endContainer; i != ancestor; i = i.parentNode) 303 | endSegments.push(i); 304 | 305 | var rangeEnds = []; 306 | if (endSegments.length > 0) for (var i = 0; i < endSegments.length; i++) { 307 | var segmentChunk = document.createRange(); 308 | if (i) { 309 | segmentChunk.setStartBefore(endSegments[i].firstChild); 310 | segmentChunk.setEndBefore(endSegments[i - 1]); 311 | } 312 | else { 313 | segmentChunk.setStartBefore( 314 | (endSegments[i].nodeType == Node.TEXT_NODE) 315 | ? endSegments[i] : endSegments[i].firstChild 316 | ); 317 | segmentChunk.setEnd(endSegments[i], range.endOffset); 318 | } 319 | rangeEnds.unshift(segmentChunk); 320 | } 321 | 322 | // now we want to deal with the elements that were in the middle 323 | // between the original starting segment and the ending segment 324 | var middleChunk = null; 325 | if ((startSegments.length > 0) && (endSegments.length > 0)) { 326 | middleChunk = document.createRange(); 327 | middleChunk.setStartAfter(startSegments[startSegments.length - 1]); 328 | middleChunk.setEndBefore(endSegments[endSegments.length - 1]); 329 | } 330 | else { 331 | // if the selection starts and stops in the same element, return original range 332 | return [range]; 333 | } 334 | 335 | rangeStarts.push(middleChunk); 336 | 337 | return rangeStarts.concat(rangeEnds); 338 | } 339 | 340 | /** 341 | * Add highlights around the current text selection. Used to visually highlight selected 342 | * text even if a focus change deselects the text 343 | */ 344 | function addHighlights() { 345 | var safeRanges = getSafeRanges(trackingRange.cloneRange()); 346 | 347 | for (var i = 0; i < safeRanges.length; i++) { 348 | window.getSelection().removeAllRanges(); 349 | 350 | // if the start and stop points are the same, skip this one 351 | if (safeRanges[i].collapsed) { 352 | continue; 353 | } 354 | if (safeRanges[i].startContainer.nodeType === 3 || safeRanges[i].endContainer.nodeType === 3) { 355 | var highlightSpan = document.createElement("span"); 356 | highlightSpan.setAttribute("class", "askboxHighlight"); 357 | safeRanges[i].surroundContents(highlightSpan); 358 | } else { 359 | var highlightSpan = document.createElement("div"); 360 | highlightSpan.setAttribute("class", "askboxHighlight"); 361 | safeRanges[i].surroundContents(highlightSpan); 362 | } 363 | } 364 | 365 | } 366 | 367 | /** 368 | * Clear highlight spans that we add for visual reference 369 | */ 370 | function clearHighlights() { 371 | $(".askboxHighlight").each(function() { 372 | var parent = $(this).parent(); 373 | $(this).contents().unwrap(); 374 | parent[0].normalize(); 375 | }); 376 | } 377 | 378 | /** 379 | * When the left mouse button is released, see if there is a text selection 380 | */ 381 | function checkForSelection(event) { 382 | // if this is a mouse up event on the left button ... 383 | if (event.which == 1) { 384 | // if the message panel is already open, user can't change selection 385 | if ($("#askboxPanel").is(":visible")) { 386 | return false; 387 | } 388 | 389 | var range = null; 390 | try { 391 | range = window.getSelection().getRangeAt(0); 392 | } catch (e) { 393 | // couldn't find a valid text selection, which is fine. Move on. 394 | return; 395 | } 396 | 397 | if (config.restrict) { 398 | // check to make sure we are within a selectable section 399 | if ( 400 | $(range.startContainer.parentNode).closest(config.restrict).length == 0 || 401 | $(range.endContainer.parentNode).closest(config.restrict).length == 0 402 | ) { 403 | // looks like we are not selecting in a valid area. Don't make 404 | // a new selection. Furthermore, clear any current selection 405 | stopTrackingSelection(); 406 | return; 407 | } 408 | } 409 | 410 | 411 | 412 | // if the selection start and stop point is the same, we have no selection 413 | // and can continue 414 | if (range.startContainer == range.endContainer && range.startOffset == range.endOffset) { 415 | stopTrackingSelection(); 416 | return; 417 | } 418 | 419 | if (!range.getClientRects) { 420 | // we don't have a way to get the client rect so let's just move on 421 | return; 422 | } 423 | 424 | startTrackingSelection(range); 425 | } 426 | } 427 | 428 | /** 429 | * In case we got query params of a previous selection, let's recreate 430 | */ 431 | function recreateSelection() { 432 | var params = window.location.search.substring(1).split("&"); 433 | var sn, so, en, eo; 434 | for (var i = 0; i < params.length; i++) { 435 | var equalIndex = params[i].indexOf("="); 436 | if (equalIndex == -1) { 437 | continue; 438 | } 439 | var param = params[i].substring(0, equalIndex); 440 | var value = decodeURI(params[i].substring(equalIndex + 1, params[i].length)); 441 | 442 | switch (param) { 443 | case "abSN": 444 | sn = value; 445 | break; 446 | case "abSO": 447 | so = value; 448 | break; 449 | case "abEN": 450 | en = value; 451 | break; 452 | case "abEO": 453 | eo = value; 454 | break; 455 | } 456 | } 457 | 458 | if (sn && so && en && eo) { 459 | selectText(sn, so, en, eo); 460 | } 461 | } 462 | 463 | /** 464 | * Add a watcher for various events 465 | */ 466 | function addEventWatches() { 467 | // when the mouse button is raised, let's check for a new text selection 468 | $(document).mouseup(checkForSelection); 469 | 470 | // called when the view is scrolled (we may need to move the button) 471 | $(window).scroll(adjustButtonPosition); 472 | } 473 | 474 | /** 475 | * Parse the DOM and add id tags to divs, p, span, and li's if missing 476 | * id tags are necessary to be accurately capture where a selection is happening 477 | */ 478 | function addMissingIds() { 479 | var index = 0; 480 | $("div:not([id])").each(function() { 481 | $(this).attr("id", "abDiv" + index++); 482 | }); 483 | $("p:not([id])").each(function() { 484 | $(this).attr("id", "abP" + index++); 485 | }); 486 | $("span:not([id])").each(function() { 487 | $(this).attr("id", "abSpan" + index++); 488 | }); 489 | $("li:not([id])").each(function() { 490 | $(this).attr("id", "abLi" + index++); 491 | }); 492 | } 493 | 494 | /** 495 | * Bootstrap the config based on query params and load in the css and fonts we need 496 | */ 497 | function bootstrap() { 498 | // find out where this script is installed so we know how to find the dependencies 499 | // also pull out any config options sent 500 | $("script").each(function(index){ 501 | var src = $(this).attr("src"); 502 | if (src && src.includes("askbox.js")) { 503 | var re = /askbox\.js.*/; 504 | ROOTURL = src.replace(/askbox\.js.*/,""); 505 | if (src.indexOf("?")) { 506 | var params = src.replace(/.*\?/,""); 507 | var vars = params.split("&"); 508 | for (var i = 0; i < vars.length; i++) { 509 | var pair = vars[i].split("="); 510 | switch(pair[0]) { 511 | case "restrict": 512 | if (pair[1] == "false") { 513 | config.restrict = false; 514 | } else { 515 | config.restrict = decodeURIComponent(pair[1]); 516 | } 517 | break; 518 | case "channel": 519 | config.channel = decodeURIComponent(pair[1]); 520 | break; 521 | case "slackurl": 522 | config.SLACK_URL = decodeURIComponent(pair[1]); 523 | break; 524 | } 525 | } 526 | } 527 | } 528 | }); 529 | 530 | // load the required font file and AskBox CSS file 531 | $("head").append($('').attr("href", "https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700,900")); 532 | $("head").append($('').attr("href", ROOTURL + "css/askbox.css")); 533 | } 534 | 535 | bootstrap(); 536 | addMissingIds(); 537 | 538 | // add our elements to the page 539 | addAskButton(); 540 | addAskBox(); 541 | addEventWatches(); 542 | recreateSelection(); 543 | }); --------------------------------------------------------------------------------