├── img └── logo.png ├── js ├── lib │ ├── Property.coffee │ ├── Promise.coffee │ ├── anime.min.js │ └── maquette.js ├── utils │ ├── Dollar.coffee │ ├── Prototypes.coffee │ ├── RateLimit.coffee │ ├── Class.coffee │ ├── ItemList.coffee │ ├── Time.coffee │ ├── Menu.coffee │ ├── ZeroFrame.coffee │ ├── Text.coffee │ ├── Form.coffee │ └── Animation.coffee ├── Head.coffee ├── User.coffee ├── SiteList.coffee ├── Site.coffee ├── SiteAdd.coffee ├── ZeroSites.coffee └── SiteLists.coffee ├── index.html ├── languages ├── zh.json ├── zh-tw.json ├── fa.json └── es.json ├── data └── users │ ├── content.json │ └── stats.json ├── css ├── Animation.css ├── Head.css ├── icons.css ├── Menu.css ├── Button.css ├── Form.css ├── ZeroSites.css └── fonts.css ├── dbschema.json ├── content.json └── LICENSE /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/ZeroSites/master/img/logo.png -------------------------------------------------------------------------------- /js/lib/Property.coffee: -------------------------------------------------------------------------------- 1 | Function::property = (prop, desc) -> 2 | Object.defineProperty @prototype, prop, desc 3 | -------------------------------------------------------------------------------- /js/utils/Dollar.coffee: -------------------------------------------------------------------------------- 1 | window.$ = (selector) -> 2 | if selector.startsWith("#") 3 | return document.getElementById(selector.replace("#", "")) 4 | -------------------------------------------------------------------------------- /js/utils/Prototypes.coffee: -------------------------------------------------------------------------------- 1 | String::startsWith = (s) -> @[...s.length] is s 2 | String::endsWith = (s) -> s is '' or @[-s.length..] is s 3 | String::repeat = (count) -> new Array( count + 1 ).join(@) 4 | 5 | window.isEmpty = (obj) -> 6 | for key of obj 7 | return false 8 | return true 9 | -------------------------------------------------------------------------------- /js/utils/RateLimit.coffee: -------------------------------------------------------------------------------- 1 | limits = {} 2 | call_after_interval = {} 3 | window.RateLimit = (interval, fn) -> 4 | if not limits[fn] 5 | call_after_interval[fn] = false 6 | fn() # First call is not delayed 7 | limits[fn] = setTimeout (-> 8 | if call_after_interval[fn] 9 | fn() 10 | delete limits[fn] 11 | delete call_after_interval[fn] 12 | ), interval 13 | else # Called within iterval, delay the call 14 | call_after_interval[fn] = true 15 | -------------------------------------------------------------------------------- /js/utils/Class.coffee: -------------------------------------------------------------------------------- 1 | class Class 2 | trace: true 3 | 4 | log: (args...) -> 5 | return unless @trace 6 | return if typeof console is 'undefined' 7 | args.unshift("[#{@.constructor.name}]") 8 | console.log(args...) 9 | @ 10 | 11 | logStart: (name, args...) -> 12 | return unless @trace 13 | @logtimers or= {} 14 | @logtimers[name] = +(new Date) 15 | @log "#{name}", args..., "(started)" if args.length > 0 16 | @ 17 | 18 | logEnd: (name, args...) -> 19 | ms = +(new Date)-@logtimers[name] 20 | @log "#{name}", args..., "(Done in #{ms}ms)" 21 | @ 22 | 23 | window.Class = Class -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ZeroSites 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /languages/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Invalid site address: only ZeroNet addresses supported": "无效的站点地址:仅支持 ZeroNet 站点地址", 3 | "This field is required": "该栏为必填项", 4 | 5 | "Popular": "热门", 6 | "New": "新增", 7 | 8 | "Address": "地址", 9 | "e.g. http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8": "例如 http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", 10 | "Title": "标题", 11 | "e.g. ZeroBlog": "例如 ZeroBlog", 12 | "Language": "语言", 13 | "Category": "类别", 14 | "Description": "描述", 15 | "e.g. ZeroNet changelog and related informations": "例如 ZeroNet 更新日志和相关信息", 16 | "Submit": "提交", 17 | 18 | "Site languages: ": "站点语言", 19 | "User: ": "用户:", 20 | "Filter: ": "筛选器:", 21 | "Submit new site": "提交新站点" 22 | } 23 | -------------------------------------------------------------------------------- /languages/zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "Invalid site address: only ZeroNet addresses supported": "無效的網站位址:僅支持 ZeroNet 網站位址", 3 | "This field is required": "該欄為必填項", 4 | 5 | "Popular": "熱門", 6 | "New": "新增", 7 | 8 | "Address": "位址", 9 | "e.g. http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8": "例如 http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", 10 | "Title": "標題", 11 | "e.g. ZeroBlog": "例如 ZeroBlog", 12 | "Language": "語言", 13 | "Category": "類別", 14 | "Description": "描述", 15 | "e.g. ZeroNet changelog and related informations": "例如 ZeroNet 更新記錄檔和相關資訊", 16 | "Submit": "提交", 17 | 18 | "Site languages: ": "網站語言", 19 | "User: ": "使用者:", 20 | "Filter: ": "篩選器:", 21 | "Submit new site": "提交新網站" 22 | } 23 | -------------------------------------------------------------------------------- /js/utils/ItemList.coffee: -------------------------------------------------------------------------------- 1 | class ItemList 2 | constructor: (@item_class, @key) -> 3 | @items = [] 4 | @items_bykey = {} 5 | 6 | sync: (rows, item_class, key) -> 7 | @items.splice(0, @items.length) # Empty items 8 | for row in rows 9 | current_obj = @items_bykey[row[@key]] 10 | if current_obj 11 | current_obj.row = row 12 | @items.push current_obj 13 | else 14 | item = new @item_class(row, @) 15 | @items_bykey[row[@key]] = item 16 | @items.push item 17 | 18 | deleteItem: (item) -> 19 | index = @items.indexOf(item) 20 | if index > -1 21 | @items.splice(index, 1) 22 | else 23 | console.log "Can't delete item", item 24 | delete @items_bykey[item.row[@key]] 25 | 26 | window.ItemList = ItemList -------------------------------------------------------------------------------- /js/Head.coffee: -------------------------------------------------------------------------------- 1 | class Head 2 | constructor: -> 3 | @active = "popular" 4 | 5 | handleMenuClick: (e) => 6 | @active = e.currentTarget.attributes.name.value 7 | Page.site_lists.update() 8 | return false 9 | 10 | render: => 11 | h("div#Head", [ 12 | h("a.logo", {href: "?Home", onclick: Page.handleLinkClick}, [ 13 | h("img", {"src": "img/logo.png", "width": 58, "height": 64}), 14 | h("h1", "ZeroSites"), 15 | # h("h2", "Sites created by ZeroNet community") 16 | ]), 17 | h("div.order", [ 18 | h("a.order-item.popular", {href: "#", name: "popular", classes: {active: @active == "popular"}, onclick: @handleMenuClick}, "Popular"), 19 | h("a.order-item.new", {href: "#", name: "new", classes: {active: @active == "new"}, onclick: @handleMenuClick}, "New") 20 | ]) 21 | ]) 22 | 23 | window.Head = Head -------------------------------------------------------------------------------- /languages/fa.json: -------------------------------------------------------------------------------- 1 | 2 | "Invalid site address: only ZeroNet addresses supported": "آدرس سایت نا‌معتبر است: فقط آدرس های زیرونت پشتیبانی میشوند", 3 | "This field is required": "این فیلد الزامی است", 4 | 5 | "Popular": "محبوب", 6 | "New": "جدید", 7 | 8 | "Address": "آدرس", 9 | "e.g. http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8": "برای نمونه: http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", 10 | "Title": "عنوان", 11 | "e.g. ZeroBlog": "برای نمونه: ZeroBlog", 12 | "Language": "زبان", 13 | "Category": "دسته‌بندی", 14 | "Description": "توضیحات", 15 | "e.g. ZeroNet changelog and related informations": "برای نمونه: لاگ تغییرات زیرونت و اطلاعات مرتبط", 16 | "Submit": "تایید", 17 | 18 | "Site languages: ": "زبان سایت", 19 | "User: ": "کاربر: ", 20 | "Filter: ": "فیلتر: ", 21 | "Submit new site": "تایید سایت جدید" 22 | } 23 | -------------------------------------------------------------------------------- /languages/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Invalid site address: only ZeroNet addresses supported": "Dirección de pagina web invalida: solo se soportan direcciones de ZeroNet", 3 | "This field is required": "Este campo es requerido", 4 | 5 | "Popular": "Popular", 6 | "New": "Nuevo", 7 | 8 | "Address": "Dirección", 9 | "e.g. http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8": "e.j. http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", 10 | "Title": "Título", 11 | "e.g. ZeroBlog": "e.j. ZeroBlog", 12 | "Language": "Lenguaje", 13 | "Category": "Categoria", 14 | "Description": "Descripción", 15 | "e.g. ZeroNet changelog and related informations": "e.j. Zeronet notas de cambios e informaciones relacionadas", 16 | "Submit": "Enviar", 17 | 18 | "Site languages: ": "Lenguaje de sitio", 19 | "User: ": "Usuario: ", 20 | "Filter: ": "Filtro: ", 21 | "Submit new site": "Enviar nuevo sitio" 22 | } 23 | -------------------------------------------------------------------------------- /data/users/content.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "1SiTEs2D3rCBxeMoLHXei2UYqFcxctdwB", 3 | "files": { 4 | "stats.json": { 5 | "sha512": "bf4d81a123991214d60a0555c88a4940d4c9caa7831ef3f29918f12fc7253a28", 6 | "size": 1042 7 | } 8 | }, 9 | "ignore": "(.+/.*|.*db)", 10 | "inner_path": "data/users/content.json", 11 | "modified": 1496440453, 12 | "signs": { 13 | "1SiTEs2D3rCBxeMoLHXei2UYqFcxctdwB": "Gx1DBETMATqpZTyDmZEU6kMYxJtYVHa5+u5SF0OelyaETAK7Tv12qK0rXBef+B8zVV1hNuC0fi6/D+iCcQ2B6XU=" 14 | }, 15 | "user_contents": { 16 | "cert_signers": { 17 | "zeroid.bit": [ 18 | "1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz" 19 | ] 20 | }, 21 | "permission_rules": { 22 | ".*": { 23 | "files_allowed": "data.json", 24 | "max_size": 10000, 25 | "signers": ["1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj"] 26 | }, 27 | "bitid/.*@zeroid.bit": {"max_size": 20000} 28 | }, 29 | "permissions": {} 30 | } 31 | } -------------------------------------------------------------------------------- /css/Animation.css: -------------------------------------------------------------------------------- 1 | .animate { transition: all 0.3s ease-out !important; } 2 | .animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; } 3 | .animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } 4 | 5 | .cursor { color: #999; animation: pulse 1.5s infinite ease-in-out; } 6 | @keyframes pulse { 7 | 0% { opacity: 0 } 8 | 5% { opacity: 1 } 9 | 30% { opacity: 1 } 10 | 70% { opacity: 0 } 11 | 100% { opacity: 0 } 12 | } 13 | 14 | .bounce { animation: bounce .3s infinite alternate ease-out; } 15 | @keyframes bounce { 16 | 0% { transform: translateY(0); opacity: 1 } 17 | 100% { transform: translateY(-3px); opacity: 0.7 } 18 | } 19 | 20 | .shake { animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; backface-visibility: hidden; transform: translate3d(0, 0, 0); } 21 | @keyframes shake { 22 | 10%, 90% { transform: translate3d(-1px, 0, 0); } 23 | 20%, 80% { transform: translate3d(2px, 0, 0); } 24 | 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 25 | 40%, 60% { transform: translate3d(4px, 0, 0); } 26 | } -------------------------------------------------------------------------------- /js/utils/Time.coffee: -------------------------------------------------------------------------------- 1 | class Time 2 | since: (timestamp) -> 3 | now = +(new Date)/1000 4 | if timestamp > 1000000000000 # In ms 5 | timestamp = timestamp/1000 6 | secs = now - timestamp 7 | if secs < 60 8 | back = "Just now" 9 | else if secs < 60*60 10 | back = "#{Math.round(secs/60)} minutes ago" 11 | else if secs < 60*60*24 12 | back = "#{Math.round(secs/60/60)} hours ago" 13 | else if secs < 60*60*24*3 14 | back = "#{Math.round(secs/60/60/24)} days ago" 15 | else 16 | back = "on "+@date(timestamp) 17 | back = back.replace(/1 ([a-z]+)s/, "1 $1") # 1 days ago fix 18 | return back 19 | 20 | 21 | date: (timestamp, format="short") -> 22 | if timestamp > 1000000000000 # In ms 23 | timestamp = timestamp/1000 24 | parts = (new Date(timestamp*1000)).toString().split(" ") 25 | if format == "short" 26 | display = parts.slice(1, 4) 27 | else 28 | display = parts.slice(1, 5) 29 | return display.join(" ").replace(/( [0-9]{4})/, ",$1") 30 | 31 | 32 | timestamp: (date="") -> 33 | if date == "now" or date == "" 34 | return parseInt(+(new Date)/1000) 35 | else 36 | return parseInt(Date.parse(date)/1000) 37 | 38 | 39 | window.Time = new Time -------------------------------------------------------------------------------- /data/users/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "site_stat": [ 3 | { 4 | "date_updated": 0, 5 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496403641", 6 | "peers": 11 7 | }, 8 | { 9 | "date_updated": 0, 10 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496401902", 11 | "peers": 402 12 | }, 13 | { 14 | "date_updated": 0, 15 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496401769", 16 | "peers": 387 17 | }, 18 | { 19 | "date_updated": 0, 20 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496403190", 21 | "peers": 436 22 | }, 23 | { 24 | "date_updated": 0, 25 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496403722", 26 | "peers": 403 27 | }, 28 | { 29 | "date_updated": 0, 30 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496403932", 31 | "peers": 19 32 | }, 33 | { 34 | "date_updated": 0, 35 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496403826", 36 | "peers": 72 37 | }, 38 | { 39 | "date_updated": 0, 40 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496403770", 41 | "peers": 20 42 | }, 43 | { 44 | "date_updated": 0, 45 | "site_uri": "1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj_1496402219", 46 | "peers": 297 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /css/Head.css: -------------------------------------------------------------------------------- 1 | #Head { margin: 0px 20px } 2 | #Head h1 { 3 | font-size: 300%; color: #EE1865; margin-top: 20px; display: inline-block; font-weight: 100; vertical-align: 12px; margin-bottom: 10px; 4 | background: -webkit-linear-gradient(left, #EB0080, #EF193E 70%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; 5 | } 6 | #Head h2 { 7 | font-size: 14px; padding-left: 138px; font-weight: lighter; color: #AAA; width: 280px; margin: 0px auto; text-align: left; margin-top: -41px; letter-spacing: 0.5px; 8 | } 9 | #Head .logo { display: inline-block; margin-right: 20px; margin-left: 25px; } 10 | 11 | #Head .order { margin-top: 35px; display: inline-block; vertical-align: top; white-space: nowrap; margin-top: 35px; } 12 | #Head .order-item { 13 | border-radius: 101px; display: inline-block; margin-right: 5px; color: #333; transition: all 0.3s; 14 | padding: 11px 21px; display: inline-block; text-transform: uppercase; text-decoration: none; letter-spacing: 2px; font-weight: lighter; font-size: 13px; 15 | } 16 | #Head .order-item:hover { transition: none; color: #ee0a73 } 17 | #Head .order-item:focus { transition: all 0.3s } 18 | #Head .order-item.active { background-color: #ee0a73; color: white; box-shadow: 0px 3px 14px -2px #ff6db0 } 19 | #Head .order-item.new.active { background-color: #2ECC71;; color: white; box-shadow: 0px 3px 14px -2px #2ECC71 } 20 | -------------------------------------------------------------------------------- /js/User.coffee: -------------------------------------------------------------------------------- 1 | class User extends Class 2 | constructor: (auth_address) -> 3 | @starred = {} 4 | if auth_address 5 | @setAuthAddress(auth_address) 6 | 7 | setAuthAddress: (auth_address) -> 8 | @auth_address = auth_address 9 | if Page.site_info.auth_address == auth_address 10 | @updateStarred() 11 | 12 | updateStarred: (cb) -> 13 | @starred = {} 14 | Page.cmd "dbQuery", ["SELECT site_star.* FROM json LEFT JOIN site_star USING (json_id) WHERE ?", {directory: "#{@auth_address}"}], (res) => 15 | for row in res 16 | @starred[row["site_uri"]] = true 17 | cb?() 18 | Page.projector.scheduleRender() 19 | 20 | getPath: -> 21 | return "data/users/#{@auth_address}" 22 | 23 | getDefaultData: -> 24 | return { 25 | "site": [], 26 | "site_star": {}, 27 | "site_comment": [] 28 | } 29 | 30 | getData: (cb) -> 31 | Page.cmd "fileGet", [@getPath()+"/data.json", false], (data) => 32 | data = JSON.parse(data) 33 | data ?= @getDefaultData() 34 | cb(data) 35 | 36 | certSelect: (cb) => 37 | Page.cmd "certSelect", {"accepted_domains": ["zeroid.bit"]}, (res) => 38 | @log "certSelected" 39 | cb?(res) 40 | 41 | onSiteInfo: (site_info) => 42 | if site_info.event?[0] == "cert_changed" 43 | @setAuthAddress(site_info.auth_address) 44 | Page.projector.scheduleRender() 45 | 46 | save: (data, cb) -> 47 | Page.cmd "fileWrite", [@getPath()+"/data.json", Text.fileEncode(data)], (res_write) => 48 | Page.cmd "siteSign", {"inner_path": @getPath()+"/data.json"}, (res_sign) => 49 | cb?(res_sign) 50 | Page.cmd "sitePublish", {"inner_path": @getPath()+"/content.json", sign: false}, (res_publish) => 51 | @log "Save result", res_write, res_sign, res_publish 52 | 53 | window.User = User -------------------------------------------------------------------------------- /js/lib/Promise.coffee: -------------------------------------------------------------------------------- 1 | # From: http://dev.bizo.com/2011/12/promises-in-javascriptcoffeescript.html 2 | 3 | class Promise 4 | @when: (tasks...) -> 5 | num_uncompleted = tasks.length 6 | args = new Array(num_uncompleted) 7 | promise = new Promise() 8 | 9 | for task, task_id in tasks 10 | ((task_id) -> 11 | task.then(() -> 12 | args[task_id] = Array.prototype.slice.call(arguments) 13 | num_uncompleted-- 14 | promise.complete.apply(promise, args) if num_uncompleted == 0 15 | ) 16 | )(task_id) 17 | 18 | return promise 19 | 20 | constructor: -> 21 | @resolved = false 22 | @end_promise = null 23 | @result = null 24 | @callbacks = [] 25 | 26 | resolve: -> 27 | if @resolved 28 | return false 29 | @resolved = true 30 | @data = arguments 31 | if not arguments.length 32 | @data = [true] 33 | @result = @data[0] 34 | for callback in @callbacks 35 | back = callback.apply callback, @data 36 | if @end_promise 37 | @end_promise.resolve(back) 38 | 39 | fail: -> 40 | @resolve(false) 41 | 42 | then: (callback) -> 43 | if @resolved == true 44 | callback.apply callback, @data 45 | return 46 | 47 | @callbacks.push callback 48 | 49 | @end_promise = new Promise() 50 | 51 | window.Promise = Promise 52 | 53 | ### 54 | s = Date.now() 55 | log = (text) -> 56 | console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ") 57 | 58 | log "Started" 59 | 60 | cmd = (query) -> 61 | p = new Promise() 62 | setTimeout ( -> 63 | p.resolve query+" Result" 64 | ), 100 65 | return p 66 | 67 | back = cmd("SELECT * FROM message").then (res) -> 68 | log res 69 | return "Return from query" 70 | .then (res) -> 71 | log "Back then", res 72 | 73 | log "Query started", back 74 | ### -------------------------------------------------------------------------------- /css/icons.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-block; vertical-align: text-bottom; background-repeat: no-repeat; height: 30px; 3 | vertical-align: middle; line-height: 30px; transition: background-color 0.3s; 4 | } 5 | .icon-comment { 6 | width: 20px; height: 16px; 7 | filter: grayscale(100%); 8 | background-image: url('data:image/svg+xml,') 9 | } 10 | .icon-comment:empty { padding-right: 0px } 11 | 12 | .icon-star { 13 | width: 20px; height: 22px; 14 | filter: grayscale(100%); 15 | background-image: url('data:image/svg+xml,'); 16 | } 17 | 18 | .icon-profile { font-size: 8px; top: -0.3em; border-radius: 0.7em 0.7em 0 0; border: 1px solid #444; width: 12px; height: 0.5em; position: relative; display: inline-block; margin-right: 3px; margin-left: 5px; } 19 | .icon-profile::before { position: absolute; content: ""; top: -1.3em; left: 15%; width: 0.8em; height: 0.7em; border-radius: 50%; border: 1px solid #111; } -------------------------------------------------------------------------------- /css/Menu.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | background-color: white; padding: 10px 0px; position: absolute; top: 0px; max-height: 0px; overflow: hidden; transform: translate(-100%, -30px); pointer-events: none; 3 | box-shadow: 0px 2px 8px rgba(0,0,0,0.3); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; z-index: 99; 4 | display: inline-block; z-index: 999; 5 | } 6 | .menu.visible { 7 | opacity: 1; max-height: 500px; transform: translate(-100%, 0px); pointer-events: all; 8 | transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1); 9 | } 10 | 11 | .menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; } 12 | .menu-item .emoji { opacity: 0.6 } 13 | .menu-item-separator { margin-top: 3px; margin-bottom: 3px; border-top: 1px solid #eee } 14 | 15 | .menu-item.noaction { cursor: default } 16 | .menu-item:hover:not(.noaction) { background-color: #F6F6F6; transition: none; color: inherit; cursor: pointer; color: black } 17 | .menu-item:active:not(.noaction), .menu-item:focus:not(.noaction) { background-color: #AF3BFF; color: white; transition: none } 18 | .menu-item.selected:before { 19 | content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1); 20 | font-weight: bold; position: absolute; margin-left: -14px; font-size: 12px; margin-top: 2px; 21 | } 22 | 23 | .menu-radio { white-space: normal; line-height: 26px; margin-left: -7px } 24 | .menu-radio a { 25 | background-color: #EEE; width: 18.5%;; text-align: center; margin-top: 2px; margin-bottom: 2px; color: #666; font-weight: bold; box-sizing: border-box; 26 | text-decoration: none; font-size: 13px; transition: all 0.3s; text-transform: uppercase; display: inline-block; border: 2px solid transparent; 27 | } 28 | .menu-radio a.selected { background-color: #AF3BFF; color: white } 29 | .menu-radio a:hover { border: 2px solid #AF3BFF; transition: none } 30 | 31 | .menu-radio a.long { font-size: 11px } -------------------------------------------------------------------------------- /dbschema.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "ZeroSites", 3 | "db_file": "data/users/zerosites.db", 4 | "version": 2, 5 | "maps": { 6 | ".+/data.json": { 7 | "to_table": [ 8 | "site", 9 | {"node": "site_star", "table": "site_star", "key_col": "site_uri", "val_col": "value"} 10 | ] 11 | }, 12 | ".+/content.json": { 13 | "to_json_table": [ "cert_user_id" ], 14 | "file_name": "data.json" 15 | }, 16 | "stats.json": { 17 | "to_table": [ 18 | "site_stat" 19 | ] 20 | } 21 | 22 | }, 23 | "tables": { 24 | "site": { 25 | "cols": [ 26 | ["site_id", "INTEGER"], 27 | ["category", "INTEGER"], 28 | ["language", "TEXT"], 29 | ["title", "TEXT"], 30 | ["description", "TEXT"], 31 | ["address", "TEXT"], 32 | ["tags", "TEXT"], 33 | ["date_added", "DATETIME"], 34 | ["json_id", "INTEGER REFERENCES json (json_id)"] 35 | ], 36 | "indexes": ["CREATE UNIQUE INDEX site_key ON site(json_id, date_added)", "CREATE INDEX site_category ON site(category)"], 37 | "schema_changed": 10 38 | }, 39 | "site_stat": { 40 | "cols": [ 41 | ["site_uri", "TEXT"], 42 | ["peers", "INTEGER"], 43 | ["date_updated", "DATETIME"], 44 | ["json_id", "INTEGER REFERENCES json (json_id)"] 45 | ], 46 | "indexes":["CREATE INDEX site_stat_key ON site_stat(json_id)", "CREATE UNIQUE INDEX site_stat_uri ON site_stat(site_uri)"], 47 | "schema_changed": 10 48 | }, 49 | "site_star": { 50 | "cols": [ 51 | ["site_uri", "TEXT"], 52 | ["value", "INTEGER"], 53 | ["json_id", "INTEGER REFERENCES json (json_id)"] 54 | ], 55 | "indexes":["CREATE INDEX site_star_key ON site_star(json_id)"], 56 | "schema_changed": 10 57 | }, 58 | "json": { 59 | "cols": [ 60 | ["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], 61 | ["directory", "TEXT"], 62 | ["file_name", "TEXT"], 63 | ["cert_user_id", "TEXT"] 64 | ], 65 | "indexes": ["CREATE UNIQUE INDEX path ON json(directory, file_name)"], 66 | "schema_changed": 10 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /content.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "1SiTEs2D3rCBxeMoLHXei2UYqFcxctdwB", 3 | "background-color": "#ECF1F5", 4 | "description": "Sites of ZeroNet", 5 | "files": { 6 | "css/all.css": { 7 | "sha512": "940ccfdf626adc9e1e1c2641be122a4d0fabb5e4f206dd18365d7084fdd3238e", 8 | "size": 116590 9 | }, 10 | "dbschema.json": { 11 | "sha512": "95dee8798b26d729501eb473f6ec9c6802b468c2e3415de0a3c820723a9c5560", 12 | "size": 1914 13 | }, 14 | "img/logo.png": { 15 | "sha512": "1b9bf2d0fc7917335b698c0f2a3f902a86b5a9a917a945bbaa04bc82f2a8645f", 16 | "size": 2383 17 | }, 18 | "index.html": { 19 | "sha512": "9c4a9a6bfe23600ac014a1d84ecf6aa15f7944860c3a1d7c11d16d4c1cb3808e", 20 | "size": 590 21 | }, 22 | "js/all.js": { 23 | "sha512": "4c68c5647606b2ced67c0611cede57ad82abe2325a8be99480ae43e9f0ecb59e", 24 | "size": 101400 25 | }, 26 | "languages/zh-tw.json": { 27 | "sha512": "2652c2927fc68380c3f4baa60776be1005c66a55dac79bbb64e9686e20e52fcb", 28 | "size": 774 29 | }, 30 | "languages/zh.json": { 31 | "sha512": "235884740efefc6cd95ab0fa604a58e4fcd1481106e153ba04d594326b1852b1", 32 | "size": 768 33 | } 34 | }, 35 | "ignore": "((js|css)/(?!all.(js|css))|data/.*)", 36 | "includes": { 37 | "data/users/content.json": { 38 | "signers": [], 39 | "signers_required": 1 40 | } 41 | }, 42 | "inner_path": "content.json", 43 | "modified": 1496850127, 44 | "postmessage_nonce_security": true, 45 | "settings": { 46 | "categories": [ 47 | [2, "Blogs"], 48 | [9, "Services"], 49 | [1, "Forums, Boards"], 50 | [10, "Chat"], 51 | [6, "Video, Image"], 52 | [8, "Guides"], 53 | [7, "News"], 54 | [5, "Politics"], 55 | [4, "Porn"], 56 | [3, "Other"] 57 | ], 58 | "languages": ["da", "de", "en", "es", "fr", "hu", "it", "nl", "pl", "pt", "pt-br", "ru", "tr", "uk", "zh", "zh-tw", "multi"] 59 | }, 60 | "signers_sign": "GxHBPHPG8y3EJjZTNmvNuDYfl4/lcdE99zbdS06DqLJrTm9VmI26FMgbkT47JwEFyHD4PaE2du1tUJdU24qIqtQ=", 61 | "signs": { 62 | "1SiTEs2D3rCBxeMoLHXei2UYqFcxctdwB": "G3FE5WuFWYBLzdbasu8gk0i+IPrz6gluo9LMfFMpjkq6R7SdcYG9Cq31Bemx9cClZH3UY7vvJFkQVxmzMmcdE04=" 63 | }, 64 | "signs_required": 1, 65 | "title": "ZeroSites", 66 | "translate": ["js/all.js"], 67 | "zeronet_version": "0.5.5" 68 | } -------------------------------------------------------------------------------- /css/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | margin-top: 4px; border: 1px solid hsla(236,100%,79%,1); color: #5d68ff; border-radius: 33px; display: inline-block; 3 | font-size: 19px; font-weight: lighter; text-align: center; transition: all 0.3s; padding: 8px 30px; background-position: -200px center; 4 | } 5 | .button:hover { background-color: #5d68ff; color: #F6F7F8; text-decoration: none; border-color: #5d68ff; transition: none } 6 | .button:hover .icon { background-color: #FFF; transition: none } 7 | .button:focus { transition: all 0.3s } 8 | .button:active { transform: translateY(1px); transition: all 0.3s, transform none; box-shadow: inset 0px 5px 7px -3px rgba(212, 212, 212, 0.41); outline: none; transition: none } 9 | 10 | .button.loading { 11 | color: rgba(0,0,0,0) !important; background: url(../img/loading.gif) no-repeat center center !important; border-color: rgba(0,0,0,0) !important; 12 | transition: all 0.5s ease-out; pointer-events: none; transition-delay: 0.5s 13 | } 14 | 15 | /* Follow */ 16 | .button-follow { width: 32px; line-height: 32px; padding: 0px; border: 1px solid #aaa; color: #999; padding-left: 1px; padding-bottom: 1px; } 17 | .button-follow:hover { background-color: rgba(255,255,255,0.3) !important; border-color: #2ecc71 !important; color: #2ecc71 } 18 | .button-follow-big { padding-left: 25px; float: none; border: 1px solid #2ecc71; color: #2ecc71; min-width: 100px; } 19 | .button-follow-big .icon-follow { margin-right: 10px; display: inline-block; transition: transform 0.3s ease-in-out } 20 | .button-follow-big:hover { border-color: #2ecc71 !important; color: #2ecc71; background-color: white; text-decoration: underline; } 21 | 22 | /* Submit */ 23 | .button-submit { 24 | padding: 15px 40px; border-radius: 30px; margin-top: 11px; background-color: #FFEB3B; letter-spacing: 2px; color: #3d3700; text-decoration: none; 25 | border: none; font-weight: bold; font-size: 12px; text-transform: uppercase; margin-left: 10px; 26 | } 27 | .button-submit:hover { color: black; background-color: #fff066 } 28 | 29 | .button-small { padding: 7px 20px; margin-left: 10px } 30 | .button-outline { background-color: white; border: 1px solid #EEE; color: #AAA; } 31 | .button-outline:hover { background-color: white; border: 1px solid #CCC; color: #777 } 32 | -------------------------------------------------------------------------------- /css/Form.css: -------------------------------------------------------------------------------- 1 | .form-takeover-container { 2 | width: 100%; height: 100%; position: fixed; background-color: rgba(84,84,84,0.5); z-index: 999; top: 0px 3 | background: linear-gradient(35deg, rgb(212, 212, 212), rgba(0, 0, 255, 0.35)); transition: all 0.3s; 4 | } 5 | .form-takeover-container.hidden { opacity: 0; pointer-events: none } 6 | 7 | .form.form-takeover { 8 | text-align: right; max-width: 700px; width: 90%; margin-left: auto; margin-right: auto; background-color: white; padding: 30px; padding-top: 5px; box-sizing: border-box; 9 | box-shadow: 0px 0px 100px -20px rgba(0,0,0,0.5); position: relative; 10 | } 11 | 12 | .form .formfield .title { 13 | font-size: 13px; text-align: right; margin-top: 40px; text-transform: uppercase; letter-spacing: 2px; display: block; 14 | margin-left: 0px; margin-bottom: -31px; width: 17%; margin-right: 10px; box-sizing: border-box; color: #999; 15 | } 16 | .form .formfield .title.invalid { color: #ee0a73; transition: all 0.3s } 17 | .form .formfield input.text { font-size: 16px; padding: 10px; width: 80%; font-weight: lighter; font-family: Roboto; border: 1px solid #DDD; margin-right: 0px; box-sizing: border-box; left: 20px; transition: all 0.3s } 18 | .form .formfield input.text:focus { border: 1px solid #AF3BFF; outline: none; transition: none } 19 | .form .formfield .radiogroup { min-height: 30px; width: 80%; margin-left: auto; text-align: left } 20 | .form .formfield .radiogroup-lang { text-transform: uppercase; } 21 | .form .formfield .radiogroup + .invalid-reason { margin-top: -29px; } 22 | .form .formfield .radio { 23 | text-decoration: none; color: #333; padding: 9px; display: inline-block; border: 1px solid #EEE; 24 | min-width: 49.7px; text-align: center; font-size: 14px; margin-top: 5px; transition: all 0.3s 25 | } 26 | .form .formfield .radio.active { background-color: #AF3BFF; color: white } 27 | .form .formfield .radio:hover { border: 1px solid #AF3BFF; transition: none } 28 | 29 | .form input.text.invalid { border: 1px solid #ee0a73 } 30 | .form .invalid-reason { position: absolute; margin-right: 30px; display: inline-block; right: 0px; padding: 3px 9px; background-color: #ee0a73; font-size: 13px; color: white; z-index: 999; pointer-events: none } 31 | .form .cancel { text-decoration: none; color: #AAA; font-weight: bold; font-size: 12px; text-transform: uppercase;} 32 | .form .cancel:hover { text-decoration: underline } 33 | -------------------------------------------------------------------------------- /js/SiteList.coffee: -------------------------------------------------------------------------------- 1 | class SiteList extends Class 2 | constructor: (@row) -> 3 | @item_list = new ItemList(Site, "site_id") 4 | @sites = @item_list.items 5 | @item_list.sync(@row.sites) 6 | @limit = 10 7 | 8 | isHidden: -> 9 | if Page.site_lists.filter_category == null 10 | return false 11 | else 12 | return Page.site_lists.filter_category != @row.id 13 | 14 | handleMoreClick: => 15 | @limit += 20 16 | @nolimit = true 17 | return false 18 | 19 | render: (i) -> 20 | if @row.title == "Other" and Page.site_lists.filter_category != @row.id and Page.site_lists.cols == 3 21 | return @renderWide(i) 22 | if Page.site_lists.filter_category == @row.id 23 | limit = 100 24 | else 25 | limit = @limit 26 | if @sites.length == 0 27 | clear = false 28 | else 29 | clear = (i % Page.site_lists.cols == 1) 30 | h("div.sitelist", {key: @row.id, classes: {empty: @sites.length == 0, hidden: @isHidden(), selected: Page.site_lists.filter_category == @row.id, nolimit: @nolimit, clear: clear},}, [ 31 | h("a.categoryname", {href: "?Category:#{@row.id}:#{Text.toUrl(@row.title)}", onclick: Page.handleLinkClick}, @row.title), 32 | h("div.sites", [ 33 | @sites[0..limit-1].map (item) -> 34 | item.render() 35 | if @sites.length > limit 36 | h("a.more", {href: "?Category:#{@row.id}:#{Text.toUrl(@row.title)}", onclick: @handleMoreClick, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, "Show more...") 37 | ]) 38 | ]) 39 | 40 | renderWide: (i) -> 41 | cols = [0,1,2] 42 | clear = false 43 | limit = @limit 44 | if @sites.length < limit * 3 45 | limit = Math.ceil(@sites.length / 3) 46 | h("div.sitelist-wide", [ 47 | cols.map (col) => 48 | h("div.sitelist.col-#{col}", {key: @row.id, classes: {empty: @sites.length == 0, hidden: @isHidden(), selected: Page.site_lists.filter_category == @row.id, clear: clear},}, [ 49 | h("a.categoryname", {href: "?Category:#{@row.id}:#{Text.toUrl(@row.title)}", onclick: Page.handleLinkClick}, @row.title), 50 | h("div.sites", [ 51 | @sites[0+col*limit..col*limit + limit-1].map (item) -> 52 | item.render() 53 | ]) 54 | ]) 55 | if @sites.length > limit * cols.length 56 | h("a.more", {href: "?Category:#{@row.id}:#{Text.toUrl(@row.title)}", onclick: @handleMoreClick, enterAnimation: Animation.slideDown}, "Show more...") 57 | ]) 58 | 59 | window.SiteList = SiteList -------------------------------------------------------------------------------- /js/utils/Menu.coffee: -------------------------------------------------------------------------------- 1 | class Menu 2 | constructor: -> 3 | @visible = false 4 | @items = [] 5 | @node = null 6 | 7 | show: => 8 | window.visible_menu?.hide() 9 | @visible = true 10 | window.visible_menu = @ 11 | 12 | hide: => 13 | @visible = false 14 | 15 | toggle: => 16 | if @visible 17 | @hide() 18 | else 19 | @show() 20 | Page.projector.scheduleRender() 21 | 22 | 23 | addItem: (title, cb, selected=false) -> 24 | @items.push([title, cb, selected]) 25 | 26 | 27 | storeNode: (node) => 28 | @node = node 29 | # Animate visible 30 | if @visible 31 | node.className = node.className.replace("visible", "") 32 | setTimeout (-> 33 | node.className += " visible" 34 | ), 20 35 | 36 | handleClick: (e) => 37 | keep_menu = true 38 | for item in @items 39 | [title, cb, selected] = item 40 | if title == e.target.textContent or e.target["data-title"] == title 41 | keep_menu = cb(item) 42 | break 43 | if keep_menu != true 44 | @hide() 45 | return false 46 | 47 | renderItem: (item) => 48 | [title, cb, selected] = item 49 | if typeof(selected) == "function" 50 | selected = selected() 51 | if title == "---" 52 | h("div.menu-item-separator") 53 | else 54 | if typeof(cb) == "string" # Url 55 | href = cb 56 | onclick = true 57 | else # Callback 58 | href = "#"+title 59 | onclick = @handleClick 60 | if typeof(title) == "function" 61 | title = title() 62 | key = "#" 63 | else 64 | key = title 65 | h("a.menu-item", {href: href, onclick: onclick, "data-title": title, key: key, classes: {"selected": selected, "noaction": (cb == null)}}, title) 66 | 67 | render: (class_name="") => 68 | if @visible or @node 69 | h("div.menu#{class_name}", {classes: {"visible": @visible}, afterCreate: @storeNode}, @items.map(@renderItem)) 70 | 71 | window.Menu = Menu 72 | 73 | # Hide menu on outside click 74 | document.body.addEventListener "mouseup", (e) -> 75 | if not window.visible_menu or not window.visible_menu.node 76 | return false 77 | isChildOf = (child, parent) -> 78 | node = child.parentNode 79 | while node != null 80 | if node == parent 81 | return true 82 | else 83 | node = node.parentNode 84 | return false 85 | if not isChildOf(e.target, window.visible_menu.node.parentNode) and not isChildOf(e.target, window.visible_menu.node) 86 | window.visible_menu.hide() 87 | Page.projector.scheduleRender() -------------------------------------------------------------------------------- /js/utils/ZeroFrame.coffee: -------------------------------------------------------------------------------- 1 | class ZeroFrame extends Class 2 | constructor: (url) -> 3 | @url = url 4 | @waiting_cb = {} 5 | @history_state = {} 6 | @wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1") 7 | @connect() 8 | @next_message_id = 1 9 | @init() 10 | 11 | 12 | init: -> 13 | @ 14 | 15 | 16 | connect: -> 17 | @target = window.parent 18 | window.addEventListener("message", @onMessage, false) 19 | @cmd("innerReady") 20 | 21 | # Save scrollTop 22 | window.addEventListener "beforeunload", (e) => 23 | @log "Save scrollTop", window.pageYOffset 24 | @history_state["scrollTop"] = window.pageYOffset 25 | @cmd "wrapperReplaceState", [@history_state, null] 26 | 27 | # Restore scrollTop 28 | @cmd "wrapperGetState", [], (state) => 29 | @handleState(state) 30 | 31 | handleState: (state) -> 32 | @history_state = state if state? 33 | @log "Restore scrollTop", state, window.pageYOffset 34 | if window.pageYOffset == 0 and state 35 | window.scroll(window.pageXOffset, state.scrollTop) 36 | 37 | 38 | onMessage: (e) => 39 | message = e.data 40 | cmd = message.cmd 41 | if cmd == "response" 42 | if @waiting_cb[message.to]? 43 | @waiting_cb[message.to](message.result) 44 | else 45 | @log "Websocket callback not found:", message 46 | else if cmd == "wrapperReady" # Wrapper inited later 47 | @cmd("innerReady") 48 | else if cmd == "ping" 49 | @response message.id, "pong" 50 | else if cmd == "wrapperOpenedWebsocket" 51 | @onOpenWebsocket() 52 | else if cmd == "wrapperClosedWebsocket" 53 | @onCloseWebsocket() 54 | else if cmd == "wrapperPopState" 55 | @handleState(message.params.state) 56 | @onRequest cmd, message.params 57 | else 58 | @onRequest cmd, message.params 59 | 60 | 61 | onRequest: (cmd, message) => 62 | @log "Unknown request", message 63 | 64 | 65 | response: (to, result) -> 66 | @send {"cmd": "response", "to": to, "result": result} 67 | 68 | 69 | cmd: (cmd, params={}, cb=null) -> 70 | @send {"cmd": cmd, "params": params}, cb 71 | 72 | cmdp: (cmd, params={}) -> 73 | p = new Promise() 74 | @send {"cmd": cmd, "params": params}, (res) -> 75 | p.resolve(res) 76 | return p 77 | 78 | send: (message, cb=null) -> 79 | message.wrapper_nonce = @wrapper_nonce 80 | message.id = @next_message_id 81 | @next_message_id += 1 82 | @target.postMessage(message, "*") 83 | if cb 84 | @waiting_cb[message.id] = cb 85 | 86 | 87 | onOpenWebsocket: => 88 | @log "Websocket open" 89 | 90 | 91 | onCloseWebsocket: => 92 | @log "Websocket close" 93 | 94 | 95 | 96 | window.ZeroFrame = ZeroFrame -------------------------------------------------------------------------------- /js/Site.coffee: -------------------------------------------------------------------------------- 1 | class Site 2 | constructor: (@row) -> 3 | @form_edit = null 4 | @ 5 | 6 | getUri: => 7 | return @row.directory + "_" + @row.site_id 8 | 9 | isNew: => 10 | return Time.timestamp() - @row.date_added < 60*60*24 11 | 12 | handleStarClick: => 13 | if not Page.site_info.cert_user_id 14 | Page.user.certSelect => 15 | @handleStarClick() 16 | return false 17 | 18 | if Page.user.starred[@getUri()] 19 | action = "removing" 20 | else 21 | action = "adding" 22 | 23 | Page.user.starred[@getUri()] = not Page.user.starred[@getUri()] 24 | Page.projector.scheduleRender() 25 | 26 | Page.user.getData (data) => 27 | if action == "adding" 28 | data.site_star[@getUri()] = 1 29 | else 30 | delete data.site_star[@getUri()] 31 | Page.user.save data, (res) => 32 | Page.site_lists.update() 33 | return false 34 | 35 | getClasses: => 36 | return { 37 | my: @row.cert_user_id == Page.site_info.cert_user_id or Page.site_info.settings.own 38 | starred: Page.user.starred[@getUri()] 39 | } 40 | 41 | saveRow: (cb) => 42 | user = new User(@row.directory) 43 | user.getData (data) => 44 | data_row = row for row in data.site when row.site_id == @row.site_id 45 | for key, val of @row 46 | if data_row[key] 47 | data_row[key] = val 48 | user.save data, (res) => 49 | Page.site_lists.update() 50 | cb?(res) 51 | 52 | deleteRow: (cb) => 53 | Page.user.getData (data) => 54 | data_row_i = i for row, i in data.site when row.site_id == @row.site_id 55 | data.site.splice(data_row_i, 1) 56 | Page.user.save data, (res) => 57 | Page.site_lists.update() 58 | cb?(res) 59 | 60 | handleEditClick: => 61 | if not @form_edit 62 | @form_edit = new Form() 63 | @form_edit.addField("text", "address", "Address", {placeholder: "e.g. http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", required: true, validate: @form_edit.shouldBeZite}) 64 | @form_edit.addField("text", "title", "Title", {placeholder: "e.g. ZeroBlog", required: true}) 65 | @form_edit.addField("radio", "language", "Language", {required: true, values: Page.languages, classes: {"radiogroup-lang": true}}) 66 | @form_edit.addField("radio", "category", "Category", {required: true, values: Page.categories}) 67 | @form_edit.addField("text", "description", "Description", {placeholder: "e.g. ZeroNet changelog and related informations", required: true}) 68 | @form_edit.setData(@row) 69 | @form_edit.saveRow = @saveRow 70 | @form_edit.deleteRow = @deleteRow 71 | Page.setFormEdit(@form_edit) 72 | return false 73 | 74 | render: => 75 | my = @row.cert_user_id == Page.site_info.cert_user_id or Page.site_info.settings.own 76 | 77 | h("a.site.nocomment", { href: Text.fixLink("http://127.0.0.1:43110/"+@row.address), key: @row.site_id, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp, classes: @getClasses()}, [ 78 | h("div.right", [ 79 | h("a.star", {href: "#Star", onclick: @handleStarClick}, 80 | h("span.num", @row.star or "") 81 | h("span.icon.icon-star", "") 82 | ), 83 | h("a.comments", {href: "#"}, 84 | h("span.num", "soon") 85 | h("span.icon.icon-comment", "") 86 | ), 87 | if @row.peers then h("div.peers", 88 | h("span.num", @row.peers) 89 | h("span.icon.icon-profile", "") 90 | ) 91 | ]) 92 | h("div.title", @row.title), 93 | if @isNew() then h("div.tag.tag-new", "New"), 94 | if @row.tags?.indexOf("popular") >= 0 then h("div.tag.tag-popular", "Popular"), 95 | if my then h("a.tag.tag-my", {href: "#Edit:#{@row.site_uri}", onclick: @handleEditClick}, "Edit"), 96 | h("div.description", @row.description) 97 | ]) 98 | 99 | window.Site = Site -------------------------------------------------------------------------------- /js/utils/Text.coffee: -------------------------------------------------------------------------------- 1 | class Text 2 | toColor: (text, saturation=30, lightness=50) -> 3 | hash = 0 4 | for i in [0..text.length-1] 5 | hash += text.charCodeAt(i)*i 6 | hash = hash % 1777 7 | return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)"; 8 | 9 | 10 | renderMarked: (text, options={}) -> 11 | options["gfm"] = true 12 | options["breaks"] = true 13 | options["renderer"] = marked_renderer 14 | text = @fixReply(text) 15 | text = marked(text, options) 16 | text = @emailLinks text 17 | return @fixHtmlLinks text 18 | 19 | emailLinks: (text) -> 20 | return text.replace(/([a-zA-Z0-9]+)@zeroid.bit/g, "$1@zeroid.bit") 21 | 22 | # Convert zeronet html links to relaitve 23 | fixHtmlLinks: (text) -> 24 | if window.is_proxy 25 | return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="http://zero') 26 | else 27 | return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="') 28 | 29 | # Convert a single link to relative 30 | fixLink: (link) -> 31 | if window.is_proxy 32 | back = link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero') 33 | return back.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1") # Domain links 34 | else 35 | return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, '') 36 | 37 | toUrl: (text) -> 38 | return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, "") 39 | 40 | getSiteUrl: (address) -> 41 | if window.is_proxy 42 | if "." in address # Domain 43 | return "http://"+address 44 | else 45 | return "http://zero/"+address 46 | else 47 | return "/"+address 48 | 49 | 50 | fixReply: (text) -> 51 | return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2") 52 | 53 | toBitcoinAddress: (text) -> 54 | return text.replace(/[^A-Za-z0-9]/g, "") 55 | 56 | 57 | jsonEncode: (obj) -> 58 | return unescape(encodeURIComponent(JSON.stringify(obj))) 59 | 60 | jsonDecode: (obj) -> 61 | return JSON.parse(decodeURIComponent(escape(obj))) 62 | 63 | fileEncode: (obj) -> 64 | if typeof(obj) == "string" 65 | return btoa(unescape(encodeURIComponent(obj))) 66 | else 67 | return btoa(unescape(encodeURIComponent(JSON.stringify(obj, undefined, '\t')))) 68 | 69 | utf8Encode: (s) -> 70 | return unescape(encodeURIComponent(s)) 71 | 72 | utf8Decode: (s) -> 73 | return decodeURIComponent(escape(s)) 74 | 75 | 76 | distance: (s1, s2) -> 77 | s1 = s1.toLocaleLowerCase() 78 | s2 = s2.toLocaleLowerCase() 79 | next_find_i = 0 80 | next_find = s2[0] 81 | match = true 82 | extra_parts = {} 83 | for char in s1 84 | if char != next_find 85 | if extra_parts[next_find_i] 86 | extra_parts[next_find_i] += char 87 | else 88 | extra_parts[next_find_i] = char 89 | else 90 | next_find_i++ 91 | next_find = s2[next_find_i] 92 | 93 | if extra_parts[next_find_i] 94 | extra_parts[next_find_i] = "" # Extra chars on the end doesnt matter 95 | extra_parts = (val for key, val of extra_parts) 96 | if next_find_i >= s2.length 97 | return extra_parts.length + extra_parts.join("").length 98 | else 99 | return false 100 | 101 | 102 | parseQuery: (query) -> 103 | params = {} 104 | parts = query.split('&') 105 | for part in parts 106 | [key, val] = part.split("=") 107 | if val 108 | params[decodeURIComponent(key)] = decodeURIComponent(val) 109 | else 110 | params["url"] = decodeURIComponent(key) 111 | return params 112 | 113 | encodeQuery: (params) -> 114 | back = [] 115 | if params.url 116 | back.push(params.url) 117 | for key, val of params 118 | if not val or key == "url" 119 | continue 120 | back.push("#{encodeURIComponent(key)}=#{encodeURIComponent(val)}") 121 | return back.join("&") 122 | 123 | sqlIn: (values) -> 124 | return "("+("'#{value}'" for value in values).join(',')+")" 125 | 126 | window.is_proxy = (document.location.host == "zero" or window.location.pathname == "/") 127 | window.Text = new Text() 128 | -------------------------------------------------------------------------------- /js/SiteAdd.coffee: -------------------------------------------------------------------------------- 1 | class SiteAdd extends Class 2 | constructor: -> 3 | @form = new Form() 4 | @db = {} 5 | @submitting = false 6 | return @ 7 | 8 | handleRadioLangClick: (e) => 9 | @form.data["language"] = e.currentTarget.value 10 | @form.invalid["language"] = false 11 | Page.projector.scheduleRender() 12 | return false 13 | 14 | handleRadioCategoryClick: (e) => 15 | @form.data["category"] = e.currentTarget.value 16 | @form.invalid["category"] = false 17 | Page.projector.scheduleRender() 18 | return false 19 | 20 | handleSubmit: => 21 | if not Page.site_info.cert_user_id 22 | Page.user.certSelect => 23 | @handleSubmit() 24 | return false 25 | 26 | if not @form.validate() 27 | return false 28 | 29 | # Only keep site address 30 | @form.data["address"] = @form.data["address"].match(/([A-Za-z0-9]{26,35}|[A-Za-z0-9\.-]{2,99}\.bit)(.*)/)[0] 31 | 32 | @submitting = true 33 | 34 | Page.user.getData (data) => 35 | row_site = @form.data 36 | row_site.date_added = Time.timestamp() 37 | row_site.site_id = row_site.date_added 38 | data.site.push(row_site) 39 | Page.user.save data, (res) => 40 | if res == "ok" 41 | @close() 42 | Page.head.active = "new" 43 | Page.setUrl("?Category:#{row_site.category}") 44 | setTimeout ( => 45 | @submitting = false 46 | @form.reset() 47 | Page.site_lists.update() 48 | ), 1000 49 | else 50 | @submitting = false 51 | 52 | return false 53 | 54 | updateDb: => 55 | @site_db = {} 56 | Page.cmd "dbQuery", "SELECT * FROM site", (res) => 57 | for row in res 58 | address = row.address.match(/([A-Za-z0-9]{26,35}|[A-Za-z0-9\.-]{2,99}\.bit)(.*)/)?[0] 59 | address = address.replace(/\/.*/, "") 60 | @site_db[address.toLowerCase()] = row 61 | 62 | shouldBeUniqueSite: (value) => 63 | address = value.match(/([A-Za-z0-9]{26,35}|[A-Za-z0-9\.-]{2,99}\.bit)(.*)/)[0] 64 | address = address.replace(/\/.*/, "") 65 | site = @site_db[address.toLowerCase()] 66 | @log address, @site_db 67 | if site 68 | return "This site is already submitted as #{site.title}!" 69 | else 70 | return null 71 | 72 | close: => 73 | Page.site_lists.state = null 74 | Page.projector.scheduleRender() 75 | 76 | render: -> 77 | h("div.form.form-siteadd", {updateAnimation: Animation.height, classes: {hidden: Page.site_lists.state != "siteadd"}}, [ 78 | h("div.formfield", 79 | @form.h("label.title", {for: "address"}, "Address"), 80 | @form.h("input.text", {type: "text", name: "address", placeholder: "e.g. http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", required: true, validate: [@form.shouldBeZite, @shouldBeUniqueSite]}), 81 | ), 82 | h("div.formfield", 83 | @form.h("label.title", {for: "title"}, "Title"), 84 | @form.h("input.text", {type: "text", name: "title", placeholder: "e.g. ZeroBlog", required: true}), 85 | ), 86 | h("div.formfield", 87 | @form.h("label.title", {for: "language"}, "Language"), 88 | @form.h("div.radiogroup.radiogroup-lang", {name: "language", value: @form.data.language, required: true}, [ 89 | Page.languages.map (lang) => 90 | [h("a.radio", {key: lang, href: "#"+lang, onclick: @handleRadioLangClick, value: lang, classes: {active: @form.data.language == lang}}, lang), " "] 91 | ]) 92 | ), 93 | h("div.formfield", 94 | @form.h("label.title", {for: "category"}, "Category"), 95 | @form.h("div.radiogroup", {name: "category", value: @form.data.category, required: true}, Page.categories.map ([id, category]) => 96 | [h("a.radio", {key: id, href: "#"+id, onclick: @handleRadioCategoryClick, value: id, classes: {active: @form.data.category == id}}, category), " "] 97 | ) 98 | ), 99 | h("div.formfield", 100 | @form.h("label.title", {for: "description"}, "Description"), 101 | @form.h("input.text", {type: "text", name: "description", placeholder: "e.g. ZeroNet changelog and related informations", required: true}), 102 | ), 103 | h("a.button.button-submit", {href: "#Submit", onclick: @handleSubmit}, "Submit") 104 | ]) 105 | 106 | window.SiteAdd = SiteAdd -------------------------------------------------------------------------------- /js/utils/Form.coffee: -------------------------------------------------------------------------------- 1 | class Form extends Class 2 | constructor: -> 3 | @hidden = false 4 | @attached = false 5 | @reset() 6 | return @ 7 | 8 | reset: => 9 | @data = {} 10 | @data_original = {} 11 | @inputs = {} 12 | @invalid = {} 13 | @nodes = {} 14 | @fields = [] 15 | 16 | setData: (data) => 17 | @data = data 18 | @data_original = JSON.parse(JSON.stringify(data)) 19 | 20 | handleInput: (e) => 21 | @data[e.target.name] = e.target.value 22 | @invalid[e.target.name] = false 23 | return false 24 | 25 | storeNode: (node) => 26 | if node.attributes.for?.value 27 | @nodes[node.attributes.for.value + "-label"] = node 28 | else 29 | @nodes[node.attributes.name.value] = node 30 | 31 | addField: (type, id, title, props) => 32 | if props.values and props.values.constructor == Array 33 | if props.values.length and props.values[0].constructor != Array 34 | props.values = ([key, key] for key in props.values) 35 | if props.values and props.values.constructor == Object 36 | props.values = ([key, val] for key, val of props.values) 37 | 38 | @fields.push({type: type, id: id, title: title, props: props}) 39 | 40 | h: (tag, props, childs) => 41 | @inputs[props.name] = [tag, props, childs] 42 | props.value ?= @data[props.name] 43 | props.id ?= props.name 44 | props.oninput ?= @handleInput 45 | props.afterCreate = @storeNode 46 | props.classes ?= {} 47 | if @invalid[props.name] or @invalid[props.for] 48 | props.classes.invalid = true 49 | else 50 | props.classes.invalid = false 51 | if @invalid[props.name] 52 | return [ 53 | h(tag, props, childs), 54 | h("div.invalid-reason", {key: "reason-#{props.name}", enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, @invalid[props.name]) 55 | ] 56 | else 57 | return h(tag, props, childs) 58 | 59 | shouldBeZite: (value) => 60 | if not value.match(/([A-Za-z0-9]{26,35}|[A-Za-z0-9\.-]{2,99}\.bit)/) 61 | return "Invalid site address: only ZeroNet addresses supported" 62 | 63 | validate: => 64 | valid = true 65 | @invalid = {} 66 | for name, [tag, props, childs] of @inputs 67 | if props.required and not props.value 68 | @invalid[name] = "This field is required" 69 | Animation.shake(@nodes[props.name]) 70 | valid = false 71 | else if props.validate 72 | if props.validate.constructor == Array 73 | for validate in props.validate 74 | field_error = validate(props.value) 75 | if field_error then break 76 | else 77 | field_error = props.validate(props.value) 78 | if field_error 79 | valid = false 80 | @invalid[name] = field_error 81 | 82 | else 83 | @invalid[name] = false 84 | 85 | Page.projector.scheduleRender() 86 | return valid 87 | 88 | handleCancelClick: => 89 | @hidden = true 90 | for key, val of @data_original 91 | @data[key] = val 92 | Page.projector.scheduleRender() 93 | return false 94 | 95 | handleSubmitClick: => 96 | if not @validate() 97 | return false 98 | @saveRow (res) => 99 | if res == "ok" 100 | @hidden = true 101 | Page.projector.scheduleRender() 102 | return false 103 | 104 | handleDeleteClick: => 105 | Page.cmd "wrapperConfirm", ["Are you sure you want to delete this item?", "Delete"], => 106 | @deleteRow (res) => 107 | if res == "ok" 108 | @hidden = true 109 | Page.projector.scheduleRender() 110 | return false 111 | 112 | handleRadioClick: (e) => 113 | id = e.currentTarget.attributes.for.value 114 | @data[id] = e.currentTarget.value 115 | @invalid[id] = false 116 | Page.projector.scheduleRender() 117 | return false 118 | 119 | renderField: (field) => 120 | props = field.props 121 | props.value = @data[field.id] 122 | props.name = field.id 123 | values = props.values 124 | if field.type == "radio" 125 | h("div.formfield", 126 | @h("label.title", {for: field.id}, field.title), 127 | @h("div.radiogroup", props, [ 128 | values.map ([key, value]) => 129 | [h("a.radio", {for: props.id, key: key, href: "#"+key, onclick: @handleRadioClick, value: key, classes: {active: @data[field.id] == key}}, value), " "] 130 | ]) 131 | ) 132 | else 133 | h("div.formfield", 134 | @h("label.title", {for: field.id}, field.title), 135 | @h("input.text", props) 136 | ) 137 | 138 | 139 | render: (classname="") => 140 | h("div.form-takeover-container#FormEdit", {afterCreate: Animation.show, classes: {hidden: @hidden}}, [ 141 | h("div.form.form-takeover#{classname}", {afterCreate: Animation.slideDown, exitAnimation: Animation.slideUp}, 142 | @fields.map @renderField 143 | h("a.cancel.link", {href: "#Cancel", onclick: @handleCancelClick}, "Cancel") 144 | if @deleteRow then h("a.button.button-submit.button-outline", {href: "#Delete", onclick: @handleDeleteClick}, "Delete") 145 | h("a.button.button-submit", {href: "#Modify", onclick: @handleSubmitClick}, "Modify") 146 | ) 147 | ]) 148 | 149 | window.Form = Form -------------------------------------------------------------------------------- /js/ZeroSites.coffee: -------------------------------------------------------------------------------- 1 | window.h = maquette.h 2 | 3 | class ZeroSites extends ZeroFrame 4 | init: -> 5 | @params = {} 6 | @site_info = null 7 | @server_info = null 8 | @history_state = {} 9 | 10 | @on_site_info = new Promise() 11 | @on_local_storage = new Promise() 12 | @on_loaded = new Promise() 13 | 14 | @user = new User() 15 | @on_site_info.then => 16 | @user.setAuthAddress(@site_info.auth_address) 17 | 18 | @local_storage = null 19 | @languages = [] 20 | @categories = [] 21 | @on_site_info.then => 22 | @languages = @site_info.content.settings.languages 23 | @categories = @site_info.content.settings.categories 24 | 25 | 26 | createProjector: -> 27 | @projector = maquette.createProjector() 28 | @head = new Head() 29 | @site_lists = new SiteLists() 30 | 31 | if base.href.indexOf("?") == -1 32 | @route("") 33 | else 34 | url = base.href.replace(/.*?\?/, "") 35 | @route(url) 36 | @history_state["url"] = url 37 | 38 | # Remove fake long body 39 | @on_loaded.then => 40 | @log "onloaded" 41 | window.requestAnimationFrame -> 42 | document.body.className = "loaded" 43 | 44 | @projector.replace($("#Head"), @head.render) 45 | @projector.replace($("#SiteLists"), @site_lists.render) 46 | @loadLocalStorage() 47 | 48 | # Update every minute to keep time since fields up-to date 49 | setInterval ( -> 50 | Page.projector.scheduleRender() 51 | ), 60*1000 52 | 53 | setFormEdit: (form_edit) -> 54 | form_edit.hidden = false 55 | @projector.replace($("#FormEdit"), form_edit.render) 56 | 57 | # Route site urls 58 | route: (query) -> 59 | @params = Text.parseQuery(query) 60 | [page, param] = @params.url.split(":") 61 | @content = @site_lists 62 | if page == "Category" 63 | @site_lists.setFilterCategory(parseInt(param)) 64 | else 65 | @site_lists.setFilterCategory(null) 66 | Page.projector.scheduleRender() 67 | @log "Route", page, param 68 | 69 | setUrl: (url, mode="push") -> 70 | url = url.replace(/.*?\?/, "") 71 | @log "setUrl", @history_state["url"], "->", url 72 | if @history_state["url"] == url 73 | @content.update() 74 | return false 75 | @history_state["url"] = url 76 | if mode == "replace" 77 | @cmd "wrapperReplaceState", [@history_state, "", url] 78 | else 79 | @cmd "wrapperPushState", [@history_state, "", url] 80 | @route url 81 | return false 82 | 83 | 84 | handleLinkClick: (e) => 85 | if e.which == 2 86 | # Middle click dont do anything 87 | return true 88 | else 89 | @log "save scrollTop", window.pageYOffset 90 | @history_state["scrollTop"] = window.pageYOffset 91 | @cmd "wrapperReplaceState", [@history_state, null] 92 | 93 | if document.body.scrollTop > 100 94 | anime({targets: document.body, scrollTop: 0, easing: "easeOutCubic", duration: 300}) 95 | 96 | @history_state["scrollTop"] = 0 97 | 98 | @on_loaded.resolved = false 99 | document.body.className = "" 100 | 101 | @setUrl e.currentTarget.search 102 | return false 103 | 104 | 105 | # Add/remove/change parameter to current site url 106 | createUrl: (key, val) -> 107 | params = JSON.parse(JSON.stringify(@params)) # Clone 108 | if typeof key == "Object" 109 | vals = key 110 | for key, val of keys 111 | params[key] = val 112 | else 113 | params[key] = val 114 | return "?"+Text.queryEncode(params) 115 | 116 | loadLocalStorage: -> 117 | @on_site_info.then => 118 | @log "Loading localstorage" 119 | @cmd "wrapperGetLocalStorage", [], (@local_storage) => 120 | @log "Loaded localstorage" 121 | @local_storage ?= {} 122 | @local_storage.filter_lang ?= {} 123 | @on_local_storage.resolve() 124 | 125 | saveLocalStorage: (cb) -> 126 | if @local_storage 127 | @cmd "wrapperSetLocalStorage", @local_storage, (res) => 128 | if cb then cb(res) 129 | 130 | 131 | onOpenWebsocket: (e) => 132 | @reloadSiteInfo() 133 | @reloadServerInfo() 134 | 135 | reloadSiteInfo: => 136 | @cmd "siteInfo", {}, (site_info) => 137 | @setSiteInfo(site_info) 138 | 139 | reloadServerInfo: => 140 | @cmd "serverInfo", {}, (server_info) => 141 | @setServerInfo(server_info) 142 | 143 | # Parse incoming requests from UiWebsocket server 144 | onRequest: (cmd, params) -> 145 | if cmd == "setSiteInfo" # Site updated 146 | @setSiteInfo(params) 147 | else if cmd == "wrapperPopState" # Site updated 148 | @log "wrapperPopState", params 149 | if params.state 150 | if not params.state.url 151 | params.state.url = params.href.replace /.*\?/, "" 152 | @on_loaded.resolved = false 153 | document.body.className = "" 154 | window.scroll(window.pageXOffset, params.state.scrollTop or 0) 155 | @route(params.state.url or "") 156 | else 157 | @log "Unknown command", cmd, params 158 | 159 | setSiteInfo: (site_info) -> 160 | @site_info = site_info 161 | @on_site_info.resolve() 162 | @site_lists.onSiteInfo(site_info) 163 | @user.onSiteInfo(site_info) 164 | @projector.scheduleRender() 165 | 166 | setServerInfo: (server_info) -> 167 | @server_info = server_info 168 | @projector.scheduleRender() 169 | 170 | # Simple return false to avoid link clicks 171 | returnFalse: -> 172 | return false 173 | 174 | window.Page = new ZeroSites() 175 | window.Page.createProjector() -------------------------------------------------------------------------------- /js/utils/Animation.coffee: -------------------------------------------------------------------------------- 1 | class Animation 2 | slideDown: (elem, props) -> 3 | h = elem.offsetHeight 4 | cstyle = window.getComputedStyle(elem) 5 | margin_top = cstyle.marginTop 6 | margin_bottom = cstyle.marginBottom 7 | padding_top = cstyle.paddingTop 8 | padding_bottom = cstyle.paddingBottom 9 | transition = cstyle.transition 10 | 11 | if elem.getBoundingClientRect().top > 1600 12 | # console.log "Skip down", elem 13 | return 14 | 15 | elem.style.boxSizing = "border-box" 16 | elem.style.overflow = "hidden" 17 | elem.style.transform = "scale(0.6)" 18 | elem.style.opacity = "0" 19 | elem.style.height = "0px" 20 | elem.style.marginTop = "0px" 21 | elem.style.marginBottom = "0px" 22 | elem.style.paddingTop = "0px" 23 | elem.style.paddingBottom = "0px" 24 | elem.style.transition = "none" 25 | 26 | setTimeout (-> 27 | elem.className += " animate-inout" 28 | elem.style.height = h+"px" 29 | elem.style.transform = "scale(1)" 30 | elem.style.opacity = "1" 31 | elem.style.marginTop = margin_top 32 | elem.style.marginBottom = margin_bottom 33 | elem.style.paddingTop = padding_top 34 | elem.style.paddingBottom = padding_bottom 35 | ), 50 36 | 37 | elem.addEventListener "transitionend", -> 38 | if elem.style.pointerEvents == "none" 39 | return 40 | elem.classList.remove("animate-inout") 41 | elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null 42 | elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null 43 | elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null 44 | elem.removeEventListener "transitionend", arguments.callee, false 45 | 46 | 47 | slideUp: (elem, remove_func, props) -> 48 | if elem.getBoundingClientRect().top > 1600 49 | remove_func() 50 | # console.log "Skip up", elem 51 | return 52 | 53 | elem.className += " animate-back" 54 | elem.style.boxSizing = "border-box" 55 | elem.style.height = elem.offsetHeight+"px" 56 | elem.style.overflow = "hidden" 57 | elem.style.transform = "scale(1)" 58 | elem.style.opacity = "1" 59 | elem.style.pointerEvents = "none" 60 | setTimeout (-> 61 | elem.style.height = "0px" 62 | elem.style.marginTop = "0px" 63 | elem.style.marginBottom = "0px" 64 | elem.style.paddingTop = "0px" 65 | elem.style.paddingBottom = "0px" 66 | elem.style.transform = "scale(0.8)" 67 | elem.style.borderTopWidth = "0px" 68 | elem.style.borderBottomWidth = "0px" 69 | elem.style.opacity = "0" 70 | ), 1 71 | elem.addEventListener "transitionend", (e) -> 72 | if e.propertyName == "opacity" or e.elapsedTime >= 0.6 73 | elem.removeEventListener "transitionend", arguments.callee, false 74 | setTimeout ( -> 75 | remove_func?() 76 | ), 2000 77 | 78 | 79 | showRight: (elem, props) -> 80 | elem.className += " animate" 81 | elem.style.opacity = 0 82 | elem.style.transform = "TranslateX(-20px) Scale(1.01)" 83 | setTimeout (-> 84 | elem.style.opacity = 1 85 | elem.style.transform = "TranslateX(0px) Scale(1)" 86 | ), 1 87 | elem.addEventListener "transitionend", -> 88 | elem.classList.remove("animate") 89 | elem.style.transform = elem.style.opacity = null 90 | 91 | show: (elem, props) -> 92 | delay = arguments[arguments.length-2]?.delay*1000 or 1 93 | elem.style.transition = "none" 94 | elem.style.opacity = 0 95 | setTimeout (-> 96 | elem.className += " animate" 97 | elem.style.opacity = 1 98 | ), 50 99 | elem.addEventListener "transitionend", (e) -> 100 | if e.propertyName == "opacity" or e.elapsedTime >= 0.3 101 | elem.classList.remove("animate") 102 | elem.style.opacity = null 103 | elem.style.transition = null 104 | elem.removeEventListener "transitionend", arguments.callee, false 105 | 106 | hide: (elem, remove_func, props) -> 107 | delay = arguments[arguments.length-2]?.delay*1000 or 1 108 | elem.className += " animate" 109 | setTimeout (-> 110 | elem.style.opacity = 0 111 | ), delay 112 | elem.addEventListener "transitionend", (e) -> 113 | if e.propertyName == "opacity" 114 | remove_func() 115 | elem.removeEventListener "transitionend", arguments.callee, false 116 | 117 | 118 | addVisibleClass: (elem, props) -> 119 | setTimeout -> 120 | elem.classList.add("visible") 121 | 122 | shake: (elem) -> 123 | elem.classList.remove("shake") 124 | setTimeout ( -> 125 | elem.classList.add("shake") 126 | ), 50 127 | 128 | height: (elem, props_old, props_new) => 129 | if props_old.classes.hidden == props_new.classes.hidden 130 | return 131 | 132 | if elem.className.indexOf("hidden") == -1 133 | elem.style.height = "auto" 134 | elem.style.transition = "none" 135 | elem.style.paddingTop = elem.style.paddingBottom = null 136 | h = elem.offsetHeight 137 | elem.style.paddingTop = elem.style.paddingBottom = "0px" 138 | elem.style.height = "0px" 139 | setTimeout ( -> 140 | elem.style.transition = null 141 | ), 1 142 | setTimeout ( -> 143 | elem.style.height = h+"px" 144 | elem.style.paddingTop = elem.style.paddingBottom = null 145 | elem.addEventListener "transitionend", (e) -> 146 | if e.propertyName == "height" or e.elapsedTime >= 0.6 147 | elem.removeEventListener "transitionend", arguments.callee, false 148 | if elem.style.height == h+"px" 149 | elem.style.height = "auto" 150 | ), 10 151 | else 152 | elem.style.height = elem.offsetHeight+"px" 153 | setTimeout ( -> 154 | elem.style.height = "0px" 155 | elem.style.paddingTop = elem.style.paddingBottom = "0px" 156 | ) 157 | 158 | 159 | window.Animation = new Animation() -------------------------------------------------------------------------------- /js/SiteLists.coffee: -------------------------------------------------------------------------------- 1 | class SiteLists extends Class 2 | constructor: -> 3 | @menu_filters = new Menu() 4 | @state = null 5 | @filter_lang = {} 6 | @site_add = new SiteAdd() 7 | @site_lists = [] 8 | @site_lists_db = {} 9 | @need_update = false 10 | 11 | @loaded = false 12 | @num_total = null 13 | 14 | Page.on_site_info.then => 15 | Page.on_local_storage.then => 16 | @filter_lang = Page.local_storage.filter_lang 17 | for [id, title] in Page.site_info.content.settings.categories 18 | site_list = new SiteList({id: id, title: title, sites: []}) 19 | @site_lists_db[id] = site_list 20 | @site_lists.push(site_list) 21 | @update() 22 | 23 | window.onresize = => 24 | if window.innerWidth < 720 25 | @cols = 1 26 | else if window.innerWidth < 1200 27 | @cols = 2 28 | else 29 | @cols = 3 30 | @log "Cols: #{@cols}" 31 | Page.projector.scheduleRender() 32 | window.onresize() 33 | 34 | update: -> 35 | if Page.head.active == "new" 36 | order = "date_added DESC" 37 | else 38 | order = "MIN(200, peers) + star * 20 DESC, title" 39 | 40 | filters = [] 41 | if not isEmpty(@filter_lang) 42 | filters.push "language IN " + Text.sqlIn(lang for lang of @filter_lang) 43 | 44 | query = """ 45 | SELECT site.*, json.*, COUNT(site_star.site_uri) AS star, site_stat.* 46 | FROM site 47 | LEFT JOIN json USING (json_id) 48 | LEFT JOIN site_star ON (site_star.site_uri = json.directory || "_" || site.site_id) 49 | LEFT JOIN site_stat ON (site_stat.site_uri = json.directory || "_" || site.site_id) 50 | #{if filters.length then "WHERE " + filters.join(" AND ") else ""} 51 | GROUP BY site.json_id, site_id 52 | ORDER BY #{order} 53 | """ 54 | @logStart("Sites") 55 | Page.cmd "dbQuery", query, (rows) => 56 | sites_db = {} 57 | # Group by category 58 | for row in rows 59 | sites_db[row["category"]] ?= [] 60 | sites_db[row["category"]].push(row) 61 | 62 | # Sync items 63 | for category, site_list of @site_lists_db 64 | site_list.item_list.sync(sites_db[category] or []) 65 | 66 | @loaded = true 67 | @num_total = rows.length 68 | @logEnd "Sites", "found: #{@num_total}" 69 | Page.on_loaded.resolve() 70 | 71 | Page.projector.scheduleRender() 72 | 73 | handleFilterLanguageClick: (e) => 74 | value = e.currentTarget.value 75 | if value == "all" 76 | for key of @filter_lang 77 | delete @filter_lang[key] 78 | else if @filter_lang[value] 79 | delete @filter_lang[value] 80 | else 81 | @filter_lang[value] = true 82 | Page.saveLocalStorage() 83 | Page.projector.scheduleRender() 84 | @update() 85 | return false 86 | 87 | renderFilterLanguage: => 88 | h("div.menu-radio", 89 | h("div", "Site languages: "), 90 | h("a.all", {href: "#all", onclick: @handleFilterLanguageClick, value: "all", classes: {selected: isEmpty(@filter_lang)}}, "Show all") 91 | for lang in Page.languages 92 | [ 93 | h("a", {href: "#"+lang, onclick: @handleFilterLanguageClick, value: lang, classes: {selected: @filter_lang[lang], long: lang.length > 2}}, lang), 94 | " " 95 | ] 96 | ) 97 | 98 | handleFiltersClick: => 99 | @menu_filters.items = [] 100 | @menu_filters.items.push [@renderFilterLanguage, null ] 101 | if @menu_filters.visible 102 | @menu_filters.hide() 103 | else 104 | @menu_filters.show() 105 | return false 106 | 107 | handleSiteAddClick: => 108 | if @state == "siteadd" 109 | @state = null 110 | else 111 | @state = "siteadd" 112 | @site_add.updateDb() 113 | return false 114 | 115 | formatFilterTitle: => 116 | if isEmpty(@filter_lang) 117 | return "None" 118 | else 119 | return (lang for lang, _ of @filter_lang).join(", ") 120 | 121 | setFilterCategory: (@filter_category) => 122 | if @loaded 123 | setTimeout ( => 124 | Page.on_loaded.resolve() 125 | ), 600 126 | 127 | getVisibleSiteLists: => 128 | if @filter_category 129 | return [@site_lists_db[@filter_category]] 130 | else 131 | return @site_lists 132 | 133 | render: => 134 | if @need_update 135 | @need_update = false 136 | @update() 137 | i = 0 138 | 139 | num_found = 0 140 | for site_list in @site_lists 141 | if not site_list.isHidden() 142 | num_found += site_list.sites.length 143 | 144 | h("div#SiteLists", {classes: {"state-siteadd": @state == "siteadd"}}, 145 | if @loaded then h("div.sitelists-right", [ 146 | if Page.site_info?.cert_user_id 147 | h("a.certselect.right-link", {href: "#Select", onclick: Page.user.certSelect}, [ 148 | h("span.symbol", "⎔"), 149 | h("span.title", "User: #{Page.site_info.cert_user_id}") 150 | ]) 151 | h("a.filter.right-link", {href: "#Filters", onmousedown: @handleFiltersClick, onclick: Page.returnFalse}, [ 152 | h("span.symbol", "◇"), 153 | h("span.title", "Filter: " + @formatFilterTitle()) 154 | ]) 155 | @menu_filters.render(".filter") 156 | h("a.siteadd.right-link", {href: "#", onclick: @handleSiteAddClick}, [ 157 | h("span.symbol", "+"), 158 | h("span.title", "Submit new site") 159 | ]) 160 | ]) 161 | @site_add.render(), 162 | if num_found == 0 and not isEmpty(@filter_lang) 163 | h("h1.empty", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, "No sites found for languages: #{(lang for lang of @filter_lang).join(', ')}") 164 | if @loaded then h("div.sitelists", @site_lists.map (site_list) -> 165 | if site_list.sites.length 166 | i++ 167 | num_found += site_list.sites.length 168 | site_list.render(i) 169 | ) 170 | h("div.clear", " ") 171 | ) 172 | 173 | onSiteInfo: (site_info) -> 174 | if site_info.event 175 | [action, inner_path] = site_info.event 176 | if action == "file_done" and inner_path.endsWith("json") 177 | RateLimit 1000, => 178 | @need_update = true 179 | Page.projector.scheduleRender() 180 | 181 | window.SiteLists = SiteLists -------------------------------------------------------------------------------- /css/ZeroSites.css: -------------------------------------------------------------------------------- 1 | body { background-color: #ECF1F5; font-family: Roboto; margin: 0px; padding: 0px; backface-visibility: hidden; overflow-x: hidden; height: 35000px } 2 | body.loaded { height: 100%; overflow: auto } 3 | 4 | .link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s } 5 | .link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none; } 6 | 7 | .clear { clear: both } 8 | 9 | h1 { text-align: center; font-family: Roboto; margin-top: 100px; font-weight: lighter; } 10 | 11 | #SiteLists { max-width: 1600px; margin: 0px auto; padding: 20px; position: relative; clear: both; margin-bottom: 100px } 12 | #SiteLists .menu { margin-top: 30px; width: 350px; } 13 | #SiteLists .menu .all { width: 97.8%; margin-left: 7px } 14 | #SiteLists .sitelists { transition: all 0.3s } 15 | 16 | #SiteLists .sitelist { transition: all 0.9s cubic-bezier(0.77, 0, 0.175, 1); overflow: hidden; opacity: 1; transform: scale(1); transform-origin: left top; white-space: nowrap; } 17 | #SiteLists .sitelist.empty { width: 0px; padding: 0px; margin: 0px; opacity: 0; transform: scale(0); border-width: 0px; } 18 | 19 | #SiteLists.state-siteadd .sitelists { opacity: 0.1; pointer-events: none } 20 | .form-siteadd { 21 | overflow: hidden; ransform-origin: top; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1); text-align: right; 22 | width: 100%; background-color: white; padding: 0px 30px; margin-left: auto; height: 0px; transform-origin: top; transform: rotateX(0deg); 23 | margin-right: 1px; box-sizing: border-box; box-shadow: 0px 0px 35px #CCC; max-width: 700px; padding-top: 10px; padding-bottom: 20px; 24 | } 25 | .form-siteadd.hidden { padding-top: 0px; padding-bottom: 0px; height: 0px; transform: rotateX(-45deg); } 26 | /*#SiteLists.state-siteadd .form-siteadd { max-height: 475px; padding-top: 10px; padding-bottom: 20px; }*/ 27 | 28 | .sitelists-right { position: absolute; right: 0px; top: -17px; text-transform: uppercase; color: #888787; font-size: 14px; letter-spacing: 0.5px; text-align: right } 29 | .sitelists-right .right-link { margin-right: 37px; text-decoration: none; color: inherit } 30 | .sitelists-right .filter .symbol, .sitelists-right .certselect .symbol { margin-right: 10px; } 31 | .sitelists-right .siteadd .symbol { margin-right: 10px; background-color: #FFF; padding: 3px; border-radius: 25px; color: #C4C7CA; width: 16px; height: 16px; display: inline-block; text-align: center; transition: all 0.3s } 32 | .sitelists-right .title { border-bottom: 2px solid rgba(0,0,0,0); transition: all 0.3s; } 33 | .sitelists-right .right-link:hover .symbol { color: #ee0a73; transition: none } 34 | .sitelists-right .right-link:hover .title { border-bottom: 2px solid #ee0a73; color: #ee0a73; transition: none } 35 | 36 | .sitelist { 37 | width: 33.33%; background-color: white; padding: 10px; border-radius: 3px; border: 1px solid #E8EDF1; box-sizing: border-box; text-align: left; 38 | box-shadow: 2px 5px 11px -3px rgba(0,0,0,0.1); display: block; vertical-align: top; margin-bottom: 10px; margin-left: -1px; max-height: 900px; float: left; 39 | } 40 | .sitelist-wide { clear: both; } 41 | .sitelist-wide .more { text-align: center; display: block; color: #ee0a73; clear: both; padding: 20px; transform: translateY(0px); transition: all 0.3s; } 42 | .sitelist-wide .sitelist { border-left-color: transparent; max-height: initial } 43 | .sitelist.col-1 .categoryname, .sitelist.col-2 .categoryname { visibility: hidden } 44 | 45 | .sitelist .categoryname { font-weight: 100; font-size: 24px; margin: 12px; color: #E91E63; display: block; text-decoration: none } 46 | .sitelist .more { 47 | text-align: center; display: block; color: #ee0a73; text-decoration: none; padding: 10px; 48 | transform: translateY(0px); transition: all 0.3s; box-shadow: 0px -20px 75px 17px #FFF; 49 | } 50 | .sitelist .more:hover { text-decoration: underline; transform: translateY(3px); box-shadow: 0px 0px 75px 17px #FFF; } 51 | 52 | .sitelist-wide .more:hover { text-decoration: underline; transform: translateY(3px); } 53 | 54 | .sitelist.hidden { width: 0px; opacity: 0 !important; padding: 0px; border-width: 0px; max-height: 0px !important; margin: 0px } 55 | .sitelist.selected { width: 60%; margin-left: 20%; max-height: none } 56 | .sitelist.nolimit { max-height: none } 57 | 58 | .site { padding: 15px; border-top: 1px solid #EEE; display: block; text-decoration: none; color: #333; transition: all 0.3s; white-space: nowrap; font-size: 90%; backface-visibility: hidden } 59 | /*.site.my { border-left: 4px solid #e6f2ff; }*/ 60 | .site:hover { background-color: #F8FAFB; transition: none } 61 | .site:active { transform: translateY(1px); transition: none; box-shadow: inset 0px 7px 25px -5px #E8E8E8; } 62 | .site:hover .go { color: #666 } 63 | .site .title { 64 | font-weight: bold; font-size: 120%; margin-bottom: 4px; display: inline-block; overflow: hidden; 65 | white-space: nowrap; max-width: 88%; text-overflow: ellipsis; border-bottom: 1px solid #FFF; line-height: 22px; 66 | } 67 | .site:hover .title { border-color: #F8FAFB } 68 | .site:visited .title { /*border-color: #cbcfff;*/ color: #888; } 69 | .site .description { 70 | color: #555; font-size: 90%; text-overflow: ellipsis; overflow: hidden; line-height: 15px; 71 | /*-webkit-line-clamp: 2; -webkit-box-orient: vertical; display: -webkit-box; white-space: pre-line; max-height: 37px;*/ 72 | } 73 | .site .go { float: right; font-size: 163%; line-height: 43px; vertical-align: middle; color: #D3D9DE } 74 | .site .tag { display: inline-block; text-transform: uppercase; font-size: 11px; margin-left: 10px; padding: 3px 10px; border-radius: 12px; vertical-align: 12px; text-decoration: none } 75 | .site .tag-new { color: #2ecc71; border: 1px solid #2ECC71; } 76 | .site .tag-popular { color: #E91E63; border: 1px solid #F48FB1; } 77 | .site.my:hover .tag-my { z-index: 999; position: relative; background-color: white; } 78 | .site .tag-my { color: #5d68ff; border: 1px solid #cbcfff } 79 | .site .tag-my:hover { text-decoration: underline; } 80 | 81 | .site .right { float: right; background-color: white; position: relative; transition: all 0.3s; text-align: right; padding-right: 15px; margin-right: -15px; } 82 | .site:hover .right { background-color: inherit; transition: none } 83 | .site .star, .site .comments { text-decoration: none; color: #DDD; font-size: 19px; padding: 9px; margin-top: -12px; margin-right: -10px; transition: all 0.3s; display: block } 84 | .site .star:hover, .site .comments:hover { color: #FF9800; transition: none } 85 | .site .star:hover .icon { opacity: 1; transition: none } 86 | .site.starred .star .icon { filter: none } 87 | .site .star:hover .num, .site .comments:hover .num { color: #111; transition: none } 88 | .site .star .num, .site .comments .num { font-size: 14px; vertical-align: 1px; margin-right: 3px; color: #666; font-weight: lighter; transition: all 0.3s } 89 | .site .star .icon { margin-left: 6px; transition: all 0.3s; opacity: 0.6 } 90 | .site .comments { padding-bottom: 0px; display: none } 91 | .site .comments .icon { opacity: 0.4; transition: all 0.3s; vertical-align: -4px; margin-left: 7px; } 92 | .site .comments:hover .icon { opacity: 1; transition: none; filter: none; } 93 | .site.nocomment .comments { opacity: 0 } 94 | .site.nocomment:hover .comments { opacity: 1 } 95 | .site .peers { padding-bottom: 0px } 96 | .site .peers .icon { opacity: 0.4; transition: all 0.3s; vertical-align: -4px; margin-left: 11px; } 97 | .site .peers .num { color: #666; font-weight: lighter; font-size: 14px; } 98 | 99 | 100 | @media only screen and (max-width: 1200px) { 101 | .sitelist { width: 50% } 102 | .sitelists-right { position: relative; } 103 | .sitelist.selected { width: 99.8%; margin-left: 0%; } 104 | } 105 | @media only screen and (max-width: 720px) { 106 | .sitelist { width: 100% } 107 | .sitelists-right { margin-top: 15px; } 108 | .right-link { display: block; margin-bottom: 5px } 109 | } -------------------------------------------------------------------------------- /js/lib/anime.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Anime v1.0.0 3 | * http://anime-js.com 4 | * JavaScript animation engine 5 | * Copyright (c) 2016 Julian Garnier 6 | * http://juliangarnier.com 7 | * Released under the MIT license 8 | */ 9 | (function(r,n){"function"===typeof define&&define.amd?define([],n):"object"===typeof module&&module.exports?module.exports=n():r.anime=n()})(this,function(){var r={duration:1E3,delay:0,loop:!1,autoplay:!0,direction:"normal",easing:"easeOutElastic",elasticity:400,round:!1,begin:void 0,update:void 0,complete:void 0},n="translateX translateY translateZ rotate rotateX rotateY rotateZ scale scaleX scaleY scaleZ skewX skewY".split(" "),e=function(){return{array:function(a){return Array.isArray(a)},object:function(a){return-1< 10 | Object.prototype.toString.call(a).indexOf("Object")},html:function(a){return a instanceof NodeList||a instanceof HTMLCollection},node:function(a){return a.nodeType},svg:function(a){return a instanceof SVGElement},number:function(a){return!isNaN(parseInt(a))},string:function(a){return"string"===typeof a},func:function(a){return"function"===typeof a},undef:function(a){return"undefined"===typeof a},"null":function(a){return"null"===typeof a},hex:function(a){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a)}, 11 | rgb:function(a){return/^rgb/.test(a)},rgba:function(a){return/^rgba/.test(a)},hsl:function(a){return/^hsl/.test(a)},color:function(a){return e.hex(a)||e.rgb(a)||e.rgba(a)||e.hsl(a)}}}(),z=function(){var a={},b={Sine:function(a){return 1-Math.cos(a*Math.PI/2)},Circ:function(a){return 1-Math.sqrt(1-a*a)},Elastic:function(a,b){if(0===a||1===a)return a;var f=1-Math.min(b,998)/1E3,h=a/1-1;return-(Math.pow(2,10*h)*Math.sin(2*(h-f/(2*Math.PI)*Math.asin(1))*Math.PI/f))},Back:function(a){return a*a*(3*a-2)}, 12 | Bounce:function(a){for(var b,f=4;a<((b=Math.pow(2,--f))-1)/11;);return 1/Math.pow(4,3-f)-7.5625*Math.pow((3*b-2)/22-a,2)}};["Quad","Cubic","Quart","Quint","Expo"].forEach(function(a,d){b[a]=function(a){return Math.pow(a,d+2)}});Object.keys(b).forEach(function(c){var d=b[c];a["easeIn"+c]=d;a["easeOut"+c]=function(a,b){return 1-d(1-a,b)};a["easeInOut"+c]=function(a,b){return.5>a?d(2*a,b)/2:1-d(-2*a+2,b)/2}});a.linear=function(a){return a};return a}(),u=function(a){return e.string(a)?a:a+""},A=function(a){return a.replace(/([a-z])([A-Z])/g, 13 | "$1-$2").toLowerCase()},B=function(a){if(e.color(a))return!1;try{return document.querySelectorAll(a)}catch(b){return!1}},v=function(a){return a.reduce(function(a,c){return a.concat(e.array(c)?v(c):c)},[])},p=function(a){if(e.array(a))return a;e.string(a)&&(a=B(a)||a);return e.html(a)?[].slice.call(a):[a]},C=function(a,b){return a.some(function(a){return a===b})},N=function(a,b){var c={};a.forEach(function(a){var f=JSON.stringify(b.map(function(b){return a[b]}));c[f]=c[f]||[];c[f].push(a)});return Object.keys(c).map(function(a){return c[a]})}, 14 | D=function(a){return a.filter(function(a,c,d){return d.indexOf(a)===c})},w=function(a){var b={},c;for(c in a)b[c]=a[c];return b},t=function(a,b){for(var c in b)a[c]=e.undef(a[c])?b[c]:a[c];return a},O=function(a){a=a.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(a,b,c,e){return b+b+c+c+e+e});var b=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(a);a=parseInt(b[1],16);var c=parseInt(b[2],16),b=parseInt(b[3],16);return"rgb("+a+","+c+","+b+")"},P=function(a){a=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(a); 15 | var b=parseInt(a[1])/360,c=parseInt(a[2])/100,d=parseInt(a[3])/100;a=function(a,b,c){0>c&&(c+=1);1c?b:c<2/3?a+(b-a)*(2/3-c)*6:a};if(0==c)c=d=b=d;else var f=.5>d?d*(1+c):d+c-d*c,h=2*d-f,c=a(h,f,b+1/3),d=a(h,f,b),b=a(h,f,b-1/3);return"rgb("+255*c+","+255*d+","+255*b+")"},k=function(a){return/([\+\-]?[0-9|auto\.]+)(%|px|pt|em|rem|in|cm|mm|ex|pc|vw|vh|deg)?/.exec(a)[2]},E=function(a,b,c){return k(b)?b:-1=a.delay&&(a.begin(b),a.begin=void 0);c.current>=b.duration?(a.loop?(c.start=+new Date,"alternate"===a.direction&&y(b,!0),e.number(a.loop)&& 25 | a.loop--,c.raf=requestAnimationFrame(c.tick)):(b.ended=!0,a.complete&&a.complete(b),b.pause()),c.last=0):c.raf=requestAnimationFrame(c.tick)}}};b.seek=function(a){L(b,a/100*b.duration)};b.pause=function(){b.running=!1;cancelAnimationFrame(c.raf);X(b);var a=m.indexOf(b);-1 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /js/lib/maquette.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['exports'], factory); 5 | } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { 6 | // CommonJS 7 | factory(exports); 8 | } else { 9 | // Browser globals 10 | factory(root.maquette = {}); 11 | } 12 | }(this, function (exports) { 13 | 'use strict'; 14 | ; 15 | ; 16 | ; 17 | ; 18 | var NAMESPACE_W3 = 'http://www.w3.org/'; 19 | var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; 20 | var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; 21 | // Utilities 22 | var emptyArray = []; 23 | var extend = function (base, overrides) { 24 | var result = {}; 25 | Object.keys(base).forEach(function (key) { 26 | result[key] = base[key]; 27 | }); 28 | if (overrides) { 29 | Object.keys(overrides).forEach(function (key) { 30 | result[key] = overrides[key]; 31 | }); 32 | } 33 | return result; 34 | }; 35 | // Hyperscript helper functions 36 | var same = function (vnode1, vnode2) { 37 | if (vnode1.vnodeSelector !== vnode2.vnodeSelector) { 38 | return false; 39 | } 40 | if (vnode1.properties && vnode2.properties) { 41 | if (vnode1.properties.key !== vnode2.properties.key) { 42 | return false; 43 | } 44 | return vnode1.properties.bind === vnode2.properties.bind; 45 | } 46 | return !vnode1.properties && !vnode2.properties; 47 | }; 48 | var toTextVNode = function (data) { 49 | return { 50 | vnodeSelector: '', 51 | properties: undefined, 52 | children: undefined, 53 | text: data.toString(), 54 | domNode: null 55 | }; 56 | }; 57 | var appendChildren = function (parentSelector, insertions, main) { 58 | for (var i = 0; i < insertions.length; i++) { 59 | var item = insertions[i]; 60 | if (Array.isArray(item)) { 61 | appendChildren(parentSelector, item, main); 62 | } else { 63 | if (item !== null && item !== undefined) { 64 | if (!item.hasOwnProperty('vnodeSelector')) { 65 | item = toTextVNode(item); 66 | } 67 | main.push(item); 68 | } 69 | } 70 | } 71 | }; 72 | // Render helper functions 73 | var missingTransition = function () { 74 | throw new Error('Provide a transitions object to the projectionOptions to do animations'); 75 | }; 76 | var DEFAULT_PROJECTION_OPTIONS = { 77 | namespace: undefined, 78 | eventHandlerInterceptor: undefined, 79 | styleApplyer: function (domNode, styleName, value) { 80 | // Provides a hook to add vendor prefixes for browsers that still need it. 81 | domNode.style[styleName] = value; 82 | }, 83 | transitions: { 84 | enter: missingTransition, 85 | exit: missingTransition 86 | } 87 | }; 88 | var applyDefaultProjectionOptions = function (projectorOptions) { 89 | return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions); 90 | }; 91 | var checkStyleValue = function (styleValue) { 92 | if (typeof styleValue !== 'string') { 93 | throw new Error('Style values must be strings'); 94 | } 95 | }; 96 | var setProperties = function (domNode, properties, projectionOptions) { 97 | if (!properties) { 98 | return; 99 | } 100 | var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor; 101 | var propNames = Object.keys(properties); 102 | var propCount = propNames.length; 103 | for (var i = 0; i < propCount; i++) { 104 | var propName = propNames[i]; 105 | /* tslint:disable:no-var-keyword: edge case */ 106 | var propValue = properties[propName]; 107 | /* tslint:enable:no-var-keyword */ 108 | if (propName === 'className') { 109 | throw new Error('Property "className" is not supported, use "class".'); 110 | } else if (propName === 'class') { 111 | if (domNode.className) { 112 | // May happen if classes is specified before class 113 | domNode.className += ' ' + propValue; 114 | } else { 115 | domNode.className = propValue; 116 | } 117 | } else if (propName === 'classes') { 118 | // object with string keys and boolean values 119 | var classNames = Object.keys(propValue); 120 | var classNameCount = classNames.length; 121 | for (var j = 0; j < classNameCount; j++) { 122 | var className = classNames[j]; 123 | if (propValue[className]) { 124 | domNode.classList.add(className); 125 | } 126 | } 127 | } else if (propName === 'styles') { 128 | // object with string keys and string (!) values 129 | var styleNames = Object.keys(propValue); 130 | var styleCount = styleNames.length; 131 | for (var j = 0; j < styleCount; j++) { 132 | var styleName = styleNames[j]; 133 | var styleValue = propValue[styleName]; 134 | if (styleValue) { 135 | checkStyleValue(styleValue); 136 | projectionOptions.styleApplyer(domNode, styleName, styleValue); 137 | } 138 | } 139 | } else if (propName === 'key') { 140 | continue; 141 | } else if (propValue === null || propValue === undefined) { 142 | continue; 143 | } else { 144 | var type = typeof propValue; 145 | if (type === 'function') { 146 | if (propName.lastIndexOf('on', 0) === 0) { 147 | if (eventHandlerInterceptor) { 148 | propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers 149 | } 150 | if (propName === 'oninput') { 151 | (function () { 152 | // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput 153 | var oldPropValue = propValue; 154 | propValue = function (evt) { 155 | evt.target['oninput-value'] = evt.target.value; 156 | // may be HTMLTextAreaElement as well 157 | oldPropValue.apply(this, [evt]); 158 | }; 159 | }()); 160 | } 161 | domNode[propName] = propValue; 162 | } 163 | } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') { 164 | if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { 165 | domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); 166 | } else { 167 | domNode.setAttribute(propName, propValue); 168 | } 169 | } else { 170 | domNode[propName] = propValue; 171 | } 172 | } 173 | } 174 | }; 175 | var updateProperties = function (domNode, previousProperties, properties, projectionOptions) { 176 | if (!properties) { 177 | return; 178 | } 179 | var propertiesUpdated = false; 180 | var propNames = Object.keys(properties); 181 | var propCount = propNames.length; 182 | for (var i = 0; i < propCount; i++) { 183 | var propName = propNames[i]; 184 | // assuming that properties will be nullified instead of missing is by design 185 | var propValue = properties[propName]; 186 | var previousValue = previousProperties[propName]; 187 | if (propName === 'class') { 188 | if (previousValue !== propValue) { 189 | throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.'); 190 | } 191 | } else if (propName === 'classes') { 192 | var classList = domNode.classList; 193 | var classNames = Object.keys(propValue); 194 | var classNameCount = classNames.length; 195 | for (var j = 0; j < classNameCount; j++) { 196 | var className = classNames[j]; 197 | var on = !!propValue[className]; 198 | var previousOn = !!previousValue[className]; 199 | if (on === previousOn) { 200 | continue; 201 | } 202 | propertiesUpdated = true; 203 | if (on) { 204 | classList.add(className); 205 | } else { 206 | classList.remove(className); 207 | } 208 | } 209 | } else if (propName === 'styles') { 210 | var styleNames = Object.keys(propValue); 211 | var styleCount = styleNames.length; 212 | for (var j = 0; j < styleCount; j++) { 213 | var styleName = styleNames[j]; 214 | var newStyleValue = propValue[styleName]; 215 | var oldStyleValue = previousValue[styleName]; 216 | if (newStyleValue === oldStyleValue) { 217 | continue; 218 | } 219 | propertiesUpdated = true; 220 | if (newStyleValue) { 221 | checkStyleValue(newStyleValue); 222 | projectionOptions.styleApplyer(domNode, styleName, newStyleValue); 223 | } else { 224 | projectionOptions.styleApplyer(domNode, styleName, ''); 225 | } 226 | } 227 | } else { 228 | if (!propValue && typeof previousValue === 'string') { 229 | propValue = ''; 230 | } 231 | if (propName === 'value') { 232 | if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) { 233 | domNode[propName] = propValue; 234 | // Reset the value, even if the virtual DOM did not change 235 | domNode['oninput-value'] = undefined; 236 | } 237 | // else do not update the domNode, otherwise the cursor position would be changed 238 | if (propValue !== previousValue) { 239 | propertiesUpdated = true; 240 | } 241 | } else if (propValue !== previousValue) { 242 | var type = typeof propValue; 243 | if (type === 'function') { 244 | throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.'); 245 | } 246 | if (type === 'string' && propName !== 'innerHTML') { 247 | if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { 248 | domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); 249 | } else { 250 | domNode.setAttribute(propName, propValue); 251 | } 252 | } else { 253 | if (domNode[propName] !== propValue) { 254 | domNode[propName] = propValue; 255 | } 256 | } 257 | propertiesUpdated = true; 258 | } 259 | } 260 | } 261 | return propertiesUpdated; 262 | }; 263 | var findIndexOfChild = function (children, sameAs, start) { 264 | if (sameAs.vnodeSelector !== '') { 265 | // Never scan for text-nodes 266 | for (var i = start; i < children.length; i++) { 267 | if (same(children[i], sameAs)) { 268 | return i; 269 | } 270 | } 271 | } 272 | return -1; 273 | }; 274 | var nodeAdded = function (vNode, transitions) { 275 | if (vNode.properties) { 276 | var enterAnimation = vNode.properties.enterAnimation; 277 | if (enterAnimation) { 278 | if (typeof enterAnimation === 'function') { 279 | enterAnimation(vNode.domNode, vNode.properties); 280 | } else { 281 | transitions.enter(vNode.domNode, vNode.properties, enterAnimation); 282 | } 283 | } 284 | } 285 | }; 286 | var nodeToRemove = function (vNode, transitions) { 287 | var domNode = vNode.domNode; 288 | if (vNode.properties) { 289 | var exitAnimation = vNode.properties.exitAnimation; 290 | if (exitAnimation) { 291 | domNode.style.pointerEvents = 'none'; 292 | var removeDomNode = function () { 293 | if (domNode.parentNode) { 294 | domNode.parentNode.removeChild(domNode); 295 | } 296 | }; 297 | if (typeof exitAnimation === 'function') { 298 | exitAnimation(domNode, removeDomNode, vNode.properties); 299 | return; 300 | } else { 301 | transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode); 302 | return; 303 | } 304 | } 305 | } 306 | if (domNode.parentNode) { 307 | domNode.parentNode.removeChild(domNode); 308 | } 309 | }; 310 | var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) { 311 | var childNode = childNodes[indexToCheck]; 312 | if (childNode.vnodeSelector === '') { 313 | return; // Text nodes need not be distinguishable 314 | } 315 | var properties = childNode.properties; 316 | var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined; 317 | if (!key) { 318 | for (var i = 0; i < childNodes.length; i++) { 319 | if (i !== indexToCheck) { 320 | var node = childNodes[i]; 321 | if (same(node, childNode)) { 322 | if (operation === 'added') { 323 | throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.'); 324 | } else { 325 | throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.'); 326 | } 327 | } 328 | } 329 | } 330 | } 331 | }; 332 | var createDom; 333 | var updateDom; 334 | var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) { 335 | if (oldChildren === newChildren) { 336 | return false; 337 | } 338 | oldChildren = oldChildren || emptyArray; 339 | newChildren = newChildren || emptyArray; 340 | var oldChildrenLength = oldChildren.length; 341 | var newChildrenLength = newChildren.length; 342 | var transitions = projectionOptions.transitions; 343 | var oldIndex = 0; 344 | var newIndex = 0; 345 | var i; 346 | var textUpdated = false; 347 | while (newIndex < newChildrenLength) { 348 | var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined; 349 | var newChild = newChildren[newIndex]; 350 | if (oldChild !== undefined && same(oldChild, newChild)) { 351 | textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated; 352 | oldIndex++; 353 | } else { 354 | var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1); 355 | if (findOldIndex >= 0) { 356 | // Remove preceding missing children 357 | for (i = oldIndex; i < findOldIndex; i++) { 358 | nodeToRemove(oldChildren[i], transitions); 359 | checkDistinguishable(oldChildren, i, vnode, 'removed'); 360 | } 361 | textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated; 362 | oldIndex = findOldIndex + 1; 363 | } else { 364 | // New child 365 | createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions); 366 | nodeAdded(newChild, transitions); 367 | checkDistinguishable(newChildren, newIndex, vnode, 'added'); 368 | } 369 | } 370 | newIndex++; 371 | } 372 | if (oldChildrenLength > oldIndex) { 373 | // Remove child fragments 374 | for (i = oldIndex; i < oldChildrenLength; i++) { 375 | nodeToRemove(oldChildren[i], transitions); 376 | checkDistinguishable(oldChildren, i, vnode, 'removed'); 377 | } 378 | } 379 | return textUpdated; 380 | }; 381 | var addChildren = function (domNode, children, projectionOptions) { 382 | if (!children) { 383 | return; 384 | } 385 | for (var i = 0; i < children.length; i++) { 386 | createDom(children[i], domNode, undefined, projectionOptions); 387 | } 388 | }; 389 | var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) { 390 | addChildren(domNode, vnode.children, projectionOptions); 391 | // children before properties, needed for value property of