UNPKG

coffeescript-ui

Version:
921 lines (757 loc) 22.2 kB
### * coffeescript-ui - Coffeescript User Interface System (CUI) * Copyright (c) 2013 - 2016 Programmfabrik GmbH * MIT Licence * https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org ### # Base class for the Coffeescript UI system. It is used for # Theme-Management as well as for basic event management tasks. # # @example Startup # marked = require('marked') class CUI @__readyFuncs = [] @__themes = [] @__ng__: true @start: -> trigger_viewport_resize = => # console.info("CUI: trigger viewport resize.") CUI.Events.trigger type: "viewport-resize" CUI.Events.listen type: "resize" node: window call: (ev, info) => # console.info("CUI: caught window resize event.") if !CUI.browser.ie trigger_viewport_resize() else CUI.scheduleCallback(ms: 500, call: trigger_viewport_resize) return CUI.Events.listen type: "drop" node: document.documentElement call: (ev) -> ev.preventDefault() CUI.Events.listen type: "keyup" node: window capture: true call: (ev) -> if ev.getKeyboard() == "C+U+I" CUI.toaster(text: "CUI!") CUI.Events.listen type: "keydown" node: window call: (ev) -> if ev.getKeyboard() == "c+" CUI.toaster(text: "CUI!") # backspace acts as "BACK" in some browser, like FF if ev.keyCode() == 8 for node in CUI.dom.elementsUntil(ev.getTarget(), null, document.documentElement) if node.tagName in ["INPUT", "TEXTAREA"] return if node.getAttribute("contenteditable") == "true" return # console.info("swalloded BACKSPACE keydown event to prevent default") ev.preventDefault() return document.body.scrollTop=0 icons = require('../scss/icons/icons.svg') CUI.Template.loadText(icons) CUI.Template.load() @chainedCall.apply(@, @__readyFuncs).always => @__ready = true @ @getPathToScript: -> if not @__pathToScript scripts = document.getElementsByTagName('script') cui_script = scripts[scripts.length - 1] if m = cui_script.src.match("(.*/).*?\.js$") @__pathToScript = m[1] else CUI.util.assert(@__pathToScript, "CUI", "Could not determine script path.") @__pathToScript @ready: (func) -> if @__ready return func.call(@) @__readyFuncs.push(func) @defaults: FileUpload: name: "files[]" debug: true asserts: true asserts_alert: 'js' # or 'cui' or 'off' or 'debugger' class: {} # Returns a resolved CUI.Promise. @resolvedPromise: -> dfr = new CUI.Deferred() dfr.resolve.apply(dfr, arguments) dfr.promise() # Returns a rejected CUI.Promise. @rejectedPromise: -> dfr = new CUI.Deferred() dfr.reject.apply(dfr, arguments) dfr.promise() # calls the as arguments passed functions in order # of appearance. if a function returns # a deferred or promise, the next function waits # for that function to complete the promise # if the argument is a value or a promise # it is used the same way # returns a promise which resolve when all # functions resolve or the first doesnt @chainedCall: -> idx = 0 __this = @ # return a real array from arguments get_args = (_arguments) -> _args = [] for arg in _arguments _args.push(arg) _args # mimic the behaviour of jQuery "when" get_return_value = (_arguments) -> _args = get_args(_arguments) if _args.length == 0 return undefined else if _args.length == 1 return _args[0] else return _args args = get_args(arguments) return_values = [] init_next = => if idx == args.length dfr.resolve.apply(dfr, return_values) return if CUI.util.isFunction(args[idx]) if __this != CUI ret = args[idx].call(__this) else ret = args[idx]() else ret = args[idx] # console.debug "idx", idx, "ret", ret, "state:", ret?.state?() idx++ if CUI.util.isPromise(ret) ret .done => return_values.push(get_return_value(arguments)) init_next() .fail => return_values.push(get_return_value(arguments)) dfr.reject.apply(dfr, return_values) else return_values.push(ret) init_next() return dfr = new CUI.Deferred() init_next() dfr.promise() # Executes 'call' function in batches of 'chunk_size' for all the 'items'. # It must be called with '.call(this, opts)' @chunkWork: (_opts = {}) -> opts = CUI.Element.readOpts _opts, "CUI.chunkWork", items: mandatory: true check: (v) -> CUI.util.isArray(v) chunk_size: mandatory: true default: 10 check: (v) -> v >= 1 timeout: mandatory: true default: 0 check: (v) -> v >= -1 call: mandatory: true check: (v) -> v instanceof Function chunk_size = opts.chunk_size timeout = opts.timeout CUI.util.assert(@ != CUI, "CUI.chunkWork", "Cannot call CUI.chunkWork with 'this' not set to the caller.") idx = 0 len = opts.items.length next_chunk = => progress = (idx+1) + " - " + Math.min(len, idx+chunk_size) + " / " +len # console.error "progress:", progress dfr.notify progress: progress idx: idx len: len chunk_size: chunk_size go_on = => if idx + chunk_size >= len dfr.resolve() else idx = idx + chunk_size if timeout == -1 next_chunk() else CUI.setTimeout ms: timeout call: next_chunk return ret = opts.call.call(@, opts.items.slice(idx, idx+opts.chunk_size), idx, len) if ret == false # interrupt this dfr.reject() return if CUI.util.isPromise(ret) ret.fail(dfr.reject).done(go_on) else go_on() return dfr = new CUI.Deferred() CUI.setTimeout ms: Math.min(0, timeout) call: => if len > 0 next_chunk() else dfr.resolve() return dfr.promise() # returns a Deferred, the Deferred # notifies the worker for each # object @chunkWorkOLD: (objects, chunkSize = 10, timeout = 0) -> dfr = new CUI.Deferred() idx = 0 do_next_chunk = => chunk = 0 while idx < objects.length and (chunk < chunkSize or chunkSize == 0) if dfr.state() == "rejected" return # console.debug idx, chunk, chunkSize, dfr.state() dfr.notify(objects[idx], idx) if idx == objects.length-1 dfr.resolve() return idx++ chunk++ if idx < objects.length CUI.setTimeout(do_next_chunk, timeout) if objects.length == 0 CUI.setTimeout => if dfr.state() == "rejected" return dfr.resolve() else CUI.setTimeout(do_next_chunk) dfr # proxy methods @proxyMethods: (target, source, methods) -> # console.debug target, source, methods for k in methods target.prototype[k] = source.prototype[k] @__timeouts = [] # list of function which we need to call if # the timeouts counter changes @__timeoutCallbacks = [] @__callTimeoutChangeCallbacks: -> tracked = @countTimeouts() for cb in @__timeoutCallbacks cb(tracked) return @__removeTimeout: (timeout) -> if CUI.util.removeFromArray(timeout, @__timeouts) if timeout.track @__callTimeoutChangeCallbacks() return @__getTimeoutById: (timeoutID, ignoreNotFound = false) -> for timeout in @__timeouts if timeout.id == timeoutID return timeout CUI.util.assert(ignoreNotFound, "CUI.__getTimeoutById", "Timeout ##{timeoutID} not found.") null @resetTimeout: (timeoutID) -> timeout = @__getTimeoutById(timeoutID) CUI.util.assert(not timeout.__isRunning, "CUI.resetTimeout", "Timeout #{timeoutID} cannot be resetted while running.", timeout: timeout) timeout.onReset?(timeout) window.clearTimeout(timeout.real_id) old_real_id = timeout.real_id tid = @__startTimeout(timeout) return tid @registerTimeoutChangeCallback: (cb) -> @__timeoutCallbacks.push(cb) @setTimeout: (_func, ms=0, track) -> if CUI.util.isPlainObject(_func) ms = _func.ms or 0 track = _func.track func = _func.call onDone = _func.onDone onReset = _func.onReset else func = _func if CUI.util.isNull(track) if ms == 0 track = false else track = true CUI.util.assert(CUI.util.isFunction(func), "CUI.setTimeout", "Function needs to be a Function (opts.call)", parameter: _func) timeout = call: => timeout.__isRunning = true func() @__removeTimeout(timeout) timeout.onDone?(timeout) ms: ms func: func # for debug purposes track: track onDone: onDone onReset: onReset @__timeouts.push(timeout) if track and ms > 0 @__callTimeoutChangeCallbacks() @__startTimeout(timeout) @__scheduledCallbacks = [] # schedules to run a function after # a timeout has occurred. does not schedule # the same function a second time # returns a deferred, which resolves when # the callback is done @scheduleCallback: (_opts) -> opts = CUI.Element.readOpts _opts, "CUI.scheduleCallback", call: mandatory: true check: Function ms: default: 0 check: (v) -> CUI.util.isInteger(v) and v >= 0 track: default: false check: Boolean idx = CUI.util.idxInArray(opts.call, @__scheduledCallbacks, (v) -> v.call == opts.call) if idx > -1 and CUI.isTimeoutRunning(@__scheduledCallbacks[idx].timeoutID) # don't schedule the same call while it is already running, schedule # a new one idx = -1 if idx == -1 idx = @__scheduledCallbacks.length # console.debug "...schedule", idx else # function already scheduled # console.info("scheduleCallback, already scheduled: ", @__scheduledCallbacks[idx].timeout, CUI.isTimeoutRunning(@__scheduledCallbacks[idx].timeout)) CUI.resetTimeout(@__scheduledCallbacks[idx].timeoutID) return @__scheduledCallbacks[idx].promise dfr = new CUI.Deferred() timeoutID = CUI.setTimeout ms: opts.ms track: opts.track call: => opts.call() dfr.resolve() cb = @__scheduledCallbacks[idx] = call: opts.call timeoutID: timeoutID promise: dfr.promise() # console.error "scheduleCallback", cb.timeoutID, opts.call dfr.done => # remove this callback after we are done CUI.util.removeFromArray(opts.call, @__scheduledCallbacks, (v) -> v.call == opts.call) cb.promise # call: function callback to cancel # return: true if found, false if not @scheduleCallbackCancel: (_opts) -> opts = CUI.Element.readOpts _opts, "CUI.scheduleCallbackCancel", call: mandatory: true check: Function idx = CUI.util.idxInArray(opts.call, @__scheduledCallbacks, (v) -> v.call == opts.call) if idx > -1 and not CUI.isTimeoutRunning(@__scheduledCallbacks[idx].timeoutID) # console.error "cancel timeout...", @__scheduledCallbacks[idx].timeoutID CUI.clearTimeout(@__scheduledCallbacks[idx].timeoutID) @__scheduledCallbacks.splice(idx, 1) return true else return false @utf8ArrayBufferToString: (arrayBuffer) -> out = [] array = new Uint8Array(arrayBuffer) len = array.length i = 0 while (i < len) c = array[i++] switch(c >> 4) when 0, 1, 2, 3, 4, 5, 6, 7 # 0xxxxxxx out.push(String.fromCharCode(c)) when 12, 13 # 110x xxxx 10xx xxxx char2 = array[i++] out.push(String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F))) when 14 # 1110 xxxx 10xx xxxx 10xx xxxx char2 = array[i++] char3 = array[i++] out.push(String.fromCharCode(((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0))) out.join("") @__startTimeout: (timeout) -> real_id = window.setTimeout(timeout.call, timeout.ms) if not timeout.id # first time we put the real id timeout.id = real_id timeout.real_id = real_id # console.error "new timeout:", timeoutID, "ms:", ms, "current timeouts:", @__timeouts.length timeout.id @countTimeouts: -> tracked = 0 for timeout in @__timeouts if timeout.track tracked++ tracked @clearTimeout: (timeoutID) -> timeout = @__getTimeoutById(timeoutID, true) # ignore not found if not timeout return window.clearTimeout(timeout.real_id) @__removeTimeout(timeout) timeout.id @isTimeoutRunning: (timeoutID) -> timeout = @__getTimeoutById(timeoutID, true) # ignore not found if not timeout?.__isRunning false else true @setInterval: (func, ms) -> window.setInterval(func, ms) @clearInterval: (interval) -> window.clearInterval(interval) # used to set a css-class while testing with webdriver. this way # we can hide things on the screen that should not irritate our screenshot comparison @startWebdriverTest: -> a= "body" CUI.dom.addClass(a, "cui-webdriver-test") @getParameterByName: (name, search=document.location.search) -> name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]") regex = new RegExp("[\\?&]" + name + "=([^&#]*)") results = regex.exec(search) if results == null "" else decodeURIComponent(results[1].replace(/\+/g, " ")) @setSessionStorage: (key, value) -> @__setStorage("sessionStorage", key, value) @getSessionStorage: (key = null) -> @__getStorage("sessionStorage", key) @clearSessionStorage: -> @__clearStorage("sessionStorage") @setLocalStorage: (key, value) -> @__setStorage("localStorage", key, value) @getLocalStorage: (key = null) -> @__getStorage("localStorage", key) @clearLocalStorage: -> @__clearStorage("localStorage") @__storage: localStorage: null, sessionStorage: null @__setStorage: (skey, key, value) -> data = @__getStorage(skey) if value == undefined delete(data[key]) else data[key] = value try window[skey].setItem("CUI", JSON.stringify(data)) catch e console.warn("CUI.__setStorage: Storage not available.", e) @__storage[skey] = JSON.stringify(data) data @__getStorage: (skey, key = null) -> try data_json = window[skey].getItem("CUI") catch e console.warn("CUI.__getStorage: Storage not available.", e) data_json = @__storage[skey] if data_json data = JSON.parse(data_json) else data = {} if key != null data[key] else data @__clearStorage: (skey) -> try window[skey].removeItem("CUI") catch e console.warn("CUI.__clearStorage: Storage not available.", e) @__storage[skey] = null @encodeUrlData: (params, replacer = null, connect = "&", connect_pair = "=") -> url = [] if replacer if CUI.util.isFunction(replacer) encode_func = replacer else encode_func = (v) -> CUI.util.stringMapReplace(v+"", replace_map) else encode_func = (v) -> encodeURIComponent(v) for k, v of params if CUI.util.isArray(v) for _v in v url.push(encode_func(k) + connect_pair + encode_func(_v)) else if not CUI.util.isEmpty(v) url.push(encode_func(k) + connect_pair + encode_func(v)) else if v != undefined url.push(encode_func(k)) url.join(connect) # keep "," and ":" in url intact, encodeURI all other parts @encodeURIComponentNicely: (str="") -> s = [] for v, idx in (str+"").split(",") if idx > 0 s.push(",") for v2, idx2 in v.split(":") if idx2 > 0 s.push(":") s.push(encodeURIComponent(v2)) s.join("") @decodeURIComponentNicely: (v) -> decodeURIComponent(v) @decodeUrlData: (url, replacer = null, connect = "&", connect_pair = "=", use_array=false) -> params = {} if replacer if CUI.util.isFunction(replacer) decode_func = replacer else decode_func = (v) -> CUI.util.stringMapReplace(v+"", replacer) else decode_func = (v) -> decodeURIComponent(v) for part in url.split(connect) if part.length == 0 continue if part.indexOf(connect_pair) > -1 pair = part.split(connect_pair) key = decode_func(pair[0]) value = decode_func(pair[1]) else key = decode_func(part) value = "" if use_array if not params[key] params[key] = [] params[key].push(value) else params[key] = value params @decodeUrlDataArray: (url, replace_map = null, connect = "&", connect_pair = "=") -> @decodeUrlData(url, replace_map, connect, connect_pair, true) # Deprecated -> Use CUI.util @mergeMap: (targetMap, mergeMap) -> for k, v of mergeMap if not targetMap.hasOwnProperty(k) targetMap[k] = v else if CUI.util.isPlainObject(targetMap[k]) and CUI.util.isPlainObject(v) CUI.util.mergeMap(targetMap[k], v) targetMap # Deprecated -> Use CUI.util @revertMap: (map) -> map_reverted = {} for k, v of map map_reverted[v] = k map_reverted # Deprecated -> Use CUI.util @stringMapReplace: (s, map) -> regex = [] for key of map if CUI.util.isEmpty(key) continue regex.push(key.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")) if regex.length > 0 s.replace(new RegExp(regex.join('|'),"g"), (word) -> map[word]) else s # Deprecated -> Use CUI.util @isFunction: (v) -> v and typeof(v) == "function" # Deprecated -> Use CUI.util @isPlainObject: (v) -> v and typeof(v) == "object" and v.constructor?.prototype.hasOwnProperty("isPrototypeOf") # Deprecated -> Use CUI.util @isEmptyObject: (v) -> for k of v return false return true # Deprecated -> Use CUI.util @isMap: (v) -> @isPlainObject(v) # Deprecated -> Use CUI.util @isArray: (v) -> Array.isArray(v) # Deprecated -> Use CUI.util @inArray: (value, array) -> array.indexOf(value) # Deprecated -> Use CUI.util @isString: (s) -> typeof(s) == "string" @downloadData: (data, fileName) -> blob = new Blob([data], type: "octet/stream") if window.navigator.msSaveOrOpenBlob window.navigator.msSaveOrOpenBlob(blob, fileName) else url = window.URL.createObjectURL(blob) @__downloadDataElement.href = url @__downloadDataElement.download = fileName @__downloadDataElement.click() window.URL.revokeObjectURL(url) # https://gist.github.com/dperini/729294 @urlRegex: new RegExp( "^" + # protocol identifier "(?:(?:(sftp|ftp|ftps|https|http))://|)" + # user:pass authentication "(?:(\\S+?)(?::(\\S*))?@)?" + "((?:(?:" + # IP address dotted notation octets # excludes loopback network 0.0.0.0 # excludes reserved space >= 224.0.0.0 # excludes network & broacast addresses # (first & last IP address of each class) "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + "|" + # host & domain name "(?:[a-z0-9\\u00a1-\\uffff](?:|[a-z\\u00a1-\\uffff0-9-]*[a-z0-9\\u00a1-\\uffff])\\.)*" + # last identifier "(?:[a-z\\u00a1-\\uffff]{2,})" + # hostname only "|" + "(?:[a-z0-9\\u00a1-\\uffff][a-z0-9-\\u00a1-\\uffff]*[a-z0-9\\u00a1-\\uffff])" + "))|)" + # port number "(?::(\\d{2,5}))?" + # resource path "(?:([/?#]\\S*))?" + "$", "i" ) @evalCode: (code) -> script = document.createElement("script") script.text = code document.head.appendChild(script).parentNode.removeChild(script) @appendToUrl: (url, data) -> for key, value of data if value == undefined continue # add token to the url if url.match(/\?/) url += "&" else url += "?" url += encodeURIComponent(key)+"="+encodeURIComponent(value) url @parseLocation: (url) -> if not CUI.util.isFunction(url?.match) or url.length == 0 return null match = url.match(@urlRegex) if not match return null # console.debug "CUI.parseLocation:", url, match p = protocol: match[1] or "" user: match[2] or "" password: match[3] or "" hostname: match[4] or "" port: match[5] or "" path: match[6] or "" origin: "" if p.hostname if not p.protocol p.protocol = "http" p.origin = p.protocol+"://"+p.hostname if p.port p.origin += ":"+p.port p.url = p.protocol+"://" if p.user p.url = p.url + p.user + ":" + p.password + "@" p.url = p.url + p.hostname if p.port p.url = p.url + ":" + p.port else p.url = "" if p.path.length > 0 _match = p.path.match(/(.*?)(|\?.*?)(|\#.*)$/) p.pathname = _match[1] p.search = _match[2] if p.search == "?" p.search = "" p.fragment = _match[3] else p.search = "" p.pathname = "" p.fragment = "" p.href = p.origin+p.path p.hash = p.fragment if p.login p.auth = btoa(p.user+":"+p.password) # url includes user+password p.url = p.url + p.path p @escapeAttribute: (data) -> if CUI.util.isNull(data) or !CUI.util.isString(data) return "" data = data.replace(/"/g, "&quot;").replace(/\'/g, "&#39;") data @loadScript: (src) -> deferred = new CUI.Deferred script = CUI.dom.element("script", charset: "utf-8", src: src) CUI.Events.listen type: "load" node: script instance: script call: (ev) => deferred.resolve(ev) return CUI.Events.listen type: "error" node: script instance: script call: (ev) => document.head.removeChild(script) deferred.reject(ev) return deferred.always => CUI.Events.ignore(instance: script) document.head.appendChild(script) return deferred.promise() # http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser @browser: (-> map = opera: `(!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0` firefox: `typeof InstallTrigger !== 'undefined'` safari: `Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0` ie: `/*@cc_on!@*/false || !!document.documentMode` chrome: !!window.chrome and !!window.chrome.webstore map.edge = not map.ie && !!window.StyleMedia map.blink = (map.chrome or map.opera) && !!window.CSS map )() CUI.ready => for k of CUI.browser if CUI.browser[k] document.body.classList.add("cui-browser-"+k) CUI.defaults.marked_opts = renderer: new marked.Renderer() gfm: true tables: true breaks: false pedantic: false smartLists: true smartypants: false # initialize a markdown renderer marked.setOptions(CUI.defaults.marked_opts) nodes = CUI.dom.htmlToNodes("<!-- CUI.CUI --><a style='display: none;'></a><!-- /CUI.CUI -->") CUI.__downloadDataElement = nodes[1] CUI.dom.append(document.body, nodes) if not window.addEventListener alert("Your browser is not supported. Please update to a current version of Google Chrome, Mozilla Firefox or Internet Explorer.") else window.addEventListener("load", => CUI.start() ) module.exports = CUI