├── 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