diff --git a/README.md b/README.md index 097be2f..e8042de 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ third party code used in this experiment * [require js](http://requirejs.org/), by [jrburke](jrburke), BSD & MIT license * [almond js](https://github.com/jrburke/almond), by [jrburke](jrburke), BSD & MIT license * [raf js](https://gist.github.com/paulirish/1579671), by [paulirish](https://github.com/paulirish), MIT license +* [reqwest js](https://github.com/ded/reqwest/), by [ded](https://github.com/ded) license --- diff --git a/index.html b/index.html index 13e3cdb..19d5ec9 100644 --- a/index.html +++ b/index.html @@ -35,10 +35,16 @@
- + - + + download bitmap file (.png) +
+ + open + Sorry, something went wrong. Maybe try again? +
diff --git a/scripts/lib/reqwest.js b/scripts/lib/reqwest.js new file mode 100644 index 0000000..de7d8da --- /dev/null +++ b/scripts/lib/reqwest.js @@ -0,0 +1,590 @@ +!function (name, context, definition) { + if (typeof module != 'undefined' && module.exports) module.exports = definition() + else if (typeof define == 'function' && define.amd) define(definition) + else context[name] = definition() +}('reqwest', this, function () { + + var win = window + , doc = document + , twoHundo = /^20\d$/ + , byTag = 'getElementsByTagName' + , readyState = 'readyState' + , contentType = 'Content-Type' + , requestedWith = 'X-Requested-With' + , head = doc[byTag]('head')[0] + , uniqid = 0 + , callbackPrefix = 'reqwest_' + (+new Date()) + , lastValue // data stored by the most recent JSONP callback + , xmlHttpRequest = 'XMLHttpRequest' + , xDomainRequest = 'XDomainRequest' + , noop = function () {} + + , isArray = typeof Array.isArray == 'function' + ? Array.isArray + : function (a) { + return a instanceof Array + } + + , defaultHeaders = { + contentType: 'application/x-www-form-urlencoded' + , requestedWith: xmlHttpRequest + , accept: { + '*': 'text/javascript, text/html, application/xml, text/xml, */*' + , xml: 'application/xml, text/xml' + , html: 'text/html' + , text: 'text/plain' + , json: 'application/json, text/javascript' + , js: 'application/javascript, text/javascript' + } + } + + , xhr = function(o) { + // is it x-domain + if (o.crossOrigin === true) { + var xhr = win[xmlHttpRequest] ? new XMLHttpRequest() : null + if (xhr && 'withCredentials' in xhr) { + return xhr + } else if (win[xDomainRequest]) { + return new XDomainRequest() + } else { + throw new Error('Browser does not support cross-origin requests') + } + } else if (win[xmlHttpRequest]) { + return new XMLHttpRequest() + } else { + return new ActiveXObject('Microsoft.XMLHTTP') + } + } + , globalSetupOptions = { + dataFilter: function (data) { + return data + } + } + + function handleReadyState(r, success, error) { + return function () { + // use _aborted to mitigate against IE err c00c023f + // (can't read props on aborted request objects) + if (r._aborted) return error(r.request) + if (r.request && r.request[readyState] == 4) { + r.request.onreadystatechange = noop + if (twoHundo.test(r.request.status)) + success(r.request) + else + error(r.request) + } + } + } + + function setHeaders(http, o) { + var headers = o.headers || {} + , h + + headers.Accept = headers.Accept + || defaultHeaders.accept[o.type] + || defaultHeaders.accept['*'] + + // breaks cross-origin requests with legacy browsers + if (!o.crossOrigin && !headers[requestedWith]) headers[requestedWith] = defaultHeaders.requestedWith + if (!headers[contentType]) headers[contentType] = o.contentType || defaultHeaders.contentType + for (h in headers) + headers.hasOwnProperty(h) && 'setRequestHeader' in http && http.setRequestHeader(h, headers[h]) + } + + function setCredentials(http, o) { + if (typeof o.withCredentials !== 'undefined' && typeof http.withCredentials !== 'undefined') { + http.withCredentials = !!o.withCredentials + } + } + + function generalCallback(data) { + lastValue = data + } + + function urlappend (url, s) { + return url + (/\?/.test(url) ? '&' : '?') + s + } + + function handleJsonp(o, fn, err, url) { + var reqId = uniqid++ + , cbkey = o.jsonpCallback || 'callback' // the 'callback' key + , cbval = o.jsonpCallbackName || reqwest.getcallbackPrefix(reqId) + // , cbval = o.jsonpCallbackName || ('reqwest_' + reqId) // the 'callback' value + , cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)') + , match = url.match(cbreg) + , script = doc.createElement('script') + , loaded = 0 + , isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1 + + if (match) { + if (match[3] === '?') { + url = url.replace(cbreg, '$1=' + cbval) // wildcard callback func name + } else { + cbval = match[3] // provided callback func name + } + } else { + url = urlappend(url, cbkey + '=' + cbval) // no callback details, add 'em + } + + win[cbval] = generalCallback + + script.type = 'text/javascript' + script.src = url + script.async = true + if (typeof script.onreadystatechange !== 'undefined' && !isIE10) { + // need this for IE due to out-of-order onreadystatechange(), binding script + // execution to an event listener gives us control over when the script + // is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html + // + // if this hack is used in IE10 jsonp callback are never called + script.event = 'onclick' + script.htmlFor = script.id = '_reqwest_' + reqId + } + + script.onload = script.onreadystatechange = function () { + if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) { + return false + } + script.onload = script.onreadystatechange = null + script.onclick && script.onclick() + // Call the user callback with the last value stored and clean up values and scripts. + fn(lastValue) + lastValue = undefined + head.removeChild(script) + loaded = 1 + } + + // Add the script to the DOM head + head.appendChild(script) + + // Enable JSONP timeout + return { + abort: function () { + script.onload = script.onreadystatechange = null + err({}, 'Request is aborted: timeout', {}) + lastValue = undefined + head.removeChild(script) + loaded = 1 + } + } + } + + function getRequest(fn, err) { + var o = this.o + , method = (o.method || 'GET').toUpperCase() + , url = typeof o === 'string' ? o : o.url + // convert non-string objects to query-string form unless o.processData is false + , data = (o.processData !== false && o.data && typeof o.data !== 'string') + ? reqwest.toQueryString(o.data) + : (o.data || null) + , http + , sendWait = false + + // if we're working on a GET request and we have data then we should append + // query string to end of URL and not post data + if ((o.type == 'jsonp' || method == 'GET') && data) { + url = urlappend(url, data) + data = null + } + + if (o.type == 'jsonp') return handleJsonp(o, fn, err, url) + + http = xhr(o) + http.open(method, url, o.async === false ? false : true) + setHeaders(http, o) + setCredentials(http, o) + if (win[xDomainRequest] && http instanceof win[xDomainRequest]) { + http.onload = fn + http.onerror = err + // NOTE: see + // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/30ef3add-767c-4436-b8a9-f1ca19b4812e + http.onprogress = function() {} + sendWait = true + } else { + http.onreadystatechange = handleReadyState(this, fn, err) + } + o.before && o.before(http) + if (sendWait) { + setTimeout(function () { + http.send(data) + }, 200) + } else { + http.send(data) + } + return http + } + + function Reqwest(o, fn) { + this.o = o + this.fn = fn + + init.apply(this, arguments) + } + + function setType(url) { + var m = url.match(/\.(json|jsonp|html|xml)(\?|$)/) + return m ? m[1] : 'js' + } + + function init(o, fn) { + + this.url = typeof o == 'string' ? o : o.url + this.timeout = null + + // whether request has been fulfilled for purpose + // of tracking the Promises + this._fulfilled = false + // success handlers + this._successHandler = function(){} + this._fulfillmentHandlers = [] + // error handlers + this._errorHandlers = [] + // complete (both success and fail) handlers + this._completeHandlers = [] + this._erred = false + this._responseArgs = {} + + var self = this + , type = o.type || setType(this.url) + + fn = fn || function () {} + + if (o.timeout) { + this.timeout = setTimeout(function () { + self.abort() + }, o.timeout) + } + + if (o.success) { + this._successHandler = function () { + o.success.apply(o, arguments) + } + } + + if (o.error) { + this._errorHandlers.push(function () { + o.error.apply(o, arguments) + }) + } + + if (o.complete) { + this._completeHandlers.push(function () { + o.complete.apply(o, arguments) + }) + } + + function complete (resp) { + o.timeout && clearTimeout(self.timeout) + self.timeout = null + while (self._completeHandlers.length > 0) { + self._completeHandlers.shift()(resp) + } + } + + function success (resp) { + resp = (type !== 'jsonp') ? self.request : resp + // use global data filter on response text + var filteredResponse = globalSetupOptions.dataFilter(resp.responseText, type) + , r = filteredResponse + try { + resp.responseText = r + } catch (e) { + // can't assign this in IE<=8, just ignore + } + if (r) { + switch (type) { + case 'json': + try { + resp = win.JSON ? win.JSON.parse(r) : eval('(' + r + ')') + } catch (err) { + return error(resp, 'Could not parse JSON in response', err) + } + break + case 'js': + resp = eval(r) + break + case 'html': + resp = r + break + case 'xml': + resp = resp.responseXML + && resp.responseXML.parseError // IE trololo + && resp.responseXML.parseError.errorCode + && resp.responseXML.parseError.reason + ? null + : resp.responseXML + break + } + } + + self._responseArgs.resp = resp + self._fulfilled = true + fn(resp) + self._successHandler(resp) + while (self._fulfillmentHandlers.length > 0) { + resp = self._fulfillmentHandlers.shift()(resp) + } + + complete(resp) + } + + function error(resp, msg, t) { + resp = self.request + self._responseArgs.resp = resp + self._responseArgs.msg = msg + self._responseArgs.t = t + self._erred = true + while (self._errorHandlers.length > 0) { + self._errorHandlers.shift()(resp, msg, t) + } + complete(resp) + } + + this.request = getRequest.call(this, success, error) + } + + Reqwest.prototype = { + abort: function () { + this._aborted = true + this.request.abort() + } + + , retry: function () { + init.call(this, this.o, this.fn) + } + + /** + * Small deviation from the Promises A CommonJs specification + * http://wiki.commonjs.org/wiki/Promises/A + */ + + /** + * `then` will execute upon successful requests + */ + , then: function (success, fail) { + success = success || function () {} + fail = fail || function () {} + if (this._fulfilled) { + this._responseArgs.resp = success(this._responseArgs.resp) + } else if (this._erred) { + fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t) + } else { + this._fulfillmentHandlers.push(success) + this._errorHandlers.push(fail) + } + return this + } + + /** + * `always` will execute whether the request succeeds or fails + */ + , always: function (fn) { + if (this._fulfilled || this._erred) { + fn(this._responseArgs.resp) + } else { + this._completeHandlers.push(fn) + } + return this + } + + /** + * `fail` will execute when the request fails + */ + , fail: function (fn) { + if (this._erred) { + fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t) + } else { + this._errorHandlers.push(fn) + } + return this + } + } + + function reqwest(o, fn) { + return new Reqwest(o, fn) + } + + // normalize newline variants according to spec -> CRLF + function normalize(s) { + return s ? s.replace(/\r?\n/g, '\r\n') : '' + } + + function serial(el, cb) { + var n = el.name + , t = el.tagName.toLowerCase() + , optCb = function (o) { + // IE gives value="" even where there is no value attribute + // 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273 + if (o && !o.disabled) + cb(n, normalize(o.attributes.value && o.attributes.value.specified ? o.value : o.text)) + } + , ch, ra, val, i + + // don't serialize elements that are disabled or without a name + if (el.disabled || !n) return + + switch (t) { + case 'input': + if (!/reset|button|image|file/i.test(el.type)) { + ch = /checkbox/i.test(el.type) + ra = /radio/i.test(el.type) + val = el.value + // WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here + ;(!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val)) + } + break + case 'textarea': + cb(n, normalize(el.value)) + break + case 'select': + if (el.type.toLowerCase() === 'select-one') { + optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null) + } else { + for (i = 0; el.length && i < el.length; i++) { + el.options[i].selected && optCb(el.options[i]) + } + } + break + } + } + + // collect up all form elements found from the passed argument elements all + // the way down to child elements; pass a '
' or form fields. + // called with 'this'=callback to use for serial() on each element + function eachFormElement() { + var cb = this + , e, i + , serializeSubtags = function (e, tags) { + var i, j, fa + for (i = 0; i < tags.length; i++) { + fa = e[byTag](tags[i]) + for (j = 0; j < fa.length; j++) serial(fa[j], cb) + } + } + + for (i = 0; i < arguments.length; i++) { + e = arguments[i] + if (/input|select|textarea/i.test(e.tagName)) serial(e, cb) + serializeSubtags(e, [ 'input', 'select', 'textarea' ]) + } + } + + // standard query string style serialization + function serializeQueryString() { + return reqwest.toQueryString(reqwest.serializeArray.apply(null, arguments)) + } + + // { 'name': 'value', ... } style serialization + function serializeHash() { + var hash = {} + eachFormElement.apply(function (name, value) { + if (name in hash) { + hash[name] && !isArray(hash[name]) && (hash[name] = [hash[name]]) + hash[name].push(value) + } else hash[name] = value + }, arguments) + return hash + } + + // [ { name: 'name', value: 'value' }, ... ] style serialization + reqwest.serializeArray = function () { + var arr = [] + eachFormElement.apply(function (name, value) { + arr.push({name: name, value: value}) + }, arguments) + return arr + } + + reqwest.serialize = function () { + if (arguments.length === 0) return '' + var opt, fn + , args = Array.prototype.slice.call(arguments, 0) + + opt = args.pop() + opt && opt.nodeType && args.push(opt) && (opt = null) + opt && (opt = opt.type) + + if (opt == 'map') fn = serializeHash + else if (opt == 'array') fn = reqwest.serializeArray + else fn = serializeQueryString + + return fn.apply(null, args) + } + + reqwest.toQueryString = function (o, trad) { + var prefix, i + , traditional = trad || false + , s = [] + , enc = encodeURIComponent + , add = function (key, value) { + // If value is a function, invoke it and return its value + value = ('function' === typeof value) ? value() : (value == null ? '' : value) + s[s.length] = enc(key) + '=' + enc(value) + } + // If an array was passed in, assume that it is an array of form elements. + if (isArray(o)) { + for (i = 0; o && i < o.length; i++) add(o[i].name, o[i].value) + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for (prefix in o) { + buildParams(prefix, o[prefix], traditional, add) + } + } + + // spaces should be + according to spec + return s.join('&').replace(/%20/g, '+') + } + + function buildParams(prefix, obj, traditional, add) { + var name, i, v + , rbracket = /\[\]$/ + + if (isArray(obj)) { + // Serialize array item. + for (i = 0; obj && i < obj.length; i++) { + v = obj[i] + if (traditional || rbracket.test(prefix)) { + // Treat each array item as a scalar. + add(prefix, v) + } else { + buildParams(prefix + '[' + (typeof v === 'object' ? i : '') + ']', v, traditional, add) + } + } + } else if (obj && obj.toString() === '[object Object]') { + // Serialize object item. + for (name in obj) { + buildParams(prefix + '[' + name + ']', obj[name], traditional, add) + } + + } else { + // Serialize scalar item. + add(prefix, obj) + } + } + + reqwest.getcallbackPrefix = function () { + return callbackPrefix + } + + // jQuery and Zepto compatibility, differences can be remapped here so you can call + // .ajax.compat(options, callback) + reqwest.compat = function (o, fn) { + if (o) { + o.type && (o.method = o.type) && delete o.type + o.dataType && (o.type = o.dataType) + o.jsonpCallback && (o.jsonpCallbackName = o.jsonpCallback) && delete o.jsonpCallback + o.jsonp && (o.jsonpCallback = o.jsonp) + } + return new Reqwest(o, fn) + } + + reqwest.ajaxSetup = function (options) { + options = options || {} + for (var k in options) { + globalSetupOptions[k] = options[k] + } + } + + return reqwest +}); \ No newline at end of file diff --git a/scripts/main.js b/scripts/main.js index a5da5c1..40d3f2c 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -4,11 +4,8 @@ var path = typeof _basepath_ === 'string' ? _basepath_ + '/' : ''; requirejs.config( { baseUrl: path + 'scripts/', - waitSeconds: 5, - urlArgs: 'bust=' + ( new Date() ).getTime(), - shim: { - 'lib/delaunay': { exports: 'triangulate' } - } + waitSeconds: 50, + urlArgs: 'bust=' + ( new Date() ).getTime() } ); @@ -19,10 +16,10 @@ require( 'src/file', 'src/dragdrop', 'src/controls', - 'src/export-png', - 'src/save-button', + 'src/export-button', 'src/import-button', 'src/random-button', + 'src/upload-imgur', 'util/feature-test', 'lib/signals-1.0.0' ], @@ -32,10 +29,10 @@ require( file, dragdrop, controls, - png, - save_button, + export_button, import_button, random_button, + imgur, testFeatures, Signal ) @@ -47,25 +44,24 @@ require( var shared = { feature: supported_features, signals: { - 'load-file' : new Signal(), - 'image-loaded' : new Signal(), - 'set-new-src' : new Signal(), - 'control-set' : new Signal(), - 'control-updated' : new Signal(), - 'export-png' : new Signal(), - 'saved' : new Signal() + 'load-file' : new Signal(), + 'image-loaded' : new Signal(), + 'set-new-src' : new Signal(), + 'control-set' : new Signal(), + 'control-updated' : new Signal(), + 'image-data-url-requested' : new Signal() } }; process.init( shared ); dragdrop.init( shared ); controls.init( shared ); - png.init( shared ); - save_button.init( shared ); + export_button.init( shared ); import_button.init( shared ); random_button.init( shared ); image.init( shared ); file.init( shared ); + imgur.init( shared ); } function showError( required_features ) diff --git a/scripts/src/export-button.js b/scripts/src/export-button.js new file mode 100644 index 0000000..64b3c28 --- /dev/null +++ b/scripts/src/export-button.js @@ -0,0 +1,39 @@ +/*global define*/ +define( + function() + { + var signals; + var export_button; + var png_link; + + function init( shared ) + { + signals = shared.signals; + export_button = document.getElementById( 'export-button' ); + png_link = document.getElementById( 'png-button' ); + + export_button.addEventListener( 'click', exportButtonClicked, false ); + png_link.addEventListener( 'click', hidePNGLink, false ); + } + + function exportButtonClicked( event ) + { + event.preventDefault(); + + signals['image-data-url-requested'].dispatch( upldatePNGLinkAddress ); + } + + function upldatePNGLinkAddress( data_url ) + { + png_link.href = data_url; + png_link.classList.add( 'is-active' ); + } + + function hidePNGLink() + { + png_link.classList.remove( 'is-active' ); + } + + return { init: init }; + } +); \ No newline at end of file diff --git a/scripts/src/export-png.js b/scripts/src/export-png.js deleted file mode 100644 index 2bad560..0000000 --- a/scripts/src/export-png.js +++ /dev/null @@ -1,31 +0,0 @@ -/*global define*/ -define( - function() - { - var signals; - var png_button; - - function init( shared ) - { - signals = shared.signals; - png_button = document.getElementById( 'png-button' ); - - signals['export-png'].add( generatePNG ); - signals['control-updated'].add( hideLink ); - png_button.addEventListener( 'click', hideLink, false ); - } - - function generatePNG( data_url ) - { - png_button.href = data_url; - png_button.classList.add( 'is-active' ); - } - - function hideLink() - { - png_button.classList.remove( 'is-active' ); - } - - return { init: init }; - } -); \ No newline at end of file diff --git a/scripts/src/process.js b/scripts/src/process.js index 6f0d8a8..55523cc 100644 --- a/scripts/src/process.js +++ b/scripts/src/process.js @@ -22,7 +22,7 @@ define( signals['image-loaded'].add( generate ); signals['control-updated'].add( controlsUpdated ); - signals['saved'].add( exportData ); + signals['image-data-url-requested'].add( exportData ); } function controlsUpdated( new_values ) @@ -96,9 +96,12 @@ define( glitched_image_data = null; } - function exportData() + function exportData( callback ) { - signals['export-png'].dispatch( canvas.toDataURL( 'image/png' ) ); + if ( typeof callback === 'function' ) + { + callback( canvas.toDataURL( 'image/png' ) ); + } } function getAdjustedValues( new_values ) diff --git a/scripts/src/save-button.js b/scripts/src/save-button.js deleted file mode 100644 index 089cf5a..0000000 --- a/scripts/src/save-button.js +++ /dev/null @@ -1,25 +0,0 @@ -/*global define*/ -define( - function() - { - var signals; - var save_button; - - function init( shared ) - { - signals = shared.signals; - save_button = document.getElementById( 'save-button' ); - - save_button.addEventListener( 'click', buttonClicked, false ); - } - - function buttonClicked( event ) - { - event.preventDefault(); - - signals['saved'].dispatch(); - } - - return { init: init }; - } -); \ No newline at end of file diff --git a/scripts/src/upload-imgur.js b/scripts/src/upload-imgur.js new file mode 100644 index 0000000..60e8b7c --- /dev/null +++ b/scripts/src/upload-imgur.js @@ -0,0 +1,100 @@ +/*global define*/ +define( + [ 'lib/reqwest' ], + function( reqwest, $ ) + { + var signals; + var imgur_button; + var imgur_url_container; + var imgur_url_input; + var imgur_url_link; + var imgur_url_error; + var is_uploading = false; + + function init( shared ) + { + signals = shared.signals; + imgur_button = document.getElementById( 'imgur-button' ); + imgur_url_container = document.getElementById( 'imgur-url-container' ); + imgur_url_input = document.getElementById( 'imgur-url-input' ); + imgur_url_link = document.getElementById( 'imgur-url-link' ); + imgur_url_error = document.getElementById( 'imgur-url-error' ); + + imgur_button.addEventListener( 'click', buttonClicked, false ); + imgur_url_input.addEventListener( 'click', selectInput, false ); + } + + function buttonClicked( event ) + { + event.preventDefault(); + + if ( ! is_uploading ) + { + signals['image-data-url-requested'].dispatch( upload ); + + imgur_url_container.classList.remove( 'is-active', 'upload-failed', 'upload-successful' ); + } + } + + function selectInput() + { + imgur_url_input.select(); + } + + //http://stackoverflow.com/questions/17805456/upload-a-canvas-image-to-imgur-api-v3-with-javascript + function upload( data_url ) + { + if ( ! is_uploading ) + { + imgur_button.classList.add( 'is-uploading' ); + + is_uploading = true; + + reqwest( + { + url: 'https://api.imgur.com/3/image.json', + method: 'POST', + headers: { + Authorization: 'Client-ID a4c24380d884932' + }, + data: { + image: data_url.split( ',' )[1], + type: 'base64' + }, + type: 'json', + crossOrigin: true, + success: imageUploaded, + error: uploadFailed + } + ); + } + } + + function imageUploaded( response ) + { + is_uploading = false; + + if ( response && response.data && response.data.link ) + { + imgur_button.classList.remove( 'is-uploading' ); + imgur_url_input.setAttribute( 'value', response.data.link ); + imgur_url_link.href = response.data.link; + imgur_url_container.classList.add( 'is-active', 'upload-successful' ); + } + + else + { + uploadFailed(); + } + } + + function uploadFailed( response ) + { + is_uploading = false; + imgur_button.classList.remove( 'is-uploading' ); + imgur_url_container.classList.add( 'is-active', 'upload-failed' ); + } + + return { init: init }; + } +); \ No newline at end of file diff --git a/styles/main.css b/styles/main.css index e2d4a2d..f6b9f99 100644 --- a/styles/main.css +++ b/styles/main.css @@ -42,12 +42,14 @@ a:hover font-family: sans-serif; font-size: 12px; text-decoration: none; + line-height: normal; } .button:hover { background-color: #06f; color: #fff; + text-decoration: none; } #random-button @@ -148,6 +150,91 @@ a:hover margin-left: 4px; } +#imgur-button +{ + width: 85px; +} + +#imgur-button.is-uploading, +#imgur-button.is-uploading:hover +{ + background-color: #eaeaea; + color: #06f; + cursor: default; +} + + #imgur-button span + { + display: block; + width: 154px; + transition: margin-left 0.5s ease-in; + transition-property: margin-left top; + text-align: left; + overflow: hidden; + } + + + #imgur-button.is-uploading span + { + margin-left: -76px; + } + + #imgur-button span::after + { + content: 'loading…'; + display: inline-block; + padding-left: 15px; + color: #999; + } + +#imgur-url-container +{ + display: inline-block; + opacity: 0; + transition: opacity 0.3s ease-in; +} + +#imgur-url-container.is-active +{ + opacity: 1; +} + + #imgur-url-input + { + padding: 4px 9px; + border: 1px #CCC solid; + border-radius: 3px; + font-family: sans-serif; + font-size: 13px; + color: #7C7C7C; + } + + #imgur-url-error + { + padding: 5px 9px; + line-height: normal; + color: #999; + } + + #imgur-url-input, + #imgur-url-link, + #imgur-url-error + { + display: none; + } + + #imgur-url-container.upload-successful #imgur-url-input, + #imgur-url-container.upload-successful #imgur-url-link + { + display: inline-block; + } + + #imgur-url-container.upload-failed #imgur-url-error + { + display: inline-block; + } + + .missing-feature { clear: both;