diff --git a/assets/www/css/back-icon.png b/assets/www/css/back-icon.png new file mode 100644 index 0000000..b2b50eb Binary files /dev/null and b/assets/www/css/back-icon.png differ diff --git a/assets/www/css/back-line.png b/assets/www/css/back-line.png new file mode 100644 index 0000000..032bc2b Binary files /dev/null and b/assets/www/css/back-line.png differ diff --git a/assets/www/css/back-mini.png b/assets/www/css/back-mini.png new file mode 100644 index 0000000..b2b50eb Binary files /dev/null and b/assets/www/css/back-mini.png differ diff --git a/assets/www/css/bright-shiny.png b/assets/www/css/bright-shiny.png new file mode 100644 index 0000000..a0b4c16 Binary files /dev/null and b/assets/www/css/bright-shiny.png differ diff --git a/assets/www/css/browser.css b/assets/www/css/browser.css new file mode 100644 index 0000000..65c89a5 --- /dev/null +++ b/assets/www/css/browser.css @@ -0,0 +1,47 @@ +joscroller { + position: absolute; + display: block; + height: auto; + width: auto; + top: 0; left: 0; bottom: 0; right: 0; + overflow: auto; + padding: 0; + margin: 0; + -webkit-transform: none; + -moz-transform: none; + -o-transform: none; + -ms-transform: none; +} +joscroller > * { + position: relative; + top: auto; +} +.flick { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; +} +.flickback { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; +} +.flickfast { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; +} + +jocard { + padding-bottom: 0; +} + +jocontainer { + height: inherit; +} + +joscroller { + display: block; + height: 100%; +} + diff --git a/assets/www/css/dark_matte.png b/assets/www/css/dark_matte.png new file mode 100644 index 0000000..5964903 Binary files /dev/null and b/assets/www/css/dark_matte.png differ diff --git a/assets/www/css/expando.png b/assets/www/css/expando.png new file mode 100644 index 0000000..24ba7d8 Binary files /dev/null and b/assets/www/css/expando.png differ diff --git a/assets/www/css/full-matte.png b/assets/www/css/full-matte.png new file mode 100644 index 0000000..358651e Binary files /dev/null and b/assets/www/css/full-matte.png differ diff --git a/assets/www/css/jo.css b/assets/www/css/jo.css new file mode 100644 index 0000000..f30fd71 --- /dev/null +++ b/assets/www/css/jo.css @@ -0,0 +1,1031 @@ +body { + margin: 0; + padding: 0; + background: #666; + word-wrap: break-word; + overflow: hidden; + font: normal 15px "Apres", "Trebuchet MS"; +} +jobutton, +joview, +jolist, +jolistitem, +jomenu, +jomenuitem, +joexpando, +joexpandotitle, +jogroup, +jocard, +jostack, +jotitle, +jocaption, +jolabel, +jodivider, +joinput, +input, +textarea, +jotextarea, +jooption, +jooptionitem, +jonavbar, +jocontainer, +jotoggle { + display: block; + margin: 0; + padding: 0; + -webkit-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; + /* background: transparent; */ + +} +/* +em, i, b, span, u, a, button, input { + display: inline; +} +*/ +.noflex { + -webkit-box-flex: 0; + -moz-box-flex: 0; + -o-box-flex: 0; + box-flex: 0; +} +.flexible { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; +} +.flex { + display: block; + display: -webkit-box; + display: -moz-box; + display: -o-box; + display: box; + margin: 0; + -moz-box-align: stretch; + -o-box-align: stretch; + box-align: stretch; +} +.listitem { + border-top: 1px solid rgba(0, 0, 0, 0.4); + margin: 0; + padding: 10px; + cursor: pointer; +} +.widgety { + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; +} +.shiny { + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + border: 1px solid rgba(0, 0, 0, 0.8); + background-image: url(shiny.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; +} +.matte { + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + background-image: url(subtle-matte-full.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 100%; + -webkit-background-size: 100% 100%; + -moz-background-size: 100% 100%; + /* + background-image: -webkit-gradient(linear, left top, left 80%, from(rgba(255, 255, 255, .3)), to(rgba(0, 0, 0, 0))); +*/ + +} +.stretch-full { + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +.widget { + margin: 0px 10px 10px 10px; +} +*.selected, *.focus { + -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.6); + -moz-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.6); + -o-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.6); +} +.selected { + background-color: #e98021; + background-image: url(shade-top.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; +} +.focus { + background-color: #fff; +} +jooptionitem:last-child { + -webkit-border-radius: 0; + -webkit-border-top-right-radius: 5px; + -webkit-border-bottom-right-radius: 5px; + -moz-border-radius: 0 5px 5px 0; + border-radius: 0 5px 5px 0; + border-right-width: 1px; +} +jobutton { + display: block; + text-align: center; + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + border: 1px solid rgba(0, 0, 0, 0.8); + background-image: url(shiny.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; +} +jobutton .focus { + background-color: #e98021; + background-image: url(shade-top.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; +} +jooption { + display: block; + display: -webkit-box; + display: -moz-box; + display: -o-box; + display: box; + margin: 0; + -moz-box-align: stretch; + -o-box-align: stretch; + box-align: stretch; + margin: 0px 10px 10px 10px; +} +jooptionitem { + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + border: 1px solid rgba(0, 0, 0, 0.8); + background-image: url(shiny.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + text-align: center; + border: 1px solid rgba(0, 0, 0, 0.8); + border-right-width: 0; + margin: 0; +} +jooptionitem:first-child { + -webkit-border-radius: 0; + -webkit-border-top-left-radius: 5px; + -webkit-border-bottom-left-radius: 5px; + -moz-border-radius: 5px 0 0 5px; + border-radius: 5px 0 0 5px; +} +jooptionitem:last-child { + -webkit-border-radius: 0; + -webkit-border-top-right-radius: 5px; + -webkit-border-bottom-right-radius: 5px; + -moz-border-radius: 0 5px 5px 0; + border-radius: 0 5px 5px 0; + border-right-width: 1px; +} +jobutton, +input, +jolabel, +textarea, +joexpando { + margin: 0px 10px 10px 10px; +} +jotitle { + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + background-image: url(subtle-matte-full.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 100%; + -webkit-background-size: 100% 100%; + -moz-background-size: 100% 100%; + /* + background-image: -webkit-gradient(linear, left top, left 80%, from(rgba(255, 255, 255, .3)), to(rgba(0, 0, 0, 0))); +*/ + + background-image: none; + text-align: center; + color: rgba(255, 255, 255, 0.8); + padding: 10px; + background-color: #313539; + font-size: 18px; + margin: 0; + border-top: none; + border-left: none; + border-right: none; +} +*:focus { + outline: none; +} +jolistitem, jomenuitem { + border-top: 1px solid rgba(0, 0, 0, 0.4); + margin: 0; + padding: 10px; + cursor: pointer; +} +jobutton.focus { + background-color: #e98021; + background-image: none; +} +joselect.focus { + background-color: transparent; +} +jolist { + margin: 0; +} +joselectlist, joexpandocontent { + display: block; + border: 1px solid rgba(0, 0, 0, 0.6); + border-top: none; + -webkit-border-radius: 0; + -webkit-border-bottom-right-radius: 10px; + -webkit-border-bottom-left-radius: 10px; + -moz-border-radius: 0 0 10px 10px; + border-radius: 0 0 10px 10px; + background-color: rgba(255, 255, 255, 0.6); +} +joexpando { + display: block; + padding-bottom: 0px; + margin-bottom: 0; +} +joexpando.open { + margin-bottom: 10px; +} +joexpandocontent { + padding-top: 10px; +} +joselectlist > *:last-child { + border-bottom: none; + -webkit-border-radius: 0; + -webkit-border-bottom-right-radius: 10px; + -webkit-border-bottom-left-radius: 10px; + -moz-border-radius: 0 0 10px 10px; + border-radius: 0 0 10px 10px; +} +joselectlist > *:last-child.select { + border-bottom: none; + -webkit-border-radius: 0; + -webkit-border-bottom-right-radius: 10px; + -webkit-border-bottom-left-radius: 10px; + -moz-border-radius: 0 0 10px 10px; + border-radius: 0 0 10px 10px; +} +joselectlist > *.first-child { + border-top: none; +} +jocard > jolist, jocard > jomenu { + margin: 0; +} +jobutton, +jotitle, +jooptionitem, +joexpandotitle, +jotoggle { + text-shadow: 0 0 5px rgba(0, 0, 0, 0.6); +} +joinput.password, input.password { + -webkit-text-security: disc; + -moz-text-security: disc; + -o-text-security: disc; + text-security: disc; +} +joinput, +jotextarea, +input, +textarea { + cursor: text; + display: block; + border: 1px solid rgba(0, 0, 0, 0.6); + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + margin: 0 10px 10px 10px; + padding: 9px; + background: rgba(255, 255, 255, 0.6); + white-space: nowrap; + overflow: hidden; + outline: none; + font-family: "Apres", "Verdana"; + font-size: 17px; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0); +} +jolist, jomenu { + font-size: 18px; +} +jotextarea, textarea { + white-space: normal; +} +joinput.disabled, +jotextarea.disabled, +input.disabled, +textarea.disabled { + color: #666; + cursor: text; +} +joexpandotitle joicon { + position: absolute; + border: none; + display: block; + height: 32px; + width: 32px; + right: 4px; + top: 10%; + background: url(expando.png) no-repeat; + -webkit-transform-origin: 16px 16px 0; + -webkit-transform: rotatez(0); + -webkit-transition: -webkit-transform 0.2s ease-out; + -moz-transform-origin: 16px 16px 0; + -moz-transform: rotate(0); + -moz-transition: -moz-transform 0.2s ease-out; + -o-transform-origin: 16px 16px 0; + -o-transform: rotate(0); + -o-transition: -o-transform 0.2s ease-out; + -ms-transform-origin: 16px 16px 0; + -ms-transform: rotate(0); + -ms-transition: -ms-transform 0.2s ease-out; +} +joexpando.open > joexpandotitle joicon { + -webkit-transform: rotatez(90deg); + -moz-transform: rotate(90deg); + -o-transform: rotate(90deg); + -ms-transform: rotate(90deg); +} +joexpando > *:last-child { + height: 0; + overflow: hidden; + opacity: 0; + margin-top: 0; + margin-bottom: 0; + -webkit-transform-style: preserve- 3 d; + -webkit-transform: rotatex(-45deg); + -webkit-transform-origin: 0 0 0; + -webkit-transition: -webkit-transform 0.2s ease-out, opacity 0.2s ease-out, height 0.2s ease-out, overflow 0.2s ease-out; + -moz-transform-style: preserve- 3 d; + -moz-transform: rotatex(-45deg); + -moz-transform-origin: 0 0 0; + -moz-transition: -moz-transform 0.2s ease-out, opacity 0.2s ease-out, height 0.2s ease-out, overflow 0.2s ease-out; + -o-transform-style: preserve- 3 d; + -o-transform: rotatex(-45deg); + -o-transform-origin: 0 0 0; + -o-transform: scaley(1); + -o-transition: -o-transform 0.2s ease-out, opacity 0.2s ease-out, height 0.2s ease-out, overflow 0.2s ease-out; + -ms-transform-style: preserve- 3 d; + -ms-transform: rotatex(-45deg); + -ms-transform-origin: 0 0 0; + -ms-transform: scaley(1); + -ms-transition: -o-transform 0.2s ease-out, opacity 0.2s ease-out, height 0.2s ease-out, overflow 0.2s ease-out; +} +joexpando.open > *:last-child { + height: 100%; + overflow: visible; + opacity: 1; + -webkit-transform: rotatex(0); + -moz-transform: rotatex(0); + -o-transform: rotatex(0); + -ms-transform: rotatex(0); +} +joexpando > *:first-child { + -webkit-transform: none; + opacity: 1; + overflow: visible; + height: auto; +} +joexpandotitle { + color: #fff; + position: relative; + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + border: 1px solid rgba(0, 0, 0, 0.8); + background-image: url(shiny.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + padding: 10px; + cursor: pointer; + text-align: left; +} +joexpando.open > *:first-child { + background-color: #e98021; + -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.6); + -moz-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.6); + -o-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.6); + -webkit-border-radius: 0; + -webkit-border-top-right-radius: 10px; + -webkit-border-top-left-radius: 10px; + -moz-border-radius: 10px 10px 0 0; + border-radius: 10px 10px 0 0; +} +jolabel, label { + margin-bottom: 5px; + text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.6); +} +jocaption { + margin: 10px; +} +johtml { + display: block; + margin: 0 10px 10px 10px; + padding: 0; +} +jodivider { + border-top: 1px solid rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + margin: 0 0 10px 0; + text-align: center; + height: 0; + display: block; +} +joflexrow > jotoggle { + -webkit-box-flex: 0; + -moz-box-flex: 0; + -o-box-flex: 0; + box-flex: 0; +} +jotoggle { + cursor: pointer; + display: block; + margin: 10px 10px 0 10px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + border: 1px solid rgba(0, 0, 0, 0.6); + background-color: inherit; + background-image: url(shade-top.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; + -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.6); + -moz-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.6); + -o-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.6); + position: relative; + overflow: hidden; + width: 90px; +} +jotoggle > * { + margin: 0px 10px 10px 10px; + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + border: 1px solid rgba(0, 0, 0, 0.8); + background-image: url(shiny.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; + border-color: #000; + margin: 0; + text-align: center; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; + padding: 10px 0; + width: 60px; + -webkit-box-shadow: 2px 0 2px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 2px 0 2px rgba(0, 0, 0, 0.2); + box-shadow: 2px 0 2px rgba(0, 0, 0, 0.2); + -webkit-transition: -webkit-transform 0.1s ease, background-color 0.1s ease; + -moz-transition: -moz-transform 0.1s ease, background-color 0.1s ease; + -o-transition: -o-transform 0.1s ease, background-color 0.1s ease; + -ms-transition: -ms-transform 0.1s ease, background-color 0.1s ease; +} +jotoggle.on { + background-color: inherit; + background-image: url(shade-top.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; +} +jotoggle.on > * { + -webkit-box-shadow: -2px 0 2px rgba(0, 0, 0, 0.2); + background-color: #e98021; + -webkit-transform: translatex(28px); + -moz-transform: translatex(26px); + -o-transform: translatex(26px); + -ms-transform: translatex(26px); +} +jotable { + display: block; + margin: 0 10px; +} +tr { + margin: 0; +} +th { + text-align: left; + padding: 5px; +} +td { + padding: 5px; +} +jolabel.left { + margin-top: 10px; + padding: 9px 0; +} +.disabled { + opacity: .2; +} +jotoolbar { + padding: 10px; +} +joflexrow { + display: block; + display: -webkit-box; + display: -moz-box; + display: -o-box; + display: box; + margin: 0; + -moz-box-align: stretch; + -o-box-align: stretch; + box-align: stretch; + width: 100%; +} +joflexrow > * { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + margin-right: 0; + position: relative; +} +joflexrow > *:last-child { + margin-right: 10px; +} +joflexcol { + display: block; + display: -webkit-box; + display: -moz-box; + display: -o-box; + display: box; + margin: 0; + -moz-box-align: stretch; + -o-box-align: stretch; + box-align: stretch; + height: 100%; + width: 100%; + -webkit-box-orient: vertical; + -webkit-box-align: stretch; + -moz-box-orient: vertical; + -moz-box-align: stretch; + -o-box-orient: vertical; + -o-box-align: stretch; + box-orient: vertical; + box-align: stretch; + margin: 0; +} +joflexcol > * { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; +} +jotitle + joflexcol, jotitle + joflexrow { + margin-top: 10px; +} +jostack { + height: 100%; + -webkit-perspective: 400px; + -moz-perspective: 400px; + -o-perspective: 400px; + -ms-perspective: 400px; +} +jostack { + -webkit-transition: opacity 0.3s ease-out; + -moz-transition: opacity 0.3s ease-out; + -o-transition: opacity 0.3s ease-out; + -ms-transition: opacity 0.3s ease-out; +} +jostack > * { + -webkit-transform-origin: 50% 100% 0; + -webkit-transform: rotatez(0) translatez(0); + -webkit-transition: -webkit-transform 0.3s ease-out, z-index 0.3s ease-out, height 0s ease, overflow 0s ease; + -moz-transform-origin: 50% 100% 0; + -moz-transform: rotatez(0) translatez(0); + -moz-transition: -moz-transform 0.3s ease-out, z-index 0.3s ease-out, height 0s ease, overflow 0s ease; + -o-transform-origin: 50% 100% 0; + -o-transform: rotatez(0); + -o-transition: -o-transform 0.3s ease-out; + -ms-transform-origin: 50% 100% 0; + -ms-transform: rotatez(0); + -ms-transition: -ms-transform 0.3s ease-out; +} +jostack > .next { + z-index: -1; + -webkit-transform: rotatez(45deg) translatey(-10%) translatex(100%); + -moz-transform: rotatez(45deg); + -o-transform: rotatez(45deg); + -ms-transform: rotatez(45deg); + height: 100%; + overflow: hidden; +} +jostack > .prev { + z-index: 1; + -webkit-transform: rotatez(-45deg) translatey(10%) translatex(-100%); + -moz-transform: rotatez(-45deg); + -o-transform: rotatez(-45deg); + -ms-transform: rotatez(-45deg); + height: 100%; + overflow: hidden; +} +* { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +jobutton { + background-repeat: repeat-x; +} +jocard { + width: 100%; + min-height: 100%; + display: -webkit-box; + display: -moz-box; + display: -o-box; + display: box; + padding: 0; + margin: 0; + -webkit-box-orient: vertical; + -webkit-box-align: stretch; + -moz-box-orient: vertical; + -moz-box-align: stretch; + -o-box-orient: vertical; + -o-box-align: stretch; + box-orient: vertical; + box-align: stretch; + background: #a6a8b9; +} +jocard > * { + /* display: block; */ + +} +jocard > *:last-child { + margin-bottom: 10px; +} +joscroller { + position: absolute; + display: block; + height: 100%; + width: 100%; + overflow: hidden; + padding: 0; + margin: 0; + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translatey(0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); +} +joscroller > * { + position: absolute; + top: 0; + -webkit-animation-fill-mode: forwards; + -webkit-transition: -webkit-transform 0s ease; + -moz-animation-fill-mode: forwards; + -moz-transition: -moz-transform 0s ease; + -o-animation-fill-mode: forwards; + -o-transition: -o-transform 0s ease; + -ms-animation-fill-mode: forwards; + -ms-transition: -ms-transform 0s ease; +} +.flick { + -webkit-transition: -webkit-transform 1.8s cubic-bezier(0, 0.2, 0, 1); + -moz-transition: -moz-transform 1.8s cubic-bezier(0, 0.2, 0, 1); + -o-transition: -o-transform 1.8s cubic-bezier(0, 0.2, 0, 1); + -ms-transition: -ms-transform 1.8s cubic-bezier(0, 0.2, 0, 1); +} +.flickback { + -webkit-transition: -webkit-transform 0.2s cubic-bezier(0, 0, 0.4, 1); + -moz-transition: -moz-transform 0.2s cubic-bezier(0, 0, 0.4, 1); + -o-transition: -o-transform 0.2s cubic-bezier(0, 0, 0.4, 1); + -ms-transition: -o-transform 0.2s cubic-bezier(0, 0, 0.4, 1); +} +.flickfast { + -webkit-transition: -webkit-transform 0.5s cubic-bezier(0, 0.2, 0, 1); + -moz-transition: -moz-transform 0.5s cubic-bezier(0, 0.2, 0, 1); + -o-transition: -o-transform 0.5s cubic-bezier(0, 0.2, 0, 1); +} +jocontainer { + margin: 0; + display: block; + position: relative; +} +jogroup { + margin: 10px; + padding: 10px 0; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + background: #c8cadb; +} +jogroup > *.last-child { + margin-bottom: 0; +} +jofooter { + display: block; + display: -webkit-box; + display: -moz-box; + display: box; + width: 100%; + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + -webkit-box-align: stretch; + -webkit-box-pack: end; + -moz-box-flex: 1; + -moz-box-orient: vertical; + -moz-box-align: stretch; + -moz-box-pack: end; + box-flex: 1; + box-orient: vertical; + box-align: stretch; + box-pack: end; + margin: 0; +} +jofooter > * { + -webkit-box-align: end; + -moz-box-align: end; + -o-box-align: end; + box-align: end; +} +joshim { + opacity: 0; + overflow: hidden; + position: absolute; + z-index: 100; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: none; + background: rgba(0, 0, 0, 0.6); + opacity: 0; + -webkit-transition: opacity .2s ease; + -moz-transition: opacity .2s ease; + -o-transition: opacity .2s ease; + -ms-transition: opacity .2s ease; +} +joshim.show { + display: block; + opacity: 1; +} +jotoolbar { + border-top: 1px solid rgba(0, 0, 0, 0.8); + padding: 10px 0; + color: rgba(255, 255, 255, 0.9); + background-color: #7e82a1; + font-weight: normal; + cursor: pointer; + outline: none; + outline-color: transparent; + padding: 10px 0; + background-image: url(subtle-matte-full.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 100%; + -webkit-background-size: 100% 100%; + -moz-background-size: 100% 100%; + /* + background-image: -webkit-gradient(linear, left top, left 80%, from(rgba(255, 255, 255, .3)), to(rgba(0, 0, 0, 0))); +*/ + + background-color: rgba(0, 0, 0, 0.8); + z-index: 1; + position: absolute; + bottom: 0; + left: 0; + right: 0; + text-align: center; +} +jotoolbar jobutton { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +jopopup { + -webkit-box-flex: 0; + -moz-box-flex: 0; + -o-box-flex: 0; + box-flex: 0; + font-size: 15px; + display: block; + overflow: hidden; + margin: 0 auto; + border: 2px solid #fff; + -webkit-box-shadow: 0 0 20px rgba(0, 0, 0, 0.6); + -moz-box-shadow: 0 0 20px rgba(0, 0, 0, 0.6); + -o-box-shadow: 0 0 20px rgba(0, 0, 0, 0.6); + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + border-radius: 20px; + background-color: #34c; + color: #fff; + background-image: url(aluminum/shiny.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 20px; + -webkit-transition: -webkit-transform 0.4s ease-in, opacity 0.4s ease-in; + -moz-transition: -moz-transform 0.4s ease-in, opacity 0.4s ease-in; + -o-transition: -o-transform 0.4s ease-in, opacity 0.4s ease-in; + -ms-transition: -m-transform 0.4s ease-in, opacity 0.4s ease-in; + -webkit-transform: scale(0.5); + -moz-transform: scale(0.5); + -o-transform: scale(0.5); + -ms-transform: scale(0.5); + max-width: 280px; + margin: 0 auto; + opacity: 0; +} +jopopup > jolist > *.select:first-childjopopup > jomenu > *.select:first-child { + border-top: none; + -webkit-border-radius: 0; + -webkit-border-top-right-radius: 20px; + -webkit-border-top-left-radius: 20px; + -moz-border-radius: 20px 20px 0 0; + border-radius: 20px 20px 0 0; +} +jopopup > jolist > *:last-child.selectjopopup > jomenu > *:first-child.select { + border-bottom: none; + -webkit-border-radius: 0; + -webkit-border-bottom-right-radius: 20px; + -webkit-border-bottom-left-radius: 20px; + -moz-border-radius: 0 0 20px 20px; + border-radius: 0 0 20px 20px; +} +jopopup.show { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + opacity: 1; +} +jopopup > joscroller { + width: 100%; +} +jonavbar { + background-image: url(shiny.png); + background-repeat: repeat-x; + background-position: top left; + background-size: 100% 50%; + -webkit-background-size: 100% 50%; + -moz-background-size: 100% 50%; + display: block; + position: relative; + margin: 0; + padding: 0; + color: #fff; + border-top: none; + border-right: none; + border-left: none; + text-align: center; + -webkit-box-flex: 0; + -moz-box-flex: 0; + -o-box-flex: 0; + box-flex: 0; + background-color: #333; + cursor: default; +} +jonavbar > joview { + display: block; + text-align: center; + padding: 10px 0; +} +jonavbar > joflexrow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +jobackbutton { + background: transparent url(back-mini.png) 10px center no-repeat; + border: 1px solid rgba(0, 0, 0, 0.3); + border-bottom-color: rgba(255, 255, 255, 0.3); + padding: 5px 10px 5px 20px; + margin: 5px 10px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + -webkit-box-shadow: inset 0 0 1px rgba(255, 255, 255, 0.6); + -moz-box-shadow: inset 0 0 1px rgba(255, 255, 255, 0.6); + -o-box-shadow: inset 0 0 1px rgba(255, 255, 255, 0.6); + display: none; + color: #ddd; +} +jobackbutton.focus, jobackbutton.selected { + background-color: rgba(0, 0, 0, 0.5); +} +jobackbutton.active { + display: block; +} +jonavbar > joflexrow > * { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; +} +jonavbar > joflexrow > jobackbutton { + -webkit-box-flex: 0; + -moz-box-flex: 0; + -o-box-flex: 0; + box-flex: 0; + max-width: 4em; +} +jopopup > jotitle { + color: #fff; + -webkit-border-radius: 0; + -webkit-border-top-right-radius: 20px; + -webkit-border-top-left-radius: 20px; + -moz-border-radius: 20px 20px 0 0; + border-radius: 20px 20px 0 0; + background: transparent; +} +jopopup > jobutton { + background-color: rgba(255, 255, 255, 0.4); +} +joflexcol { + height: 100%; + width: 100%; +} +html { + -webkit-text-size-adjust: none; +} +body { + -webkit-backface-visibility: hidden; +} diff --git a/assets/www/css/lite-matte.png b/assets/www/css/lite-matte.png new file mode 100644 index 0000000..b44edd4 Binary files /dev/null and b/assets/www/css/lite-matte.png differ diff --git a/assets/www/css/sample.html b/assets/www/css/sample.html new file mode 100644 index 0000000..dd7a78a --- /dev/null +++ b/assets/www/css/sample.html @@ -0,0 +1,53 @@ + + + + + + + + joTest + + + + + + + + + Hello, Jo + This is a sample static HTML page with Jo UI markup. It is designed + to make it easier to design stylesheets which work with Jo. + + + Menu sample + + + Menu Item 1 + Menu Item 2 + Menu Item 3 + + + + Dialog Sample + + Username + Jo + Password + 12345 + Realm + Disabled Input + + + + Go, Jo + Cancel + + + + + diff --git a/assets/www/css/shade-top.png b/assets/www/css/shade-top.png new file mode 100644 index 0000000..875f54f Binary files /dev/null and b/assets/www/css/shade-top.png differ diff --git a/assets/www/css/shade.png b/assets/www/css/shade.png new file mode 100644 index 0000000..6610cbb Binary files /dev/null and b/assets/www/css/shade.png differ diff --git a/assets/www/css/shiny-button.png b/assets/www/css/shiny-button.png new file mode 100644 index 0000000..bde7fc0 Binary files /dev/null and b/assets/www/css/shiny-button.png differ diff --git a/assets/www/css/shiny.png b/assets/www/css/shiny.png new file mode 100644 index 0000000..2add889 Binary files /dev/null and b/assets/www/css/shiny.png differ diff --git a/assets/www/css/subtle-matte-full.png b/assets/www/css/subtle-matte-full.png new file mode 100644 index 0000000..ad8e1dc Binary files /dev/null and b/assets/www/css/subtle-matte-full.png differ diff --git a/assets/www/css/subtle-shiny-button.png b/assets/www/css/subtle-shiny-button.png new file mode 100644 index 0000000..3872295 Binary files /dev/null and b/assets/www/css/subtle-shiny-button.png differ diff --git a/assets/www/css/subtle-shiny.png b/assets/www/css/subtle-shiny.png new file mode 100644 index 0000000..e01951a Binary files /dev/null and b/assets/www/css/subtle-shiny.png differ diff --git a/assets/www/css/webos.css b/assets/www/css/webos.css new file mode 100644 index 0000000..2941173 --- /dev/null +++ b/assets/www/css/webos.css @@ -0,0 +1,34 @@ +joscroller > * { + -webkit-user-drag: element; +} + +jotoolbar joflexrow jobutton.back { + display: none; +} +jotoolbar { + padding-top: 10px; + padding-bottom: 5px; +} + +jopopup { + max-width: inherit; + margin: 0; + -webkit-border-radius: 20px 20px 0 0; + -webkit-transform: translateY(100%); + border-bottom: none; +} + +jopopup.show { + -webkit-transform: translateY(0); +} + +joshim>joflexcol>*:first-child { + -webkit-box-flex: 1; +} +joshim>joflexcol>*:last-child { + display: none; +} + +jobackbutton.active { + display: none; +} diff --git a/assets/www/index.html b/assets/www/index.html index d94367f..2c9dca6 100644 --- a/assets/www/index.html +++ b/assets/www/index.html @@ -1,10 +1,19 @@ - -PhoneGap - - - -

Hello World

- - \ No newline at end of file + + PhoneGap + + + + + + + + + + + + + + + diff --git a/assets/www/cordova-1.5.0.js b/assets/www/js/cordova-1.5.0.js similarity index 100% rename from assets/www/cordova-1.5.0.js rename to assets/www/js/cordova-1.5.0.js diff --git a/assets/www/js/jo.js b/assets/www/js/jo.js new file mode 100644 index 0000000..b52122c --- /dev/null +++ b/assets/www/js/jo.js @@ -0,0 +1,6146 @@ +/** + joLog + ===== + + Wrapper for `console.log()` (or whatever device-specific logging you have). Also could + be extended to send log information to a RESTful service as well, handy for devices + which don't have decent logging abilities. + + Use + --- + + It's an all-in-one utility that's smart enough to ferret out whatever you throw at it + and display it in the console. + + joLog("x=", x, "listdata=", listdata); + + Basically, fill it up with strings, variables, objects, arrays and the function will + produce a string version of each argument (where appropriate; browser debuggers tend to + display objects nicely) in the same console line. Simple, effective, easy to use. + +*/ + +joLog = function() { + var strings = []; + + for (var i = 0; i < arguments.length; i++) { + // TODO: stringify for objects and arrays + strings.push(arguments[i]); + } + + // spit out our line + console.log(strings.join(" ")); +} +/** + - - - + + jo + == + + Singleton which the framework uses to store global infomation. It also is + responsible for initializing the rest of the framework, detecting your environment, + and notifying your application when jo is ready to use. + + Methods + ------- + + - `load()` + + This method should be called after your DOM is loaded and before your app uses + jo. Typically, you can call this function from your document's `onLoad` method, + but it is recommended you use more device-specific "ready" notification if + they are available. + + - `getPlatform()` + + Returns the platform you're running in as a string. Usually this is not needed, + but can be useful. + + - `getVersion()` + + Returns the version of jo you loaded in the form of a string (e.g. `0.1.1`). + + - `matchPlatform(string)` + + Feed in a string list of desired platforms (e.g. `"mozilla chrome ipad"`), + and returns true if the identified platform is in the test list. + + Events + ------ + + - `loadEvent` + - `unloadEvent` + + These events are fired after jo loads or unloads, and can be used in your + application to perform initialization or cleanup tasks. + + Function + ======== + + jo extends the Function object to add a few goodies which augment JavaScript + in a farily non-intrusive way. + + Methods + ------- + + - `extend(superclass, prototype)` + + Gives you an easy way to extend a class using JavaScript's natural prototypal + inheritance. See Class Patterns for more information. + + - `bind(context)` + + Returns a private function wrapper which automagically resolves context + for `this` when your method is called. + + HTMLElement + =========== + + This is a standard DOM element for JavaScript. Most of the jo views, continers + and controls deal with these so your application doesn't need to. + + Methods + ------- + + Not a complete list by any means, but the useful ones for our + purposes are: + + - `appendChild(node)` + - `insertChild(before, node)` + - `removeChild(node)` + + Properties + ---------- + + jo uses these properties quite a bit: + + - `innerHTML` + - `className` + - `style` + +*/ + +// syntactic sugar to make it easier to extend a class +Function.prototype.extend = function(superclass, proto) { + // create our new subclass + this.prototype = new superclass(); + + // optional subclass methods and properties + if (proto) { + for (var i in proto) + this.prototype[i] = proto[i]; + } +}; + +// add bind() method if we don't have it already +if (typeof Function.prototype.bind === 'undefined') { + Function.prototype.bind = function(context) { + var self = this; + + function callbind() { + return self.apply(context, arguments); + } + + return callbind; + }; +} + +// hacky kludge for hacky browsers +if (typeof HTMLElement === 'undefined') + HTMLElement = Object; + +// no console.log? sad... +if (typeof console === 'undefined' || typeof console.log !== 'function') + console = {log: function(msg) { }}; + +// just a place to hang our hat +jo = { + platform: "webkit", + version: "0.4.1", + + useragent: [ + 'ipad', + 'iphone', + 'webos', + 'bada', + 'android', + 'opera', + 'chrome', + 'safari', + 'mozilla', + 'gecko', + 'explorer' + ], + + debug: false, + setDebug: function(state) { + this.debug = state; + }, + + flag: { + stopback: false + }, + + load: function(call, context) { + joDOM.enable(); + + this.loadEvent = new joSubject(this); + this.unloadEvent = new joSubject(this); + + // capture these events, prevent default for applications + document.body.onMouseDown = function(e) { e.preventDefault(); }; + document.body.onDragStart = function(e) { e.preventDefault(); }; + + // quick test to see which environment we're in + if (typeof navigator == 'object' && navigator.userAgent) { + var agent = navigator.userAgent.toLowerCase(); + for (var i = 0; i < this.useragent.length; i++) { + if (agent.indexOf(this.useragent[i]) >= 0) { + this.platform = this.useragent[i]; + break; + } + } + } + + if (joEvent) { + // detect if we're on a touch or mouse based browser + var o = document.createElement('div'); + var test = ("ontouchstart" in o); + if (!test) { + o.setAttribute("ontouchstart", 'return;'); + test = (typeof o.ontouchstart === 'function'); + } + joEvent.touchy = test; + o = null; + } + + if (joGesture) + joGesture.load(); + + var s = joScroller.prototype; + + // setup transition css hooks for the scroller + if (typeof document.body.style.webkitTransition !== "undefined") { + // webkit, leave everything alone + } + else if (typeof document.body.style.MozTransition !== "undefined") { + // mozilla with transitions + s.transitionEnd = "transitionend"; + s.setPosition = function(x, y, node) { + node.style.MozTransform = "translate(" + x + "px," + y + "px)"; + }; + } + else if (typeof document.body.style.msTransform !== "undefined") { + // IE9 with transitions + s.transitionEnd = "transitionend"; + s.setPosition = function(x, y, node) { + node.style.msTransform = "translate(" + x + "px," + y + "px)"; + }; + } + else if (typeof document.body.style.OTransition !== "undefined") { + // opera with transitions + s.transitionEnd = "otransitionend"; + s.setPosition = function(x, y, node) { + node.style.OTransform = "translate(" + x + "px," + y + "px)"; + }; + } + else { + // no transitions, disable flick scrolling + s.velocity = 0; + s.bump = 0; + s.transitionEnd = "transitionend"; + s.setPosition = function(x, y, node) { + if (this.vertical) + node.style.top = y + "px"; + + if (this.horizontal) + node.style.left = x + "px"; + }; + } + + joLog("Jo", this.version, "loaded for", this.platform, "environment"); + + this.loadEvent.fire(); + }, + + tagMap: {}, + tagMapLoaded: false, + + // make a map of node.tagName -> joView class constructor + initTagMap: function() { + // we only do this once per session + if (this.tagMapLoaded) + return; + + var key = this.tagMap; + + // defaults + key.JOVIEW = joView; + key.BODY = joScreen; + + // run through all our children of joView + // and add to our joCollect.view object + for (var p in window) { + var o = window[p]; + if (typeof o === 'function' + && o.prototype + && typeof o.prototype.tagName !== 'undefined' + && o.prototype instanceof joView) { + var tag = o.prototype.tagName.toUpperCase(); + + if (o.prototype.type) { + // handle tags with multiple types + if (!key[tag]) + key[tag] = {}; + + key[tag][o.prototype.type] = o; + } + else { + key[tag] = o; + } + } + } + }, + + getPlatform: function() { + return this.platform; + }, + + matchPlatform: function(test) { + return (test.indexOf(this.platform) >= 0); + }, + + getVersion: function() { + return this.version; + }, + + getLanguage: function() { + return this.language; + } +}; + +/** + joDOM + ====== + + Singleton with utility methods for manipulating DOM elements. + + Methods + ------- + + - `get(id)` + + Returns an HTMLElement which has the given id or if the + id is not a string returns the value of id. + + - `create(type, style)` + + Type is a valid HTML tag type. Style is the same as `setStyle()` + method. Returns an HTMLElement. + + // simple + var x = joDOM.create("div", "mycssclass"); + + // more interesting + var x = joDOM.create("div", { + id: "name", + className: "selected", + background: "#fff", + color: "#000" + }); + + - `setStyle(tag, style)` + + Style can be an object literal with + style information (including "id" or "className") or a string. If + it's a string, it will simply use the style string as the className + for the new element. + + Note that the preferred and most cross-platform method for working + with the DOM is to use `className` and possibly `id` and put your + actual style information in your CSS file. That said, sometimes it's + easier to just set the background color in the code. Up to you. + + - `getParentWithin(node, ancestor)` + + Returns an HTMLElement which is + the first child of the ancestor which is a parent of a given node. + + - `addCSSClass(HTMLElement, classname)` + + Adds a CSS class to an element unless it is already there. + + - `removeCSSClass(HTMLElement, classname)` + + Removes a CSS class from an element if it exists. + + - `toggleCSSClass(HTMLElement, classname)` + + Auto add or remove a class from an element. + + - `pageOffsetLeft(HTMLElement)` and `pageOffsetHeight(HTMLElement)` + + Returns the "true" left and top, in pixels, of a given element relative + to the page. + + - `applyCSS(css, stylenode)` + + Applies a `css` string to the app. Useful for quick changes, like backgrounds + and other goodies. Basically creates an inline `'; + + document.body.appendChild(css); + + return css; + }, + + removeCSS: function(node) { + document.body.removeChild(node); + }, + + loadCSS: function(filename, oldnode) { + // you can just replace the source for a given + // link if one is passed in + if (oldnode) + var css = oldnode; + else + var css = joDOM.create('link'); + + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = filename + (jo.debug ? ("?" + joTime.timestamp()) : ""); + + if (!oldnode) + document.body.appendChild(css); + + return css; + }, + + pageOffsetLeft: function(node) { + var l = 0; + + while (typeof node !== 'undefined' && node && node.parentNode !== window) { + if (node.offsetLeft) + l += node.offsetLeft; + + node = node.parentNode; + } + + return l; + }, + + pageOffsetTop: function(node) { + var t = 0; + + while (typeof node !== 'undefined' && node && node.parentNode !== window) { + t += node.offsetTop; + node = node.parentNode; + } + + return t; + } +}; + +joCSSRule = function(data) { + this.setData(data); +}; +joCSSRule.prototype = { + container: null, + + setData: function(data) { + this.data = data || ""; + this.enable(); + }, + + clear: function() { + this.setData(); + }, + + disable: function() { + joDOM.removeCSS(this.container); + }, + + enable: function() { + this.container = joDOM.applyCSS(this.data, this.container); + } +}; +/** + joEvent + ======== + + Singleton with DOM event model utility methods. Ideally, application-level + code shouldn't have to use this, but library code does. + + Methods + ------- + - `on(HTMLElement, event, Function, context, data)` + + Set a DOM event listener for an HTMLElement which calls a given Function + with an optional context for `this` and optional static data. Returns a + reference to the handler function, which is required if you need to `remove()` + later. + + - `capture(HTMLElement, event, function, context, data)` + + This is the same os `on()`, but captures the event at the node before its + children. If in doubt, use `on()` instead. + + - `remove(HTMLElement, event, handler)` + + Removes a previously declared DOM event. Note that `handler` is the return + value of the `on()` and `capture()` methods. + + - `stop(event)` + + Stop event propogation. + + - `preventDefault(event)` + + Prevent default action for this event. + + - `block(event)` + + Useful for preventing dragging the window around in some browsers, also highlighting + text in a desktop browser. + + - `getTarget(event)` + + Returns the HTMLElement which a DOM event relates to. + +*/ + +joEvent = { + eventMap: { + "mousedown": "touchstart", + "mousemove": "touchmove", + "mouseup": "touchend", + "mouseout": "touchcancel" + }, + touchy: false, + + getTarget: function(e) { + if (!e) + var e = window.event; + + return e.target ? e.target : e.srcElement; + }, + + capture: function(element, event, call, context, data) { + return this.on(element, event, call, context, data, true); + }, + + on: function(element, event, call, context, data, capture) { + if (!call || !element) + return false; + + if (this.touchy) { + if (this.eventMap[event]) + event = this.eventMap[event]; + } + + var element = joDOM.get(element); + var call = call; + var data = data || ""; + + function wrappercall(e) { + // support touchy platforms, + // might reverse this to turn non-touch into touch + if (e.touches && e.touches.length == 1) { + var touches = e.touches[0]; + e.pageX = touches.pageX; + e.pageY = touches.pageY; + e.screenX = touches.screenX; + e.screenY = touches.screenY; + e.clientX = touches.clientX; + e.clientY = touches.clientY; + } + + if (context) + call.call(context, e, data); + else + call(e, data); + }; + + // annoying kludge for Mozilla + wrappercall.capture = capture || false; + + if (!window.addEventListener) + element.attachEvent("on" + event, wrappercall); + else + element.addEventListener(event, wrappercall, capture || false); + + return wrappercall; + }, + + remove: function(element, event, call, capture) { + if (this.touchy) { + if (this.eventMap[event]) { + event = this.eventMap[event]; + } + } + + if (typeof element.removeEventListener !== 'undefined') + element.removeEventListener(event, call, capture || false); + }, + + stop: function(e) { + if (e.stopPropagation) + e.stopPropagation(); + else + e.cancelBubble = true; + }, + + preventDefault: function(e) { + e.preventDefault(); + }, + + block: function(e) { + if (window.event) + var e = window.event; + + if (typeof e.target == 'undefined') + e.target = e.srcElement; + + switch (e.target.nodeName.toLowerCase()) { + case 'input': + case 'textarea': + return true; + break; + default: + return false; + } + } +}; +/** + joSubject + ========== + + Class for custom events using the Observer Pattern. This is designed to be used + inside a subject to create events which observers can subscribe to. Unlike + the classic observer pattern, a subject can fire more than one event when called, + and each observer gets data from the subject. This is very similar to YUI 2.x + event model. + + You can also "lock" the notification chain by using the `capture()` method, which + tells the event to only notify the most recent subscriber (observer) which requested + to capture the event exclusively. + + Methods + ------- + + - `subscribe(Function, context, data)` + + Both `context` and `data` are optional. Also, you may use the `Function.bind(this)` + approach instead of passing in the `context` as a separate argument. + All subscribers will be notified when the event is fired. + + - `unsubscribe(Function, context)` + + Does what you'd think. The `context` is only required if you used one when + you set up a subscriber. + + - `capture(Function, context, data)` + + Only the last subscriber to capture this event will be notified until it is + released. Note that you can stack `capture()` calls to produce a modal event + heiarchy. Used in conjunction with the `resume()` method, you can build an + event chain where each observer can fire the next based on some decision making. + + - `release(Function, context)` + + Removes the most recent subscription called with `capture()`, freeing up the next + subscribers in the list to be notified the next time the event is fired. + + - `fire(data)` + + Calls subscriber methods for all observers, and passes in: `data` from the subject, + a reference to the `subject` and any static `data` which was passed in the + `subscribe()` call. + + - `resume(data)` + + If you used `capture()` to subscribe to this event, you can continue notifying + other subscribers in the chain with this method. The `data` parameter, as in + `fire()`, is optional. + + Use + --- + + ### In the subject (or "publisher") object + + // inside the Subject, we setup an event observers can subscribe to + this.changeEvent = new joSubject(this); + + // to fire the event inside the Subject + this.changeEvent.fire(somedata); + + ### In the observer (or "subscriber") object + + // simple case, using Function.bind() + somesubject.changeEvent.subscribe(this.mymethod.bind()); + + // explicit context (this) + somesubject.changeEvent.subscribe(this.mymethod, this); + + // optional data which gets passed with the event fires + somesubject.changeEvent.subscribe(this.mymethod, this, "hello"); + + This is a very flexible way to handle messages between objects. Each subject + may have multiple events which any number of observer objects can subscribe + to. + +*/ +joSubject = function(subject) { + this.subscriptions = []; + this.subject = subject; +}; +joSubject.prototype = { + last: -1, + + subscribe: function(call, observer, data) { + if (!call) + return false; + + var o = { "call": call }; + + if (observer) + o.observer = observer; + + if (data) + o.data = data; + + this.subscriptions.push(o); + + return this.subject; + }, + + unsubscribe: function(call, observer) { + if (!call) + return false; + + for (var i = 0, l = this.subscriptions.length; i < l; i++) { + var sub = this.subscriptions[i]; + if (sub.call === call && (typeof sub.observer === 'undefined' || sub.observer === observer)) { + this.subscriptions.splice(i, 1); + break; + } + } + + return this.subject; + }, + + resume: function(data) { + if (this.last != -1) + this.fire(data, true); + + return this.subject; + }, + + fire: function(data, resume) { + if (typeof data === 'undefined') + data = ""; + + var i = (resume) ? (this.last || 0) : 0; + + // reset our call stack + this.last = -1; + + for (var l = this.subscriptions.length; i < l; i++) { + var sub = this.subscriptions[i]; + var subjectdata = (typeof sub.data !== 'undefined') ? sub.data : null; + + if (sub.observer) + sub.call.call(sub.observer, data, this.subject, subjectdata); + else + sub.call(data, this.subject, subjectdata); + + // if this subscriber wants to capture events, + // stop calling other subscribers + if (sub.capture) { + this.last = i + 1; + break; + } + } + + return this.subject; + }, + + capture: function(call, observer, data) { + if (!call) + return false; + + var o = { "call": call, capture: true }; + + if (observer) + o.observer = observer; + + if (data) + o.data = data; + + this.subscriptions.unshift(o); + + return this.subject; + }, + + release: function(call, observer) { + return this.unsubscribe(call, observer); + } +}; +/** + joTime + ====== + + Time utility functions. More will be added, but only as needed by the + framework. There are entire libraries dedicated to extensive datetime + manipulation, and Jo doesn't pretend to be one of them. + + Methods + ------- + + - `timestamp()` + + Returns a current timestamp in milliseconds from 01/01/1970 from + the system clock. + + Constants + --------- + + - `SEC`, `MIN`, `HOUR`, `DAY` + + Convenience global constants which make it easier to manipulate + timestamps. + + Use + --- + + var twoHoursLater = joTime.timestamp() + (HOUR * 2); + +*/ + +var SEC = 1000; +var MIN = 60 * SEC; +var HOUR = 60 * MIN; +var DAY = 24 * HOUR; + +joTime = { + timestamp: function() { + var now = new Date(); + return now / 1; + } +}; +/** + joDefer + ======= + + Utility function which calls a given method within a given context after `n` + milliseconds with optional static data. + + Use + ----- + + joDefer(Function, context, delay, data); + + Note that delay defaults to 100ms if not specified, and `data` is optional. + + joYield + ======= + + Deprecated, use joDefer instead. + +*/ +function joDefer(call, context, delay, data) { + if (!delay) + var delay = 100; + + if (!context) + var context = this; + + var timer = window.setTimeout(function() { + call.call(context, data); + }, delay); + + return timer; +}; +joYield = joDefer;/** + joCache + ======= + + A singleton which makes it easy to setup deferred object creation and cached + results. This is a performance menchanism initially designed for UI views, but + could be extended to handle data requests and other object types. + + Methods + ------- + + - `set(key, call, context)` + + Defines a factory (`call`) for building an object keyed from the `key` string. + The `context` argument is optional, but provides a reference for `this`. + + - `get(key)` + + Returns an object based on the `key` string. If an object has not been created + which corresponds to the `key`, joCache will call the constructor defined to + create it and store the reference for future calls to `get()`. + + Use + --- + + Defining a view for on-demand use: + + joCache.set("home", function() { + return new joCard([ + new joTitle("Home"), + new joMenu([ + "Top Stories", + "Latest News", + "Old News", + "No News" + ]) + ]); + }); + + Displaying a view later: + + mystack.push(joCache.get("home")); + + // the first call to get() will instantiate + // the view, subsequent calls will return the + // view that was created the first time + + // you can pass parameters into your view factory + var x = joCache.get("home", "My Title"); + + // note that if you want to use joCache to cache + // views which differ based on parameters passed in, + // you probably want your own caching mechanism instead. + +*/ + +joCache = { + cache: {}, + + set: function(key, call, context) { + if (call) + this.cache[key] = { "call": call, "context": context || this }; + }, + + get: function(key) { + var cache = this.cache[key] || null; + if (cache) { + if (!cache.view) + cache.view = cache.call.apply(cache.context, arguments); + + return cache.view; + } + else { + return new joView("View not found: " + key); + } + } +}; + +/** + joChain + ======== + + Class which strings asyncronous calls together. + + > In serious need of rework; doesn't meet original goal of sequencing + > these calls. This class might also become deprecated. + + Methods + ------- + + - `add(Function, context, data)` + - `start()` + - `stop()` + - `next()` + +*/ + +joChain = function() { + this.queue = []; + this.active = false; + + this.addEvent = new joSubject("add", this); + this.startEvent = new joSubject("start", this); + this.stopEvent = new joSubject("stop", this); + this.nextEvent = new joSubject("next", this); + + this.stop(); + + this.delay = 100; +}; +joChain.prototype = { + add: function(call, context, data) { + if (!context) + var context = this; + + if (!data) + var data = ""; + + this.queue.push({ + "call":call, + "context": context, + "data": data + }); + + if (this.active && !this.timer) + this.next(); + }, + + start: function() { + this.active = true; + + this.startEvent.fire(); + + this.next(); + }, + + stop: function() { + this.active = false; + + if (this.timer != null) + window.clearTimeout(this.timer); + + this.timer = null; + + this.stopEvent.fire(); + }, + + next: function() { + var nextcall = this.queue.shift(); + + if (!nextcall) { + this.timer = null; + return; + } + + this.nextEvent.fire(nextcall); + + nextcall.call.call(nextcall.context, nextcall.data); + + if (this.queue.length) + this.timer = joEvent.yield(this.next, this, this.delay); + else + this.timer = null; + } +}; +/** + joClipboard + =========== + + Singleton which abstracts the system clipboard. Note that this is a platform + dependant interface. By default, the class will simply store the contents in + a special joPreference named "joClipboardData" to provide clipboard capabilities + within your app. + + > Even if you think you're just going to use the default behavior, it is + > recommended that you never manipulate the "joClipboardData" preference directly. + + Methods + ------- + + - `get()` + - `set(String)` + + Low level methods which use just strings. At this time, you will need to + stringify your own data when setting, and extract your data when getting. + + - `cut(joControl)` + - `copy(joControl)` + - `paste(joControl)` + + High level methods which work with any joControl or subclass. If a control + supports selections, `cut()` will automatically remove the selection after + copying its contents. Otherwise, `cut()` will work the same as `copy()`. + + > Note: this is not working yet, steer clear (or contribute some working code!) + +*/ +joClipboard = { + data: "", + + get: function() { + return joPreference.get("joClipboardData") || this.data; + }, + + set: function(clip) { + // don't feed it junk; stringify it first + // TODO: detect non-strings and stringify them + this.data = clip; + joPreference.set("joClipboardData"); + } +}; +/* + not used at this time +*/ + +/** + joDataSource + ============= + + Wraps data acquisition in an event-driven class. Objects can + subscribe to the `changeEvent` to update their own data. + + This base class can be used as-is as a data dispatcher, but is + designed to be extended to handle asynchronous file or SQL queries. + + Methods + ------- + - `set()` + - `get()` + - `clear()` + - `setQuery(...)` + - `getQuery()` + - `load()` + - `refresh()` + + Events + ------ + + - `changeEvent` + - `errorEvent` + + > Under construction, use with care. + +*/ +joDataSource = function(data) { + this.changeEvent = new joSubject(this); + this.errorEvent = new joSubject(this); + + if (typeof data !== "undefined") + this.setData(data); + else + this.data = ""; +}; +joDataSource.prototype = { + autoSave: true, + data: null, + + setQuery: function(query) { + this.query = query; + }, + + setAutoSave: function(state) { + this.autoSave = state; + return this; + }, + + setData: function(data) { + var last = this.data; + this.data = data; + + if (data !== last) + this.changeEvent.fire(data); + }, + + getData: function() { + return this.data; + }, + + getDataCount: function() { + return this.getData().length; + }, + + getPageCount: function() { + if (this.pageSize) + return Math.floor(this.getData().length / this.pageSize) + 1; + else + return 1; + }, + + getPage: function(index) { + var start = index * this.pageSize; + var end = start + this.pageSize; + + if (end > this.getData().length) + end = this.getData().length; + + if (start < 0) + start = 0; + + return this.data.slice(start, end); + }, + + refresh: function() { + // needs to make a new query object + }, + + setPageSize: function(length) { + this.pageSize = length; + }, + + getPageSze: function() { + return this.pageSize; + }, + + load: function(data) { + this.data = data; + this.changeEvent.fire(data); + }, + + error: function(msg) { + this.errorEvent.fire(msg); + } +}; +/** + joRecord + ======== + + An event-driven wrapper for an object and its properties. Useful as a + data interface for forms and other collections of UI controls. + + Extends + ------- + + - joDataSource + + Methods + ------- + + - `link(property)` + + Returns a reference to a joProperty object which can be used with UI + controls (children of joControl) to automatically save or load data + based on user interaction. + + - `save()` + + Saves the object's data. The base class does not itself save the data; + you will need to make your own action for the save method, or have + something which subscribes to the `saveEvent`. + + - `load()` + + Loads the object's data, and fires off notifications to any UI controls + which are linked to this joRecord object. Same as the `save()` method, + you will have to make this function do some actual file loading if that's + what you want it to do. + + - `getProperty(property)` + - `setProperty(property, value)` + + Get or set a given property. Used in conjunction with `setAutoSave()`, + `setProprty()` will also trigger a call to the `save()` method. + + - `getDelegate(property)` + + Returns a reference to the joProperty object which fires off events + for data changes for that property. If none exists, one is created. + This method is used by the `link()` method, and can be overriden if + you extend this class to provide some other flavor of a joDataSource + to manage events for your properties. + + Use + --- + + // setup a joRecord + var r = new joRecord({ + user: "Jo", + password: "1234", + active: true + }); + + // bind it to some fields + var x = new joGroup([ + new joLabel("User"), + new joInput(r.link("user")), + new joLabel("Password"), + new joPasswordInput(r.link("password")), + new joFlexBox([ + new joLabel("Active"), + new joToggle(r.link("active")) + ]) + ]); + + And if you want the data to be persistent, or interact with some + cloud service, you'll need to do something like this: + + // make something happen to load the data + r.load = function() { + // some AJAX or SQL call here + }; + + // make something happen to save the data + r.save = function() { + // some AJAX or SQL call here + }; + + You could also make your own subclass of joRecord with your own save + and load methods using `extend()` like this: + + var preferences = function() { + // call to the superclass constructor + joRecord.apply(this, arguments); + }; + preferences.extend(joRecord, { + save: function() { + // do an AJAX or SQL call here + }, + + load: function() { + // do an AJAX or SQL call here + } + } + + See Class Patterns for more details on this method of "subclassing" + in JavaScript. + +*/ +joRecord = function(data) { + joDataSource.call(this, data); + this.delegate = {}; +}; +joRecord.extend(joDataSource, { + link: function(p) { + return this.getDelegate(p); + }, + + getDelegate: function(p) { + if (!this.delegate[p]) + this.delegate[p] = new joProperty(this, p); + + return this.delegate[p]; + }, + + getProperty: function(p) { + return this.data[p]; + }, + + setProperty: function(p, data) { + if (this.data[p] === data) + return; + + this.data[p] = data; + this.changeEvent.fire(this); + + if (this.autoSave) + this.save(); + + return this; + }, + + load: function() { + console.log("TODO: extend the load() method"); + return this; + }, + + save: function() { + console.log("TODO: extend the save() method"); + return this; + } +}); + +/** + joProperty + ========== + + Used by joRecord to provide an event-driven binding to properties. + This class is instantiated by joRecord and not of much use on its own. + + Extends + ------- + + - joDataSource + + Use + --- + + See joRecord for examples. +*/ +joProperty = function(datasource, p) { + joDataSource.call(this); + + this.changeEvent = new joSubject(this); + datasource.changeEvent.subscribe(this.onSourceChange, this); + + this.datasource = datasource; + this.p = p; +}; +joProperty.extend(joDataSource, { + setData: function(data) { + if (this.datasource) + this.datasource.setProperty(this.p, data); + + return this; + }, + + getData: function() { + if (!this.datasource) + return null; + + return this.datasource.getProperty(this.p); + }, + + onSourceChange: function() { + this.changeEvent.fire(this.getData()); + } +}); +/** + - - - + + joDatabase + =========== + + Wrapper class for WebKit SQLite database. + + Methods + ------- + + - `open(datafile, size)` + + `datafile` is a filename, `size` is an optional parameter for initial + allocation size for the database. + + - `close()` + + - `now()` + + *Deprecated* convenience method which returns a SQLite-formatted date + string for use in queries. Should be replaced with a utility function + in joTime. +*/ +joDatabase = function(datafile, size) { + this.openEvent = new joEvent.Subject(this); + this.closeEvent = new joEvent.Subject(this); + this.errorEvent = new joEvent.Subject(this); + + this.datafile = datafile; + this.size = size || 256000; + this.db = null; +}; +joDatabase.prototype = { + open: function() { + this.db = openDatabase(this.datafile, "1.0", this.datafile, this.size); + + if (this.db) { + this.openEvent.fire(); + } + else { + joLog("DataBase Error", this.db); + this.errorEvent.fire(); + } + }, + + close: function() { + this.db.close(); + this.closeEvent.fire(); + }, + + now: function(offset) { + var date = new Date(); + + if (offset) + date.setDate(date.valueOf() + (offset * 1000 * 60 * 60 * 24)); + + return date.format("yyyy-mm-dd"); + } +}; +/** + joSQLDataSource + ================ + + SQL flavor of joDataSource which uses "HTML5" SQL found in webkit. + + Methods + ------- + + - `setDatabase(joDatabase)` + - `setQuery(query)` + - `setParameters(arguments)` + - `execute(query, arguments)` + + Events + ------ + + - `changeEvent` + + Fired when data is loaded after an `execute()` or when data is cleared. + + - `errorEvent` + + Fired when some sort of SQL error happens. + + Extends + ------- + + - joDataSource +*/ +joSQLDataSource = function(db, query, args) { + this.db = db; + this.query = (typeof query == 'undefined') ? "" : query; + this.args = (typeof args == 'undefined') ? [] : args; + + this.changeEvent = new joEvent.subject(this); + this.errorEvent = new joEvent.subject(this); +}; +joSQLDataSource.prototype = { + setDatabase: function(db) { + this.db = db; + }, + + setQuery: function(query) { + this.query = query; + }, + + setData: function(data) { + this.data = data; + this.changeEvent.fire(); + }, + + clear: function() { + this.data = []; + this.changeEvent.fire(); + }, + + setParameters: function(args) { + this.args = args; + }, + + execute: function(query, args) { + this.setQuery(query || ""); + this.setParameters(args); + + if (this.query) + this.refresh(); + }, + + refresh: function() { + if (!this.db) { + this.errorEvent.fire(); +// joLog("query error: no db!"); + return; + } + + var self = this; + + if (arguments.length) { + var args = []; + for (var i = 0; i < arguments.length; i++) + args.push(arguments[i]); + } + else { + var args = this.args; + } + + var query = this.query; + + function success(t, result) { + self.data = []; + + for (var i = 0, l = result.rows.length; i < l; i++) { + var row = result.rows.item(i); + + self.data.push(row); + } + + self.changeEvent.fire(self.data); + } + + function error() { + joLog('SQL error', query, "argument count", args.length); + self.errorEvent.fire(); + } + + this.db.db.transaction(function(t) { + t.executeSql(query, args, success, error); + }); + } +}; +/** + joFileSource + ============ + + A special joDataSource which loads and handles a file. This class + wraps joFile. + + Extends + ------- + + - `joDataSource` + +*/ +joFileSource = function(url, timeout) { + this.changeEvent = new joSubject(this); + this.errorEvent = new joSubject(this); + + if (timeout) + this.setTimeout(timeout); + + if (url) + this.setQuery(url); +}; +joFileSource.extend(joDataSource, { + baseurl: '', + query: '', + + load: function() { + var get = this.baseurl + this.query; + + joFile(get, this.callBack, this); + }, + + callBack: function(data, error) { + if (error) + this.errorEvent.fire(error); + else + this.setData(data); + } +}); + +/** + joFile + ====== + + A utility method which uses XMLHttpRequest to load a text-like file + from either a remote server or a local file. + + > Note that some browsers and mobile devices will *not* allow you to + > load from just any URL, and some will restrict use with local files + > especially (I'm looking at you, FireFox). + > + > If your aim is to load JavaScript-like data (also, JSON), you may want + > to look at joScript instead, which uses script tags to accomplish the job. + + Calling + ------- + + joFile(url, call, context, timeout) + + Where + ----- + + - `url` is a well-formed URL, or, in most cases, a relative url to a local + file + + - `call` is a function to call when the operation completes + + - `context` is an optional scope for the function to call (i.e. value of `this`). + You can also ignore this parameter (or pass in `null` and use `Function.bind(this)` + instead. + + - `timeout` is an optional parameter which tells joFile to wait, in seconds, + for a response before throwing an error. + + Use + --- + + // simple call with a global callback + var x = joFile("about.html", App.loadAbout); + + // an inline function + var y = joFile("http://joapp.com/index.html", function(data, error) { + if (error) { + console.log("error loading file"); + return; + } + + console.log(data); + }); +*/ +joFile = function(url, call, context, timeout) { + var req = new XMLHttpRequest(); + + if (!req) + return onerror(); + + // 30 second default on requests + if (!timeout) + var timeout = 60 * SEC; + + var timer = (timeout > 0) ? setTimeout(onerror, timeout) : null; + + req.open('GET', url, true); + req.onreadystatechange = onchange; + req.onError = onerror; + req.send(null); + + function onchange(e) { + if (timer) + timer = clearTimeout(timer); + + if (req.readyState == 4) + handler(req.responseText, 0); + } + + function onerror() { + handler(null, true); + } + + function handler(data, error) { + if (call) { + if (context) + call.call(context, data, error); + else + call(error, data, error); + } + } +} + +/** + joScript + ======== + + Script tag loader function which can be used to dynamically load script + files or make RESTful calls to many JSON services (provided they have some + sort of callback ability). This is a low-level utility function. + + > Need a URL with some examples of this. + + Calling + ------- + + `joScript(url, callback, context, errorcallback, errorcontext)` + + - url + - callback is a function (supports bind, in which case context is optional) + - context (usually `this`, and is optional) + + Returns + ------- + + Calls your handler method and passes a truthy value if there was an error. + + Use + --- + + joScript("myscript.js", function(error, url) { + if (error) + console.log("script " + url + " didn't load."); + }, this); + +*/ +function joScript(url, call, context) { + var node = joDOM.create('script'); + + if (!node) + return; + + node.onload = onload; + node.onerror = onerror; + node.src = url; + document.body.appendChild(node); + + function onerror() { + handler(true); + } + + function onload() { + handler(false); + } + + function handler(error) { + if (call) { + if (context) + call.call(context, error, url); + else + call(error, url); + } + + document.body.removeChild(node); + node = null; + } +} + +/** + joPreference + ============ + + A class used for storing and retrieving preferences in your application. + + *The interface for this is changing.* joPreference will become a specialized + application-level extension of joRecord in the near future. Until then, you + should use joRecord to achieve this use-case. + + Extends + ------- + + - joRecord + +*/ + +// placeholder for now +joPreference = joRecord; +/** + joYQL + ===== + + A joDataSource geared for YQL RESTful JSON calls. YQL is like SQL, but for cloud + services. Pretty amazing stuff: + + > The Yahoo! Query Language is an expressive SQL-like language that lets you query, + > filter, and join data across Web services. With YQL, apps run faster with fewer lines of + > code and a smaller network footprint. + > + > Yahoo! and other websites across the Internet make much of their structured data + > available to developers, primarily through Web services. To access and query these + > services, developers traditionally endure the pain of locating the right URLs and + > documentation to access and query each Web service. + > + > With YQL, developers can access and shape data across the Internet through one + > simple language, eliminating the need to learn how to call different APIs. + + [Yahoo! Query Language Home](http://developer.yahoo.com/yql/) + + Use + --- + + A simple one-shot use would look like: + + // setup our data source + var yql = new joYQL("select * from rss where url='http://davebalmer.wordpress.com'"); + + // subscribe to load events + yql.loadEvent.subscribe(function(data) { + joLog("received data!"); + }); + + // kick off our call + yql.exec(); + + A more robust example with parameters in the query could look something + like this: + + // quick/dirty augmentation of the setQuery method + var yql = new joYQL(); + yql.setQuery = function(feed, limit) { + this.query = "select * from rss where url='" + + feed + "' limit " + limit + + " | sort(field=pubDate)"; + }; + + // we can hook up a list to display the results + var list = new joList(yql).attach(document.body); + list.formatItem = function(data, index) { + var html = new joListItem(data.title + " (" + data.pubDate + ")", index); + }; + + // later, we make our call with our parameters + yql.exec("http://davebalmer.wordpress.com", 10); + + Methods + ------- + - `setQuery()` + + Designed to be augmented, see the example above. + + - `exec()` + + Extends + ------- + + - joDataSource + +*/ + +joYQL = function(query) { + joDataSource.call(this); + + this.setQuery(query); +}; +joYQL.extend(joDataSource, { + baseurl: 'http://query.yahooapis.com/v1/public/yql?', + format: 'json', + query: '', + + exec: function() { + var get = this.baseurl + "q=" + encodeURIComponent(this.query) + + "&format=" + this.format + "&callback=" + joDepot(this.load, this); + + joScript(get, this.callBack, this); + }, + + load: function(data) { + var results = data.query && data.query.results && data.query.results.item; + + if (!results) + this.errorEvent.fire(data); + else { + this.data = results; + this.changeEvent.fire(results); + } + }, + + callBack: function(error) { + if (error) + this.errorEvent.fire(); + } +}); + + +/* + Used by joYQL for RESTful calls, may be abstracted into + a restful superclass, but that will be dependant on a + callback paramter as well. +*/ +joDepotCall = []; +joDepot = function(call, context) { + joDepotCall.push(handler); + + function handler(data) { + if (context) + call.call(context, data); + else + call(data); + }; + + return "joDepotCall[" + (joDepotCall.length - 1) + "]"; +}; +/** + joInterface + =========== + + *EXPERIMENTAL* + + > This utility method is experimental! Be very careful with it. *NOTE* that + > for now, this class requires you to remove whitespace in your HTML. If you + > don't know a good approach offhand to do that, then this thing probably isn't + > ready for you yet. + + This class parses the DOM tree for a given element and attempts to + attach appropriate joView subclasses to all the relevant HTML nodes. + Returns an object with references to all elements with the `id` + attribute set. This method helps turn HTML into HTML + JavaScript. + + Use + --- + + // an HTML element by its ID + var x = new joInterface("someid"); + + // a known HTML element + var y = new joInterface(someHTMLElement); + + // the entire document body (careful, see below) + var z = new joInterface(); + + Returns + ------- + + A new object with a property for each element ID found. For example: + + + + Login + + Username + + Password + + + Login + + + Parsed with this JavaScript: + + // walk the DOM, find nodes, create controls for each + var x = new joInterface("login"); + + Produces these properties: + + - `x.login` is a reference to a `new joCard` + - `x.username` is a reference to a `new joInput` + - `x.password` is a reference to a `new joPassword` + - `x.loginbutton` is a reference to a `new joButton` + + This in essence flattens your UI to a single set of properties you can + use to access the controls that were created from your DOM structure. + + In addition, any unrecognized tags which have an `id` attribute set will + also be loaded into the properties. + + Parsing complex trees + --------------------- + + Yes, you can make a joInterface that encapsulates your entire UI with HTML. + This is not recommended for larger or more complex applications, some + reasons being: + + - Rendering speed: if you're defining multiple views within a `` + (or another subclass of joContainer), your users will see a flicker and + longer load time while the window renders your static tags and the extra + views for the stack are removed from view. + + - Double rendering: again with `` tags, you're going to see a separate + render when the first view is redrawn (has to). + + - Load time: especially if you're doing a mobile app, this could be a biggie. + You are almost always going to be better off building the app controls with + JavaScript (especially in conjunction with joCache, which only creates DOM + nodes for a given view structure on demand). + + If you really want to use HTML as your primary means of defining your UI, you're + better off putting your major UI components inside of a `
` (or other tag) + with `display: none` set in its CSS property. Like this: + + +
+ + About this app + + This is my app, it is cool. + + Done + + + ... etc ... + +
+ + Then in your JavaScript: + + // pull in all our card views from HTML + var cards = new joInterface("cards"); + + Definitely use this class judiciously or you'll end up doing a lot of recatoring + as your application grows. + + Flattening UI widget references + ------------------------------- + + This is both good and bad, depending on your coding style and complexity of + your app. Because all the tags with an ID attribute (regardless of where they + are in your tag tree) get a single corresponding property reference, things + could get very messy in larger apps. Again, be smart. + +*/ +joInterface = function(parent) { + // initialize our tag lookup object + jo.initTagMap(); + + // surprise! we're only using our prototype once and + // just returning references to the nodes with ID attributes + return this.get(parent); +}; +joInterface.prototype = { + get: function(parent) { + parent = joDOM.get(parent); + + if (!parent) + parent = document.body; + + var ui = {}; + + // pure evil -- seriously + var setContainer = joView.setContainer; + var draw = joView.draw; + + parse(parent); + + // evil purged + joView.setContainer = setContainer; + joView.draw = draw; + + function parse(node) { + if (!node) + return; + + var args = ""; + + // handle all the leaves first + if (node.childNodes && node.firstChild) { + // spin through child nodes, build our list + var kids = node.childNodes; + args = []; + + for (var i = 0, l = kids.length; i < l; i++) { + var p = parse(kids[i]); + + if (p) + args.push(p); + } + } + + // make this control + return newview(node, args); + } + + // create appropriate joView widget from the tag type, + // otherwise return the node itself + function newview(node, args) { + var tag = node.tagName; + var view = node; + +// console.log(tag, node.nodeType); + + if (jo.tagMap[tag]) { + if (args instanceof Array && args.length) { + if (args.length == 1) + args = args[0]; + } + + if (args instanceof Text) + args = node.nodeData; + + if (!args) + args = node.value || node.checked || node.innerText || node.innerHTML; + +// console.log(args); + + joView.setContainer = function() { + this.container = node; + + return this; + }; + + if (typeof jo.tagMap[tag] === "function") { + var o = jo.tagMap[tag]; + } + else { + var t = node.type || node.getAttribute("type"); + var o = jo.tagMap[tag][t]; + } + + if (typeof o === "function") + var view = new o(args); + else + joLog("joInterface can't process ", tag, "'type' attribute?"); + } + + // keep track of named controls + if (node.id) + ui[node.id] = view; + + return view; + } + + // send back our object with named controls as properties +// console.log(ui); + return ui; + } +}; +/** + joCollect + ========= + + *DEPRECATED* use joInterface instead. This function is planned + to die when jo goes beta. + +*/ +joCollect = { + get: function(parent) { + // this is what happens when you announced something not + // quite fully baked + return new joInterface(parent); + } +}; +/** + joView + ======= + + Base class for all other views, containers, controls and other visual doo-dads. + + Use + ----- + + var x = new joView(data); + + Where `data` is either a text or HTML string, an HTMLElement, or any joView object + or subclass. + + Methods + ------- + + - `setData(data)` + - `getData()` + - `createContainer(type, classname)` + - `setContainer(HTMLElement)` + - `getContainer()` + - `clear()` + - `refresh()` + + - `attach(HTMLElement or joView)` + - `detach(HTMLElement or joView)` + + Convenience methods which allow you to append a view or DOM node to the + current view (or detach it). + +*/ +joView = function(data) { + this.changeEvent = new joSubject(this); + + this.setContainer(); + + if (data) + this.setData(data); +}; +joView.prototype = { + tagName: "joview", + busyNode: null, + container: null, + data: null, + + getContainer: function() { + return this.container; + }, + + setContainer: function(container) { + this.container = joDOM.get(container); + + if (!this.container) + this.container = this.createContainer(); + + this.setEvents(); + + return this; + }, + + createContainer: function() { + return joDOM.create(this); + }, + + clear: function() { + this.data = ""; + + if (this.container) + this.container.innerHTML = ""; + + this.changeEvent.fire(); + }, + + setData: function(data) { + this.data = data; + this.refresh(); + + return this; + }, + + getData: function() { + return this.data; + }, + + refresh: function() { + if (!this.container || typeof this.data == "undefined") + return 0; + + this.container.innerHTML = ""; + this.draw(); + + this.changeEvent.fire(this.data); + }, + + draw: function() { + this.container.innerHTML = this.data; + }, + + setStyle: function(style) { + joDOM.setStyle(this.container, style); + + return this; + }, + + attach: function(parent) { + if (!this.container) + return this; + + var node = joDOM.get(parent) || document.body; + node.appendChild(this.container); + + return this; + }, + + detach: function(parent) { + if (!this.container) + return this; + + var node = joDOM.get(parent) || document.body; + + if (this.container && this.container.parentNode === node) + node.removeChild(this.container); + + return this; + }, + + setEvents: function() {} +}; +/** + joContainer + ============ + + A view which is designed to contain other views and controls. Subclass to provide + different layout types. A container can be used to intantiate an entire tree of + controls at once, and is a very powerful UI component in jo. + + Use + --- + + // plain container + var x = new joContainer(); + + // HTML or plain text + var y = new joContainer("Some HTML"); + + // HTMLElement + var w = new joContainer(joDOM.get("mydiv")); + + // nested inline structure with text, HTML, joViews or HTMLElements + var z = new joContainer([ + new joTitle("Hello"), + new joList([ + "Red", + "Green", + "Blue" + ]), + new joFieldset([ + "Name", new joInput(joPreference.bind("name")), + "Phone", new joInput(joPreference.bind("phone")) + ]), + new joButton("Done") + ]); + + // set an optional title string, used with joNavbar + z.setTitle("About"); + + Extends + ------- + + - joView + + Events + ------ + + - `changeEvent` + + Methods + ------- + + - `setData(data)` + + The constructor calls this method if you provide `data` when you instantiate + (see example above) + + - `push(data)` + + Same support as `setData()`, but places the new content at the end of the + existing content. + + - `setTitle(string)` + - `getTitle(string)` + + Titles are optional, but used with joStack & joStackScroller to update a + joNavbar control automagically. + +*/ +joContainer = function(data) { + joView.apply(this, arguments); +}; +joContainer.extend(joView, { + tagName: "jocontainer", + title: null, + + getContent: function() { + return this.container.childNodes; + }, + + setTitle: function(title) { + this.title = title; + return this; + }, + + setData: function(data) { + this.data = data; + this.refresh(); + return this; + }, + + activate: function() {}, + + deactivate: function() {}, + + push: function(data) { + if (typeof data === 'object') { + if (data instanceof Array) { + // we have a list of stuff + for (var i = 0; i < data.length; i++) + this.push(data[i]); + } + else if (data instanceof joView && data.container !== this.container) { + // ok, we have a single widget here + this.container.appendChild(data.container); + } + else if (data instanceof HTMLElement) { + // DOM element attached directly + this.container.appendChild(data); + } + } + else { + // shoving html directly in does work + var o = document.createElement("div"); + o.innerHTML = data; + this.container.appendChild(o); + } + }, + + getTitle: function() { + return this.title; + }, + + refresh: function() { + if (this.container) + this.container.innerHTML = ""; + + this.draw(); + this.changeEvent.fire(); + }, + + draw: function() { + this.push(this.data); + } +}); +/** + joControl + ========= + + Interactive, data-driven control class which may be bound to a joDataSource, + can receive focus events, and can fire off important events which other objects + can listen for and react to. + + Extends + ------- + + - joView + + Events + ------ + + - `changeEvent` + - `selectEvent` + + Methods + ------- + + - `setValue(value)` + + Many controls have a *value* in addition to their *data*. This is + particularly useful for `joList`, `joMenu`, `joOption` and other controls + which has a list of possibilities (the data) and a current seletion from those + (the value). + + - `enable()` + - `disable()` + + Enable or disable the control, pretty much does what you'd expect. + + - `focus()` + - `blur()` + + Manually control focus for this control. + + - `setDataSource(joDataSource)` + + Tells this control to bind its data to any `joDataSource` or subclass. + + - `setValueSource(joDataSource)` + + Tells this control to bind its *value* to any `joDataSource` type. + + - `setReadOnly(state)` + + Certain controls can have their interaction turned off. State is either `true` + or `false`. + + See Also + -------- + + - joRecord and joProperty are specialized joDataSource classes which + make it simple to bind control values to a data structure. + +*/ +joControl = function(data, value) { + this.selectEvent = new joSubject(this); + this.enabled = true; + this.value = null; + + if (typeof value !== "undefined" && value != null) { + if (value instanceof joDataSource) + this.setValueSource(value); + else + this.value = value; + } + + if (data instanceof joDataSource) { + // we want to bind directly to some data + joView.call(this); + this.setDataSource(data); + } + else { + joView.apply(this, arguments); + } +}; +joControl.extend(joView, { + tagName: "jocontrol", + + setEvents: function() { + // not sure what we want to do here, want to use + // gesture system, but that's not defined + joEvent.on(this.container, "click", this.onMouseDown, this); + joEvent.on(this.container, "blur", this.onBlur, this); + joEvent.on(this.container, "focus", this.onFocus, this); + }, + + onMouseDown: function(e) { + this.select(e); + }, + + select: function(e) { + if (e) + joEvent.stop(e); + + this.selectEvent.fire(this.data); + }, + + enable: function() { + joDOM.removeCSSClass(this.container, 'disabled'); + this.container.contentEditable = true; + this.enabled = true; + + return this; + }, + + disable: function() { + joDOM.addCSSClass(this.container, 'disabled'); + this.container.contentEditable = false; + this.enabled = false; + + return this; + }, + + setReadOnly: function(value) { + if (typeof value === 'undefined' || value) + this.container.setAttribute('readonly', '1'); + else + this.container.removeAttribute('readonly'); + + return this; + }, + + onFocus: function(e) { + joEvent.stop(e); + + if (this.enabled) + joFocus.set(this); + }, + + onBlur: function(e) { + this.data = (this.container.value) ? this.container.value : this.container.innerHTML; + joEvent.stop(e); + + if (this.enabled) { + this.blur(); + + this.changeEvent.fire(this.data); + } + }, + + focus: function(e) { + if (!this.enabled) + return; + + joDOM.addCSSClass(this.container, 'focus'); + + if (!e) + this.container.focus(); + + return this; + }, + + setValue: function(value) { + this.value = value; + this.changeEvent.fire(value); + + return this; + }, + + getValue: function() { + return this.value; + }, + + blur: function() { + joDOM.removeCSSClass(this.container, 'focus'); + + return this; + }, + + setDataSource: function(source) { + this.dataSource = source; + source.changeEvent.subscribe(this.setData, this); + this.setData(source.getData() || null); + this.changeEvent.subscribe(source.setData, source); + + return this; + }, + + setValueSource: function(source) { + this.valueSource = source; + source.changeEvent.subscribe(this.setValue, this); + this.setValue(source.getData() || null); + this.selectEvent.subscribe(source.setData, source); + + return this; + } +}); +/** + joButton + ======== + + Button control. + + // simple invocation + var x = new joButton("Done"); + + // optionally pass in a CSS classname to style the button + var y = new joButton("Cancel", "cancelbutton"); + + // like other controls, you can pass in a joDataSource + // which could be useful, so why not + var z = new joButton(joPreference.bind("processname")); + + Extends + ------- + + - joControl + + Methods + ------- + + - enable() + - disable() + +*/ + +joButton = function(data, classname) { + // call super + joControl.apply(this, arguments); + this.enabled = true; + + if (classname) + this.container.className = classname; +}; +joButton.extend(joControl, { + tagName: "jobutton", + + createContainer: function() { + var o = joDOM.create(this.tagName); + + if (o) + o.setAttribute("tabindex", "1"); + + return o; + }, + + enable: function() { + this.container.setAttribute("tabindex", "1"); + return joControl.prototype.enable.call(this); + }, + + disable: function() { + // this doesn't seem to work in safari doh + this.container.removeAttribute("tabindex"); + return joControl.prototype.disable.call(this); + } +}); +/** + - - - + + joBusy + ====== + + The idea here is to make a generic "spinner" control which you + can overlay on other controls. It's still in flux, don't use it + just yet. + + Extends + ------- + + - joView + + Methods + ------- + + - `setMessage(status)` + + You can update the status message in this busy box so users + have a better idea why the busy box is showing. +*/ + +joBusy = function(data) { + joContainer.apply(this, arguments); +}; +joBusy.extend(joContainer, { + tagName: "jobusy", + + draw: function() { + this.container.innerHTML = ""; + for (var i = 0; i < 9; i++) + this.container.appendChild(joDom.create("jobusyblock")); + }, + + setMessage: function(msg) { + this.message = msg || ""; + }, + + setEvents: function() { + return this; + } +}); +/** + joList + ======= + + A widget class which expects an array of any data type and renders the + array as a list. The list control handles DOM interactions with only a + single touch event to determine which item was selected. + + Extends + ------- + + - joControl + + Events + ------ + + - `selectEvent` + + Fired when an item is selected from the list. The data in the call is the + index of the item selected. + + - `changeEvent` + + Fired when the data is changed for the list. + + Methods + ------- + + - `formatItem(data, index)` + + When subclassing or augmenting, this is the method responsible for + rendering a list item's data. + + - `compareItems(a, b)` + + For sorting purposes, this method is called and should be overriden + to support custom data types. + + // general logic and approriate return values + if (a > b) + return 1; + else if (a == b) + return 0; + else + return -1 + + - `setIndex(index)` + - `getIndex()` + + *DEPRECATED* USe `setValue()` and `getValue()` instead, see joControl. + + - `refresh()` + + - `setDefault(message)` + + Will present this message (HTML string) when the list is empty. + Normally the list is empty; this is a convenience for "zero state" + UI requirements. + + - `getNodeData(index)` + + - `getLength()` + + - `next()` + + - `prev()` + + - `setAutoSort(boolean)` + +*/ +joList = function() { + // these are being deprecated in the BETA + // for now, we'll keep references to the new stuff + this.setIndex = this.setValue; + this.getIndex = this.getValue; + + joControl.apply(this, arguments); +}; +joList.extend(joControl, { + tagName: "jolist", + defaultMessage: "", + lastNode: null, + value: null, + autoSort: false, + + setDefault: function(msg) { + this.defaultMessage = msg; + + if (typeof this.data === 'undefined' || !this.data || !this.data.length) { + if (typeof msg === 'object') { + this.innerHTML = ""; + if (msg instanceof joView) + this.container.appendChild(msg.container); + else if (msg instanceof HTMLElement) + this.container.appendChild(msg); + } + else { + this.innerHTML = msg; + } + } + + return this; + }, + + draw: function() { + var html = ""; + var length = 0; + + if (typeof this.data === 'undefined' || !this.data || !this.data.length) { + if (this.defaultMessage) + this.container.innerHTML = this.defaultMessage; + + return; + } + + for (var i = 0, l = this.data.length; i < l; i++) { + var element = this.formatItem(this.data[i], i, length); + + if (element == null) + continue; + + if (typeof element === "string") + html += element; + else + this.container.appendChild((element instanceof joView) ? element.container : element); + + ++length; + } + + // support setting the contents with innerHTML in one go, + // or getting back HTMLElements ready to append to the contents + if (html.length) + this.container.innerHTML = html; + + // refresh our current selection + if (this.value >= 0) + this.setValue(this.value, true); + + return; + }, + + deselect: function() { + if (typeof this.container == 'undefined' + || !this.container['childNodes']) + return; + + var node = this.getNode(this.value); + if (node) { + if (this.lastNode) { + joDOM.removeCSSClass(this.lastNode, "selected"); + this.value = null; + } + } + + return this; + }, + + setValue: function(index, silent) { + this.value = index; + + if (index == null) + return; + + if (typeof this.container === 'undefined' + || !this.container + || !this.container.firstChild) { + return this; + } + + var node = this.getNode(this.value); + if (node) { + if (this.lastNode) + joDOM.removeCSSClass(this.lastNode, "selected"); + + joDOM.addCSSClass(node, "selected"); + this.lastNode = node; + } + + if (index >= 0 && !silent) { + this.fireSelect(index); + this.changeEvent.fire(index); + } + + return this; + }, + + getNode: function(index) { + return this.container.childNodes[index]; + }, + + fireSelect: function(index) { + this.selectEvent.fire(index); + }, + + getValue: function() { + return this.value; + }, + + onMouseDown: function(e) { + joEvent.stop(e); + + var node = joEvent.getTarget(e); + var index = -1; + + while (index == -1 && node !== this.container) { + index = node.getAttribute("index") || -1; + node = node.parentNode; + } + + if (index >= 0) + this.setValue(index); + }, + + refresh: function() { +// this.value = null; +// this.lastNode = null; + + if (this.autoSort) + this.sort(); + + joControl.prototype.refresh.apply(this); + }, + + getNodeData: function(index) { + if (this.data && this.data.length && index >= 0 && index < this.data.length) + return this.data[index]; + else + return null; + }, + + getLength: function() { + return this.length || this.data.length || 0; + }, + + sort: function() { + this.data.sort(this.compareItems); + }, + + getNodeIndex: function(element) { + var index = element.getAttribute('index'); + if (typeof index !== "undefined" && index != null) + return parseInt(index) + else + return -1; + }, + + formatItem: function(itemData, index) { + var element = document.createElement('jolistitem'); + element.innerHTML = itemData; + element.setAttribute("index", index); + + return element; + }, + + compareItems: function(a, b) { + if (a > b) + return 1; + else if (a == b) + return 0; + else + return -1; + }, + + setAutoSort: function(state) { + this.autoSort = state; + return this; + }, + + next: function() { + if (this.getValue() < this.getLength() - 1) + this.setValue(this.value + 1); + }, + + prev: function() { + if (this.getValue() > 0) + this.setValue(this.value - 1); + } +}); +/** + - - - + + joBusy + ====== + + The idea here is to make a generic "spinner" control which you + can overlay on other controls. It's still in flux, don't use it + just yet. + + Extends + ------- + + - joView + + Methods + ------- + + - `setMessage(status)` + + You can update the status message in this busy box so users + have a better idea why the busy box is showing. +*/ + +joBusy = function(data) { + joContainer.apply(this, arguments); +}; +joBusy.extend(joContainer, { + tagName: "jobusy", + + draw: function() { + this.container.innerHTML = ""; + for (var i = 0; i < 9; i++) + this.container.appendChild(joDom.create("jobusyblock")); + }, + + setMessage: function(msg) { + this.message = msg || ""; + }, + + setEvents: function() { + return this; + } +}); +/** + joCaption + ========= + + Basically, a paragraph of text. + + Extends + ------- + + - joControl + +*/ +joCaption = function(data) { + joControl.apply(this, arguments); +}; +joCaption.extend(joControl, { + tagName: "jocaption" +}); + +/** + joCard + ====== + + Special container for card views, more of an application-level view. + + Extends + ------- + + - joContainer + + Methods + ------- + + - `activate()` + - `deactivate()` + + These methods are called automatically by various joView objects, for + now joStack is the only one which does. Basically, allows you to add + application-level handlers to initialize or cleanup a joCard. + +*/ +joCard = function(data) { + joContainer.apply(this, arguments); +}; +joCard.extend(joContainer, { + tagName: "jocard" +}); + +/** + joStack + ======== + + A UI container which keeps an array of views which can be pushed and popped. + The DOM elements for a given view are removed from the DOM tree when popped + so we keep the render tree clean. + + Extends + ------- + + - joView + + Methods + ------- + + - `push(joView | HTMLElement)` + + Pushes a new joView (or HTMLELement) onto the stack. + + - `pop()` + + Pulls the current view off the stack and goes back to the previous view. + + - `home()` + + Return to the first view, pop everything else off the stack. + + - `show()` + - `hide()` + + Controls the visibility of the entire stack. + + - `forward()` + - `back()` + + Much like your browser forward and back buttons, only for the stack. + + - `setLocked(boolean)` + + The `setLocked()` method tells the stack to keep the first view pushed onto the + stack set; that is, `pop()` won't remove it. Most apps will probably use this, + so setting it as a default for now. + + Events + ------ + + - `showEvent` + - `hideEvent` + - `homeEvent` + - `pushEvent` + - `popEvent` + + Notes + ----- + + Should set classNames to new/old views to allow for CSS transitions to be set + (swiping in/out, cross fading, etc). Currently, it does none of this. + + Also, some weirdness with the new `forward()` and `back()` methods in conjuction + with `push()` -- need to work on that, or just have your app rigged to `pop()` + on back to keep the nesting simple. + +*/ +joStack = function(data) { + this.visible = false; + + this.data = []; + + joContainer.apply(this, arguments); + + // yes, nice to have one control, but we need an array + if (this.data && !(this.data instanceof Array)) + this.data = [ this.data ]; + else if (this.data.length > 1) + this.data = [ this.data[0] ]; + + // we need to clear inlined stuff out for this to work + if (this.container && this.container.firstChild) + this.container.innerHTML = ""; + + // default to keep first card on the stack; won't pop() off + this.setLocked(true); + + this.pushEvent = new joSubject(this); + this.popEvent = new joSubject(this); + this.homeEvent = new joSubject(this); + this.showEvent = new joSubject(this); + this.hideEvent = new joSubject(this); + this.backEvent = new joSubject(this); + this.forwardEvent = new joSubject(this); + + this.index = 0; + this.lastIndex = 0; + this.lastNode = null; +}; +joStack.extend(joContainer, { + tagName: "jostack", + type: "fixed", + eventset: false, + + setEvents: function() { + // do not setup DOM events for the stack + }, + + onClick: function(e) { + joEvent.stop(e); + }, + + forward: function() { + if (this.index < this.data.length - 1) { + this.index++; + this.draw(); + this.forwardEvent.fire(); + } + }, + + back: function() { + if (this.index > 0) { + this.index--; + this.draw(); + this.backEvent.fire(); + } + }, + + draw: function() { + if (!this.container) + this.createContainer(); + + if (!this.data || !this.data.length) + return; + + // short term hack for webos + // not happy with it but works for now + jo.flag.stopback = this.index ? true : false; + + var container = this.container; + var oldchild = this.lastNode; + var newnode = getnode(this.data[this.index]); + var newchild = this.getChildStyleContainer(newnode); + + function getnode(o) { + return (o instanceof joView) ? o.container : o; + } + + if (!newchild) + return; + + if (this.index > this.lastIndex) { + var oldclass = "prev"; + var newclass = "next"; + joDOM.addCSSClass(newchild, newclass); + } + else if (this.index < this.lastIndex) { + var oldclass = "next"; + var newclass = "prev"; + joDOM.addCSSClass(newchild, newclass); + } + else { +// this.getContentContainer().innerHTML = ""; + } + + this.appendChild(newnode); + + var self = this; + var transitionevent = null; + + joDefer(animate, this, 1); + + function animate() { + // FIXME: AHHH must have some sort of transition for this to work, + // need to check computed style for transition to make this + // better + if (typeof window.onwebkittransitionend !== 'undefined') + transitionevent = joEvent.on(newchild, "webkitTransitionEnd", cleanup, self); + else + joDefer(cleanup, this, 200); + + if (newclass && newchild) + joDOM.removeCSSClass(newchild, newclass); + + if (oldclass && oldchild) + joDOM.addCSSClass(oldchild, oldclass); + } + + function cleanup() { + if (oldchild) { + self.removeChild(oldchild); + joDOM.removeCSSClass(oldchild, "next"); + joDOM.removeCSSClass(oldchild, "prev"); + } + + if (newchild) { + if (transitionevent) + joEvent.remove(newchild, "webkitTransitionEnd", transitionevent); + + joDOM.removeCSSClass(newchild, "next"); + joDOM.removeCSSClass(newchild, "prev"); + } + } + + if (typeof this.data[this.index].activate !== "undefined") + this.data[this.index].activate.call(this.data[this.index]); + + this.lastIndex = this.index; + this.lastNode = newchild; + }, + + appendChild: function(child) { + this.container.appendChild(child); + }, + + getChildStyleContainer: function(child) { + return child; + }, + + getChild: function() { + return this.container.firstChild; + }, + + getContentContainer: function() { + return this.container; + }, + + removeChild: function(child) { + if (child && child.parentNode === this.container) + this.container.removeChild(child); + }, + + isVisible: function() { + return this.visible; + }, + + push: function(o) { +// if (!this.data || !this.data.length || o !== this.data[this.data.length - 1]) +// return; + + // don't push the same view we already have + if (this.data && this.data.length && this.data[this.data.length - 1] === o) + return; + + this.data.push(o); + this.index = this.data.length - 1; + this.draw(); + this.pushEvent.fire(o); + }, + + // lock the stack so the first pushed view stays put + setLocked: function(state) { + this.locked = (state) ? 1 : 0; + }, + + pop: function() { + if (this.data.length > this.locked) { + var o = this.data.pop(); + this.index = this.data.length - 1; + + this.draw(); + + if (typeof o.deactivate === "function") + o.deactivate.call(o); + + if (!this.data.length) + this.hide(); + } + + if (this.data.length > 0) + this.popEvent.fire(); + }, + + home: function() { + if (this.data && this.data.length && this.data.length > 1) { + var o = this.data[0]; + var c = this.data[this.index]; + + if (o === c) + return; + + this.data = [o]; + this.lastIndex = 1; + this.index = 0; +// this.lastNode = null; + this.draw(); + + this.popEvent.fire(); + this.homeEvent.fire(); + } + }, + + showHome: function() { + this.home(); + + if (!this.visible) { + this.visible = true; + joDOM.addCSSClass(this.container, "show"); + this.showEvent.fire(); + } + }, + + getTitle: function() { + var c = this.data[this.index]; + if (typeof c.getTitle === 'function') + return c.getTitle(); + else + return false; + }, + + show: function() { + if (!this.visible) { + this.visible = true; + joDOM.addCSSClass(this.container, "show"); + + joDefer(this.showEvent.fire, this.showEvent, 500); + } + }, + + hide: function() { + if (this.visible) { + this.visible = false; + joDOM.removeCSSClass(this.container, "show"); + + joDefer(this.hideEvent.fire, this.hideEvent, 500); + } + } +}); +/** + joScroller + ========== + + A scroller container. Ultimately, mobile webkit implementations + should properly support scrolling elements that have the CSS + `overflow` property set to `scroll` or `auto`. Why don't they, + anyway? Until some sanity is adopted, we need to handle this scrolling + issue ourselves. joScroller expects a single child to manage + scrolling for. + + Use + --- + + // make a scroller and set its child later + var x = new joScroller(); + x.setData(myCard); + + // or define things inline, not always a good idea + var y = new joScroller(new joList(mydata)); + + // you can dump a big hunk of HTML in there, too + // since jo wraps strings in a container element, this works + var z = new joScroller('Some giant HTML as a string'); + + Extends + ------- + + - joContainer + + Methods + ------- + + - `scrollBy(position)` + - `scrollTo(position or joView or HTMLElement)` + + Scrolls to the position or the view or element. If you + specify an element or view, make sure that element is a + child node, or you'll get interesting results. + + - `setScroll(horizontal, vertical)` + + Tells this scroller to allow scrolling the vertical, horizontal, both or none. + + // free scroller + z.setScroll(true, true); + + // horizontal + z.setScroll(true, false); + + // no scrolling + z.setScroll(false, false); + +*/ + +joScroller = function(data) { + this.points = []; + this.eventset = false; + + this.horizontal = 0; + this.vertical = 1; + this.inMotion = false; + this.moved = false; + this.mousemove = null; + this.mouseup = null; + this.bump = 0; + + // Call Super + joContainer.apply(this, arguments); +}; +joScroller.extend(joContainer, { + tagName: "joscroller", + velocity: 1.6, + transitionEnd: "webkitTransitionEnd", + + setEvents: function() { + joEvent.capture(this.container, "click", this.onClick, this); + joEvent.on(this.container, "mousedown", this.onDown, this); + }, + + onFlick: function(e) { + // placeholder + }, + + onClick: function(e) { + if (this.moved) { + this.moved = false; + joEvent.stop(e); + joEvent.preventDefault(e); + } + }, + + onDown: function(e) { + joEvent.stop(e); + + this.reset(); + + var node = this.container.firstChild; + + joDOM.removeCSSClass(node, "flick"); + joDOM.removeCSSClass(node, "flickback"); + joDOM.removeCSSClass(node, "flickfast"); + + this.start = this.getMouse(e); + this.points.unshift(this.start); + this.inMotion = true; + + if (!this.mousemove) { + this.mousemove = joEvent.capture(document.body, "mousemove", this.onMove, this); + this.mouseup = joEvent.capture(document.body, "mouseup", this.onUp, this); + } + }, + + reset: function() { + this.points = []; + this.moved = false; + this.inMotion = false; + }, + + onMove: function(e) { + if (!this.inMotion) + return; + + joEvent.stop(e); + e.preventDefault(); + + var point = this.getMouse(e); + + var y = point.y - this.points[0].y; + var x = point.x - this.points[0].x; + +// if (y == 0) +// return; + + this.points.unshift(point); + + if (this.points.length > 7) + this.points.pop(); + + // cleanup points if the user drags slowly to avoid unwanted flicks + var self = this; + this.timer = window.setTimeout(function() { + if (self.inMotion && self.points.length > 1) + self.points.pop(); + }, 100); + + this.scrollBy(x, y, true); + + if (!this.moved && this.points.length > 3) + this.moved = true; + }, + + onUp: function (e) { + if (!this.inMotion) + return; + + joEvent.remove(document.body, "mousemove", this.mousemove, true); + joEvent.remove(document.body, "mouseup", this.mouseup, true); + + this.mousemove = null; + this.inMotion = false; + + joEvent.stop(e); + joEvent.preventDefault(e); + + var end = this.getMouse(e); + var node = this.container.firstChild; + + var top = this.getTop(); + var left = this.getLeft(); + + var dy = 0; + var dx = 0; + + for (var i = 0; i < this.points.length - 1; i++) { + dy += (this.points[i].y - this.points[i + 1].y); + dx += (this.points[i].x - this.points[i + 1].x); + } + + var max = 0 - node.offsetHeight + this.container.offsetHeight; + var maxx = 0 - node.offsetWidth + this.container.offsetWidth; + + // if the velocity is "high" then it was a flick + if ((Math.abs(dy) * this.vertical > 4 || Math.abs(dx) * this.horizontal > 4)) { + var flick = dy * (this.velocity * (node.offsetHeight / this.container.offsetHeight)); + var flickx = dx * (this.velocity * (node.offsetWidth / this.container.offsetWidth)); + + // we want to move quickly if we're going to land past + // the top or bottom + if ((flick + top < max || flick + top > 0) + || (flickx + left < maxx || flickx + left > 0)) { + joDOM.addCSSClass(node, "flickfast"); + } + else { + joDOM.addCSSClass(node, "flick"); + } + + this.scrollBy(flickx, flick, false); + + joDefer(this.snapBack, this, 3000); + } + else { + joDefer(this.snapBack, this, 10); + } + + }, + + getMouse: function(e) { + return { + x: (this.horizontal) ? e.screenX : 0, + y: (this.vertical) ? e.screenY : 0 + }; + }, + + scrollBy: function(x, y, test) { + var node = this.container.firstChild; + + var top = this.getTop(); + var left = this.getLeft(); + + var dy = Math.floor(top + y); + var dx = Math.floor(left + x); + + if (this.vertical && (node.offsetHeight <= this.container.offsetHeight)) + return; + + var max = 0 - node.offsetHeight + this.container.offsetHeight; + var maxx = 0 - node.offsetWidth + this.container.offsetWidth; + + var ody = dy; + var odx = dx; + + if (this.bump) { + if (dy > this.bump) + dy = this.bump; + else if (dy < max - this.bump) + dy = max - this.bump; + + if (dx > this.bump) + dx = this.bump; + else if (dy < maxx - this.bump) + dx = maxx - this.bump; + } + + if (!this.eventset) + this.eventset = joEvent.capture(node, this.transitionEnd, this.snapBack, this); + + if (top != dx || left != dy) + this.moveTo(dx, dy); + }, + + scrollTo: function(y, instant) { + var node = this.container.firstChild; + + if (!node) + return; + + if (typeof y == 'object') { + if (y instanceof HTMLElement) + var e = y; + else if (y instanceof joView) + var e = y.container; + + var t = 0 - e.offsetTop; + var h = e.offsetHeight + 80; + + var y = top; + + var top = this.getTop(); + var bottom = top - this.container.offsetHeight; + + if (t - h < bottom) + y = (t - h) + this.container.offsetHeight; + + if (y < t) + y = t; + } + + if (y < 0 - node.offsetHeight) + y = 0 - node.offsetHeight; + else if (y > 0) + y = 0; + + if (!instant) { + joDOM.addCSSClass(node, 'flick'); + } + else { + joDOM.removeCSSClass(node, 'flick'); + joDOM.removeCSSClass(node, 'flickback'); + } + + this.moveTo(0, y); + }, + + // called after a flick transition to snap the view + // back into our container if necessary. + snapBack: function() { + var node = this.container.firstChild; + var top = this.getTop(); + var left = this.getLeft(); + + var dy = top; + var dx = left; + + var max = 0 - node.offsetHeight + this.container.offsetHeight; + var maxx = 0 - node.offsetWidth + this.container.offsetWidth; + + if (this.eventset) + joEvent.remove(node, this.transitionEnd, this.eventset); + + this.eventset = null; + + joDOM.removeCSSClass(node, 'flick'); + + if (dy > 0) + dy = 0; + else if (dy < max) + dy = max; + + if (dx > 0) + dx = 0; + else if (dx < maxx) + dx = maxx; + + if (dx != left || dy != top) { + joDOM.addCSSClass(node, 'flickback'); + this.moveTo(dx, dy); + } + }, + + setScroll: function(x, y) { + this.horizontal = x ? 1 : 0; + this.vertical = y ? 1 : 0; + return this; + }, + + moveTo: function(x, y) { + var node = this.container.firstChild; + + if (!node) + return; + + this.setPosition(x * this.horizontal, y * this.vertical, node); + + node.jotop = y; + node.joleft = x; + }, + + setPosition: function(x, y, node) { + node.style.webkitTransform = "translate3d(" + x + "px, " + y + "px, 0)"; + }, + + getTop: function() { + return this.container.firstChild.jotop || 0; + }, + + getLeft: function() { + return this.container.firstChild.joleft || 0; + }, + + setData: function(data) { + joContainer.prototype.setData.apply(this, arguments); + } +}); +/** + joDivider + ========= + + Simple visual divider. + + Extends + ------- + + - joView + +*/ +joDivider = function(data) { + joView.apply(this, arguments); +}; +joDivider.extend(joView, { + tagName: "jodivider" +}); + +/** + joExpando + ========= + + A compound UI element which allows the user to hide/show its contents. + The first object passed in becomes the trigger control for the container, + and the second becomes the container which expands and contracts. This + action is controlled in the CSS by the presence of the "open" class. + + Use + --- + + This is a typical pattern: + + // normal look & feel + var x = new joExpando([ + new joExpandoTitle("Options"), + new joExpandoContent([ + new joLabel("Label"), + new joInput("sample field") + ]) + ]); + + Note that joExpando doesn't care what sort of controls you tell it + to use. In this example, we have a joButton that hides and shows a + DOM element: + + // you can use other things though + var y = new joExpando([ + new joButton("More..."), + joDOM.get("someelementid") + ]); + + Extends + ------- + + - joContainer + + Methods + ------- + + - `open()` + - `close()` + - `toggle()` + + Events + ------ + + - `openEvent` + - `closeEvent` + +*/ +joExpando = function(data) { + this.openEvent = new joSubject(this); + this.closeEvent = new joSubject(this); + + joContainer.apply(this, arguments); +}; +joExpando.extend(joContainer, { + tagName: "joexpando", + + draw: function() { + if (!this.data) + return; + + joContainer.prototype.draw.apply(this, arguments); + this.setToggleEvent(); + }, + + setEvents: function() { + }, + + setToggleEvent: function() { + joEvent.on(this.container.childNodes[0], "click", this.toggle, this); + }, + + toggle: function() { + if (this.container.className.indexOf("open") >= 0) + this.close(); + else + this.open(); + }, + + open: function() { + joDOM.addCSSClass(this.container, "open"); + this.openEvent.fire(); + }, + + close: function() { + joDOM.removeCSSClass(this.container, "open"); + this.closeEvent.fire(); + } +}); + + +/** + joExpandoContent + ================ + + New widget to contain expando contents. This is normally used with + joExpando, but not required. + + Extends + ------- + - joContainer +*/ +joExpandoContent = function() { + joContainer.apply(this, arguments); +}; +joExpandoContent.extend(joContainer, { + tagName: "joexpandocontent" +}); + + +/** + + joExpandoTitle + ============== + + Common UI element to trigger a joExpando. Contains a stylable + arrow image which indicates open/closed state. + + Extends + ------- + + - joControl + + Use + --- + + See joExpando use. + +*/ +joExpandoTitle = function(data) { + joControl.apply(this, arguments); +}; +joExpandoTitle.extend(joControl, { + tagName: "joexpandotitle", + + setData: function() { + joView.prototype.setData.apply(this, arguments); + this.draw(); + }, + + draw: function() { + this.container.innerHTML = this.data + ""; + } +}); +/** + joFlexrow + ========= + + Uses the flexible box model in CSS to stretch elements evenly across a row. + + Use + --- + + // a simple row of things + var x = new joFlexrow([ + new joButton("OK"), + new joButton("Cancel") + ]); + + // making a control stretch + var y = new joFlexrow(new joInput("Bob")); + + Extends + ------- + + - joContainer + +*/ +joFlexrow = function(data) { + joContainer.apply(this, arguments); +}; +joFlexrow.extend(joContainer, { + tagName: "joflexrow" +}); + +/** + joFlexcol + ========= + + Uses the flexible box model in CSS to stretch elements evenly across a column. + + Use + --- + + // fill up a vertical space with things + var x = new joFlexcol([ + new joNavbar(), + new joStackScroller() + ]); + + Extends + ------- + + - joContainer + +*/ +joFlexcol = function(data) { + joContainer.apply(this, arguments); +}; +joFlexcol.extend(joContainer, { + tagName: "joflexcol" +}); +/** + joFocus + ======= + + Singleton which manages global input and event focus among joControl objects. + + Methods + ------- + + - `set(joControl)` + + Unsets focus on the last control, and sets focus on the control passed in. + + - `clear()` + + Unsets focus on the last control. + + - `refresh()` + + Sets focus back to the last control that was focused. + +*/ + +joFocus = { + last: null, + + set: function(control) { + if (this.last && this.last !== control) + this.last.blur(); + + if (control && control instanceof joControl) { + control.focus(); + this.last = control; + } + }, + + get: function(control) { + return this.last; + }, + + refresh: function() { +// joLog("joFocus.refresh()"); + if (this.last) + this.last.focus(); + }, + + clear: function() { + this.set(); + } +}; + +/** + joFooter + ====== + + Attempt to make a filler object which pushed subsequent joView objects + further down in the container if possible (to attach its contents to + the bottom of a card, for eaxmple). + + > This behavior requires a working box model to attach properly to the bottom + > of your container view. + + Extends + ------- + + - joContainer + +*/ +joFooter = function(data) { + joContainer.apply(this, arguments); +}; +joFooter.extend(joContainer, { + tagName: "jofooter" +}); +/** + joGesture + ========= + + Experimental global gesture handler (keyboard, dpad, back, home, flick?). + This needs a lot more fleshing out, so it's not (quite) ready for general + consumption. + + Events + ------ + + - `upEvent` + - `downEvent` + - `leftEvent` + - `rightEvent` + - `backEvent` + - `forwardEvent` + - `homeEvent` + - `closeEvent` + - `activateEvent` + - `deactivateEvent` + + > Note that the events setup here are for the browser + > or webOS. The `setEvents` method most likely needs to change + > based on which OS you're running, although looking more deeply + > into PhoneGap event layer. + +*/ +joGesture = { + load: function() { + this.upEvent = new joSubject(this); + this.downEvent = new joSubject(this); + this.leftEvent = new joSubject(this); + this.rightEvent = new joSubject(this); + this.forwardEvent = new joSubject(this); + this.backEvent = new joSubject(this); + this.homeEvent = new joSubject(this); + this.closeEvent = new joSubject(this); + this.activateEvent = new joSubject(this); + this.deactivateEvent = new joSubject(this); + this.resizeEvent = new joSubject(this); + + this.setEvents(); + }, + + // by default, set for browser + setEvents: function() { + joEvent.on(document.body, "keydown", this.onKeyDown, this); + joEvent.on(document.body, "keyup", this.onKeyUp, this); + + joEvent.on(document.body, "unload", this.closeEvent, this); + joEvent.on(window, "activate", this.activateEvent, this); + joEvent.on(window, "deactivate", this.deactivateEvent, this); + + joEvent.on(window, "resize", this.resize, this); + }, + + resize: function() { + this.resizeEvent.fire(window); + }, + + onKeyUp: function(e) { + if (!e) + var e = window.event; + + if (e.keyCode == 18) { + this.altkey = false; + + return; + } + + if (e.keyCode == 27) { + if (jo.flag.stopback) { + joEvent.stop(e); + joEvent.preventDefault(e); + } + + this.backEvent.fire("back"); + return; + } + + if (!this.altkey) + return; + + joEvent.stop(e); + + switch (e.keyCode) { + case 37: + this.leftEvent.fire("left"); + break; + case 38: + this.upEvent.fire("up"); + break; + case 39: + this.rightEvent.fire("right"); + break; + case 40: + this.downEvent.fire("down"); + break; + case 27: + this.backEvent.fire("back"); + break; + case 13: + this.forwardEvent.fire("forward"); + break; + } + }, + + onKeyDown: function(e) { + if (!e) + var e = window.event; + + if (e.keyCode == 27) { + joEvent.stop(e); + joEvent.preventDefault(e); + } + else if (e.keyCode == 13 && joFocus.get() instanceof joInput) { + joEvent.stop(e); + } + else if (e.keyCode == 18) { + this.altkey = true; + } + + return; + } +}; +/** + joGroup + ======= + + Group of controls, purely visual. + + Extends + ------- + + - joContainer + +*/ +joGroup = function(data) { + joContainer.apply(this, arguments); +}; +joGroup.extend(joContainer, { + tagName: "jogroup" +}); +/** + joHTML + ====== + + A simple HTML content control. One interesting feature is it intercepts all + `` tag interactions and fires off a `selectEvent` with the contents of + the tag's `href` property. + + This is a relatively lightweight approach to displaying arbitrary HTML + data inside your app, but it is _not_ recommended you allow external + JavaScript inside the HTML chunk in question. + + Also keep in mind that your app document already _has_ ``, `` and + `` tags. When you use the `setData()` method on this view, _make sure + you don't use any of these tags_ to avoid weird issues. + + > In a future version, it is feasible to load in stylesheets references in + > the HTML document's `` section. For now, that entire can of worms + > will be avoided, and it's left up to you, the developer, to load in any + > required CSS files using `joDOM.loadCSS()`. + + Extends + ------- + + - joControl + + Use + --- + + // simple html string + var x = new joHTML("

Hello World!

Sup?

"); + + // use a joDataSource like a file loader + var y = new joHTML(new joFileSource("sample.html")); + +*/ +joHTML = function(data) { + joControl.apply(this, arguments); +}; +joHTML.extend(joControl, { + tagName: "johtml", + + setEvents: function() { + // limited events, no focus for example + joEvent.on(this.container, "click", this.onClick, this); + }, + + // special sauce -- we want to trap any a href click events + // and return them in our select event -- don't need to be + // refreshing our entire page, after all + onClick: function(e) { + joEvent.stop(e); + joEvent.preventDefault(e); + + // figure out what was clicked, look for an href + var container = this.container; + var hrefnode = findhref(joEvent.getTarget(e)); + + if (hrefnode) { + // whoa we have an
tag clicked + this.selectEvent.fire(hrefnode.href); + } + + function findhref(node) { + if (!node) + return null; + + if (node.href) + return node; + + if (typeof node.parentNode !== "undefined" && node.parentNode !== container) + return findhref(node.parentNode); + else + return null; + } + } +}); + +/** + joInput + ======= + + Single-line text input control. When you instantiate or use `setData()`, you can + either pass in an initial value or a reference to a joDataSource object which it, + like other joControl instances, will bind to. + + Use + --- + + // simple value, simple field + var x = new joInput(a); + + // set up a simple joRecord instance with some default data + var pref = new joRecord({ + username: "Bob", + password: "password" + }); + + // attach the value to a data structure property + var y = new joInput(pref.link("username")); + + Extends + ------- + + - joControl + + Methods + ------- + + - `focus()` + - `blur()` + + You can manually set focus or call the `blur()` method (which also + triggers a data save). + + - `setData()` + + Pass in either some arbitrary value for the control, or a reference to + a joDataSource if you want to automatically bind to a storage system + (e.g. joPreference). + +*/ +joInput = function(data) { + joControl.apply(this, arguments); +}; +joInput.extend(joControl, { + tagName: "input", + type: "text", + + setData: function(data) { + if (data !== this.data) { + this.data = data; + + if (typeof this.container.value !== "undefined") + this.container.value = data; + else + this.container.innerHTML = data; + + this.changeEvent.fire(this.data); + } + }, + + getData: function() { + if (typeof this.container.value !== "undefined") + return this.container.value; + else + return this.container.innerHTML; + }, + + enable: function() { + this.container.setAttribute("tabindex", "1"); + joControl.prototype.enable.call(this); + }, + + disable: function() { + this.container.removeAttribute("tabindex"); + joControl.prototype.disable.call(this); + }, + + createContainer: function() { + var o = joDOM.create(this); + + if (!o) + return; + + o.setAttribute("type", "text"); + o.setAttribute("tabindex", "1"); + o.contentEditable = this.enabled; + + return o; + }, + + setEvents: function() { + joControl.prototype.setEvents.call(this); + joEvent.on(this.container, "keydown", this.onKeyDown, this); + }, + + onKeyDown: function(e) { + if (e.keyCode == 13) { + e.preventDefault(); + joEvent.stop(e); + } + return false; + }, + + draw: function() { + if (this.container.value) + this.value = this.data; + else + this.innerHTML = this.value; + }, + + onMouseDown: function(e) { + joEvent.stop(e); + this.focus(); + }, + + storeData: function() { + this.data = this.getData(); + if (this.dataSource) + this.dataSource.set(this.value); + } +}); + +/** + joLabel + ======= + + Label view, purely a visual presentation. Usually placed in front + of input fields and other controls. + + Extends + ------- + + - joView + +*/ +joLabel = function(data) { + joControl.apply(this, arguments); +}; +joLabel.extend(joControl, { + tagName: "jolabel" +}); + +/** + joMenu + ====== + + Simple menu class with optional icons. + + Extends + ------- + + - joList + + Methods + ------- + + - `setData(menudata)` + + See the example below for the format of the menu data. + + Use + --- + + // simple inline menu; you can always setup the menu items (or change + // them) but using the `setData()` method, same as any joView + var menu = new joMenu([ + { title: "About" }, + { title: "Frequently Asked Questions", id: "faq" }, + { title: "Visit our website", id: "visit", icon: "images/web" } + ]); + + // simple inline function event handler + menu.selectEvent.subscribe(function(id) { + switch (id) { + case "0": + // the "About" line; if no id, the index of the menu item is used + stack.push(aboutCard); + break; + case "faq": + stack.push(faqCard); + break; + case "visit": + stack.push(visitCard); + break; + } + }); + + Advanced Use + ------------ + + This could actually be called "more consistent and simple" use. If your menus + are static, you could always setup an id-based dispatch delegate which pushes + the appropriate card based on the menu `id` selected. + + You could use the `id` in conjunction with view keys you create with joCache. + The handler would then be something like: + + menu.selectEvent.subscribe(function(id) { + mystack.push(joCache.get(id)); + }); + +*/ +joMenu = function() { + joList.apply(this, arguments); +}; +joMenu.extend(joList, { + tagName: "jomenu", + itemTagName: "jomenuitem", + value: null, + + fireSelect: function(index) { + if (typeof this.data[index].id !== "undefined" && this.data[index].id) + this.selectEvent.fire(this.data[index].id); + else + this.selectEvent.fire(index); + }, + + formatItem: function(item, index) { + var o = joDOM.create(this.itemTagName); + + // TODO: not thrilled with this system of finding the + // selected item. It's flexible but annoying to code to. + o.setAttribute("index", index); + + // quick/dirty + if (typeof item === "object") { + o.innerHTML = item.title; + if (item.icon) { + o.style.backgroundImage = "url(" + item.icon + ")"; + joDOM.addCSSClass(o, "icon"); + } + } + else { + o.innerHTML = item; + } + + return o; + } +}); +/** + joOption + ======== + + This controls lets the user select one of a few options. Basically, this + is a menu with a horizontal layout (depending on your CSS). + + Use + --- + + // simple set of options + var x = new joOption([ + "Red", + "Blue", + "Green" + ]); + + // set the current value + x.setValue(2); + + // or, associate the value with a joRecord property + var pref = new joRecord(); + + var y = new joOption([ + "Orange", + "Banana", + "Grape", + "Lime" + ], pref.link("fruit")); + + // you can even associate the list with a datasource + var fruits = new joDataSource( ... some query stuff ...); + var z = new joOption(fruits, pref.link("fruit")); + + + Extends + ------- + + - joMenu + +*/ +joOption = function() { + joMenu.apply(this, arguments); +}; +joOption.extend(joMenu, { + tagName: "jooption", + itemTagName: "jooptionitem" +}); +/** + joPasswordInput + =============== + + Secret data input field (e.g. displays `******` instead of `secret`). + + Extends + ------- + + - joInput + + > Note that this requires CSS3 which is known not to be currently supported + > in Opera or Internet Explorer. + +*/ +joPasswordInput = function(data) { + joInput.apply(this, arguments); +}; +joPasswordInput.extend(joInput, { + className: "password", + type: "password" +}); +/** + joPopup + ======= + + A simple popup control. Pass in the UI contents as you would + any other subclass of joContainer (e.g. joCard). + + Methods + ------- + + - `show()` + - `hide()` + + These do what you'd expect. + + Extends + ------- + + - joContainer + + Events + ------ + + - `showEvent` + - `hideEvent` + + +*/ + +joPopup = function() { + this.showEvent = new joSubject(this); + this.hideEvent = new joSubject(this); + + joContainer.apply(this, arguments); +}; +joPopup.extend(joContainer, { + tagName: "jopopup", + + setEvents: function() { + joEvent.on(this.container, "mousedown", this.onClick, this); + }, + + onClick: function(e) { + joEvent.stop(e); + }, + + hide: function() { + joEvent.on(this.container, "webkitTransitionEnd", this.onHide, this); + this.container.className = 'hide'; + }, + + onHide: function() { + this.hideEvent.fire(); + }, + + show: function() { + this.container.className = 'show'; + this.showEvent.fire(); + } +}); +/** + joScreen + ======== + + Abstraction layer for the device screen. Uses document.body as its + DOM element and allows other controls to be nested within (usually + a joStack or other high-level containers or controls). + + Methods + ------- + + - `alert(title, message, buttons)` + + Simple alert box. The `buttons` parameter is optional; a simple + "OK" button is added if nothing is specified. + + - `showPopup(joView)` + - `hidePopup(joView)` + + These methods allow you to do a completely custom modal joPopup. + Pass in either a joView, an array of them, or and HTMLElement + or a string, the same as you would when you create a joCard or + other child of joContainer. + + Extends + ------- + + - joContainer + + Use + --- + + var x = new joScreen([ + new joNav(), + new joStack(), + new joToolbar() + ]); + + // show a simple alert dialog + x.alert("Hello", "This is an alert"); + + // a more complex alert + x.alert("Hola", "Do you like this alert?", [ + { label: "Yes", action: yesFunction, context: this }, + { label: "No", action: noFunction, context: this } + ]); + + // a completely custom popup + x.showPopup(myView); + + Events + ------ + + - `resizeEvent` + - `menuEvent` + - `activateEvent` + - `deactivateEvent` + - `backEvent` + - `forwardEvent` + +*/ + +joScreen = function() { + this.resizeEvent = new joSubject(this); + this.menuEvent = new joSubject(this); + this.activateEvent = new joSubject(this); + this.deactivateEvent = new joSubject(this); + this.backEvent = new joSubject(this); + this.forwardEvent = new joSubject(this); + + joContainer.apply(this, arguments); +}; +joScreen.extend(joContainer, { + tagName: "screen", + + setupEvents: function() { + joEvent.on(window, "resize", this.resizeEvent.fire, this); + joEvent.on(window, "appmenushow", this.menuEvent.fire, this); + joEvent.on(window, "activate", this.activateEvent.fire, this); + joEvent.on(window, "deactivate", this.deactivateEvent.fire, this); + joEvent.on(window, "back", this.backEvent.fire, this); + }, + + createContainer: function() { + return document.body; + }, + + // show a popup made from your own UI controls + showPopup: function(data) { + // take a view, a DOM element or some HTML and + // make it pop up in the screen. + if (!this.popup) { + this.shim = new joShim( + new joFlexcol([ + ' ', + this.popup = new joPopup(data), + ' ' + ]) + ); + } + else { + this.popup.setData(data); + } +// this.shim.showEvent.subscribe(this.popup.show, this); + this.shim.show(); + this.popup.show(); + }, + + hidePopup: function() { + if (this.shim) + this.shim.hide(); + }, + + // shortcut to a simple alert dialog, not the most efficient + // way to do this, but for now, it serves its purpose and + // the API is clean enough. + alert: function(title, msg, options, context) { + var buttons = []; + var callback; + + var context = (typeof context === 'object') ? context : null; + + if (typeof options === 'object') { + if (options instanceof Array) { + // we have several options + for (var i = 0; i < options.length; i++) + addbutton(options[i]); + } + else { + addbutton(options); + } + } + else if (typeof options === 'string') { + addbutton({ label: options }); + } + else { + if (typeof options === 'function') + callback = options; + + addbutton(); + } + + var view = [ + new joTitle(title), + new joHTML(msg), + buttons + ]; + this.showPopup(view); + + var self = this; + + function addbutton(options) { + if (!options) + var options = { label: 'OK' }; + + var button = new joButton(options.label); + button.selectEvent.subscribe( + function() { + if (options.action) + options.action.call(options.context); + + defaultaction(); + }, options.context || self + ); + + buttons.push(button); + } + + function defaultaction() { + self.hidePopup(); + if (callback) { + if (context) + callback.call(context); + else + callback(); + } + } + } +}); + +/** + joShim + ====== + + A simple screen dimmer. Used mostly for popups and other + modal use cases. + + Methods + ------- + - `show()` + - `hide()` + + These do what you'd expect. + + Extends + ------- + - joView + + Events + ------ + + - `showEvent` + - `hideEvent` + +*/ + +joShim = function() { + this.showEvent = new joSubject(this); + this.hideEvent = new joSubject(this); + this.selectEvent = new joSubject(this); + + joContainer.apply(this, arguments); +}; +joShim.extend(joContainer, { + tagName: "joshim", + + setEvents: function() { + joEvent.on(this.container, "mousedown", this.onMouseDown, this); + }, + + onMouseDown: function(e) { + joEvent.stop(e); + this.hide(); +// this.selectEvent.fire(); + }, + + hide: function() { + this.container.className = ''; + joEvent.on(this.container, "webkitTransitionEnd", this.onHide, this); + }, + + show: function() { + this.attach(); + + this.container.className = 'show'; + joEvent.on(this.container, "webkitTransitionEnd", this.onShow, this); + + // default parent to the document body + if (!this.lastParent) + this.lastParent = document.body; + }, + + onShow: function() { + this.showEvent.fire(); + }, + + onHide: function() { + this.detach(); + this.hideEvent.fire(); + } +}); +/** + joSound + ======== + + Play preloaded sound effects using the HTML5 `Audio` object. This module could + be wildly different for various platforms. Be warned. + + Methods + ------- + + - `play()` + - `pause()` + - `rewind()` + - `stop()` + + Basic sound controls. + + - `setLoop(n)` + + Tell the joSound to automatically loop `n` times. Set to `-1` to loop + continuously until `pause()`. + + - `setVolume(level)` + + Level is a decimal value from `0` to `1`. So, half volume would be `0.5`. + + Events + ------ + + - `endedEvent` + - `errorEvent` + +*/ +joSound = function(filename, repeat) { + this.endedEvent = new joSubject(this); + this.errorEvent = new joSubject(this); + + if (typeof Audio == 'undefined') + return; + + this.filename = filename; + this.audio = new Audio(); + this.audio.autoplay = false; + + if (!this.audio) + return; + + joDefer(function() { + this.audio.src = filename; + this.audio.load(); + }, this, 5); + + this.setRepeatCount(repeat); + + joEvent.on(this.audio, "ended", this.onEnded, this); + +// this.pause(); +}; +joSound.prototype = { + play: function() { + if (!this.audio || this.audio.volume == 0) + return; + + this.audio.play(); + + return this; + }, + + onEnded: function(e) { + this.endedEvent.fire(this.repeat); + + if (++this.repeat < this.repeatCount) + this.play(); + else + this.repeat = 0; + }, + + setRepeatCount: function(repeat) { + this.repeatCount = repeat; + this.repeat = 0; + + return this; + }, + + pause: function() { + if (!this.audio) + return; + + this.audio.pause(); + + return this; + }, + + rewind: function() { + if (!this.audio) + return; + + try { + this.audio.currentTime = 0.0; + } + catch (e) { + joLog("joSound: can't rewind..."); + } + + this.repeat = 0; + + return this; + }, + + stop: function() { + this.pause(); + this.rewind(); + + this.repeat = 0; + + return this; + }, + + setVolume: function(vol) { + if (!this.audio || vol < 0 || vol > 1) + return; + + this.audio.volume = vol; + + return this; + } +}; +/** + joStackScroller + =============== + + What happens when you mix joStack and joScroller? You get this + class. Use exactly as you would joStack, only it automatically + puts a scroller in the stack as needed. At some point, this + might get folded into joStack, but for now it's a special class. + + It also handles the `scrollTo()` and `scrollBy()` methods from + joScroller. + + Extends + ------- + - joStack + - joScroller +*/ + +joStackScroller = function(data) { + this.scrollers = [ + new joScroller(), + new joScroller() + ]; + this.scroller = this.scrollers[0]; + + joStack.apply(this, arguments); + + this.scroller.attach(this.container); +}; +joStackScroller.extend(joStack, { + type: "scroll", + scrollerindex: 1, + scroller: null, + scrollers: [], + + switchScroller: function() { + this.scrollerindex = this.scrollerindex ? 0 : 1; + this.scroller = this.scrollers[this.scrollerindex]; + }, + + getLastScroller: function() { + return this.scrollers[this.scrollerindex ? 0 : 1]; + }, + + scrollTo: function(something) { + this.scroller.scrollTo(something); + }, + + scrollBy: function(y) { + this.scroller.scrollBy(y); + }, + + getChildStyleContainer: function() { + return this.scroller.container; + }, + + getContentContainer: function() { + return this.scroller.container; + }, + + appendChild: function(child) { + var scroller = this.scroller; + scroller.setData(child); + this.container.appendChild(scroller.container); + }, + + getChild: function() { + return this.scroller.container || null; + }, + + forward: function() { + if (this.index < this.data.length - 1) + this.switchScroller(); + + joStack.prototype.forward.call(this); + }, + + back: function() { + if (this.index > 0) + this.switchScroller(); + + joStack.prototype.forward.call(this); + }, + + home: function() { + if (this.data && this.data.length && this.data.length > 1) { + this.switchScroller(); + joStack.prototype.home.call(this); + } + }, + + push: function(o) { + // don't push the same view we already have + if (this.data && this.data.length && this.data[this.data.length - 1] === o) + return; + + this.switchScroller(); + + joDOM.removeCSSClass(o, 'flick'); + joDOM.removeCSSClass(o, 'flickback'); + + this.scroller.setData(o); + this.scroller.scrollTo(0, true); + + joStack.prototype.push.call(this, o); + }, + + pop: function() { + if (this.data.length > this.locked) + this.switchScroller(); + + joStack.prototype.pop.call(this); + } +}); + +/** + joTabBar + ========= + + Tab bar widget. + + Extends + ------- + + - joList + + Model + ----- + + Data is expected to be an array of `{ type: "", label: ""}` objects, + in the display order for the bar. + +*/ +joTabBar = function() { + joList.apply(this, arguments); +}; +joTabBar.extend(joList, { + tagName: "jotabbar", + + formatItem: function(data, index) { + var o = document.createElement("jotab"); + + if (data.label) + o.innerHTML = data.label; + + if (data.type) + o.className = data.type; + + o.setAttribute("index", index); + + return o; + } +}); +/** + joTable + ======= + + Table control, purely visual representation of tabular data (usually + an array of arrays). + + Use + --- + + // simple table with inline data + var x = new joTable([ + ["Nickname", "Phone", "Email"], + ["Bob", "555-1234", "bob@bobco.not"], + ["Jo", "555-3456", "jo@joco.not"], + ["Jane", "555-6789", "jane@janeco.not"] + ]); + + // we can respond to touch events in the table + x.selectEvent.subscribe(function(index, table) { + // get the current row and column + joLog("Table cell clicked:", table.getRow(), table.getCol()); + + // you can also get at the cell HTML element as well + joDOM.setStyle(table.getNode(), { fontWeight: "bold" }); + }); + + Extends + ------- + + - joList + + Methods + ------- + + - `setCell(row, column)` + + Sets the active cell for the table, also makes it editiable and sets focus. + + - `getRow()`, `getCol()` + + Return the current row or column +*/ + +joTable = function(data) { + joList.apply(this, arguments); +}; +joTable.extend(joList, { + tagName: "jotable", + + // default row formatter + formatItem: function(row, index) { + var tr = document.createElement("tr"); + + for (var i = 0, l = row.length; i < l; i++) { + var o = document.createElement(index ? "td" : "th"); + o.innerHTML = row[i]; + + // this is a little brittle, but plays nicely with joList's select event + o.setAttribute("index", index * l + i); + tr.appendChild(o); + } + + return tr; + }, + + // override joList's getNode + getNode: function(index) { + var row = this.getRow(index); + var col = this.getCol(index); + + return this.container.childNodes[row].childNodes[col]; + }, + + getRow: function(index) { + if (typeof index == "undefined") + var index = this.getIndex(); + + var rowsize = this.data[0].length; + return Math.floor(index / rowsize); + }, + + getCol: function(index) { + if (typeof index == "undefined") + var index = this.getIndex(); + + var rowsize = this.data[0].length; + return index % rowsize; + } +}); + +/** + joTextarea + ========== + + Multi-line text input control. When you instantiate or use `setData()`, you can + either pass in an initial value or a reference to a joDataSource object which it, + like other joControl instances, will bind to. + + Basically, this is just a multi-line version of joInput. + + Use + --- + + // simple multi-line field + var sample = "This is some sample text to edit."; + var x = new joTextarea(sample); + + // setting the style inline using chaining + var f = new joTextarea(sample).setStyle({ + minHeight: "100px", + maxHeight: "300px" + }); + + // adding a simple change event handler using chaining + var h = new joTextarea(sample).changeEvent.subscribe(function(data) { + joLog("text area changed:", data); + }); + + // attach the value to a preference + var y = new joTextarea(joPreference.bind("username")); + + // attach input control to a custom joDataSource + var username = new joDataSource("bob"); + var z = new joTextarea(username); + + Extends + ------- + + - joInput + +*/ +joTextarea = function(data) { + joInput.apply(this, arguments); +}; +joTextarea.extend(joInput, { + tagName: "textarea", + + onKeyDown: function(e) { + // here we want the enter key to work, overriding joInput's behavior + return false; + } +}); + +/** + joTitle + ======= + + Title view, purely a visual presentation. + + Extends + ------- + + - joContainer + +*/ +joTitle = function(data) { + joView.apply(this, arguments); +}; +joTitle.extend(joView, { + tagName: "jotitle" +}); + +/** + joToolbar + ========= + + Locks UI controls to the bottom of whatever you put this container into. + + Extends + ------- + + - joContainer + +*/ +joToolbar = function(data) { + joContainer.apply(this, arguments); +}; +joToolbar.extend(joContainer, { + tagName: "jotoolbar" +}); +joForm = function() { + joContainer.apply(this, arguments); +}; +joForm.extend(joContainer, { + tagName: "form" +});/** + joDialog + ======== + + This is a higher level container that wraps a joPopup with a joShim. +*/ +joDialog = function(data) { + joShim.call(this, new joFlexcol([ + '', + new joPopup( + new joScroller(data) + ).setStyle("show"), + '' + ])); +}; +joDialog.extend(joShim, { +}); +/** + joSelectList + ============ + + A selection list of options used by joSelect. + + Extends + ------- + + - joList +*/ + +joSelectList = function() { + joList.apply(this, arguments); +}; +joSelectList.extend(joList, { + tagName: "joselectlist" +}); +/** + joNavbar + ======== + + Floating navigation control. Usually tied to a joStack or joStackScroller. + Will handle display of a "back" button (controllable in CSS) and show the + title string of the current view in a stack (if it exists). + + Use + --- + + // make a stack + var stack = new joStackScroller(); + + // new navbar + var x = new joNavbar(); + + // link to a stack + x.setStack(stack); + + Methods + ------- + + - `back()` + + Signals the associated stack to move back in its stack (i.e. calls + the stack's `pop()` method). + + - `setStack(joStack or joStackScroller)` + + Links this control to a stack. + +*/ + +joNavbar = function(title) { + if (title) + this.firstTitle = title; + + var ui = [ + this.titlebar = new joView(title || ' ').setStyle('title'), + new joFlexrow([this.back = new joBackButton('Back').selectEvent.subscribe(this.back, this), ""]) + ]; + + joContainer.call(this, ui); +}; +joNavbar.extend(joContainer, { + tagName: "jonavbar", + stack: null, + + back: function() { + if (this.stack) + this.stack.pop(); + }, + + setStack: function(stack) { + if (this.stack) { + this.stack.pushEvent.unsubscribe(this.update, this); + this.stack.popEvent.unsubscribe(this.update, this); + } + + if (!stack) { + this.stack = null; + return; + } + + this.stack = stack; + + stack.pushEvent.subscribe(this.update, this); + stack.popEvent.subscribe(this.update, this); + + this.refresh(); + }, + + update: function() { + if (!this.stack) + return; + + joDOM.removeCSSClass(this.back, 'selected'); + joDOM.removeCSSClass(this.back, 'focus'); + +// console.log('update ' + this.stack.data.length); + + if (this.stack.data.length > 1) + joDOM.addCSSClass(this.back, 'active'); + else + joDOM.removeCSSClass(this.back, 'active'); + + var title = this.stack.getTitle(); + + if (typeof title === 'string') + this.titlebar.setData(title); + else + this.titlebar.setData(this.firstTitle); + }, + + setTitle: function(title) { + this.titlebar.setData(title); + this.firstTitle = title; + return this; + } +}); + + +/** + joBackButton + ============ + + A "back" button, which can be made to be shown only in appropriate + platforms (e.g. iOS, Safari, Chrome) through CSS styling. + + See joNavbar for more information. + + Extends + ------- + + - joButton + +*/ +joBackButton = function() { + joButton.apply(this, arguments); +}; +joBackButton.extend(joButton, { + tagName: "jobackbutton" +}); +/** + joSelect + ======== + + Multi-select control which presents a set of options for the user + to choose from. + + Methods + ------- + + - `setValue(value)` + + Set the current value, based on the index for the option list. + + - `getValue()` + + Returns the index of the current selected item. + + Extends + ------- + + - joExpando + + Consumes + -------- + + - joSelectTitle + - joSelectList + + Properties + ---------- + + - `field` + + Reference to the value field for this control. + + - `list` + + Reference to the joSelectList for this control. + + Use + --- + + // pass in an array of options + var x = new joSelect([ "Apples", "Oranges", "Grapes" ]); + + // pass in a current value + var y = new joSelect([ "Apples", "Oranges", "Grapes" ], 2); + + // respond to the change event + y.changeEvent = function(value, list) { + console.log("Fruit: " + list.getNodeValue(value)); + }); + +*/ +joSelect = function(data, value) { + var v = value; + if (value instanceof joDataSource) + v = value.getData(); + + var ui = [ + this.field = new joSelectTitle(v), + this.list = new joSelectList(data, value) + ]; + + this.field.setList(this.list); + + this.changeEvent = this.list.changeEvent; + this.selectEvent = this.list.selectEvent; + + joExpando.call(this, ui); + this.container.setAttribute("tabindex", 1); + + this.field.setData(this.list.value); + + this.list.selectEvent.subscribe(this.setValue, this); +}; +joSelect.extend(joExpando, { + setValue: function(value, list) { + if (list) { + this.field.setData(value); + this.close(); + } + else { + this.field.setData(value); + } + }, + + getValue: function() { + return this.list.getValue(); + }, + + setEvents: function() { + joControl.prototype.setEvents.call(this); + }, + + onBlur: function(e) { + joEvent.stop(e); + joDOM.removeCSSClass(this, "focus"); + this.close(); + } +}); + +/** + joSelectTitle + ============= + + joSelect flavor of joExpandoTitle. + + Extends + ------- + + - joExpandoTitle +*/ +joSelectTitle = function() { + joExpandoTitle.apply(this, arguments); +}; +joSelectTitle.extend(joExpandoTitle, { + list: null, + + setList: function(list) { + this.list = list; + }, + + setData: function(value) { + if (this.list) + joExpandoTitle.prototype.setData.call(this, this.list.getNodeData(value) || "Select..."); + else + joExpandoTitle.prototype.setData.call(this, value); + } +}); +/** + joToggle + ======== + + Boolean widget (on or off). + + Methods + ------- + + - `setLabels(Array)` + + You can change the labels for this control, which default to "Off" and "On". + + Extends + ------- + + - joControl + + Use + --- + + // simple + var x = new joToggle(); + + // with value + var y = new joToggle(true); + + // with custom labels + var z = new joToggle().setLabels(["No", "Yes"]); + + See Data Driven Controls for more examples. + +*/ +joToggle = function(data) { + joControl.call(this, data); +}; +joToggle.extend(joControl, { + tagName: "jotoggle", + button: null, + labels: ["Off", "On"], + + setLabels: function(labels) { + if (labels instanceof Array) + this.labels = labels; + else if (arguments.length == 2) + this.labels = arguments; + + this.draw(); + + return this; + }, + + select: function(e) { + if (e) + joEvent.stop(e); + + this.setData((this.data) ? false : true); + }, + + onBlur: function(e) { + joEvent.stop(e); + this.blur(); + }, + + draw: function() { + if (!this.container) + return; + + if (!this.container.firstChild) { + this.button = joDOM.create("div"); + this.container.appendChild(this.button); + } + + this.button.innerHTML = this.labels[(this.data) ? 1 : 0]; + + if (this.data) + joDOM.addCSSClass(this.container, "on"); + else + joDOM.removeCSSClass(this.container, "on"); + } +}); +/** + joSlider + ======== + + Slider control, horizontal presentation (may be extended later to allow for + vertical and x/y). + + Extends + ------- + + - joControl + + Methods + ------- + + - `setRange(min, max, snap)` + + Where `min`/`max` is a number, either integer or decimal, doesn't matter. If `max` + and `min` are integers, then `snap` defaults to `1`, otherwise it is set to `0` (no + snap, which allows free movement). + + The optional `snap` value adjusts the granularuty of choices. Set to `0` for + free-floating, or any other positive number. Any `snap` that is less than `0` + or greater than the total range of possible values will be ignored. + + Use + --- + + // basic slider, will allow any decimal value + // between 0 and 1, defaults to 0 + var x = new joSlider(); + + // custom range and default value set + var y = new joSlider(0).setRange(-10, 10); + + // percent slider, with 5% snap + var z = new joSlider(0).setRange(0, 100, 5); + + // responding to change events + var r = new joSlider().changEvent.subscribe(function(value) { + console.log(value); + }, this); + +*/ + +joSlider = function(value) { + this.min = 0; + this.max = 1; + this.snap = 0; + this.range = 1; + this.thumb = null; + this.horizontal = 1; + this.vertical = 0; + this.moved = false; + this.jump = true; + + joControl.call(this, null, value); +}; +joSlider.extend(joControl, { + tagName: "joslider", + + setRange: function(min, max, snap) { + if (min >= max) { + joLog("WARNING: joSlider.setRange, min must be less than max."); + return this; + } + + this.min = min || 0; + this.max = max || 1; + + if (min < 0 && max >= 0) + this.range = Math.abs(min) + max; + else if (min < 0 && max <= 0) + this.range = min - max; + else + this.range = max - min; + + if (typeof snap !== 'undefined') + this.snap = (snap >= 0 && snap <= this.range) ? snap : 0; + else + this.snap = 0; + + this.setValue(this.value); + + return this; + }, + + setValue: function(value, silent) { + var v = this.adjustValue(value); + + if (v != this.value) { + joControl.prototype.setValue.call(this, v); + if (!silent) + this.draw(); + } + + return this; + }, + + adjustValue: function(v) { + var value = v; + + if (this.snap) + value = Math.floor(value / this.snap) * this.snap; + + if (value < this.min) + value = this.min; + else if (value > this.max) + value = this.max; + + return value; + }, + + createContainer: function() { + var o = joDOM.create(this.tagName); + + if (o) { + o.setAttribute("tabindex", "1"); + + var t = joDOM.create("josliderthumb"); + o.appendChild(t); + this.thumb = t; + } + + return o; + }, + + onDown: function(e) { + joEvent.stop(e); + + this.reset(); + + var node = this.container.firstChild; + + this.inMotion = true; + this.moved = false; + + if (!this.mousemove) { + this.mousemove = joEvent.on(document.body, "mousemove", this.onMove, this); + this.mouseup = joEvent.capture(document.body, "mouseup", this.onUp, this); + } + }, + + reset: function() { + this.moved = false; + this.inMotion = false; + this.firstX = -1; + this.firstY = -1; + }, + + onMove: function(e) { + if (!this.inMotion) + return; + + joEvent.stop(e); + e.preventDefault(); + + var point = this.getMouse(e); + + var y = point.y; + var x = point.x; + + if (this.firstX == -1) { + this.firstX = x; + this.firstY = y; + + this.ox = this.thumb.offsetLeft; + this.oy = this.thumb.offsetTop; + } + + var x = (x - this.firstX) + this.ox; + var y = (y - this.firstY) + this.oy; + + if (x > 4 || y > 4) + this.moved = true; + + var t = this.thumb.offsetWidth; + var w = this.container.offsetWidth - t; + + if (x < 0) + x = 0; + else if (x > w) + x = w; + + if (!this.snap) + this.moveTo(x); + + this.setValue((x / w) * this.range + this.min, !this.snap); + }, + + moveTo: function(x) { + this.thumb.style.left = x + "px"; + }, + + initValue: function(value) { + var t = this.container.firstChild.offsetWidth; + var w = this.container.offsetWidth - t; + + var x = Math.floor((this.value / this.range) * w); + + this.moveTo(x); + + return this; + }, + + onUp: function (e) { + if (!this.inMotion) + return; + + joEvent.remove(document.body, "mousemove", this.mousemove); + joEvent.remove(document.body, "mouseup", this.mouseup); + this.mousemove = null; + + joEvent.stop(e); + joEvent.preventDefault(e); + + joDefer(function() { + this.reset(); + }, this); + }, + + setEvents: function() { + joEvent.on(this.container, "click", this.onClick, this); + joEvent.on(this.thumb, "mousedown", this.onDown, this); + + // we have to adjust if the window changes size + joGesture.resizeEvent.subscribe(this.draw, this); + + console.log('setevents'); + }, + + onClick: function(e) { + if (this.inMotion || this.moved) + return; + + joEvent.stop(e); + joEvent.preventDefault(e); + + var point = this.getMouse(e); + + var l = joDOM.pageOffsetLeft(this.container); +// console.log(l); + + var x = Math.floor((point.x - l) - this.thumb.offsetWidth * 1.5); + +// console.log(x); + + var t = this.thumb.offsetWidth; + + x = x - t; + + var w = this.container.offsetWidth - t; + + if ((x < t && this.snap) || x < 0) + x = 0; + else if (x > w) + x = w; + +// this.moveTo(x); + + this.setValue((x / w) * this.range + this.min); + }, + + getMouse: function(e) { + return { + x: (this.horizontal) ? e.screenX : 0, + y: (this.vertical) ? e.screenY : 0 + }; + }, + + draw: function() { + if (!this.container) + this.setContainer(); + + this.initValue(this.value); + } +}); + diff --git a/assets/www/js/jo_min.js b/assets/www/js/jo_min.js new file mode 100644 index 0000000..d689a98 --- /dev/null +++ b/assets/www/js/jo_min.js @@ -0,0 +1,379 @@ + +joLog=function(){var strings=[];for(var i=0;i=0){this.platform=this.useragent[i];break;}}} +if(joEvent){var o=document.createElement('div');var test=("ontouchstart"in o);if(!test){o.setAttribute("ontouchstart",'return;');test=(typeof o.ontouchstart==='function');} +joEvent.touchy=test;o=null;} +if(joGesture) +joGesture.load();var s=joScroller.prototype;if(typeof document.body.style.webkitTransition!=="undefined"){} +else if(typeof document.body.style.MozTransition!=="undefined"){s.transitionEnd="transitionend";s.setPosition=function(x,y,node){node.style.MozTransform="translate("+x+"px,"+y+"px)";};} +else if(typeof document.body.style.msTransform!=="undefined"){s.transitionEnd="transitionend";s.setPosition=function(x,y,node){node.style.msTransform="translate("+x+"px,"+y+"px)";};} +else if(typeof document.body.style.OTransition!=="undefined"){s.transitionEnd="otransitionend";s.setPosition=function(x,y,node){node.style.OTransform="translate("+x+"px,"+y+"px)";};} +else{s.velocity=0;s.bump=0;s.transitionEnd="transitionend";s.setPosition=function(x,y,node){if(this.vertical) +node.style.top=y+"px";if(this.horizontal) +node.style.left=x+"px";};} +joLog("Jo",this.version,"loaded for",this.platform,"environment");this.loadEvent.fire();},tagMap:{},tagMapLoaded:false,initTagMap:function(){if(this.tagMapLoaded) +return;var key=this.tagMap;key.JOVIEW=joView;key.BODY=joScreen;for(var p in window){var o=window[p];if(typeof o==='function'&&o.prototype&&typeof o.prototype.tagName!=='undefined'&&o.prototype instanceof joView){var tag=o.prototype.tagName.toUpperCase();if(o.prototype.type){if(!key[tag]) +key[tag]={};key[tag][o.prototype.type]=o;} +else{key[tag]=o;}}}},getPlatform:function(){return this.platform;},matchPlatform:function(test){return(test.indexOf(this.platform)>=0);},getVersion:function(){return this.version;},getLanguage:function(){return this.language;}};joDOM={enabled:false,get:function(id){if(typeof id==="string"){return document.getElementById(id);} +else if(typeof id==='object'){if(id instanceof joView) +return id.container;else +return id;}},remove:function(node){if(node.parentNode){node.parentNode.removeChild(node);}},enable:function(){this.enabled=true;},getParentWithin:function(node,ancestor){while(node.parentNode!==window&&node.parentNode!==ancestor){node=node.parentNode;} +return node;},addCSSClass:function(node,classname){var node=joDOM.get(node);if(typeof node.className!=="undefined"){var n=node.className.split(/\s+/);for(var i=0,l=n.length;i'+style+'';document.body.appendChild(css);return css;},removeCSS:function(node){document.body.removeChild(node);},loadCSS:function(filename,oldnode){if(oldnode) +var css=oldnode;else +var css=joDOM.create('link');css.rel='stylesheet';css.type='text/css';css.href=filename+(jo.debug?("?"+joTime.timestamp()):"");if(!oldnode) +document.body.appendChild(css);return css;},pageOffsetLeft:function(node){var l=0;while(typeof node!=='undefined'&&node&&node.parentNode!==window){if(node.offsetLeft) +l+=node.offsetLeft;node=node.parentNode;} +return l;},pageOffsetTop:function(node){var t=0;while(typeof node!=='undefined'&&node&&node.parentNode!==window){t+=node.offsetTop;node=node.parentNode;} +return t;}};joCSSRule=function(data){this.setData(data);};joCSSRule.prototype={container:null,setData:function(data){this.data=data||"";this.enable();},clear:function(){this.setData();},disable:function(){joDOM.removeCSS(this.container);},enable:function(){this.container=joDOM.applyCSS(this.data,this.container);}};joEvent={eventMap:{"mousedown":"touchstart","mousemove":"touchmove","mouseup":"touchend","mouseout":"touchcancel"},touchy:false,getTarget:function(e){if(!e) +var e=window.event;return e.target?e.target:e.srcElement;},capture:function(element,event,call,context,data){return this.on(element,event,call,context,data,true);},on:function(element,event,call,context,data,capture){if(!call||!element) +return false;if(this.touchy){if(this.eventMap[event]) +event=this.eventMap[event];} +var element=joDOM.get(element);var call=call;var data=data||"";function wrappercall(e){if(e.touches&&e.touches.length==1){var touches=e.touches[0];e.pageX=touches.pageX;e.pageY=touches.pageY;e.screenX=touches.screenX;e.screenY=touches.screenY;e.clientX=touches.clientX;e.clientY=touches.clientY;} +if(context) +call.call(context,e,data);else +call(e,data);};wrappercall.capture=capture||false;if(!window.addEventListener) +element.attachEvent("on"+event,wrappercall);else +element.addEventListener(event,wrappercall,capture||false);return wrappercall;},remove:function(element,event,call,capture){if(this.touchy){if(this.eventMap[event]){event=this.eventMap[event];}} +if(typeof element.removeEventListener!=='undefined') +element.removeEventListener(event,call,capture||false);},stop:function(e){if(e.stopPropagation) +e.stopPropagation();else +e.cancelBubble=true;},preventDefault:function(e){e.preventDefault();},block:function(e){if(window.event) +var e=window.event;if(typeof e.target=='undefined') +e.target=e.srcElement;switch(e.target.nodeName.toLowerCase()){case'input':case'textarea':return true;break;default:return false;}}};joSubject=function(subject){this.subscriptions=[];this.subject=subject;};joSubject.prototype={last:-1,subscribe:function(call,observer,data){if(!call) +return false;var o={"call":call};if(observer) +o.observer=observer;if(data) +o.data=data;this.subscriptions.push(o);return this.subject;},unsubscribe:function(call,observer){if(!call) +return false;for(var i=0,l=this.subscriptions.length;ithis.getData().length) +end=this.getData().length;if(start<0) +start=0;return this.data.slice(start,end);},refresh:function(){},setPageSize:function(length){this.pageSize=length;},getPageSze:function(){return this.pageSize;},load:function(data){this.data=data;this.changeEvent.fire(data);},error:function(msg){this.errorEvent.fire(msg);}};joRecord=function(data){joDataSource.call(this,data);this.delegate={};};joRecord.extend(joDataSource,{link:function(p){return this.getDelegate(p);},getDelegate:function(p){if(!this.delegate[p]) +this.delegate[p]=new joProperty(this,p);return this.delegate[p];},getProperty:function(p){return this.data[p];},setProperty:function(p,data){if(this.data[p]===data) +return;this.data[p]=data;this.changeEvent.fire(this);if(this.autoSave) +this.save();return this;},load:function(){console.log("TODO: extend the load() method");return this;},save:function(){console.log("TODO: extend the save() method");return this;}});joProperty=function(datasource,p){joDataSource.call(this);this.changeEvent=new joSubject(this);datasource.changeEvent.subscribe(this.onSourceChange,this);this.datasource=datasource;this.p=p;};joProperty.extend(joDataSource,{setData:function(data){if(this.datasource) +this.datasource.setProperty(this.p,data);return this;},getData:function(){if(!this.datasource) +return null;return this.datasource.getProperty(this.p);},onSourceChange:function(){this.changeEvent.fire(this.getData());}});joDatabase=function(datafile,size){this.openEvent=new joEvent.Subject(this);this.closeEvent=new joEvent.Subject(this);this.errorEvent=new joEvent.Subject(this);this.datafile=datafile;this.size=size||256000;this.db=null;};joDatabase.prototype={open:function(){this.db=openDatabase(this.datafile,"1.0",this.datafile,this.size);if(this.db){this.openEvent.fire();} +else{joLog("DataBase Error",this.db);this.errorEvent.fire();}},close:function(){this.db.close();this.closeEvent.fire();},now:function(offset){var date=new Date();if(offset) +date.setDate(date.valueOf()+(offset*1000*60*60*24));return date.format("yyyy-mm-dd");}};joSQLDataSource=function(db,query,args){this.db=db;this.query=(typeof query=='undefined')?"":query;this.args=(typeof args=='undefined')?[]:args;this.changeEvent=new joEvent.subject(this);this.errorEvent=new joEvent.subject(this);};joSQLDataSource.prototype={setDatabase:function(db){this.db=db;},setQuery:function(query){this.query=query;},setData:function(data){this.data=data;this.changeEvent.fire();},clear:function(){this.data=[];this.changeEvent.fire();},setParameters:function(args){this.args=args;},execute:function(query,args){this.setQuery(query||"");this.setParameters(args);if(this.query) +this.refresh();},refresh:function(){if(!this.db){this.errorEvent.fire();return;} +var self=this;if(arguments.length){var args=[];for(var i=0;i0)?setTimeout(onerror,timeout):null;req.open('GET',url,true);req.onreadystatechange=onchange;req.onError=onerror;req.send(null);function onchange(e){if(timer) +timer=clearTimeout(timer);if(req.readyState==4) +handler(req.responseText,0);} +function onerror(){handler(null,true);} +function handler(data,error){if(call){if(context) +call.call(context,data,error);else +call(error,data,error);}}} +function joScript(url,call,context){var node=joDOM.create('script');if(!node) +return;node.onload=onload;node.onerror=onerror;node.src=url;document.body.appendChild(node);function onerror(){handler(true);} +function onload(){handler(false);} +function handler(error){if(call){if(context) +call.call(context,error,url);else +call(error,url);} +document.body.removeChild(node);node=null;}} +joPreference=joRecord;joYQL=function(query){joDataSource.call(this);this.setQuery(query);};joYQL.extend(joDataSource,{baseurl:'http://query.yahooapis.com/v1/public/yql?',format:'json',query:'',exec:function(){var get=this.baseurl+"q="+encodeURIComponent(this.query) ++"&format="+this.format+"&callback="+joDepot(this.load,this);joScript(get,this.callBack,this);},load:function(data){var results=data.query&&data.query.results&&data.query.results.item;if(!results) +this.errorEvent.fire(data);else{this.data=results;this.changeEvent.fire(results);}},callBack:function(error){if(error) +this.errorEvent.fire();}});joDepotCall=[];joDepot=function(call,context){joDepotCall.push(handler);function handler(data){if(context) +call.call(context,data);else +call(data);};return"joDepotCall["+(joDepotCall.length-1)+"]";};joInterface=function(parent){jo.initTagMap();return this.get(parent);};joInterface.prototype={get:function(parent){parent=joDOM.get(parent);if(!parent) +parent=document.body;var ui={};var setContainer=joView.setContainer;var draw=joView.draw;parse(parent);joView.setContainer=setContainer;joView.draw=draw;function parse(node){if(!node) +return;var args="";if(node.childNodes&&node.firstChild){var kids=node.childNodes;args=[];for(var i=0,l=kids.length;i=0) +this.setValue(this.value,true);return;},deselect:function(){if(typeof this.container=='undefined'||!this.container['childNodes']) +return;var node=this.getNode(this.value);if(node){if(this.lastNode){joDOM.removeCSSClass(this.lastNode,"selected");this.value=null;}} +return this;},setValue:function(index,silent){this.value=index;if(index==null) +return;if(typeof this.container==='undefined'||!this.container||!this.container.firstChild){return this;} +var node=this.getNode(this.value);if(node){if(this.lastNode) +joDOM.removeCSSClass(this.lastNode,"selected");joDOM.addCSSClass(node,"selected");this.lastNode=node;} +if(index>=0&&!silent){this.fireSelect(index);this.changeEvent.fire(index);} +return this;},getNode:function(index){return this.container.childNodes[index];},fireSelect:function(index){this.selectEvent.fire(index);},getValue:function(){return this.value;},onMouseDown:function(e){joEvent.stop(e);var node=joEvent.getTarget(e);var index=-1;while(index==-1&&node!==this.container){index=node.getAttribute("index")||-1;node=node.parentNode;} +if(index>=0) +this.setValue(index);},refresh:function(){if(this.autoSort) +this.sort();joControl.prototype.refresh.apply(this);},getNodeData:function(index){if(this.data&&this.data.length&&index>=0&&indexb) +return 1;else if(a==b) +return 0;else +return-1;},setAutoSort:function(state){this.autoSort=state;return this;},next:function(){if(this.getValue()0) +this.setValue(this.value-1);}});joBusy=function(data){joContainer.apply(this,arguments);};joBusy.extend(joContainer,{tagName:"jobusy",draw:function(){this.container.innerHTML="";for(var i=0;i<9;i++) +this.container.appendChild(joDom.create("jobusyblock"));},setMessage:function(msg){this.message=msg||"";},setEvents:function(){return this;}});joCaption=function(data){joControl.apply(this,arguments);};joCaption.extend(joControl,{tagName:"jocaption"});joCard=function(data){joContainer.apply(this,arguments);};joCard.extend(joContainer,{tagName:"jocard"});joStack=function(data){this.visible=false;this.data=[];joContainer.apply(this,arguments);if(this.data&&!(this.data instanceof Array)) +this.data=[this.data];else if(this.data.length>1) +this.data=[this.data[0]];if(this.container&&this.container.firstChild) +this.container.innerHTML="";this.setLocked(true);this.pushEvent=new joSubject(this);this.popEvent=new joSubject(this);this.homeEvent=new joSubject(this);this.showEvent=new joSubject(this);this.hideEvent=new joSubject(this);this.backEvent=new joSubject(this);this.forwardEvent=new joSubject(this);this.index=0;this.lastIndex=0;this.lastNode=null;};joStack.extend(joContainer,{tagName:"jostack",type:"fixed",eventset:false,setEvents:function(){},onClick:function(e){joEvent.stop(e);},forward:function(){if(this.index0){this.index--;this.draw();this.backEvent.fire();}},draw:function(){if(!this.container) +this.createContainer();if(!this.data||!this.data.length) +return;jo.flag.stopback=this.index?true:false;var container=this.container;var oldchild=this.lastNode;var newnode=getnode(this.data[this.index]);var newchild=this.getChildStyleContainer(newnode);function getnode(o){return(o instanceof joView)?o.container:o;} +if(!newchild) +return;if(this.index>this.lastIndex){var oldclass="prev";var newclass="next";joDOM.addCSSClass(newchild,newclass);} +else if(this.indexthis.locked){var o=this.data.pop();this.index=this.data.length-1;this.draw();if(typeof o.deactivate==="function") +o.deactivate.call(o);if(!this.data.length) +this.hide();} +if(this.data.length>0) +this.popEvent.fire();},home:function(){if(this.data&&this.data.length&&this.data.length>1){var o=this.data[0];var c=this.data[this.index];if(o===c) +return;this.data=[o];this.lastIndex=1;this.index=0;this.draw();this.popEvent.fire();this.homeEvent.fire();}},showHome:function(){this.home();if(!this.visible){this.visible=true;joDOM.addCSSClass(this.container,"show");this.showEvent.fire();}},getTitle:function(){var c=this.data[this.index];if(typeof c.getTitle==='function') +return c.getTitle();else +return false;},show:function(){if(!this.visible){this.visible=true;joDOM.addCSSClass(this.container,"show");joDefer(this.showEvent.fire,this.showEvent,500);}},hide:function(){if(this.visible){this.visible=false;joDOM.removeCSSClass(this.container,"show");joDefer(this.hideEvent.fire,this.hideEvent,500);}}});joScroller=function(data){this.points=[];this.eventset=false;this.horizontal=0;this.vertical=1;this.inMotion=false;this.moved=false;this.mousemove=null;this.mouseup=null;this.bump=0;joContainer.apply(this,arguments);};joScroller.extend(joContainer,{tagName:"joscroller",velocity:1.6,transitionEnd:"webkitTransitionEnd",setEvents:function(){joEvent.capture(this.container,"click",this.onClick,this);joEvent.on(this.container,"mousedown",this.onDown,this);},onFlick:function(e){},onClick:function(e){if(this.moved){this.moved=false;joEvent.stop(e);joEvent.preventDefault(e);}},onDown:function(e){joEvent.stop(e);this.reset();var node=this.container.firstChild;joDOM.removeCSSClass(node,"flick");joDOM.removeCSSClass(node,"flickback");joDOM.removeCSSClass(node,"flickfast");this.start=this.getMouse(e);this.points.unshift(this.start);this.inMotion=true;if(!this.mousemove){this.mousemove=joEvent.capture(document.body,"mousemove",this.onMove,this);this.mouseup=joEvent.capture(document.body,"mouseup",this.onUp,this);}},reset:function(){this.points=[];this.moved=false;this.inMotion=false;},onMove:function(e){if(!this.inMotion) +return;joEvent.stop(e);e.preventDefault();var point=this.getMouse(e);var y=point.y-this.points[0].y;var x=point.x-this.points[0].x;this.points.unshift(point);if(this.points.length>7) +this.points.pop();var self=this;this.timer=window.setTimeout(function(){if(self.inMotion&&self.points.length>1) +self.points.pop();},100);this.scrollBy(x,y,true);if(!this.moved&&this.points.length>3) +this.moved=true;},onUp:function(e){if(!this.inMotion) +return;joEvent.remove(document.body,"mousemove",this.mousemove,true);joEvent.remove(document.body,"mouseup",this.mouseup,true);this.mousemove=null;this.inMotion=false;joEvent.stop(e);joEvent.preventDefault(e);var end=this.getMouse(e);var node=this.container.firstChild;var top=this.getTop();var left=this.getLeft();var dy=0;var dx=0;for(var i=0;i4||Math.abs(dx)*this.horizontal>4)){var flick=dy*(this.velocity*(node.offsetHeight/this.container.offsetHeight));var flickx=dx*(this.velocity*(node.offsetWidth/this.container.offsetWidth));if((flick+top0)||(flickx+left0)){joDOM.addCSSClass(node,"flickfast");} +else{joDOM.addCSSClass(node,"flick");} +this.scrollBy(flickx,flick,false);joDefer(this.snapBack,this,3000);} +else{joDefer(this.snapBack,this,10);}},getMouse:function(e){return{x:(this.horizontal)?e.screenX:0,y:(this.vertical)?e.screenY:0};},scrollBy:function(x,y,test){var node=this.container.firstChild;var top=this.getTop();var left=this.getLeft();var dy=Math.floor(top+y);var dx=Math.floor(left+x);if(this.vertical&&(node.offsetHeight<=this.container.offsetHeight)) +return;var max=0-node.offsetHeight+this.container.offsetHeight;var maxx=0-node.offsetWidth+this.container.offsetWidth;var ody=dy;var odx=dx;if(this.bump){if(dy>this.bump) +dy=this.bump;else if(dythis.bump) +dx=this.bump;else if(dy0) +y=0;if(!instant){joDOM.addCSSClass(node,'flick');} +else{joDOM.removeCSSClass(node,'flick');joDOM.removeCSSClass(node,'flickback');} +this.moveTo(0,y);},snapBack:function(){var node=this.container.firstChild;var top=this.getTop();var left=this.getLeft();var dy=top;var dx=left;var max=0-node.offsetHeight+this.container.offsetHeight;var maxx=0-node.offsetWidth+this.container.offsetWidth;if(this.eventset) +joEvent.remove(node,this.transitionEnd,this.eventset);this.eventset=null;joDOM.removeCSSClass(node,'flick');if(dy>0) +dy=0;else if(dy0) +dx=0;else if(dx=0) +this.close();else +this.open();},open:function(){joDOM.addCSSClass(this.container,"open");this.openEvent.fire();},close:function(){joDOM.removeCSSClass(this.container,"open");this.closeEvent.fire();}});joExpandoContent=function(){joContainer.apply(this,arguments);};joExpandoContent.extend(joContainer,{tagName:"joexpandocontent"});joExpandoTitle=function(data){joControl.apply(this,arguments);};joExpandoTitle.extend(joControl,{tagName:"joexpandotitle",setData:function(){joView.prototype.setData.apply(this,arguments);this.draw();},draw:function(){this.container.innerHTML=this.data+"";}});joFlexrow=function(data){joContainer.apply(this,arguments);};joFlexrow.extend(joContainer,{tagName:"joflexrow"});joFlexcol=function(data){joContainer.apply(this,arguments);};joFlexcol.extend(joContainer,{tagName:"joflexcol"});joFocus={last:null,set:function(control){if(this.last&&this.last!==control) +this.last.blur();if(control&&control instanceof joControl){control.focus();this.last=control;}},get:function(control){return this.last;},refresh:function(){if(this.last) +this.last.focus();},clear:function(){this.set();}};joFooter=function(data){joContainer.apply(this,arguments);};joFooter.extend(joContainer,{tagName:"jofooter"});joGesture={load:function(){this.upEvent=new joSubject(this);this.downEvent=new joSubject(this);this.leftEvent=new joSubject(this);this.rightEvent=new joSubject(this);this.forwardEvent=new joSubject(this);this.backEvent=new joSubject(this);this.homeEvent=new joSubject(this);this.closeEvent=new joSubject(this);this.activateEvent=new joSubject(this);this.deactivateEvent=new joSubject(this);this.resizeEvent=new joSubject(this);this.setEvents();},setEvents:function(){joEvent.on(document.body,"keydown",this.onKeyDown,this);joEvent.on(document.body,"keyup",this.onKeyUp,this);joEvent.on(document.body,"unload",this.closeEvent,this);joEvent.on(window,"activate",this.activateEvent,this);joEvent.on(window,"deactivate",this.deactivateEvent,this);joEvent.on(window,"resize",this.resize,this);},resize:function(){this.resizeEvent.fire(window);},onKeyUp:function(e){if(!e) +var e=window.event;if(e.keyCode==18){this.altkey=false;return;} +if(e.keyCode==27){if(jo.flag.stopback){joEvent.stop(e);joEvent.preventDefault(e);} +this.backEvent.fire("back");return;} +if(!this.altkey) +return;joEvent.stop(e);switch(e.keyCode){case 37:this.leftEvent.fire("left");break;case 38:this.upEvent.fire("up");break;case 39:this.rightEvent.fire("right");break;case 40:this.downEvent.fire("down");break;case 27:this.backEvent.fire("back");break;case 13:this.forwardEvent.fire("forward");break;}},onKeyDown:function(e){if(!e) +var e=window.event;if(e.keyCode==27){joEvent.stop(e);joEvent.preventDefault(e);} +else if(e.keyCode==13&&joFocus.get()instanceof joInput){joEvent.stop(e);} +else if(e.keyCode==18){this.altkey=true;} +return;}};joGroup=function(data){joContainer.apply(this,arguments);};joGroup.extend(joContainer,{tagName:"jogroup"});joHTML=function(data){joControl.apply(this,arguments);};joHTML.extend(joControl,{tagName:"johtml",setEvents:function(){joEvent.on(this.container,"click",this.onClick,this);},onClick:function(e){joEvent.stop(e);joEvent.preventDefault(e);var container=this.container;var hrefnode=findhref(joEvent.getTarget(e));if(hrefnode){this.selectEvent.fire(hrefnode.href);} +function findhref(node){if(!node) +return null;if(node.href) +return node;if(typeof node.parentNode!=="undefined"&&node.parentNode!==container) +return findhref(node.parentNode);else +return null;}}});joInput=function(data){joControl.apply(this,arguments);};joInput.extend(joControl,{tagName:"input",type:"text",setData:function(data){if(data!==this.data){this.data=data;if(typeof this.container.value!=="undefined") +this.container.value=data;else +this.container.innerHTML=data;this.changeEvent.fire(this.data);}},getData:function(){if(typeof this.container.value!=="undefined") +return this.container.value;else +return this.container.innerHTML;},enable:function(){this.container.setAttribute("tabindex","1");joControl.prototype.enable.call(this);},disable:function(){this.container.removeAttribute("tabindex");joControl.prototype.disable.call(this);},createContainer:function(){var o=joDOM.create(this);if(!o) +return;o.setAttribute("type","text");o.setAttribute("tabindex","1");o.contentEditable=this.enabled;return o;},setEvents:function(){joControl.prototype.setEvents.call(this);joEvent.on(this.container,"keydown",this.onKeyDown,this);},onKeyDown:function(e){if(e.keyCode==13){e.preventDefault();joEvent.stop(e);} +return false;},draw:function(){if(this.container.value) +this.value=this.data;else +this.innerHTML=this.value;},onMouseDown:function(e){joEvent.stop(e);this.focus();},storeData:function(){this.data=this.getData();if(this.dataSource) +this.dataSource.set(this.value);}});joLabel=function(data){joControl.apply(this,arguments);};joLabel.extend(joControl,{tagName:"jolabel"});joMenu=function(){joList.apply(this,arguments);};joMenu.extend(joList,{tagName:"jomenu",itemTagName:"jomenuitem",value:null,fireSelect:function(index){if(typeof this.data[index].id!=="undefined"&&this.data[index].id) +this.selectEvent.fire(this.data[index].id);else +this.selectEvent.fire(index);},formatItem:function(item,index){var o=joDOM.create(this.itemTagName);o.setAttribute("index",index);if(typeof item==="object"){o.innerHTML=item.title;if(item.icon){o.style.backgroundImage="url("+item.icon+")";joDOM.addCSSClass(o,"icon");}} +else{o.innerHTML=item;} +return o;}});joOption=function(){joMenu.apply(this,arguments);};joOption.extend(joMenu,{tagName:"jooption",itemTagName:"jooptionitem"});joPasswordInput=function(data){joInput.apply(this,arguments);};joPasswordInput.extend(joInput,{className:"password",type:"password"});joPopup=function(){this.showEvent=new joSubject(this);this.hideEvent=new joSubject(this);joContainer.apply(this,arguments);};joPopup.extend(joContainer,{tagName:"jopopup",setEvents:function(){joEvent.on(this.container,"mousedown",this.onClick,this);},onClick:function(e){joEvent.stop(e);},hide:function(){joEvent.on(this.container,"webkitTransitionEnd",this.onHide,this);this.container.className='hide';},onHide:function(){this.hideEvent.fire();},show:function(){this.container.className='show';this.showEvent.fire();}});joScreen=function(){this.resizeEvent=new joSubject(this);this.menuEvent=new joSubject(this);this.activateEvent=new joSubject(this);this.deactivateEvent=new joSubject(this);this.backEvent=new joSubject(this);this.forwardEvent=new joSubject(this);joContainer.apply(this,arguments);};joScreen.extend(joContainer,{tagName:"screen",setupEvents:function(){joEvent.on(window,"resize",this.resizeEvent.fire,this);joEvent.on(window,"appmenushow",this.menuEvent.fire,this);joEvent.on(window,"activate",this.activateEvent.fire,this);joEvent.on(window,"deactivate",this.deactivateEvent.fire,this);joEvent.on(window,"back",this.backEvent.fire,this);},createContainer:function(){return document.body;},showPopup:function(data){if(!this.popup){this.shim=new joShim(new joFlexcol([' ',this.popup=new joPopup(data),' ']));} +else{this.popup.setData(data);} +this.shim.show();this.popup.show();},hidePopup:function(){if(this.shim) +this.shim.hide();},alert:function(title,msg,options,context){var buttons=[];var callback;var context=(typeof context==='object')?context:null;if(typeof options==='object'){if(options instanceof Array){for(var i=0;i1) +return;this.audio.volume=vol;return this;}};joStackScroller=function(data){this.scrollers=[new joScroller(),new joScroller()];this.scroller=this.scrollers[0];joStack.apply(this,arguments);this.scroller.attach(this.container);};joStackScroller.extend(joStack,{type:"scroll",scrollerindex:1,scroller:null,scrollers:[],switchScroller:function(){this.scrollerindex=this.scrollerindex?0:1;this.scroller=this.scrollers[this.scrollerindex];},getLastScroller:function(){return this.scrollers[this.scrollerindex?0:1];},scrollTo:function(something){this.scroller.scrollTo(something);},scrollBy:function(y){this.scroller.scrollBy(y);},getChildStyleContainer:function(){return this.scroller.container;},getContentContainer:function(){return this.scroller.container;},appendChild:function(child){var scroller=this.scroller;scroller.setData(child);this.container.appendChild(scroller.container);},getChild:function(){return this.scroller.container||null;},forward:function(){if(this.index0) +this.switchScroller();joStack.prototype.forward.call(this);},home:function(){if(this.data&&this.data.length&&this.data.length>1){this.switchScroller();joStack.prototype.home.call(this);}},push:function(o){if(this.data&&this.data.length&&this.data[this.data.length-1]===o) +return;this.switchScroller();joDOM.removeCSSClass(o,'flick');joDOM.removeCSSClass(o,'flickback');this.scroller.setData(o);this.scroller.scrollTo(0,true);joStack.prototype.push.call(this,o);},pop:function(){if(this.data.length>this.locked) +this.switchScroller();joStack.prototype.pop.call(this);}});joTabBar=function(){joList.apply(this,arguments);};joTabBar.extend(joList,{tagName:"jotabbar",formatItem:function(data,index){var o=document.createElement("jotab");if(data.label) +o.innerHTML=data.label;if(data.type) +o.className=data.type;o.setAttribute("index",index);return o;}});joTable=function(data){joList.apply(this,arguments);};joTable.extend(joList,{tagName:"jotable",formatItem:function(row,index){var tr=document.createElement("tr");for(var i=0,l=row.length;i1) +joDOM.addCSSClass(this.back,'active');else +joDOM.removeCSSClass(this.back,'active');var title=this.stack.getTitle();if(typeof title==='string') +this.titlebar.setData(title);else +this.titlebar.setData(this.firstTitle);},setTitle:function(title){this.titlebar.setData(title);this.firstTitle=title;return this;}});joBackButton=function(){joButton.apply(this,arguments);};joBackButton.extend(joButton,{tagName:"jobackbutton"});joSelect=function(data,value){var v=value;if(value instanceof joDataSource) +v=value.getData();var ui=[this.field=new joSelectTitle(v),this.list=new joSelectList(data,value)];this.field.setList(this.list);this.changeEvent=this.list.changeEvent;this.selectEvent=this.list.selectEvent;joExpando.call(this,ui);this.container.setAttribute("tabindex",1);this.field.setData(this.list.value);this.list.selectEvent.subscribe(this.setValue,this);};joSelect.extend(joExpando,{setValue:function(value,list){if(list){this.field.setData(value);this.close();} +else{this.field.setData(value);}},getValue:function(){return this.list.getValue();},setEvents:function(){joControl.prototype.setEvents.call(this);},onBlur:function(e){joEvent.stop(e);joDOM.removeCSSClass(this,"focus");this.close();}});joSelectTitle=function(){joExpandoTitle.apply(this,arguments);};joSelectTitle.extend(joExpandoTitle,{list:null,setList:function(list){this.list=list;},setData:function(value){if(this.list) +joExpandoTitle.prototype.setData.call(this,this.list.getNodeData(value)||"Select...");else +joExpandoTitle.prototype.setData.call(this,value);}});joToggle=function(data){joControl.call(this,data);};joToggle.extend(joControl,{tagName:"jotoggle",button:null,labels:["Off","On"],setLabels:function(labels){if(labels instanceof Array) +this.labels=labels;else if(arguments.length==2) +this.labels=arguments;this.draw();return this;},select:function(e){if(e) +joEvent.stop(e);this.setData((this.data)?false:true);},onBlur:function(e){joEvent.stop(e);this.blur();},draw:function(){if(!this.container) +return;if(!this.container.firstChild){this.button=joDOM.create("div");this.container.appendChild(this.button);} +this.button.innerHTML=this.labels[(this.data)?1:0];if(this.data) +joDOM.addCSSClass(this.container,"on");else +joDOM.removeCSSClass(this.container,"on");}});joSlider=function(value){this.min=0;this.max=1;this.snap=0;this.range=1;this.thumb=null;this.horizontal=1;this.vertical=0;this.moved=false;this.jump=true;joControl.call(this,null,value);};joSlider.extend(joControl,{tagName:"joslider",setRange:function(min,max,snap){if(min>=max){joLog("WARNING: joSlider.setRange, min must be less than max.");return this;} +this.min=min||0;this.max=max||1;if(min<0&&max>=0) +this.range=Math.abs(min)+max;else if(min<0&&max<=0) +this.range=min-max;else +this.range=max-min;if(typeof snap!=='undefined') +this.snap=(snap>=0&&snap<=this.range)?snap:0;else +this.snap=0;this.setValue(this.value);return this;},setValue:function(value,silent){var v=this.adjustValue(value);if(v!=this.value){joControl.prototype.setValue.call(this,v);if(!silent) +this.draw();} +return this;},adjustValue:function(v){var value=v;if(this.snap) +value=Math.floor(value/this.snap)*this.snap;if(valuethis.max) +value=this.max;return value;},createContainer:function(){var o=joDOM.create(this.tagName);if(o){o.setAttribute("tabindex","1");var t=joDOM.create("josliderthumb");o.appendChild(t);this.thumb=t;} +return o;},onDown:function(e){joEvent.stop(e);this.reset();var node=this.container.firstChild;this.inMotion=true;this.moved=false;if(!this.mousemove){this.mousemove=joEvent.on(document.body,"mousemove",this.onMove,this);this.mouseup=joEvent.capture(document.body,"mouseup",this.onUp,this);}},reset:function(){this.moved=false;this.inMotion=false;this.firstX=-1;this.firstY=-1;},onMove:function(e){if(!this.inMotion) +return;joEvent.stop(e);e.preventDefault();var point=this.getMouse(e);var y=point.y;var x=point.x;if(this.firstX==-1){this.firstX=x;this.firstY=y;this.ox=this.thumb.offsetLeft;this.oy=this.thumb.offsetTop;} +var x=(x-this.firstX)+this.ox;var y=(y-this.firstY)+this.oy;if(x>4||y>4) +this.moved=true;var t=this.thumb.offsetWidth;var w=this.container.offsetWidth-t;if(x<0) +x=0;else if(x>w) +x=w;if(!this.snap) +this.moveTo(x);this.setValue((x/w)*this.range+this.min,!this.snap);},moveTo:function(x){this.thumb.style.left=x+"px";},initValue:function(value){var t=this.container.firstChild.offsetWidth;var w=this.container.offsetWidth-t;var x=Math.floor((this.value/this.range)*w);this.moveTo(x);return this;},onUp:function(e){if(!this.inMotion) +return;joEvent.remove(document.body,"mousemove",this.mousemove);joEvent.remove(document.body,"mouseup",this.mouseup);this.mousemove=null;joEvent.stop(e);joEvent.preventDefault(e);joDefer(function(){this.reset();},this);},setEvents:function(){joEvent.on(this.container,"click",this.onClick,this);joEvent.on(this.thumb,"mousedown",this.onDown,this);joGesture.resizeEvent.subscribe(this.draw,this);console.log('setevents');},onClick:function(e){if(this.inMotion||this.moved) +return;joEvent.stop(e);joEvent.preventDefault(e);var point=this.getMouse(e);var l=joDOM.pageOffsetLeft(this.container);var x=Math.floor((point.x-l)-this.thumb.offsetWidth*1.5);var t=this.thumb.offsetWidth;x=x-t;var w=this.container.offsetWidth-t;if((xw) +x=w;this.setValue((x/w)*this.range+this.min);},getMouse:function(e){return{x:(this.horizontal)?e.screenX:0,y:(this.vertical)?e.screenY:0};},draw:function(){if(!this.container) +this.setContainer();this.initValue(this.value);}}); \ No newline at end of file diff --git a/assets/www/js/tafl.js b/assets/www/js/tafl.js new file mode 100644 index 0000000..4ddddbe --- /dev/null +++ b/assets/www/js/tafl.js @@ -0,0 +1,292 @@ +/* + * This is the JS Tafl Game lib and part of Androtafl. + */ + + +Array.prototype.has = function(obj) { + var i = this.length; + while (i--) { + if (this[i] === obj) { + return true; + } + } + return false; +}; + +String.prototype.setCharAt = function(index,chr) { + if(index > this.length-1) return this; + return this.substr(0,index) + chr + this.substr(index+1); +} + +function TaflState() { + 'use strict'; + + this.board = ['']; + this.N = 0; + this.to_move = 'w'; + this.legal_moves_cache = null; + this.winner = null; + this.win_reason = null; + + this.loadBoard = function(board) { + this.board = board; + this.N = board.length; + if (board[0].length !== this.N) throw "BoardNotSquare"; + }; + + this.clone = function() { + var i, t = new TaflState(); + + for (i = 0; i < this.N; ++i) { + t.board[i] = this.board[i]; + } + + t.N = this.N; + t.to_move = this.to_move; + t.legal_moves_cache = this.legal_moves_cache; + t.winner = this.winner; + t.win_reason = this.win_reason; + } +} + +var Tafl = { + legal_moves: function(s) { + if (s.legal_moves_cache) return s.legal_moves_cache; + + var i,j,k, N=s.N, moves=[], legal_fields; + + + for (i=0; i= s.N || l+2*dj < 0 || l+2*dj >= s.N) continue; + + if (this.is_not_moving_color(s, [k+di, l+dj]) && + this.is_moving_color(s, [k+2*di, l+2*dj]) && + s.board[k+di][l+dj].toUpperCase() !== 'K') { + captures.push([k+di, l+dj]); + } + } + + // Check if king is captured separately + king_pos = this.find_king(s); + i = king_pos[0]; j=king_pos[1]; + + king_capturers = ['W',',']; // Kings can be captured by white stones + // or the throne + if (i > 0 && j > 0 && i < s.N-1 && j < s.N-1) { // King is in not on + // an edge + if (king_capturers.has(s.board[i][j-1]) && king_capturers.has(s.board[i][j+1]) && + king_capturers.has(s.board[i-1][j]) && king_capturers.has(s.board[i+1][j])) { + captures.push([i,j]); + } + } + + + return captures; + }, + + is_moving_color: function(s, pos) { + var i=pos[0], j=pos[1]; + + if (s.to_move === s.board[i][j]) return true; + // K and k are both kings + if (s.to_move === 'B' && s.board[i][j].toUpperCase() === 'K') return true; + + return false; + }, + + is_not_moving_color: function(s, pos) { // Returns true if it's a piece, but not of the current color + if (this.is_moving_color(s, pos) || ['.', ','].has(s.board[pos[0]][pos[1]])) { + return false; + } else { + return true; + } + }, + + find_king: function(s) { + var i,j; + + for (i=0; i= 0) return [i,j]; + } + + return null; + }, + + terminal_test: function(s) { + var i, j, king_found = false, corners; + + // find position of king and check if he's on an edge + for (i=0; i= 0) { // Found king + + // King is at an edge + if (i === 0 || i === s.N-1 || j === 0 || j === s.N-1) { + s.winner = 'B'; + s.win_reason = 'KingEscaped'; + return true; + } + + king_found = true; + break; + } + } + + if (king_found === false) { + s.winner = 'W'; + s.win_reason = 'NoKing'; + return true; // King was captured + } + + // Are there any legal moves? If not, player loses + if (this.legal_moves(s).length == 0) { + if (s.to_move === 'W') s.winner = 'B'; + else s.winner = 'W'; + s.win_reason = 'NoLegalMoves'; + return true; + } + + return false; // no one won, yet + }, + + movelist_has_move: function(movelist, move) { + var i; + for (i = 0; i < movelist.length; ++i) { + if (movelist[i][0][0] === move[0][0] && + movelist[i][0][1] === move[0][1] && + movelist[i][1][0] === move[1][0] && + movelist[i][1][1] === move[1][1]) + return true; + } + return false; + }, + + initialStates: { + Hnefatafl: { + board: function(){return [ + "...WWWWW...", // W = attacker + ".....W.....", // B = defender + "...........", // k = king on throne + "W....B....W", + "W...BBB...W", + "WW.BBkBB.WW", + "W...BBB...W", + "W....B....W", + "...........", + ".....W.....", + "...WWWWW..."]}, + to_move: function(){return "W"}, + } + } +}; + +var TaflUnitTest = function() { + function test(name,a,b) { + if (a !== b) console.log("Unit test '" + name + "' failed: '" + a + "' !== '" + b + "'"); + //else console.log("Unit test " + name + " passed"); + } + + var state = new TaflState(); + state.loadBoard(Tafl.initialStates.Hnefatafl.board()); + state.to_move = Tafl.initialStates.Hnefatafl.to_move(); + + test("is_moving_color 1", true, Tafl.is_moving_color(state, [0, 4])); // W + test("is_moving_color 2", false, Tafl.is_moving_color(state, [1, 4])); // . + test("is_moving_color 3", false, Tafl.is_moving_color(state, [4, 5])); // B + test("is_moving_color 4", false, Tafl.is_moving_color(state, [5, 5])); // k + + test("is_not_moving_color 1", false, Tafl.is_not_moving_color(state, [0, 0])); // W + test("is_not_moving_color 2", false, Tafl.is_not_moving_color(state, [1, 4])); // . + test("is_not_moving_color 3", true, Tafl.is_not_moving_color(state, [4, 5])); // B + test("is_not_moving_color 4", true, Tafl.is_not_moving_color(state, [4, 5])); // k + + + test("loadBoard", state.board[3], "W....B....W"); + + Tafl.make_move(state, [[10, 7], [10, 9]]); + + test("make_move white", state.board[10], "...WWWW..W."); + + Tafl.make_move(state, [[5, 7], [10, 7]]); + + test("make_move black 1", state.board[5], "WW.BBkB..WW"); + test("make_move black 2", state.board[10], "...WWWWB.W."); + + Tafl.make_move(state, [[10, 9], [10, 8]]); + + test("capture", state.board[10], "...WWWW.W.."); +}; diff --git a/assets/www/js/ui.js b/assets/www/js/ui.js new file mode 100644 index 0000000..ffd3c84 --- /dev/null +++ b/assets/www/js/ui.js @@ -0,0 +1,132 @@ + +jo.load(); + +//Test +//TaflUnitTest(); +var uiScreen; + +var tafl_game = new TaflState(); +tafl_game.loadBoard(Tafl.initialStates.Hnefatafl.board()); +tafl_game.to_move = Tafl.initialStates.Hnefatafl.to_move(); + +var N = tafl_game.board.length; +var uiBoard; + +function prepareBoard(board) { + var ret_board = []; + for (var i=0; i" + winner + " player wins."); + } +} + + +// Build page +var uiCard = new joCard([ + new joTitle("Andotafl"), + uiBoard +]); + +var uiStack = new joStack(); +var uiScreen = new joScreen(uiStack); + +uiStack.push(uiCard); diff --git a/assets/www/tafl.js b/assets/www/tafl.js deleted file mode 100644 index 07fa8cb..0000000 --- a/assets/www/tafl.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * This is the JS Tafl Game lib and part of Androtafl. - */ - -(function () { - 'use strict'; - - Array.prototype.has = function(obj) { - var i = this.length; - while (i--) { - if (this[i] === obj) { - return true; - } - } - return false; - }; - - function TaflState() { - this.board = ['']; - this.N = 0; - this.to_move = 'w'; - this.legal_moves_cache = null; - this.winner = null; - - this.loadBoard = function(board) { - this.board = board; - this.N = board.length; - if (board[0].length !== this.N) throw "BoardNotSquare"; - }; - - this.clone = function() { - var i, t = new TaflState(); - - for (i = 0; i < this.N; ++i) { - t.board[i] = this.board[i]; - } - - t.N = this.N; - t.to_move = this.to_move; - t.legal_moves_cache = this.legal_moves_cache; - t.winner = this.winner; - } - } - - var Tafl = { - legal_moves: function(s) { - if (s.legal_moves_cache) return s.legal_moves_cache; - - var i,j,k, N=s.N, moves=[], legal_fields; - - - for (i=0; i 0 && j > 0 && i < s.N-1 && j < s.N-1) { // King is in not on - // an edge - if (king_capturers.has(s.board[i][j-1]) && king_capturers.has(s.board[i][j+1]) && - king_capturers.has(s.board[i-1][j]) && king_capturers.has(s.board[i+1][j])) { - catches.push([i,j]); - } - } - - - return catches; - }, - - is_moving_color: function(s, move) { - var i=move[0], j=move[1]; - - if (s.to_move === s.board[i][j]) return true; - // K and k are both kings - if (s.to_move === 'B' && s.board[i][j].toUpperCase() === 'K') return true; - - return false; - }, - - find_king: function(s) { - for (i=0; i 0) s.board[i].find('k'); // King is on throne? - if (j >= 0) return [i,j]; - } - - return null; - } - - terminal_test: function(s) { - var i, j, king_found = false, corners; - - // find position of king and check if he's on an edge - for (i=0; i 0) s.board[i].find('k'); // King is on throne? - - if (j >= 0) { // Found king - - // King is at an edge - if (i === 0 || i === s.N-1 || j === 0 || j === s.N-1) { - s.winner = 'B'; - return true; - } - - kind_found = true; - break; - } - } - - if (! king_found) { - s.winner = 'W'; - return true; // King was captured - } - - // Are there any legal moves? If not, player loses - if (this.legal_moves(s).length == 0) { - if (s.to_move === 'W') s.winner = 'B'; - else s.winner = 'W'; - return true; - } - - return false; // no one won, yet - } - }; -})();