15 | Join the Turtl Community! Ask questions, suggest features, and 16 | get help from the team or from other community members. This is 17 | the one place for discussion about Turtl! 18 |
19 |35 | Check out our roadmap, upcoming features, and current bug reports 36 | on our Github. 37 |
38 |44 | For general enquiries (sales, legal, etc), please email info@turtlapp.com. 45 | Please keep in mind, this is not a support email. 46 |
47 |11 | Until we are able to support ourselves by providing a Turtl Premium service, any 12 | and all donations help further continuing development. If you like 13 | using Turtl, please consider helping us make it better by giving anything you 14 | can. Thank you! 15 |
16 | 17 |1FXpcXhdiurerBjLm47YE6PemMxdFtcQt1
35 | 0x8738DaE708A23845f686B586cAcAC9C30f17b0dA
41 | 12 | The desktop and mobile apps give you access to your data from any 13 | device. 14 |
15 | {% include download.html %} 16 | 17 |20 | The bookmarking extensions put a button in your browser that makes it easy to 21 | bookmark the current page you're on. This is great for *quickly* saving sites 22 | to read later or for creating a list of pages you frequently visit. 23 |
24 | 25 |35 | Want to verify your Turtl download? Follow these instructions: 36 |
37 |DEDF113E54248344163716B55C66FAD13222D757
).
40 |
41 | $ wget https://keybase.io/orthecreedence/pgp_keys.asc?fingerprint=dedf113e54248344163716b55c66fad13222d757 -O turtl.asc
42 | $ gpg --import turtl.asc
43 | $ gpg --verify desktop.sha256sums.sig desktop.sha256sums
65 | gpg: Signature made Tue, Jul 24, 2018 9:12:17 PM CDT
66 | gpg: using RSA key 57CE4D9CD8D276B4
67 | gpg: Good signature from "Andrew Lyon "
68 | Primary key fingerprint: DEDF 113E 5424 8344 1637 16B5 5C66 FAD1 3222 D757
69 | Subkey fingerprint: B25B DF8F 8BB7 7454 ACFF BA84 57CE 4D9C D8D2 76B4
70 | $ wget https://github.com/turtl/desktop/releases/download/v{{site.releases.desktop}}/turtl-{{site.releases.desktop}}-windows64.msi
75 | $ sha256sum turtl-{{site.releases.desktop}}-windows64.msi
76 | fb57740d84e6838d83035949477c061cc4d1879074ffb05a336a22cb37f8eb9b *turtl-{{site.releases.desktop}}-windows64.msi
77 | $ cat desktop.sha256sums | grep windows64
78 | fb57740d84e6838d83035949477c061cc4d1879074ffb05a336a22cb37f8eb9b *turtl-{{site.releases.desktop}}-windows64.msi
79 | If those two hashes (fb57740...
) match, your Turtl build has not been tampered with.
80 | If the two hashes above do not match, then do NOT install Turtl!
81 | $$
to use)?
in-app to see shortcuts)44 | Whether it's bookmarks or passwords, files or shopping 45 | lists...Turtl organizes it all and makes it easy to find later. 46 | Sync across your devices. Leave nothing behind. 47 |
48 |58 | Turtl uses high-end cryptography to protect your data. 59 | Whether you're worried about information leaks, competitive 60 | advantage, or blanket government surveillance, Turtl works 61 | hard to make sure only you, and those you choose, can see 62 | your data. 63 |
64 |65 | Read more about Turtl's security » 66 |
67 |77 | Just because Turtl is secure and private doesn't mean you 78 | can't share with your teammates or family. Choose who you 79 | want to have access to your data without compromising your 80 | security. 81 |
82 |142 | We love privacy, but don't take our word for it. 143 | Turtl is open source 144 | and you can read the code yourself! 145 |
148 | Want more control of your data? 149 | Install your own Turtl server at home or at work. 150 |
153 | Turtl is a community project, and we love when people like you help out! 154 |
164 | Don't want to pay for our premium service?
165 | Consider making a donation to support the project!
166 |
Thanks for giving Turtl a try!
Return to the homepage »'); 116 | }) 117 | .catch(function(err) { 118 | this.error('There was a problem cancelling: '+err.message); 119 | }); 120 | }, 121 | }); 122 | 123 | const ManageModel = Composer.Model.extend({ 124 | get_url: function() { 125 | const api_url = window.location.toString().match(/turtl\.loc/) ? 126 | 'http://api.turtl.loc:8181' : 127 | 'https://apiv3.turtlapp.com'; 128 | return api_url+'/payment/'+encodeURIComponent(this.id())+'/'+encodeURIComponent(this.get('manage_token')); 129 | }, 130 | 131 | grab: function() { 132 | var req = { 133 | url: this.get_url(), 134 | }; 135 | return Sexhr(req) 136 | .bind(this) 137 | .spread(function(payment, _xhr) { 138 | this.set(JSON.parse(payment)); 139 | return this; 140 | }) 141 | .catch(this.err.bind(this)); 142 | }, 143 | 144 | check_email: function(email) { 145 | var req = { 146 | url: this.get_url()+'/email/'+encodeURIComponent(email), 147 | }; 148 | return Sexhr(req) 149 | .bind(this) 150 | .spread(function(res, _xhr) { 151 | var emailobj = JSON.parse(res); 152 | return emailobj.username; 153 | }) 154 | .catch(this.err.bind(this)); 155 | }, 156 | 157 | update: function() { 158 | var req = { 159 | url: this.get_url(), 160 | method: 'PUT', 161 | headers: {'Content-Type': 'application/json'}, 162 | data: JSON.stringify(this.toJSON()), 163 | }; 164 | return Sexhr(req) 165 | .bind(this) 166 | .spread(function(payment, _xhr) { 167 | this.reset(JSON.parse(payment)); 168 | return this; 169 | }) 170 | .catch(this.err.bind(this)); 171 | }, 172 | 173 | cancel: function() { 174 | var req = { 175 | url: this.get_url(), 176 | method: 'DELETE', 177 | }; 178 | var modal = UIkit.modal($('loading-modal')); 179 | modal.show(); 180 | return Sexhr(req) 181 | .catch(this.err.bind(this)) 182 | .finally(function() { 183 | modal.hide(); 184 | }); 185 | }, 186 | 187 | err: function(errobj) { 188 | console.log(errobj); 189 | throw JSON.parse(errobj.msg).error.message; 190 | }, 191 | }); 192 | 193 | -------------------------------------------------------------------------------- /js/mootools-more-1.6.0.js: -------------------------------------------------------------------------------- 1 | /* MooTools: the javascript framework. license: MIT-style license. copyright: Copyright (c) 2006-2018 [Valerio Proietti](https://mootools.net/).*/ 2 | /*! 3 | Web Build: https://mootools.net/more/builder/51368179dd40043585fe86ca0c128850 4 | */ 5 | /* 6 | --- 7 | 8 | script: More.js 9 | 10 | name: More 11 | 12 | description: MooTools More 13 | 14 | license: MIT-style license 15 | 16 | authors: 17 | - Guillermo Rauch 18 | - Thomas Aylott 19 | - Scott Kyle 20 | - Arian Stolwijk 21 | - Tim Wienk 22 | - Christoph Pojer 23 | - Aaron Newton 24 | - Jacob Thornton 25 | 26 | requires: 27 | - Core/MooTools 28 | 29 | provides: [MooTools.More] 30 | 31 | ... 32 | */ 33 | 34 | MooTools.More = { 35 | version: '1.6.0', 36 | build: '45b71db70f879781a7e0b0d3fb3bb1307c2521eb' 37 | }; 38 | 39 | /* 40 | --- 41 | 42 | name: Events.Pseudos 43 | 44 | description: Adds the functionality to add pseudo events 45 | 46 | license: MIT-style license 47 | 48 | authors: 49 | - Arian Stolwijk 50 | 51 | requires: [Core/Class.Extras, Core/Slick.Parser, MooTools.More] 52 | 53 | provides: [Events.Pseudos] 54 | 55 | ... 56 | */ 57 | 58 | (function(){ 59 | 60 | Events.Pseudos = function(pseudos, addEvent, removeEvent){ 61 | 62 | var storeKey = '_monitorEvents:'; 63 | 64 | var storageOf = function(object){ 65 | return { 66 | store: object.store ? function(key, value){ 67 | object.store(storeKey + key, value); 68 | } : function(key, value){ 69 | (object._monitorEvents || (object._monitorEvents = {}))[key] = value; 70 | }, 71 | retrieve: object.retrieve ? function(key, dflt){ 72 | return object.retrieve(storeKey + key, dflt); 73 | } : function(key, dflt){ 74 | if (!object._monitorEvents) return dflt; 75 | return object._monitorEvents[key] || dflt; 76 | } 77 | }; 78 | }; 79 | 80 | var splitType = function(type){ 81 | if (type.indexOf(':') == -1 || !pseudos) return null; 82 | 83 | var parsed = Slick.parse(type).expressions[0][0], 84 | parsedPseudos = parsed.pseudos, 85 | l = parsedPseudos.length, 86 | splits = []; 87 | 88 | while (l--){ 89 | var pseudo = parsedPseudos[l].key, 90 | listener = pseudos[pseudo]; 91 | if (listener != null) splits.push({ 92 | event: parsed.tag, 93 | value: parsedPseudos[l].value, 94 | pseudo: pseudo, 95 | original: type, 96 | listener: listener 97 | }); 98 | } 99 | return splits.length ? splits : null; 100 | }; 101 | 102 | return { 103 | 104 | addEvent: function(type, fn, internal){ 105 | var split = splitType(type); 106 | if (!split) return addEvent.call(this, type, fn, internal); 107 | 108 | var storage = storageOf(this), 109 | events = storage.retrieve(type, []), 110 | eventType = split[0].event, 111 | args = Array.slice(arguments, 2), 112 | stack = fn, 113 | self = this; 114 | 115 | split.each(function(item){ 116 | var listener = item.listener, 117 | stackFn = stack; 118 | if (listener == false) eventType += ':' + item.pseudo + '(' + item.value + ')'; 119 | else stack = function(){ 120 | listener.call(self, item, stackFn, arguments, stack); 121 | }; 122 | }); 123 | 124 | events.include({type: eventType, event: fn, monitor: stack}); 125 | storage.store(type, events); 126 | 127 | if (type != eventType) addEvent.apply(this, [type, fn].concat(args)); 128 | return addEvent.apply(this, [eventType, stack].concat(args)); 129 | }, 130 | 131 | removeEvent: function(type, fn){ 132 | var split = splitType(type); 133 | if (!split) return removeEvent.call(this, type, fn); 134 | 135 | var storage = storageOf(this), 136 | events = storage.retrieve(type); 137 | if (!events) return this; 138 | 139 | var args = Array.slice(arguments, 2); 140 | 141 | removeEvent.apply(this, [type, fn].concat(args)); 142 | events.each(function(monitor, i){ 143 | if (!fn || monitor.event == fn) removeEvent.apply(this, [monitor.type, monitor.monitor].concat(args)); 144 | delete events[i]; 145 | }, this); 146 | 147 | storage.store(type, events); 148 | return this; 149 | } 150 | 151 | }; 152 | 153 | }; 154 | 155 | var pseudos = { 156 | 157 | once: function(split, fn, args, monitor){ 158 | fn.apply(this, args); 159 | this.removeEvent(split.event, monitor) 160 | .removeEvent(split.original, fn); 161 | }, 162 | 163 | throttle: function(split, fn, args){ 164 | if (!fn._throttled){ 165 | fn.apply(this, args); 166 | fn._throttled = setTimeout(function(){ 167 | fn._throttled = false; 168 | }, split.value || 250); 169 | } 170 | }, 171 | 172 | pause: function(split, fn, args){ 173 | clearTimeout(fn._pause); 174 | fn._pause = fn.delay(split.value || 250, this, args); 175 | } 176 | 177 | }; 178 | 179 | Events.definePseudo = function(key, listener){ 180 | pseudos[key] = listener; 181 | return this; 182 | }; 183 | 184 | Events.lookupPseudo = function(key){ 185 | return pseudos[key]; 186 | }; 187 | 188 | var proto = Events.prototype; 189 | Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent)); 190 | 191 | ['Request', 'Fx'].each(function(klass){ 192 | if (this[klass]) this[klass].implement(Events.prototype); 193 | }); 194 | 195 | })(); 196 | 197 | /* 198 | --- 199 | 200 | name: Element.Event.Pseudos 201 | 202 | description: Adds the functionality to add pseudo events for Elements 203 | 204 | license: MIT-style license 205 | 206 | authors: 207 | - Arian Stolwijk 208 | 209 | requires: [Core/Element.Event, Core/Element.Delegation, Events.Pseudos] 210 | 211 | provides: [Element.Event.Pseudos, Element.Delegation.Pseudo] 212 | 213 | ... 214 | */ 215 | 216 | (function(){ 217 | 218 | var pseudos = {relay: false}, 219 | copyFromEvents = ['once', 'throttle', 'pause'], 220 | count = copyFromEvents.length; 221 | 222 | while (count--) pseudos[copyFromEvents[count]] = Events.lookupPseudo(copyFromEvents[count]); 223 | 224 | DOMEvent.definePseudo = function(key, listener){ 225 | pseudos[key] = listener; 226 | return this; 227 | }; 228 | 229 | var proto = Element.prototype; 230 | [Element, Window, Document].invoke('implement', Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent)); 231 | 232 | })(); 233 | 234 | /* 235 | --- 236 | 237 | name: Element.Event.Pseudos.Keys 238 | 239 | description: Adds functionality fire events if certain keycombinations are pressed 240 | 241 | license: MIT-style license 242 | 243 | authors: 244 | - Arian Stolwijk 245 | 246 | requires: [Element.Event.Pseudos] 247 | 248 | provides: [Element.Event.Pseudos.Keys] 249 | 250 | ... 251 | */ 252 | 253 | (function(){ 254 | 255 | var keysStoreKey = '$moo:keys-pressed', 256 | keysKeyupStoreKey = '$moo:keys-keyup'; 257 | 258 | 259 | DOMEvent.definePseudo('keys', function(split, fn, args){ 260 | 261 | var event = args[0], 262 | keys = [], 263 | pressed = this.retrieve(keysStoreKey, []), 264 | value = split.value; 265 | 266 | if (value != '+') keys.append(value.replace('++', function(){ 267 | keys.push('+'); // shift++ and shift+++a 268 | return ''; 269 | }).split('+')); 270 | else keys = ['+']; 271 | 272 | pressed.include(event.key); 273 | 274 | if (keys.every(function(key){ 275 | return pressed.contains(key); 276 | })) fn.apply(this, args); 277 | 278 | this.store(keysStoreKey, pressed); 279 | 280 | if (!this.retrieve(keysKeyupStoreKey)){ 281 | var keyup = function(event){ 282 | (function(){ 283 | pressed = this.retrieve(keysStoreKey, []).erase(event.key); 284 | this.store(keysStoreKey, pressed); 285 | }).delay(0, this); // Fix for IE 286 | }; 287 | this.store(keysKeyupStoreKey, keyup).addEvent('keyup', keyup); 288 | } 289 | 290 | }); 291 | 292 | DOMEvent.defineKeys({ 293 | '16': 'shift', 294 | '17': 'control', 295 | '18': 'alt', 296 | '20': 'capslock', 297 | '33': 'pageup', 298 | '34': 'pagedown', 299 | '35': 'end', 300 | '36': 'home', 301 | '144': 'numlock', 302 | '145': 'scrolllock', 303 | '186': ';', 304 | '187': '=', 305 | '188': ',', 306 | '190': '.', 307 | '191': '/', 308 | '192': '`', 309 | '219': '[', 310 | '220': '\\', 311 | '221': ']', 312 | '222': "'", 313 | '107': '+', 314 | '109': '-', // subtract 315 | '189': '-' // dash 316 | }); 317 | 318 | })(); 319 | 320 | /* 321 | --- 322 | 323 | script: String.Extras.js 324 | 325 | name: String.Extras 326 | 327 | description: Extends the String native object to include methods useful in managing various kinds of strings (query strings, urls, html, etc). 328 | 329 | license: MIT-style license 330 | 331 | authors: 332 | - Aaron Newton 333 | - Guillermo Rauch 334 | - Christopher Pitt 335 | 336 | requires: 337 | - Core/String 338 | - Core/Array 339 | - MooTools.More 340 | 341 | provides: [String.Extras] 342 | 343 | ... 344 | */ 345 | 346 | (function(){ 347 | 348 | var special = { 349 | 'a': /[àáâãäåăą]/g, 350 | 'A': /[ÀÁÂÃÄÅĂĄ]/g, 351 | 'c': /[ćčç]/g, 352 | 'C': /[ĆČÇ]/g, 353 | 'd': /[ďđ]/g, 354 | 'D': /[ĎÐ]/g, 355 | 'e': /[èéêëěę]/g, 356 | 'E': /[ÈÉÊËĚĘ]/g, 357 | 'g': /[ğ]/g, 358 | 'G': /[Ğ]/g, 359 | 'i': /[ìíîï]/g, 360 | 'I': /[ÌÍÎÏ]/g, 361 | 'l': /[ĺľł]/g, 362 | 'L': /[ĹĽŁ]/g, 363 | 'n': /[ñňń]/g, 364 | 'N': /[ÑŇŃ]/g, 365 | 'o': /[òóôõöøő]/g, 366 | 'O': /[ÒÓÔÕÖØ]/g, 367 | 'r': /[řŕ]/g, 368 | 'R': /[ŘŔ]/g, 369 | 's': /[ššş]/g, 370 | 'S': /[ŠŞŚ]/g, 371 | 't': /[ťţ]/g, 372 | 'T': /[ŤŢ]/g, 373 | 'u': /[ùúûůüµ]/g, 374 | 'U': /[ÙÚÛŮÜ]/g, 375 | 'y': /[ÿý]/g, 376 | 'Y': /[ŸÝ]/g, 377 | 'z': /[žźż]/g, 378 | 'Z': /[ŽŹŻ]/g, 379 | 'th': /[þ]/g, 380 | 'TH': /[Þ]/g, 381 | 'dh': /[ð]/g, 382 | 'DH': /[Ð]/g, 383 | 'ss': /[ß]/g, 384 | 'oe': /[œ]/g, 385 | 'OE': /[Œ]/g, 386 | 'ae': /[æ]/g, 387 | 'AE': /[Æ]/g 388 | }, 389 | 390 | tidy = { 391 | ' ': /[\xa0\u2002\u2003\u2009]/g, 392 | '*': /[\xb7]/g, 393 | '\'': /[\u2018\u2019]/g, 394 | '"': /[\u201c\u201d]/g, 395 | '...': /[\u2026]/g, 396 | '-': /[\u2013]/g, 397 | // '--': /[\u2014]/g, 398 | '»': /[\uFFFD]/g 399 | }, 400 | 401 | conversions = { 402 | ms: 1, 403 | s: 1000, 404 | m: 6e4, 405 | h: 36e5 406 | }, 407 | 408 | findUnits = /(\d*.?\d+)([msh]+)/; 409 | 410 | var walk = function(string, replacements){ 411 | var result = string, key; 412 | for (key in replacements) result = result.replace(replacements[key], key); 413 | return result; 414 | }; 415 | 416 | var getRegexForTag = function(tag, contents){ 417 | tag = tag || (contents ? '' : '\\w+'); 418 | var regstr = contents ? '<' + tag + '(?!\\w)[^>]*>([\\s\\S]*?)<\/' + tag + '(?!\\w)>' : '<\/?' + tag + '\/?>|<' + tag + '[\\s|\/][^>]*>'; 419 | return new RegExp(regstr, 'gi'); 420 | }; 421 | 422 | String.implement({ 423 | 424 | standardize: function(){ 425 | return walk(this, special); 426 | }, 427 | 428 | repeat: function(times){ 429 | return new Array(times + 1).join(this); 430 | }, 431 | 432 | pad: function(length, str, direction){ 433 | if (this.length >= length) return this; 434 | 435 | var pad = (str == null ? ' ' : '' + str) 436 | .repeat(length - this.length) 437 | .substr(0, length - this.length); 438 | 439 | if (!direction || direction == 'right') return this + pad; 440 | if (direction == 'left') return pad + this; 441 | 442 | return pad.substr(0, (pad.length / 2).floor()) + this + pad.substr(0, (pad.length / 2).ceil()); 443 | }, 444 | 445 | getTags: function(tag, contents){ 446 | return this.match(getRegexForTag(tag, contents)) || []; 447 | }, 448 | 449 | stripTags: function(tag, contents){ 450 | return this.replace(getRegexForTag(tag, contents), ''); 451 | }, 452 | 453 | tidy: function(){ 454 | return walk(this, tidy); 455 | }, 456 | 457 | truncate: function(max, trail, atChar){ 458 | var string = this; 459 | if (trail == null && arguments.length == 1) trail = '…'; 460 | if (string.length > max){ 461 | string = string.substring(0, max); 462 | if (atChar){ 463 | var index = string.lastIndexOf(atChar); 464 | if (index != -1) string = string.substr(0, index); 465 | } 466 | if (trail) string += trail; 467 | } 468 | return string; 469 | }, 470 | 471 | ms: function(){ 472 | // "Borrowed" from https://gist.github.com/1503944 473 | var units = findUnits.exec(this); 474 | if (units == null) return Number(this); 475 | return Number(units[1]) * conversions[units[2]]; 476 | } 477 | 478 | }); 479 | 480 | })(); 481 | 482 | /* 483 | --- 484 | 485 | script: Element.Forms.js 486 | 487 | name: Element.Forms 488 | 489 | description: Extends the Element native object to include methods useful in managing inputs. 490 | 491 | license: MIT-style license 492 | 493 | authors: 494 | - Aaron Newton 495 | 496 | requires: 497 | - Core/Element 498 | - String.Extras 499 | - MooTools.More 500 | 501 | provides: [Element.Forms] 502 | 503 | ... 504 | */ 505 | 506 | Element.implement({ 507 | 508 | tidy: function(){ 509 | this.set('value', this.get('value').tidy()); 510 | }, 511 | 512 | getTextInRange: function(start, end){ 513 | return this.get('value').substring(start, end); 514 | }, 515 | 516 | getSelectedText: function(){ 517 | if (this.setSelectionRange) return this.getTextInRange(this.getSelectionStart(), this.getSelectionEnd()); 518 | return document.selection.createRange().text; 519 | }, 520 | 521 | getSelectedRange: function(){ 522 | if (this.selectionStart != null){ 523 | return { 524 | start: this.selectionStart, 525 | end: this.selectionEnd 526 | }; 527 | } 528 | 529 | var pos = { 530 | start: 0, 531 | end: 0 532 | }; 533 | var range = this.getDocument().selection.createRange(); 534 | if (!range || range.parentElement() != this) return pos; 535 | var duplicate = range.duplicate(); 536 | 537 | if (this.type == 'text'){ 538 | pos.start = 0 - duplicate.moveStart('character', -100000); 539 | pos.end = pos.start + range.text.length; 540 | } else { 541 | var value = this.get('value'); 542 | var offset = value.length; 543 | duplicate.moveToElementText(this); 544 | duplicate.setEndPoint('StartToEnd', range); 545 | if (duplicate.text.length) offset -= value.match(/[\n\r]*$/)[0].length; 546 | pos.end = offset - duplicate.text.length; 547 | duplicate.setEndPoint('StartToStart', range); 548 | pos.start = offset - duplicate.text.length; 549 | } 550 | return pos; 551 | }, 552 | 553 | getSelectionStart: function(){ 554 | return this.getSelectedRange().start; 555 | }, 556 | 557 | getSelectionEnd: function(){ 558 | return this.getSelectedRange().end; 559 | }, 560 | 561 | setCaretPosition: function(pos){ 562 | if (pos == 'end') pos = this.get('value').length; 563 | this.selectRange(pos, pos); 564 | return this; 565 | }, 566 | 567 | getCaretPosition: function(){ 568 | return this.getSelectedRange().start; 569 | }, 570 | 571 | selectRange: function(start, end){ 572 | if (this.setSelectionRange){ 573 | this.focus(); 574 | this.setSelectionRange(start, end); 575 | } else { 576 | var value = this.get('value'); 577 | var diff = value.substr(start, end - start).replace(/\r/g, '').length; 578 | start = value.substr(0, start).replace(/\r/g, '').length; 579 | var range = this.createTextRange(); 580 | range.collapse(true); 581 | range.moveEnd('character', start + diff); 582 | range.moveStart('character', start); 583 | range.select(); 584 | } 585 | return this; 586 | }, 587 | 588 | insertAtCursor: function(value, select){ 589 | var pos = this.getSelectedRange(); 590 | var text = this.get('value'); 591 | this.set('value', text.substring(0, pos.start) + value + text.substring(pos.end, text.length)); 592 | if (select !== false) this.selectRange(pos.start, pos.start + value.length); 593 | else this.setCaretPosition(pos.start + value.length); 594 | return this; 595 | }, 596 | 597 | insertAroundCursor: function(options, select){ 598 | options = Object.append({ 599 | before: '', 600 | defaultMiddle: '', 601 | after: '' 602 | }, options); 603 | 604 | var value = this.getSelectedText() || options.defaultMiddle; 605 | var pos = this.getSelectedRange(); 606 | var text = this.get('value'); 607 | 608 | if (pos.start == pos.end){ 609 | this.set('value', text.substring(0, pos.start) + options.before + value + options.after + text.substring(pos.end, text.length)); 610 | this.selectRange(pos.start + options.before.length, pos.end + options.before.length + value.length); 611 | } else { 612 | var current = text.substring(pos.start, pos.end); 613 | this.set('value', text.substring(0, pos.start) + options.before + current + options.after + text.substring(pos.end, text.length)); 614 | var selStart = pos.start + options.before.length; 615 | if (select !== false) this.selectRange(selStart, selStart + current.length); 616 | else this.setCaretPosition(selStart + text.length); 617 | } 618 | return this; 619 | } 620 | 621 | }); 622 | 623 | /* 624 | --- 625 | 626 | script: Fx.Scroll.js 627 | 628 | name: Fx.Scroll 629 | 630 | description: Effect to smoothly scroll any element, including the window. 631 | 632 | license: MIT-style license 633 | 634 | authors: 635 | - Valerio Proietti 636 | 637 | requires: 638 | - Core/Fx 639 | - Core/Element.Event 640 | - Core/Element.Dimensions 641 | - MooTools.More 642 | 643 | provides: [Fx.Scroll] 644 | 645 | ... 646 | */ 647 | 648 | (function(){ 649 | 650 | Fx.Scroll = new Class({ 651 | 652 | Extends: Fx, 653 | 654 | options: { 655 | offset: {x: 0, y: 0}, 656 | wheelStops: true 657 | }, 658 | 659 | initialize: function(element, options){ 660 | this.element = this.subject = document.id(element); 661 | this.parent(options); 662 | 663 | if (typeOf(this.element) != 'element') this.element = document.id(this.element.getDocument().body); 664 | 665 | if (this.options.wheelStops){ 666 | var stopper = this.element, 667 | cancel = this.cancel.pass(false, this); 668 | this.addEvent('start', function(){ 669 | stopper.addEvent('mousewheel', cancel); 670 | }, true); 671 | this.addEvent('complete', function(){ 672 | stopper.removeEvent('mousewheel', cancel); 673 | }, true); 674 | } 675 | }, 676 | 677 | set: function(){ 678 | var now = Array.flatten(arguments); 679 | this.element.scrollTo(now[0], now[1]); 680 | return this; 681 | }, 682 | 683 | compute: function(from, to, delta){ 684 | return [0, 1].map(function(i){ 685 | return Fx.compute(from[i], to[i], delta); 686 | }); 687 | }, 688 | 689 | start: function(x, y){ 690 | if (!this.check(x, y)) return this; 691 | var scroll = this.element.getScroll(); 692 | return this.parent([scroll.x, scroll.y], [x, y]); 693 | }, 694 | 695 | calculateScroll: function(x, y){ 696 | var element = this.element, 697 | scrollSize = element.getScrollSize(), 698 | scroll = element.getScroll(), 699 | size = element.getSize(), 700 | offset = this.options.offset, 701 | values = {x: x, y: y}; 702 | 703 | for (var z in values){ 704 | if (!values[z] && values[z] !== 0) values[z] = scroll[z]; 705 | if (typeOf(values[z]) != 'number') values[z] = scrollSize[z] - size[z]; 706 | values[z] += offset[z]; 707 | } 708 | 709 | return [values.x, values.y]; 710 | }, 711 | 712 | toTop: function(){ 713 | return this.start.apply(this, this.calculateScroll(false, 0)); 714 | }, 715 | 716 | toLeft: function(){ 717 | return this.start.apply(this, this.calculateScroll(0, false)); 718 | }, 719 | 720 | toRight: function(){ 721 | return this.start.apply(this, this.calculateScroll('right', false)); 722 | }, 723 | 724 | toBottom: function(){ 725 | return this.start.apply(this, this.calculateScroll(false, 'bottom')); 726 | }, 727 | 728 | toElement: function(el, axes){ 729 | axes = axes ? Array.convert(axes) : ['x', 'y']; 730 | var scroll = isBody(this.element) ? {x: 0, y: 0} : this.element.getScroll(); 731 | var position = Object.map(document.id(el).getPosition(this.element), function(value, axis){ 732 | return axes.contains(axis) ? value + scroll[axis] : false; 733 | }); 734 | return this.start.apply(this, this.calculateScroll(position.x, position.y)); 735 | }, 736 | 737 | toElementEdge: function(el, axes, offset){ 738 | axes = axes ? Array.convert(axes) : ['x', 'y']; 739 | el = document.id(el); 740 | var to = {}, 741 | position = el.getPosition(this.element), 742 | size = el.getSize(), 743 | scroll = this.element.getScroll(), 744 | containerSize = this.element.getSize(), 745 | edge = { 746 | x: position.x + size.x, 747 | y: position.y + size.y 748 | }; 749 | 750 | ['x', 'y'].each(function(axis){ 751 | if (axes.contains(axis)){ 752 | if (edge[axis] > scroll[axis] + containerSize[axis]) to[axis] = edge[axis] - containerSize[axis]; 753 | if (position[axis] < scroll[axis]) to[axis] = position[axis]; 754 | } 755 | if (to[axis] == null) to[axis] = scroll[axis]; 756 | if (offset && offset[axis]) to[axis] = to[axis] + offset[axis]; 757 | }, this); 758 | 759 | if (to.x != scroll.x || to.y != scroll.y) this.start(to.x, to.y); 760 | return this; 761 | }, 762 | 763 | toElementCenter: function(el, axes, offset){ 764 | axes = axes ? Array.convert(axes) : ['x', 'y']; 765 | el = document.id(el); 766 | var to = {}, 767 | position = el.getPosition(this.element), 768 | size = el.getSize(), 769 | scroll = this.element.getScroll(), 770 | containerSize = this.element.getSize(); 771 | 772 | ['x', 'y'].each(function(axis){ 773 | if (axes.contains(axis)){ 774 | to[axis] = position[axis] - (containerSize[axis] - size[axis]) / 2; 775 | } 776 | if (to[axis] == null) to[axis] = scroll[axis]; 777 | if (offset && offset[axis]) to[axis] = to[axis] + offset[axis]; 778 | }, this); 779 | 780 | if (to.x != scroll.x || to.y != scroll.y) this.start(to.x, to.y); 781 | return this; 782 | } 783 | 784 | }); 785 | 786 | 787 | 788 | function isBody(element){ 789 | return (/^(?:body|html)$/i).test(element.tagName); 790 | } 791 | 792 | })(); 793 | 794 | /* 795 | --- 796 | 797 | script: Fx.Slide.js 798 | 799 | name: Fx.Slide 800 | 801 | description: Effect to slide an element in and out of view. 802 | 803 | license: MIT-style license 804 | 805 | authors: 806 | - Valerio Proietti 807 | 808 | requires: 809 | - Core/Fx 810 | - Core/Element.Style 811 | - MooTools.More 812 | 813 | provides: [Fx.Slide] 814 | 815 | ... 816 | */ 817 | 818 | Fx.Slide = new Class({ 819 | 820 | Extends: Fx, 821 | 822 | options: { 823 | mode: 'vertical', 824 | wrapper: false, 825 | hideOverflow: true, 826 | resetHeight: false 827 | }, 828 | 829 | initialize: function(element, options){ 830 | element = this.element = this.subject = document.id(element); 831 | this.parent(options); 832 | options = this.options; 833 | 834 | var wrapper = element.retrieve('wrapper'), 835 | styles = element.getStyles('margin', 'position', 'overflow'); 836 | 837 | if (options.hideOverflow) styles = Object.append(styles, {overflow: 'hidden'}); 838 | if (options.wrapper) wrapper = document.id(options.wrapper).setStyles(styles); 839 | 840 | if (!wrapper) wrapper = new Element('div', { 841 | styles: styles 842 | }).wraps(element); 843 | 844 | element.store('wrapper', wrapper).setStyle('margin', 0); 845 | if (element.getStyle('overflow') == 'visible') element.setStyle('overflow', 'hidden'); 846 | 847 | this.now = []; 848 | this.open = true; 849 | this.wrapper = wrapper; 850 | 851 | this.addEvent('complete', function(){ 852 | this.open = (wrapper['offset' + this.layout.capitalize()] != 0); 853 | if (this.open && this.options.resetHeight) wrapper.setStyle('height', ''); 854 | }, true); 855 | }, 856 | 857 | vertical: function(){ 858 | this.margin = 'margin-top'; 859 | this.layout = 'height'; 860 | this.offset = this.element.offsetHeight; 861 | }, 862 | 863 | horizontal: function(){ 864 | this.margin = 'margin-left'; 865 | this.layout = 'width'; 866 | this.offset = this.element.offsetWidth; 867 | }, 868 | 869 | set: function(now){ 870 | this.element.setStyle(this.margin, now[0]); 871 | this.wrapper.setStyle(this.layout, now[1]); 872 | return this; 873 | }, 874 | 875 | compute: function(from, to, delta){ 876 | return [0, 1].map(function(i){ 877 | return Fx.compute(from[i], to[i], delta); 878 | }); 879 | }, 880 | 881 | start: function(how, mode){ 882 | if (!this.check(how, mode)) return this; 883 | this[mode || this.options.mode](); 884 | 885 | var margin = this.element.getStyle(this.margin).toInt(), 886 | layout = this.wrapper.getStyle(this.layout).toInt(), 887 | caseIn = [[margin, layout], [0, this.offset]], 888 | caseOut = [[margin, layout], [-this.offset, 0]], 889 | start; 890 | 891 | switch (how){ 892 | case 'in': start = caseIn; break; 893 | case 'out': start = caseOut; break; 894 | case 'toggle': start = (layout == 0) ? caseIn : caseOut; 895 | } 896 | return this.parent(start[0], start[1]); 897 | }, 898 | 899 | slideIn: function(mode){ 900 | return this.start('in', mode); 901 | }, 902 | 903 | slideOut: function(mode){ 904 | return this.start('out', mode); 905 | }, 906 | 907 | hide: function(mode){ 908 | this[mode || this.options.mode](); 909 | this.open = false; 910 | return this.set([-this.offset, 0]); 911 | }, 912 | 913 | show: function(mode){ 914 | this[mode || this.options.mode](); 915 | this.open = true; 916 | return this.set([0, this.offset]); 917 | }, 918 | 919 | toggle: function(mode){ 920 | return this.start('toggle', mode); 921 | } 922 | 923 | }); 924 | 925 | Element.Properties.slide = { 926 | 927 | set: function(options){ 928 | this.get('slide').cancel().setOptions(options); 929 | return this; 930 | }, 931 | 932 | get: function(){ 933 | var slide = this.retrieve('slide'); 934 | if (!slide){ 935 | slide = new Fx.Slide(this, {link: 'cancel'}); 936 | this.store('slide', slide); 937 | } 938 | return slide; 939 | } 940 | 941 | }; 942 | 943 | Element.implement({ 944 | 945 | slide: function(how, mode){ 946 | how = how || 'toggle'; 947 | var slide = this.get('slide'), toggle; 948 | switch (how){ 949 | case 'hide': slide.hide(mode); break; 950 | case 'show': slide.show(mode); break; 951 | case 'toggle': 952 | var flag = this.retrieve('slide:flag', slide.open); 953 | slide[flag ? 'slideOut' : 'slideIn'](mode); 954 | this.store('slide:flag', !flag); 955 | toggle = true; 956 | break; 957 | default: slide.start(how, mode); 958 | } 959 | if (!toggle) this.eliminate('slide:flag'); 960 | return this; 961 | } 962 | 963 | }); 964 | 965 | /* 966 | --- 967 | 968 | script: Keyboard.js 969 | 970 | name: Keyboard 971 | 972 | description: KeyboardEvents used to intercept events on a class for keyboard and format modifiers in a specific order so as to make alt+shift+c the same as shift+alt+c. 973 | 974 | license: MIT-style license 975 | 976 | authors: 977 | - Perrin Westrich 978 | - Aaron Newton 979 | - Scott Kyle 980 | 981 | requires: 982 | - Core/Events 983 | - Core/Options 984 | - Core/Element.Event 985 | - Element.Event.Pseudos.Keys 986 | 987 | provides: [Keyboard] 988 | 989 | ... 990 | */ 991 | 992 | (function(){ 993 | 994 | var Keyboard = this.Keyboard = new Class({ 995 | 996 | Extends: Events, 997 | 998 | Implements: [Options], 999 | 1000 | options: {/* 1001 | onActivate: function(){}, 1002 | onDeactivate: function(){},*/ 1003 | defaultEventType: 'keydown', 1004 | active: false, 1005 | manager: null, 1006 | events: {}, 1007 | nonParsedEvents: ['activate', 'deactivate', 'onactivate', 'ondeactivate', 'changed', 'onchanged'] 1008 | }, 1009 | 1010 | initialize: function(options){ 1011 | if (options && options.manager){ 1012 | this._manager = options.manager; 1013 | delete options.manager; 1014 | } 1015 | this.setOptions(options); 1016 | this._setup(); 1017 | }, 1018 | 1019 | addEvent: function(type, fn, internal){ 1020 | return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn, internal); 1021 | }, 1022 | 1023 | removeEvent: function(type, fn){ 1024 | return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn); 1025 | }, 1026 | 1027 | toggleActive: function(){ 1028 | return this[this.isActive() ? 'deactivate' : 'activate'](); 1029 | }, 1030 | 1031 | activate: function(instance){ 1032 | if (instance){ 1033 | if (instance.isActive()) return this; 1034 | //if we're stealing focus, store the last keyboard to have it so the relinquish command works 1035 | if (this._activeKB && instance != this._activeKB){ 1036 | this.previous = this._activeKB; 1037 | this.previous.fireEvent('deactivate'); 1038 | } 1039 | //if we're enabling a child, assign it so that events are now passed to it 1040 | this._activeKB = instance.fireEvent('activate'); 1041 | Keyboard.manager.fireEvent('changed'); 1042 | } else if (this._manager){ 1043 | //else we're enabling ourselves, we must ask our parent to do it for us 1044 | this._manager.activate(this); 1045 | } 1046 | return this; 1047 | }, 1048 | 1049 | isActive: function(){ 1050 | return this._manager ? (this._manager._activeKB == this) : (Keyboard.manager == this); 1051 | }, 1052 | 1053 | deactivate: function(instance){ 1054 | if (instance){ 1055 | if (instance === this._activeKB){ 1056 | this._activeKB = null; 1057 | instance.fireEvent('deactivate'); 1058 | Keyboard.manager.fireEvent('changed'); 1059 | } 1060 | } else if (this._manager){ 1061 | this._manager.deactivate(this); 1062 | } 1063 | return this; 1064 | }, 1065 | 1066 | relinquish: function(){ 1067 | if (this.isActive() && this._manager && this._manager.previous) this._manager.activate(this._manager.previous); 1068 | else this.deactivate(); 1069 | return this; 1070 | }, 1071 | 1072 | //management logic 1073 | manage: function(instance){ 1074 | if (instance._manager) instance._manager.drop(instance); 1075 | this._instances.push(instance); 1076 | instance._manager = this; 1077 | if (!this._activeKB) this.activate(instance); 1078 | return this; 1079 | }, 1080 | 1081 | drop: function(instance){ 1082 | instance.relinquish(); 1083 | this._instances.erase(instance); 1084 | if (this._activeKB == instance){ 1085 | if (this.previous && this._instances.contains(this.previous)) this.activate(this.previous); 1086 | else this._activeKB = this._instances[0]; 1087 | } 1088 | return this; 1089 | }, 1090 | 1091 | trace: function(){ 1092 | Keyboard.trace(this); 1093 | }, 1094 | 1095 | each: function(fn){ 1096 | Keyboard.each(this, fn); 1097 | }, 1098 | 1099 | /* 1100 | PRIVATE METHODS 1101 | */ 1102 | 1103 | _instances: [], 1104 | 1105 | _disable: function(instance){ 1106 | if (this._activeKB == instance) this._activeKB = null; 1107 | }, 1108 | 1109 | _setup: function(){ 1110 | this.addEvents(this.options.events); 1111 | //if this is the root manager, nothing manages it 1112 | if (Keyboard.manager && !this._manager) Keyboard.manager.manage(this); 1113 | if (this.options.active) this.activate(); 1114 | else this.relinquish(); 1115 | }, 1116 | 1117 | _handle: function(event, type){ 1118 | //Keyboard.stop(event) prevents key propagation 1119 | if (event.preventKeyboardPropagation) return; 1120 | 1121 | var bubbles = !!this._manager; 1122 | if (bubbles && this._activeKB){ 1123 | this._activeKB._handle(event, type); 1124 | if (event.preventKeyboardPropagation) return; 1125 | } 1126 | this.fireEvent(type, event); 1127 | 1128 | if (!bubbles && this._activeKB) this._activeKB._handle(event, type); 1129 | } 1130 | 1131 | }); 1132 | 1133 | var parsed = {}; 1134 | var modifiers = ['shift', 'control', 'alt', 'meta']; 1135 | var regex = /^(?:shift|control|ctrl|alt|meta)$/; 1136 | 1137 | Keyboard.parse = function(type, eventType, ignore){ 1138 | if (ignore && ignore.contains(type.toLowerCase())) return type; 1139 | 1140 | type = type.toLowerCase().replace(/^(keyup|keydown):/, function($0, $1){ 1141 | eventType = $1; 1142 | return ''; 1143 | }); 1144 | 1145 | if (!parsed[type]){ 1146 | if (type != '+'){ 1147 | var key, mods = {}; 1148 | type.split('+').each(function(part){ 1149 | if (regex.test(part)) mods[part] = true; 1150 | else key = part; 1151 | }); 1152 | 1153 | mods.control = mods.control || mods.ctrl; // allow both control and ctrl 1154 | 1155 | var keys = []; 1156 | modifiers.each(function(mod){ 1157 | if (mods[mod]) keys.push(mod); 1158 | }); 1159 | 1160 | if (key) keys.push(key); 1161 | parsed[type] = keys.join('+'); 1162 | } else { 1163 | parsed[type] = type; 1164 | } 1165 | } 1166 | 1167 | return eventType + ':keys(' + parsed[type] + ')'; 1168 | }; 1169 | 1170 | Keyboard.each = function(keyboard, fn){ 1171 | var current = keyboard || Keyboard.manager; 1172 | while (current){ 1173 | fn(current); 1174 | current = current._activeKB; 1175 | } 1176 | }; 1177 | 1178 | Keyboard.stop = function(event){ 1179 | event.preventKeyboardPropagation = true; 1180 | }; 1181 | 1182 | Keyboard.manager = new Keyboard({ 1183 | active: true 1184 | }); 1185 | 1186 | Keyboard.trace = function(keyboard){ 1187 | keyboard = keyboard || Keyboard.manager; 1188 | var hasConsole = window.console && console.log; 1189 | if (hasConsole) console.log('the following items have focus: '); 1190 | Keyboard.each(keyboard, function(current){ 1191 | if (hasConsole) console.log(document.id(current.widget) || current.wiget || current); 1192 | }); 1193 | }; 1194 | 1195 | var handler = function(event){ 1196 | var keys = []; 1197 | modifiers.each(function(mod){ 1198 | if (event[mod]) keys.push(mod); 1199 | }); 1200 | 1201 | if (!regex.test(event.key)) keys.push(event.key); 1202 | Keyboard.manager._handle(event, event.type + ':keys(' + keys.join('+') + ')'); 1203 | }; 1204 | 1205 | document.addEvents({ 1206 | 'keyup': handler, 1207 | 'keydown': handler 1208 | }); 1209 | 1210 | })(); 1211 | 1212 | /* 1213 | --- 1214 | 1215 | script: Color.js 1216 | 1217 | name: Color 1218 | 1219 | description: Class for creating and manipulating colors in JavaScript. Supports HSB -> RGB Conversions and vice versa. 1220 | 1221 | license: MIT-style license 1222 | 1223 | authors: 1224 | - Valerio Proietti 1225 | 1226 | requires: 1227 | - Core/Array 1228 | - Core/String 1229 | - Core/Number 1230 | - Core/Hash 1231 | - Core/Function 1232 | - MooTools.More 1233 | 1234 | provides: [Color] 1235 | 1236 | ... 1237 | */ 1238 | 1239 | (function(){ 1240 | 1241 | var Color = this.Color = new Type('Color', function(color, type){ 1242 | if (arguments.length >= 3){ 1243 | type = 'rgb'; color = Array.slice(arguments, 0, 3); 1244 | } else if (typeof color == 'string'){ 1245 | if (color.match(/rgb/)) color = color.rgbToHex().hexToRgb(true); 1246 | else if (color.match(/hsb/)) color = color.hsbToRgb(); 1247 | else color = color.hexToRgb(true); 1248 | } 1249 | type = type || 'rgb'; 1250 | switch (type){ 1251 | case 'hsb': 1252 | var old = color; 1253 | color = color.hsbToRgb(); 1254 | color.hsb = old; 1255 | break; 1256 | case 'hex': color = color.hexToRgb(true); break; 1257 | } 1258 | color.rgb = color.slice(0, 3); 1259 | color.hsb = color.hsb || color.rgbToHsb(); 1260 | color.hex = color.rgbToHex(); 1261 | return Object.append(color, this); 1262 | }); 1263 | 1264 | Color.implement({ 1265 | 1266 | mix: function(){ 1267 | var colors = Array.slice(arguments); 1268 | var alpha = (typeOf(colors.getLast()) == 'number') ? colors.pop() : 50; 1269 | var rgb = this.slice(); 1270 | colors.each(function(color){ 1271 | color = new Color(color); 1272 | for (var i = 0; i < 3; i++) rgb[i] = Math.round((rgb[i] / 100 * (100 - alpha)) + (color[i] / 100 * alpha)); 1273 | }); 1274 | return new Color(rgb, 'rgb'); 1275 | }, 1276 | 1277 | invert: function(){ 1278 | return new Color(this.map(function(value){ 1279 | return 255 - value; 1280 | })); 1281 | }, 1282 | 1283 | setHue: function(value){ 1284 | return new Color([value, this.hsb[1], this.hsb[2]], 'hsb'); 1285 | }, 1286 | 1287 | setSaturation: function(percent){ 1288 | return new Color([this.hsb[0], percent, this.hsb[2]], 'hsb'); 1289 | }, 1290 | 1291 | setBrightness: function(percent){ 1292 | return new Color([this.hsb[0], this.hsb[1], percent], 'hsb'); 1293 | } 1294 | 1295 | }); 1296 | 1297 | this.$RGB = function(r, g, b){ 1298 | return new Color([r, g, b], 'rgb'); 1299 | }; 1300 | 1301 | this.$HSB = function(h, s, b){ 1302 | return new Color([h, s, b], 'hsb'); 1303 | }; 1304 | 1305 | this.$HEX = function(hex){ 1306 | return new Color(hex, 'hex'); 1307 | }; 1308 | 1309 | Array.implement({ 1310 | 1311 | rgbToHsb: function(){ 1312 | var red = this[0], 1313 | green = this[1], 1314 | blue = this[2], 1315 | hue = 0, 1316 | max = Math.max(red, green, blue), 1317 | min = Math.min(red, green, blue), 1318 | delta = max - min, 1319 | brightness = max / 255, 1320 | saturation = (max != 0) ? delta / max : 0; 1321 | 1322 | if (saturation != 0){ 1323 | var rr = (max - red) / delta; 1324 | var gr = (max - green) / delta; 1325 | var br = (max - blue) / delta; 1326 | if (red == max) hue = br - gr; 1327 | else if (green == max) hue = 2 + rr - br; 1328 | else hue = 4 + gr - rr; 1329 | hue /= 6; 1330 | if (hue < 0) hue++; 1331 | } 1332 | return [Math.round(hue * 360), Math.round(saturation * 100), Math.round(brightness * 100)]; 1333 | }, 1334 | 1335 | hsbToRgb: function(){ 1336 | var br = Math.round(this[2] / 100 * 255); 1337 | if (this[1] == 0){ 1338 | return [br, br, br]; 1339 | } else { 1340 | var hue = this[0] % 360; 1341 | var f = hue % 60; 1342 | var p = Math.round((this[2] * (100 - this[1])) / 10000 * 255); 1343 | var q = Math.round((this[2] * (6000 - this[1] * f)) / 600000 * 255); 1344 | var t = Math.round((this[2] * (6000 - this[1] * (60 - f))) / 600000 * 255); 1345 | switch (Math.floor(hue / 60)){ 1346 | case 0: return [br, t, p]; 1347 | case 1: return [q, br, p]; 1348 | case 2: return [p, br, t]; 1349 | case 3: return [p, q, br]; 1350 | case 4: return [t, p, br]; 1351 | case 5: return [br, p, q]; 1352 | } 1353 | } 1354 | return false; 1355 | } 1356 | 1357 | }); 1358 | 1359 | String.implement({ 1360 | 1361 | rgbToHsb: function(){ 1362 | var rgb = this.match(/\d{1,3}/g); 1363 | return (rgb) ? rgb.rgbToHsb() : null; 1364 | }, 1365 | 1366 | hsbToRgb: function(){ 1367 | var hsb = this.match(/\d{1,3}/g); 1368 | return (hsb) ? hsb.hsbToRgb() : null; 1369 | } 1370 | 1371 | }); 1372 | 1373 | })(); 1374 | 1375 | -------------------------------------------------------------------------------- /js/sexhr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sexhr.js 3 | * 4 | * A simple but useful promise-enabled wrapper around XHR. Takes care of a lot 5 | * of common tasks without abstracting away any power or freedom. 6 | * ----------------------------------------------------------------------------- 7 | * 8 | * Copyright (c) 2015, Lyon Bros Enterprises, LLC. (http://www.lyonbros.com) 9 | * 10 | * Licensed under The MIT License. 11 | * Redistributions of files must retain the above copyright notice. 12 | */ 13 | 14 | (function() { 15 | "use strict"; 16 | this.Sexhr = function(options) 17 | { 18 | options || (options = {}); 19 | 20 | return new Promise(function(resolve, reject) { 21 | var url = options.url; 22 | var method = (options.method || 'get').toUpperCase(); 23 | var emulate = options.emulate || true; 24 | 25 | if(!options.url) throw new Error('no url given'); 26 | 27 | url = url.replace(/#.*$/, ''); 28 | var qs = []; 29 | if(options.querydata) 30 | { 31 | qs = Object.keys(options.querydata) 32 | .map(function(key) { 33 | return key + '=' + encodeURIComponent(options.querydata[key]); 34 | }); 35 | } 36 | if(emulate && ['GET', 'POST'].indexOf(method) < 0) 37 | { 38 | qs.push('_method='+method); 39 | method = 'POST'; 40 | } 41 | if(qs.length) 42 | { 43 | var querystring = qs.join('&'); 44 | if(url.match(/\?/)) 45 | { 46 | url = url.replace(/&$/) + '&' + querystring; 47 | } 48 | else 49 | { 50 | url += '?' + querystring; 51 | } 52 | } 53 | 54 | var xhr = new XMLHttpRequest(); 55 | xhr.open(method, url, true); 56 | xhr.responseType = options.response_type || ''; 57 | if(options.timeout) xhr.timeout = options.timeout; 58 | 59 | Object.keys(options.headers || {}).forEach(function(k) { 60 | xhr.setRequestHeader(k, options.headers[k]); 61 | }); 62 | xhr.onload = function(e) 63 | { 64 | if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) 65 | { 66 | var value = xhr.response; 67 | return resolve([value, xhr]); 68 | } 69 | else if(xhr.status >= 400) 70 | { 71 | reject({xhr: xhr, code: xhr.status, msg: xhr.response}); 72 | } 73 | }; 74 | xhr.onabort = function(e) 75 | { 76 | reject({xhr: xhr, code: -2, msg: 'aborted'}); 77 | }; 78 | xhr.onerror = function(e) 79 | { 80 | reject({xhr: xhr, code: -1, msg: 'error'}); 81 | }; 82 | xhr.ontimeout = function(e) 83 | { 84 | reject({xhr: xhr, code: -3, msg: 'timeout'}); 85 | }; 86 | 87 | // set xhr.on[progress|abort|etc] 88 | Object.keys(options).forEach(function(k) { 89 | if(k.substr(0, 2) != 'on') return false; 90 | if(['onload', 'onerror', 'onabort', 'ontimeout'].indexOf(k) >= 0) return false; 91 | var fn = options[k]; 92 | xhr[k] = function(e) { fn(e, xhr); }; 93 | }); 94 | // set xhr.upload.on[progress|abort|etc] 95 | Object.keys(options.upload || {}).forEach(function(k) { 96 | if(k.substr(0, 2) != 'on') return false; 97 | var fn = options[k]; 98 | xhr.upload[k] = function(e) { fn(e, xhr); }; 99 | }); 100 | 101 | if(options.override) options.override(xhr); 102 | 103 | xhr.send(options.data); 104 | }); 105 | }; 106 | }).apply((typeof exports != 'undefined') ? exports : this); 107 | 108 | -------------------------------------------------------------------------------- /js/stripe.js: -------------------------------------------------------------------------------- 1 | const load_modal_template = function(modal, template_data) { 2 | template_data || (template_data = {}); 3 | const get_modals = function() { return document.body.getElements('.active-payment-modal'); }; 4 | get_modals().forEach(function(el) { 5 | if(el.getStyle('display') == 'none') { 6 | el.destroy(); 7 | } 8 | }); 9 | 10 | var copy = modal.clone() 11 | .addClass('active-payment-modal'); 12 | copy.set('html', copy.get('html').replace(/\[\[([a-z0-9_-]+)\]\]/gi, function(_, match) { 13 | return template_data[match] || ''; 14 | })); 15 | var active_modal = get_modals()[0]; 16 | if(active_modal && active_modal.getStyle('display') != 'none') { 17 | active_modal.set('html', copy.get('html')); 18 | } else { 19 | copy.addEvent('click:relay(.close-modal)', function(e) { 20 | get_modals().map(function(el) { 21 | UIkit.modal(el).hide(); 22 | el.destroy(); 23 | }); 24 | }); 25 | UIkit.modal(copy).show(); 26 | } 27 | }; 28 | 29 | const stripe_success = function(res) { 30 | if(!res) return; 31 | load_modal_template($('payment-success-modal'), res); 32 | }; 33 | 34 | const stripe_error = function(errobj) { 35 | var errormsg = errobj.msg; 36 | try { var jsonerr = JSON.parse(errormsg); } catch(_) {} 37 | var msg = jsonerr ? jsonerr.error.message : errormsg; 38 | var tpl = { 39 | error: msg, 40 | } 41 | load_modal_template($('payment-error-modal'), tpl); 42 | }; 43 | 44 | const stripe_handler = function(btnsel, type) { 45 | const payment_types = { 46 | 'premium': { 47 | title: 'Turtl Premium ($3/mo)', 48 | button: 'Get Premium', 49 | amount: 300, 50 | }, 51 | 'business': { 52 | title: 'Turtl Business ($8/mo)', 53 | button: 'Get Business', 54 | amount: 800, 55 | }, 56 | }; 57 | const payment_title = payment_types[type].title; 58 | const button_txt = payment_types[type].button; 59 | const amount = payment_types[type].amount; 60 | 61 | var handler = StripeCheckout.configure({ 62 | key: stripe_pubkey, 63 | image: "/images/logo.svg", 64 | name: "Turtl", 65 | description: payment_title, 66 | amount: amount, 67 | zipCode: true, 68 | panelLabel: button_txt, 69 | allowRememberMe: false, 70 | token: function(tokenobj) { 71 | var url = window.location.toString().match(/turtl\.loc/) ? 72 | 'http://api.turtl.loc:8181/payment' : 73 | 'https://apiv3.turtlapp.com/payment'; 74 | var paymentinfo = { 75 | type: type, 76 | token: tokenobj, 77 | }; 78 | var req = { 79 | url: url, 80 | method: 'POST', 81 | data: JSON.stringify(paymentinfo), 82 | headers: { 83 | 'Content-Type': 'application/json', 84 | }, 85 | }; 86 | load_modal_template($('payment-loading-modal')); 87 | Sexhr(req) 88 | .spread(function(res, xhr) { 89 | _paq.push(['trackGoal', 4, amount / 100]); 90 | stripe_success(JSON.parse(res)); 91 | }) 92 | .catch(function(err) { 93 | stripe_error(err); 94 | }); 95 | }, 96 | }); 97 | 98 | document.getElement(btnsel).addEventListener('click', function(e) { 99 | e.preventDefault(); 100 | _paq.push(['trackGoal', 1, amount / 100]); 101 | handler.open(); 102 | }); 103 | }; 104 | 105 | -------------------------------------------------------------------------------- /keybase.txt: -------------------------------------------------------------------------------- 1 | ================================================================== 2 | https://keybase.io/orthecreedence 3 | -------------------------------------------------------------------- 4 | 5 | I hereby claim: 6 | 7 | * I am an admin of https://turtlapp.com 8 | * I am orthecreedence (https://keybase.io/orthecreedence) on keybase. 9 | * I have a public key with fingerprint DEDF 113E 5424 8344 1637 16B5 5C66 FAD1 3222 D757 10 | 11 | To do so, I am signing this object: 12 | 13 | { 14 | "body": { 15 | "key": { 16 | "eldest_kid": "010162d53a2e14a912d94d7c22e87e8592c1f0264b4a6b75cf35c4b23b4d104d0e370a", 17 | "fingerprint": "dedf113e54248344163716b55c66fad13222d757", 18 | "host": "keybase.io", 19 | "key_id": "5c66fad13222d757", 20 | "kid": "010162d53a2e14a912d94d7c22e87e8592c1f0264b4a6b75cf35c4b23b4d104d0e370a", 21 | "uid": "c51221b7dbba12757a50d885987c9b00", 22 | "username": "orthecreedence" 23 | }, 24 | "revoke": { 25 | "sig_ids": [ 26 | "e7935869ac7fec3fa62e38442f2e0f13dcd619fc4d136950177ada38e09cdd180f" 27 | ] 28 | }, 29 | "service": { 30 | "hostname": "turtlapp.com", 31 | "protocol": "https:" 32 | }, 33 | "type": "web_service_binding", 34 | "version": 1 35 | }, 36 | "ctime": 1472855194, 37 | "expire_in": 157680000, 38 | "prev": "22edd66b47aecfbdcf4335a59ab071d0ea37bd732a7e123e62a53ae418a9bd46", 39 | "seqno": 19, 40 | "tag": "signature" 41 | } 42 | 43 | which yields the signature: 44 | 45 | -----BEGIN PGP MESSAGE----- 46 | Version: GnuPG v1 47 | 48 | owGtkntQVFUcxxcMkUdAqLwiB2/UH7XAPfd9t4cpijaaQqNuJQL33nPuclnYXXaX 49 | RYZWISfRQQMSAydqwkoCgT+iIUcCfPAIMkaMGKnBSSAaw4AiBfExnWXsv/7s/HPm 50 | nPP9fX7f8z2n/PFlOn8v48BrNdeuur7y+u6mrDP23W8qJGQrLCAMhYQZLU0oGyKH 51 | M92sQcJAkIAEHAVZWqIQYCQRUFBkIK9QFBJ4JLAipQCVpDhGZiRO5llFpVmFkSla 52 | ZiAgGUgimiclQk+omsWE7Da7ZnFiLERQBYBGLEMxAs0wgKN5wMksq3CcKkFAUxQF 53 | eZbHhZlWh6cCm5MlB4rXrHgPL9KX7P2H/n/2nbeEU1hAUUDmoSxLgMKNJJaEAsYI 54 | vCLKJOkROpDdIuUgrLbanZlIsSMEkUVBhFtP2JHLakaedB2aCVt3EIY9BOJFmhU4 55 | UVJ4FSm0KnEUogWGoVQKkSqgoQI5IKoKdkRzIksCnpegRAuIFBUIgUCqxF7Mxn1d 56 | mrIE92T1yIMzz+7Mlmy2eMWag93Z7FanVbFm45NMp9PmMGBXbYSzwObR5iM5/REl 57 | XdYsED8VLnEhu0OzWggDwE0Up+bBAoanBJYFIqMn0D6bZkfpmkfB8pxA4uFphFwY 58 | iXOGkONkhpeQospQURmaZiVWlGSSBzheieZlyNOUxCNA0YijJPxWiAGCJMqQ4QjP 59 | vXItVswW9YRTMmEmjs4i4XvhRP2PeEc/pvPy1y338fZ8Yp2/X8i/P/t5U/A9nxrb 60 | zOaDD25SxzIWKzYFRu6FTQ93dWWMBSWmPfNsx47CYub4N+c6d7pd4fWxw7UxAcp6 61 | +b2p6o6S0trGgZnZ/hudE75HTw5WMFFBZ35YF/76htbPsyoS6/Z5511IDstT2T15 62 | Cbq048sesOH39u+/i34NsqWey61LJkpqCjqmzOaXDuRu/6BfF7Dx65OpbatzY4dG 63 | j4za7CW/fH+0JqqyJSKy6q0tLQ3PuS+fmn359ibL5QrAhrzZM9/XsK3XEhZj2tLl 64 | 4/aN9B17Yrzst1cc1ZnJh6787juq3M6KWzU7AXpaa3dH7XI1L3/f+NTQaq+WDfM9 65 | N8ZS2ET9wbNZYb25jcGnSx2tcULPUENuzNsZG0Oj1eYvEorakyaDRxL6hk9PmXeG 66 | 3o37cfjOJXnur4/euDZtrNp+KzPi497FxrWTJ54c/2OSezUuR2yLryxbcb84x+8z 67 | w9S4Lu2TWyEhK5l3uo2Hhv6Uo+ruDPqFfvriietFxWDdbHlixqqEF7oKCkdSjFXO 68 | d8uabBcjprpt7pGiNWFXomIDrANp44uV9Ws+PNVzYMfZUN/5mYXdMwvTSQ9Dq8kz 69 | 8Hxq2dWfzn95ODrQYM8/Flk/nPM37JprrihxbzZNTKzsDM5L0Bc8/e1I+bYVPwey 70 | I/2a1/X01pT1jgU/sXS2pfXSdHu7Y23fVu3CYGHr1vzupIG5g9NtF8P+AQ== 71 | =j2tc 72 | -----END PGP MESSAGE----- 73 | 74 | And finally, I am proving ownership of this host by posting or 75 | appending to this document. 76 | 77 | View my publicly-auditable identity here: https://keybase.io/orthecreedence 78 | 79 | ================================================================== 80 | -------------------------------------------------------------------------------- /manage/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Manage your subscription" 4 | permalink: "manage/" 5 | body_class: 'manage' 6 | --- 7 | 8 |
17 | Your billing email is used to contact you about your payment 18 | and your account. Your Turtl account email is used to link 19 | your billing info to your Turtl account so we know which 20 | account to give subscription features to. 21 |
22 | 38 |Features | 72 |Basic | 73 |Premium | 74 |Business | 75 |
---|---|---|---|
Encrypt and secure all your private data | 80 |81 | | 82 | | 83 | |
Use Turtl on unlimited devices | 86 |87 | | 88 | | 89 | |
Collaboraters per Space | 92 |3 | 93 |20 | 94 |50 | 95 |
Storage | 98 |50MB | 99 |10GB | 100 |50GB | 101 |
Support | 104 |Community | 105 |Community | 106 |Dedicated | 107 |
Cost | 110 |Free | 111 |$3/mo | 112 |$8/mo | 113 |
120 | Need something more than what we offer above? Get in touch and we'll figure out a plan for you! 121 |
122 |139 | Your payment information was successfully processed. 140 |
141 |142 | Please save this link, which will allow you to manage your Turtl 143 | subscription: 144 |
145 |[[manage_url]]146 |
147 | This link has also been emailed to you at [[email]]
!
148 |
161 | Unfortunately, it looks like there was a problem processing your 162 | payment: 163 |
164 |[[error]]
165 | Please try again, or email us at support@turtlapp.com for help.
166 |178 | The Turtl service is free and in most cases should work for most users. 179 | However, beginning soon, Turtl will launch a Premium service that will 180 | include more storage and will unlock various features in the Turtl apps. 181 |
182 |183 | In the meantime while we figure out what our Premium service will offer, 184 | please consider making a donation to aid in Turtl's development! 185 |
186 |187 | If you want the total freedom and security of Turtl without paying us, 188 | you are more than free to run your own Turtl server. 189 |
190 |10 | Lost your password? Want to sign up again with your email address? 11 |
12 | 13 |14 | You came to the right place! Enter your email here and we'll send you an email 15 | with a link in it that will let you remove your account. 16 |
17 | 18 |19 | Beware! If you complete this process, all the data in your account will be lost! 20 |
21 | 22 |