diff --git a/rss/gopherjs/actions/actions.go b/rss/gopherjs/actions/actions.go
new file mode 100644
index 0000000..ba058e0
--- /dev/null
+++ b/rss/gopherjs/actions/actions.go
@@ -0,0 +1,45 @@
+package actions
+
+import "github.com/drekle/go/rss/gopherjs/store/model"
+
+// ReplaceItems is an action that replaces all items with the specified ones.
+type ReplaceItems struct {
+ Items []*model.Item
+}
+
+// AddItem is an action which adds a single item with the specified title.
+type AddItem struct {
+ Title string
+}
+
+// DestroyItem is an action which destroys the item specified by the index.
+type DestroyItem struct {
+ Index int
+}
+
+// SetTitle is an action which specifies the title of an existing item.
+type SetTitle struct {
+ Index int
+ Title string
+}
+
+// SetCompleted is an action which specifies the completed state of an existing
+// item.
+type SetCompleted struct {
+ Index int
+ Completed bool
+}
+
+// SetAllCompleted is an action which marks all existing items as being
+// completed or not.
+type SetAllCompleted struct {
+ Completed bool
+}
+
+// ClearCompleted is an action which clears the completed items.
+type ClearCompleted struct{}
+
+// SetFilter is an action which sets the filter for the viewed items.
+type SetFilter struct {
+ Filter model.FilterState
+}
diff --git a/rss/gopherjs/components/filterbutton.go b/rss/gopherjs/components/filterbutton.go
new file mode 100644
index 0000000..00e4434
--- /dev/null
+++ b/rss/gopherjs/components/filterbutton.go
@@ -0,0 +1,42 @@
+package components
+
+import (
+ "github.com/drekle/go/rss/gopherjs/actions"
+ "github.com/drekle/go/rss/gopherjs/dispatcher"
+ "github.com/drekle/go/rss/gopherjs/store"
+ "github.com/drekle/go/rss/gopherjs/store/model"
+ "github.com/gopherjs/vecty"
+ "github.com/gopherjs/vecty/elem"
+ "github.com/gopherjs/vecty/event"
+ "github.com/gopherjs/vecty/prop"
+)
+
+// FilterButton is a vecty.Component which allows the user to select a filter
+// state.
+type FilterButton struct {
+ vecty.Core
+
+ Label string `vecty:"prop"`
+ Filter model.FilterState `vecty:"prop"`
+}
+
+func (b *FilterButton) onClick(event *vecty.Event) {
+ dispatcher.Dispatch(&actions.SetFilter{
+ Filter: b.Filter,
+ })
+}
+
+// Render implements the vecty.Component interface.
+func (b *FilterButton) Render() vecty.ComponentOrHTML {
+ return elem.ListItem(
+ elem.Anchor(
+ vecty.Markup(
+ vecty.MarkupIf(store.Filter == b.Filter, vecty.Class("selected")),
+ prop.Href("#"),
+ event.Click(b.onClick).PreventDefault(),
+ ),
+
+ vecty.Text(b.Label),
+ ),
+ )
+}
diff --git a/rss/gopherjs/components/itemview.go b/rss/gopherjs/components/itemview.go
new file mode 100644
index 0000000..fd0ff04
--- /dev/null
+++ b/rss/gopherjs/components/itemview.go
@@ -0,0 +1,117 @@
+package components
+
+import (
+ "github.com/drekle/go/rss/gopherjs/actions"
+ "github.com/drekle/go/rss/gopherjs/dispatcher"
+ "github.com/drekle/go/rss/gopherjs/store/model"
+ "github.com/gopherjs/vecty"
+ "github.com/gopherjs/vecty/elem"
+ "github.com/gopherjs/vecty/event"
+ "github.com/gopherjs/vecty/prop"
+ "github.com/gopherjs/vecty/style"
+)
+
+// ItemView is a vecty.Component which represents a single item in the TODO
+// list.
+type ItemView struct {
+ vecty.Core
+
+ Index int `vecty:"prop"`
+ Item *model.Item `vecty:"prop"`
+ editing bool
+ editTitle string
+ input *vecty.HTML
+}
+
+// Key implements the vecty.Keyer interface.
+func (p *ItemView) Key() interface{} {
+ return p.Index
+}
+
+func (p *ItemView) onDestroy(event *vecty.Event) {
+ dispatcher.Dispatch(&actions.DestroyItem{
+ Index: p.Index,
+ })
+}
+
+func (p *ItemView) onToggleCompleted(event *vecty.Event) {
+ dispatcher.Dispatch(&actions.SetCompleted{
+ Index: p.Index,
+ Completed: event.Target.Get("checked").Bool(),
+ })
+}
+
+func (p *ItemView) onStartEdit(event *vecty.Event) {
+ p.editing = true
+ p.editTitle = p.Item.Title
+ vecty.Rerender(p)
+ p.input.Node().Call("focus")
+}
+
+func (p *ItemView) onEditInput(event *vecty.Event) {
+ p.editTitle = event.Target.Get("value").String()
+ vecty.Rerender(p)
+}
+
+func (p *ItemView) onStopEdit(event *vecty.Event) {
+ p.editing = false
+ vecty.Rerender(p)
+ dispatcher.Dispatch(&actions.SetTitle{
+ Index: p.Index,
+ Title: p.editTitle,
+ })
+}
+
+// Render implements the vecty.Component interface.
+func (p *ItemView) Render() vecty.ComponentOrHTML {
+ p.input = elem.Input(
+ vecty.Markup(
+ vecty.Class("edit"),
+ prop.Value(p.editTitle),
+ event.Input(p.onEditInput),
+ ),
+ )
+
+ return elem.ListItem(
+ vecty.Markup(
+ vecty.ClassMap{
+ "completed": p.Item.Completed,
+ "editing": p.editing,
+ },
+ ),
+
+ elem.Div(
+ vecty.Markup(
+ vecty.Class("view"),
+ ),
+
+ elem.Input(
+ vecty.Markup(
+ vecty.Class("toggle"),
+ prop.Type(prop.TypeCheckbox),
+ prop.Checked(p.Item.Completed),
+ event.Change(p.onToggleCompleted),
+ ),
+ ),
+ elem.Label(
+ vecty.Markup(
+ event.DoubleClick(p.onStartEdit),
+ ),
+ vecty.Text(p.Item.Title),
+ ),
+ elem.Button(
+ vecty.Markup(
+ vecty.Class("destroy"),
+ event.Click(p.onDestroy),
+ ),
+ ),
+ ),
+ elem.Form(
+ vecty.Markup(
+ style.Margin(style.Px(0)),
+ event.Submit(p.onStopEdit).PreventDefault(),
+ ),
+ p.input,
+ ),
+ )
+}
diff --git a/rss/gopherjs/components/pageview.go b/rss/gopherjs/components/pageview.go
new file mode 100644
index 0000000..556b0fc
--- /dev/null
+++ b/rss/gopherjs/components/pageview.go
@@ -0,0 +1,208 @@
+package components
+
+import (
+ "strconv"
+
+ "github.com/drekle/go/rss/gopherjs/actions"
+ "github.com/drekle/go/rss/gopherjs/dispatcher"
+ "github.com/drekle/go/rss/gopherjs/store"
+ "github.com/drekle/go/rss/gopherjs/store/model"
+ "github.com/gopherjs/vecty"
+ "github.com/gopherjs/vecty/elem"
+ "github.com/gopherjs/vecty/event"
+ "github.com/gopherjs/vecty/prop"
+ "github.com/gopherjs/vecty/style"
+)
+
+// PageView is a vecty.Component which represents the entire page.
+type PageView struct {
+ vecty.Core
+
+ Items []*model.Item `vecty:"prop"`
+ newItemTitle string
+}
+
+func (p *PageView) onNewItemTitleInput(event *vecty.Event) {
+ p.newItemTitle = event.Target.Get("value").String()
+ vecty.Rerender(p)
+}
+
+func (p *PageView) onAdd(event *vecty.Event) {
+ dispatcher.Dispatch(&actions.AddItem{
+ Title: p.newItemTitle,
+ })
+ p.newItemTitle = ""
+ vecty.Rerender(p)
+}
+
+func (p *PageView) onClearCompleted(event *vecty.Event) {
+ dispatcher.Dispatch(&actions.ClearCompleted{})
+}
+
+func (p *PageView) onToggleAllCompleted(event *vecty.Event) {
+ dispatcher.Dispatch(&actions.SetAllCompleted{
+ Completed: event.Target.Get("checked").Bool(),
+ })
+}
+
+// Render implements the vecty.Component interface.
+func (p *PageView) Render() vecty.ComponentOrHTML {
+ return elem.Body(
+ elem.Section(
+ vecty.Markup(
+ vecty.Class("todoapp"),
+ ),
+
+ p.renderHeader(),
+ vecty.If(len(store.Items) > 0,
+ p.renderItemList(),
+ p.renderFooter(),
+ ),
+ ),
+
+ p.renderInfo(),
+ )
+}
+
+func (p *PageView) renderHeader() *vecty.HTML {
+ return elem.Header(
+ vecty.Markup(
+ vecty.Class("header"),
+ ),
+
+ elem.Heading1(
+ vecty.Text("todos"),
+ ),
+ elem.Form(
+ vecty.Markup(
+ style.Margin(style.Px(0)),
+ event.Submit(p.onAdd).PreventDefault(),
+ ),
+
+ elem.Input(
+ vecty.Markup(
+ vecty.Class("new-todo"),
+ prop.Placeholder("What needs to be done?"),
+ prop.Autofocus(true),
+ prop.Value(p.newItemTitle),
+ event.Input(p.onNewItemTitleInput),
+ ),
+ ),
+ ),
+ )
+}
+
+func (p *PageView) renderFooter() *vecty.HTML {
+ count := store.ActiveItemCount()
+ var itemsLeftText = " items left"
+ if count == 1 {
+ itemsLeftText = " item left"
+ }
+
+ return elem.Footer(
+ vecty.Markup(
+ vecty.Class("footer"),
+ ),
+
+ elem.Span(
+ vecty.Markup(
+ vecty.Class("todo-count"),
+ ),
+
+ elem.Strong(
+ vecty.Text(strconv.Itoa(count)),
+ ),
+ vecty.Text(itemsLeftText),
+ ),
+
+ elem.UnorderedList(
+ vecty.Markup(
+ vecty.Class("filters"),
+ ),
+ &FilterButton{Label: "All", Filter: model.All},
+ vecty.Text(" "),
+ &FilterButton{Label: "Active", Filter: model.Active},
+ vecty.Text(" "),
+ &FilterButton{Label: "Completed", Filter: model.Completed},
+ ),
+
+ vecty.If(store.CompletedItemCount() > 0,
+ elem.Button(
+ vecty.Markup(
+ vecty.Class("clear-completed"),
+ event.Click(p.onClearCompleted),
+ ),
+ vecty.Text("Clear completed ("+strconv.Itoa(store.CompletedItemCount())+")"),
+ ),
+ ),
+ )
+}
+
+func (p *PageView) renderInfo() *vecty.HTML {
+ return elem.Footer(
+ vecty.Markup(
+ vecty.Class("info"),
+ ),
+
+ elem.Paragraph(
+ vecty.Text("Double-click to edit a todo"),
+ ),
+ elem.Paragraph(
+ vecty.Text("Created by "),
+ elem.Anchor(
+ vecty.Markup(
+ prop.Href("http://github.com/neelance"),
+ ),
+ vecty.Text("Richard Musiol"),
+ ),
+ ),
+ elem.Paragraph(
+ vecty.Text("Part of "),
+ elem.Anchor(
+ vecty.Markup(
+ prop.Href("http://todomvc.com"),
+ ),
+ vecty.Text("TodoMVC"),
+ ),
+ ),
+ )
+}
+
+func (p *PageView) renderItemList() *vecty.HTML {
+ var items vecty.List
+ for i, item := range store.Items {
+ if (store.Filter == model.Active && item.Completed) || (store.Filter == model.Completed && !item.Completed) {
+ continue
+ }
+ items = append(items, &ItemView{Index: i, Item: item})
+ }
+
+ return elem.Section(
+ vecty.Markup(
+ vecty.Class("main"),
+ ),
+
+ elem.Input(
+ vecty.Markup(
+ vecty.Class("toggle-all"),
+ prop.ID("toggle-all"),
+ prop.Type(prop.TypeCheckbox),
+ prop.Checked(store.CompletedItemCount() == len(store.Items)),
+ event.Change(p.onToggleAllCompleted),
+ ),
+ ),
+ elem.Label(
+ vecty.Markup(
+ prop.For("toggle-all"),
+ ),
+ vecty.Text("Mark all as complete"),
+ ),
+
+ elem.UnorderedList(
+ vecty.Markup(
+ vecty.Class("todo-list"),
+ ),
+ items,
+ ),
+ )
+}
diff --git a/rss/gopherjs/dispatcher/dispatcher.go b/rss/gopherjs/dispatcher/dispatcher.go
new file mode 100644
index 0000000..f4f0e55
--- /dev/null
+++ b/rss/gopherjs/dispatcher/dispatcher.go
@@ -0,0 +1,29 @@
+package dispatcher
+
+// ID is a unique identifier representing a registered callback function.
+type ID int
+
+var idCounter ID
+var callbacks = make(map[ID]func(action interface{}))
+
+// Dispatch dispatches the given action to all registered callbacks.
+func Dispatch(action interface{}) {
+ for _, c := range callbacks {
+ c(action)
+ }
+}
+
+// Register registers the callback to handle dispatched actions, the returned
+// ID may be used to unregister the callback later.
+func Register(callback func(action interface{})) ID {
+ idCounter++
+ id := idCounter
+ callbacks[id] = callback
+ return id
+}
+
+// Unregister unregisters the callback previously registered via a call to
+// Register.
+func Unregister(id ID) {
+ delete(callbacks, id)
+}
diff --git a/rss/gopherjs/example.go b/rss/gopherjs/example.go
new file mode 100644
index 0000000..bd7dbf0
--- /dev/null
+++ b/rss/gopherjs/example.go
@@ -0,0 +1,47 @@
+package main
+
+import (
+ "encoding/json"
+
+ "github.com/drekle/go/rss/gopherjs/actions"
+ "github.com/drekle/go/rss/gopherjs/components"
+ "github.com/drekle/go/rss/gopherjs/dispatcher"
+ "github.com/drekle/go/rss/gopherjs/store"
+ "github.com/drekle/go/rss/gopherjs/store/model"
+ "github.com/gopherjs/gopherjs/js"
+ "github.com/gopherjs/vecty"
+)
+
+func main() {
+ attachLocalStorage()
+
+ vecty.SetTitle("GopherJS • TodoMVC")
+ vecty.AddStylesheet("node_modules/todomvc-common/base.css")
+ vecty.AddStylesheet("node_modules/todomvc-app-css/index.css")
+ p := &components.PageView{}
+ store.Listeners.Add(p, func() {
+ p.Items = store.Items
+ vecty.Rerender(p)
+ })
+ vecty.RenderBody(p)
+}
+
+func attachLocalStorage() {
+ store.Listeners.Add(nil, func() {
+ data, err := json.Marshal(store.Items)
+ if err != nil {
+ println("failed to store items: " + err.Error())
+ }
+ js.Global.Get("localStorage").Set("items", string(data))
+ })
+
+ if data := js.Global.Get("localStorage").Get("items"); data != js.Undefined {
+ var items []*model.Item
+ if err := json.Unmarshal([]byte(data.String()), &items); err != nil {
+ println("failed to load items: " + err.Error())
+ }
+ dispatcher.Dispatch(&actions.ReplaceItems{
+ Items: items,
+ })
+ }
+}
diff --git a/rss/gopherjs/node_modules/todomvc-app-css/index.css b/rss/gopherjs/node_modules/todomvc-app-css/index.css
new file mode 100644
index 0000000..ba79a58
--- /dev/null
+++ b/rss/gopherjs/node_modules/todomvc-app-css/index.css
@@ -0,0 +1,378 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+body {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ font-weight: 300;
+}
+
+button,
+input[type="checkbox"] {
+ outline: none;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+ 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp h1 {
+ position: absolute;
+ top: -155px;
+ width: 100%;
+ font-size: 100px;
+ font-weight: 100;
+ text-align: center;
+ color: rgba(175, 47, 47, 0.15);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ outline: none;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
+}
+
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+label[for='toggle-all'] {
+ display: none;
+}
+
+.toggle-all {
+ position: absolute;
+ top: -55px;
+ left: -12px;
+ width: 60px;
+ height: 34px;
+ text-align: center;
+ border: none; /* Mobile Safari */
+}
+
+.toggle-all:before {
+ content: '❯';
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked:before {
+ color: #737373;
+}
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: 506px;
+ padding: 13px 17px 12px 17px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle:after {
+ content: url('data:image/svg+xml;utf8,');
+}
+
+.todo-list li .toggle:checked:after {
+ content: url('data:image/svg+xml;utf8,');
+}
+
+.todo-list li label {
+ white-space: pre;
+ word-break: break-word;
+ padding: 15px 60px 15px 15px;
+ margin-left: 45px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ margin-bottom: 11px;
+ transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+ color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+ content: '×';
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li .edit {
+ display: none;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+.footer {
+ color: #777;
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
+ 0 8px 0 -3px #f6f6f6,
+ 0 9px 1px -3px rgba(0, 0, 0, 0.2),
+ 0 16px 0 -6px #f6f6f6,
+ 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a.selected,
+.filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+ position: relative;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
+
+.info {
+ margin: 65px auto 0;
+ color: #bfbfbf;
+ font-size: 10px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.info p {
+ line-height: 1;
+}
+
+.info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+}
+
+.info a:hover {
+ text-decoration: underline;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+
+ .todo-list li .toggle {
+ height: 40px;
+ }
+
+ .toggle-all {
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+ -webkit-appearance: none;
+ appearance: none;
+ }
+}
+
+@media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+
+ .filters {
+ bottom: 10px;
+ }
+}
diff --git a/rss/gopherjs/node_modules/todomvc-app-css/package.json b/rss/gopherjs/node_modules/todomvc-app-css/package.json
new file mode 100644
index 0000000..0aa3fe6
--- /dev/null
+++ b/rss/gopherjs/node_modules/todomvc-app-css/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "todomvc-app-css",
+ "version": "2.0.1",
+ "description": "CSS for TodoMVC apps",
+ "license": "CC-BY-4.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tastejs/todomvc-app-css"
+ },
+ "author": {
+ "name": "Sindre Sorhus",
+ "email": "sindresorhus@gmail.com",
+ "url": "sindresorhus.com"
+ },
+ "files": [
+ "index.css"
+ ],
+ "keywords": [
+ "todomvc",
+ "tastejs",
+ "app",
+ "todo",
+ "template",
+ "css",
+ "style",
+ "stylesheet"
+ ],
+ "gitHead": "f1bb1aa9b19888f339055418374a9b3a2d4c6fc5",
+ "bugs": {
+ "url": "https://github.com/tastejs/todomvc-app-css/issues"
+ },
+ "homepage": "https://github.com/tastejs/todomvc-app-css",
+ "_id": "todomvc-app-css@2.0.1",
+ "scripts": {},
+ "_shasum": "f64d50b744a8a83c1151a08055b88f3aa5ccb052",
+ "_from": "todomvc-app-css@*",
+ "_npmVersion": "2.5.1",
+ "_nodeVersion": "0.12.0",
+ "_npmUser": {
+ "name": "sindresorhus",
+ "email": "sindresorhus@gmail.com"
+ },
+ "maintainers": [
+ {
+ "name": "sindresorhus",
+ "email": "sindresorhus@gmail.com"
+ },
+ {
+ "name": "addyosmani",
+ "email": "addyosmani@gmail.com"
+ },
+ {
+ "name": "passy",
+ "email": "phartig@rdrei.net"
+ },
+ {
+ "name": "stephenplusplus",
+ "email": "sawchuk@gmail.com"
+ }
+ ],
+ "dist": {
+ "shasum": "f64d50b744a8a83c1151a08055b88f3aa5ccb052",
+ "tarball": "http://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.1.tgz"
+ },
+ "directories": {},
+ "_resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.1.tgz"
+}
diff --git a/rss/gopherjs/node_modules/todomvc-app-css/readme.md b/rss/gopherjs/node_modules/todomvc-app-css/readme.md
new file mode 100644
index 0000000..6ddbebf
--- /dev/null
+++ b/rss/gopherjs/node_modules/todomvc-app-css/readme.md
@@ -0,0 +1,28 @@
+# todomvc-app-css
+
+> CSS for TodoMVC apps
+
+
+
+
+## Install
+
+
+```
+$ npm install --save todomvc-app-css
+```
+
+
+## Getting started
+
+```html
+
+```
+
+See the [TodoMVC app template](https://github.com/tastejs/todomvc-app-template).
+
+
+
+## License
+
+
This work by Sindre Sorhus is licensed under a Creative Commons Attribution 4.0 International License.
diff --git a/rss/gopherjs/node_modules/todomvc-common/base.css b/rss/gopherjs/node_modules/todomvc-common/base.css
new file mode 100644
index 0000000..da65968
--- /dev/null
+++ b/rss/gopherjs/node_modules/todomvc-common/base.css
@@ -0,0 +1,141 @@
+hr {
+ margin: 20px 0;
+ border: 0;
+ border-top: 1px dashed #c5c5c5;
+ border-bottom: 1px dashed #f7f7f7;
+}
+
+.learn a {
+ font-weight: normal;
+ text-decoration: none;
+ color: #b83f45;
+}
+
+.learn a:hover {
+ text-decoration: underline;
+ color: #787e7e;
+}
+
+.learn h3,
+.learn h4,
+.learn h5 {
+ margin: 10px 0;
+ font-weight: 500;
+ line-height: 1.2;
+ color: #000;
+}
+
+.learn h3 {
+ font-size: 24px;
+}
+
+.learn h4 {
+ font-size: 18px;
+}
+
+.learn h5 {
+ margin-bottom: 0;
+ font-size: 14px;
+}
+
+.learn ul {
+ padding: 0;
+ margin: 0 0 30px 25px;
+}
+
+.learn li {
+ line-height: 20px;
+}
+
+.learn p {
+ font-size: 15px;
+ font-weight: 300;
+ line-height: 1.3;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+#issue-count {
+ display: none;
+}
+
+.quote {
+ border: none;
+ margin: 20px 0 60px 0;
+}
+
+.quote p {
+ font-style: italic;
+}
+
+.quote p:before {
+ content: '“';
+ font-size: 50px;
+ opacity: .15;
+ position: absolute;
+ top: -20px;
+ left: 3px;
+}
+
+.quote p:after {
+ content: '”';
+ font-size: 50px;
+ opacity: .15;
+ position: absolute;
+ bottom: -42px;
+ right: 3px;
+}
+
+.quote footer {
+ position: absolute;
+ bottom: -40px;
+ right: 0;
+}
+
+.quote footer img {
+ border-radius: 3px;
+}
+
+.quote footer a {
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+.speech-bubble {
+ position: relative;
+ padding: 10px;
+ background: rgba(0, 0, 0, .04);
+ border-radius: 5px;
+}
+
+.speech-bubble:after {
+ content: '';
+ position: absolute;
+ top: 100%;
+ right: 30px;
+ border: 13px solid transparent;
+ border-top-color: rgba(0, 0, 0, .04);
+}
+
+.learn-bar > .learn {
+ position: absolute;
+ width: 272px;
+ top: 8px;
+ left: -300px;
+ padding: 10px;
+ border-radius: 5px;
+ background-color: rgba(255, 255, 255, .6);
+ transition-property: left;
+ transition-duration: 500ms;
+}
+
+@media (min-width: 899px) {
+ .learn-bar {
+ width: auto;
+ padding-left: 300px;
+ }
+
+ .learn-bar > .learn {
+ left: 8px;
+ }
+}
diff --git a/rss/gopherjs/node_modules/todomvc-common/base.js b/rss/gopherjs/node_modules/todomvc-common/base.js
new file mode 100644
index 0000000..3c6723f
--- /dev/null
+++ b/rss/gopherjs/node_modules/todomvc-common/base.js
@@ -0,0 +1,249 @@
+/* global _ */
+(function () {
+ 'use strict';
+
+ /* jshint ignore:start */
+ // Underscore's Template Module
+ // Courtesy of underscorejs.org
+ var _ = (function (_) {
+ _.defaults = function (object) {
+ if (!object) {
+ return object;
+ }
+ for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
+ var iterable = arguments[argsIndex];
+ if (iterable) {
+ for (var key in iterable) {
+ if (object[key] == null) {
+ object[key] = iterable[key];
+ }
+ }
+ }
+ }
+ return object;
+ }
+
+ // By default, Underscore uses ERB-style template delimiters, change the
+ // following template settings to use alternative delimiters.
+ _.templateSettings = {
+ evaluate : /<%([\s\S]+?)%>/g,
+ interpolate : /<%=([\s\S]+?)%>/g,
+ escape : /<%-([\s\S]+?)%>/g
+ };
+
+ // When customizing `templateSettings`, if you don't want to define an
+ // interpolation, evaluation or escaping regex, we need one that is
+ // guaranteed not to match.
+ var noMatch = /(.)^/;
+
+ // Certain characters need to be escaped so that they can be put into a
+ // string literal.
+ var escapes = {
+ "'": "'",
+ '\\': '\\',
+ '\r': 'r',
+ '\n': 'n',
+ '\t': 't',
+ '\u2028': 'u2028',
+ '\u2029': 'u2029'
+ };
+
+ var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
+
+ // JavaScript micro-templating, similar to John Resig's implementation.
+ // Underscore templating handles arbitrary delimiters, preserves whitespace,
+ // and correctly escapes quotes within interpolated code.
+ _.template = function(text, data, settings) {
+ var render;
+ settings = _.defaults({}, settings, _.templateSettings);
+
+ // Combine delimiters into one regular expression via alternation.
+ var matcher = new RegExp([
+ (settings.escape || noMatch).source,
+ (settings.interpolate || noMatch).source,
+ (settings.evaluate || noMatch).source
+ ].join('|') + '|$', 'g');
+
+ // Compile the template source, escaping string literals appropriately.
+ var index = 0;
+ var source = "__p+='";
+ text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+ source += text.slice(index, offset)
+ .replace(escaper, function(match) { return '\\' + escapes[match]; });
+
+ if (escape) {
+ source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+ }
+ if (interpolate) {
+ source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+ }
+ if (evaluate) {
+ source += "';\n" + evaluate + "\n__p+='";
+ }
+ index = offset + match.length;
+ return match;
+ });
+ source += "';\n";
+
+ // If a variable is not specified, place data values in local scope.
+ if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+ source = "var __t,__p='',__j=Array.prototype.join," +
+ "print=function(){__p+=__j.call(arguments,'');};\n" +
+ source + "return __p;\n";
+
+ try {
+ render = new Function(settings.variable || 'obj', '_', source);
+ } catch (e) {
+ e.source = source;
+ throw e;
+ }
+
+ if (data) return render(data, _);
+ var template = function(data) {
+ return render.call(this, data, _);
+ };
+
+ // Provide the compiled function source as a convenience for precompilation.
+ template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
+
+ return template;
+ };
+
+ return _;
+ })({});
+
+ if (location.hostname === 'todomvc.com') {
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+ ga('create', 'UA-31081062-1', 'auto');
+ ga('send', 'pageview');
+ }
+ /* jshint ignore:end */
+
+ function redirect() {
+ if (location.hostname === 'tastejs.github.io') {
+ location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
+ }
+ }
+
+ function findRoot() {
+ var base = location.href.indexOf('examples/');
+ return location.href.substr(0, base);
+ }
+
+ function getFile(file, callback) {
+ if (!location.host) {
+ return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
+ }
+
+ var xhr = new XMLHttpRequest();
+
+ xhr.open('GET', findRoot() + file, true);
+ xhr.send();
+
+ xhr.onload = function () {
+ if (xhr.status === 200 && callback) {
+ callback(xhr.responseText);
+ }
+ };
+ }
+
+ function Learn(learnJSON, config) {
+ if (!(this instanceof Learn)) {
+ return new Learn(learnJSON, config);
+ }
+
+ var template, framework;
+
+ if (typeof learnJSON !== 'object') {
+ try {
+ learnJSON = JSON.parse(learnJSON);
+ } catch (e) {
+ return;
+ }
+ }
+
+ if (config) {
+ template = config.template;
+ framework = config.framework;
+ }
+
+ if (!template && learnJSON.templates) {
+ template = learnJSON.templates.todomvc;
+ }
+
+ if (!framework && document.querySelector('[data-framework]')) {
+ framework = document.querySelector('[data-framework]').dataset.framework;
+ }
+
+ this.template = template;
+
+ if (learnJSON.backend) {
+ this.frameworkJSON = learnJSON.backend;
+ this.frameworkJSON.issueLabel = framework;
+ this.append({
+ backend: true
+ });
+ } else if (learnJSON[framework]) {
+ this.frameworkJSON = learnJSON[framework];
+ this.frameworkJSON.issueLabel = framework;
+ this.append();
+ }
+
+ this.fetchIssueCount();
+ }
+
+ Learn.prototype.append = function (opts) {
+ var aside = document.createElement('aside');
+ aside.innerHTML = _.template(this.template, this.frameworkJSON);
+ aside.className = 'learn';
+
+ if (opts && opts.backend) {
+ // Remove demo link
+ var sourceLinks = aside.querySelector('.source-links');
+ var heading = sourceLinks.firstElementChild;
+ var sourceLink = sourceLinks.lastElementChild;
+ // Correct link path
+ var href = sourceLink.getAttribute('href');
+ sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
+ sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
+ } else {
+ // Localize demo links
+ var demoLinks = aside.querySelectorAll('.demo-link');
+ Array.prototype.forEach.call(demoLinks, function (demoLink) {
+ if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
+ demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
+ }
+ });
+ }
+
+ document.body.className = (document.body.className + ' learn-bar').trim();
+ document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
+ };
+
+ Learn.prototype.fetchIssueCount = function () {
+ var issueLink = document.getElementById('issue-count-link');
+ if (issueLink) {
+ var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.onload = function (e) {
+ var parsedResponse = JSON.parse(e.target.responseText);
+ if (parsedResponse instanceof Array) {
+ var count = parsedResponse.length;
+ if (count !== 0) {
+ issueLink.innerHTML = 'This app has ' + count + ' open issues';
+ document.getElementById('issue-count').style.display = 'inline';
+ }
+ }
+ };
+ xhr.send();
+ }
+ };
+
+ redirect();
+ getFile('learn.json', Learn);
+})();
diff --git a/rss/gopherjs/node_modules/todomvc-common/package.json b/rss/gopherjs/node_modules/todomvc-common/package.json
new file mode 100644
index 0000000..2ef2ff9
--- /dev/null
+++ b/rss/gopherjs/node_modules/todomvc-common/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "todomvc-common",
+ "version": "1.0.2",
+ "description": "Common TodoMVC utilities used by our apps",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tastejs/todomvc-common"
+ },
+ "author": {
+ "name": "TasteJS"
+ },
+ "main": "base.js",
+ "files": [
+ "base.js",
+ "base.css"
+ ],
+ "keywords": [
+ "todomvc",
+ "tastejs",
+ "util",
+ "utilities"
+ ],
+ "gitHead": "e82d0c79e01687ce7407df786cc784ad82166cb3",
+ "bugs": {
+ "url": "https://github.com/tastejs/todomvc-common/issues"
+ },
+ "homepage": "https://github.com/tastejs/todomvc-common",
+ "_id": "todomvc-common@1.0.2",
+ "scripts": {},
+ "_shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
+ "_from": "todomvc-common@*",
+ "_npmVersion": "2.7.4",
+ "_nodeVersion": "0.12.2",
+ "_npmUser": {
+ "name": "sindresorhus",
+ "email": "sindresorhus@gmail.com"
+ },
+ "dist": {
+ "shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
+ "tarball": "http://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz"
+ },
+ "maintainers": [
+ {
+ "name": "sindresorhus",
+ "email": "sindresorhus@gmail.com"
+ },
+ {
+ "name": "addyosmani",
+ "email": "addyosmani@gmail.com"
+ },
+ {
+ "name": "passy",
+ "email": "phartig@rdrei.net"
+ },
+ {
+ "name": "stephenplusplus",
+ "email": "sawchuk@gmail.com"
+ }
+ ],
+ "directories": {},
+ "_resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz"
+}
diff --git a/rss/gopherjs/node_modules/todomvc-common/readme.md b/rss/gopherjs/node_modules/todomvc-common/readme.md
new file mode 100644
index 0000000..7a5de51
--- /dev/null
+++ b/rss/gopherjs/node_modules/todomvc-common/readme.md
@@ -0,0 +1,15 @@
+# todomvc-common
+
+> Common TodoMVC utilities used by our apps
+
+
+## Install
+
+```
+$ npm install --save todomvc-common
+```
+
+
+## License
+
+MIT © [TasteJS](http://tastejs.com)
diff --git a/rss/gopherjs/store/model/model.go b/rss/gopherjs/store/model/model.go
new file mode 100644
index 0000000..62f93b9
--- /dev/null
+++ b/rss/gopherjs/store/model/model.go
@@ -0,0 +1,21 @@
+package model
+
+// Item represents a single TODO item in the store.
+type Item struct {
+ Title string
+ Completed bool
+}
+
+// FilterState represents a viewing filter for TODO items in the store.
+type FilterState int
+
+const (
+ // All is a FilterState which shows all items.
+ All FilterState = iota
+
+ // Active is a FilterState which shows only non-completed items.
+ Active
+
+ // Completed is a FilterState which shows only completed items.
+ Completed
+)
diff --git a/rss/gopherjs/store/store.go b/rss/gopherjs/store/store.go
new file mode 100644
index 0000000..5f3290e
--- /dev/null
+++ b/rss/gopherjs/store/store.go
@@ -0,0 +1,85 @@
+package store
+
+import (
+ "github.com/drekle/go/rss/gopherjs/actions"
+ "github.com/drekle/go/rss/gopherjs/dispatcher"
+ "github.com/drekle/go/rss/gopherjs/store/model"
+ "github.com/drekle/go/rss/gopherjs/store/storeutil"
+)
+
+var (
+ // Items represents all of the TODO items in the store.
+ Items []*model.Item
+
+ // Filter represents the active viewing filter for items.
+ Filter = model.All
+
+ // Listeners is the listeners that will be invoked when the store changes.
+ Listeners = storeutil.NewListenerRegistry()
+)
+
+func init() {
+ dispatcher.Register(onAction)
+}
+
+// ActiveItemCount returns the current number of items that are not completed.
+func ActiveItemCount() int {
+ return count(false)
+}
+
+// CompletedItemCount returns the current number of items that are completed.
+func CompletedItemCount() int {
+ return count(true)
+}
+
+func count(completed bool) int {
+ count := 0
+ for _, item := range Items {
+ if item.Completed == completed {
+ count++
+ }
+ }
+ return count
+}
+
+func onAction(action interface{}) {
+ switch a := action.(type) {
+ case *actions.ReplaceItems:
+ Items = a.Items
+
+ case *actions.AddItem:
+ Items = append(Items, &model.Item{Title: a.Title, Completed: false})
+
+ case *actions.DestroyItem:
+ copy(Items[a.Index:], Items[a.Index+1:])
+ Items = Items[:len(Items)-1]
+
+ case *actions.SetTitle:
+ Items[a.Index].Title = a.Title
+
+ case *actions.SetCompleted:
+ Items[a.Index].Completed = a.Completed
+
+ case *actions.SetAllCompleted:
+ for _, item := range Items {
+ item.Completed = a.Completed
+ }
+
+ case *actions.ClearCompleted:
+ var activeItems []*model.Item
+ for _, item := range Items {
+ if !item.Completed {
+ activeItems = append(activeItems, item)
+ }
+ }
+ Items = activeItems
+
+ case *actions.SetFilter:
+ Filter = a.Filter
+
+ default:
+ return // don't fire listeners
+ }
+
+ Listeners.Fire()
+}
diff --git a/rss/gopherjs/store/storeutil/storeutil.go b/rss/gopherjs/store/storeutil/storeutil.go
new file mode 100644
index 0000000..3419eb3
--- /dev/null
+++ b/rss/gopherjs/store/storeutil/storeutil.go
@@ -0,0 +1,40 @@
+// Package storeutil contains a ListenerRegistry type.
+package storeutil
+
+// ListenerRegistry is a listener registry.
+// The zero value is unfit for use; use NewListenerRegistry to create an instance.
+type ListenerRegistry struct {
+ listeners map[interface{}]func()
+}
+
+// NewListenerRegistry creates a listener registry.
+func NewListenerRegistry() *ListenerRegistry {
+ return &ListenerRegistry{
+ listeners: make(map[interface{}]func()),
+ }
+}
+
+// Add adds listener with key to the registry.
+// key may be nil, then an arbitrary unused key is assigned.
+// It panics if a listener with same key is already present.
+func (r *ListenerRegistry) Add(key interface{}, listener func()) {
+ if key == nil {
+ key = new(int)
+ }
+ if _, ok := r.listeners[key]; ok {
+ panic("duplicate listener key")
+ }
+ r.listeners[key] = listener
+}
+
+// Remove removes a listener with key from the registry.
+func (r *ListenerRegistry) Remove(key interface{}) {
+ delete(r.listeners, key)
+}
+
+// Fire invokes all listeners in the registry.
+func (r *ListenerRegistry) Fire() {
+ for _, l := range r.listeners {
+ l()
+ }
+}