Files: 40aa5761d29866aa4ab08ff139a33262cbc14177 / book / html / layout / detail.js
6898 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, when, computed, Value } = require('mutant') |
3 | var htmlEscape = require('html-escape') |
4 | const addSuggest = require('suggest-box') |
5 | |
6 | exports.needs = nest({ |
7 | 'book.obs.book': 'first', |
8 | 'about.html.image': 'first', |
9 | 'about.obs.name': 'first', |
10 | 'keys.sync.id': 'first', |
11 | 'emoji.async.suggest': 'first', |
12 | 'emoji.sync.url': 'first', |
13 | 'message.html': { |
14 | 'markdown': 'first' |
15 | }, |
16 | 'book.html': { |
17 | 'title': 'first', |
18 | 'authors': 'first', |
19 | 'description': 'first', |
20 | 'images': 'first' |
21 | } |
22 | }) |
23 | |
24 | exports.gives = nest('book.html.layout') |
25 | |
26 | exports.create = (api) => { |
27 | return nest('book.html.layout', bookLayout) |
28 | |
29 | function renderEmoji (emoji, url) { |
30 | if (!url) return ':' + emoji + ':' |
31 | return ` |
32 | <img |
33 | src="${htmlEscape(url)}" |
34 | alt=":${htmlEscape(emoji)}:" |
35 | title=":${htmlEscape(emoji)}:" |
36 | class="emoji" |
37 | > |
38 | ` |
39 | } |
40 | |
41 | function simpleMarkdown(text) { |
42 | if (text.startsWith(':')) |
43 | return renderEmoji(text, api.emoji.sync.url(text.match(/:([^:]*)/)[1])) |
44 | else |
45 | return text |
46 | } |
47 | |
48 | function ratingEdit(isEditing, value) { |
49 | return when(isEditing, |
50 | h('input', { |
51 | 'ev-input': e => value.set(e.target.value), |
52 | value, |
53 | placeholder: 'your rating' // REVIEW - I spread the keys over several lines to make this easier to read |
54 | }), |
55 | h('span.text', { innerHTML: computed(value, simpleMarkdown) }) |
56 | ) |
57 | } |
58 | |
59 | function ratingTypeEdit(isEditing, value) { |
60 | let getEmojiSuggestions = api.emoji.async.suggest() |
61 | |
62 | let ratingTypeInput = h('input', {'ev-input': e => value.set(e.target.value), |
63 | value: value, placeholder: 'rating type' }) |
64 | |
65 | let suggestWrapper = h('span.ratingType', ratingTypeInput) |
66 | |
67 | addSuggest(ratingTypeInput, (inputText, cb) => { |
68 | if (inputText[0] === ':') { |
69 | cb(null, getEmojiSuggestions(inputText.slice(1))) |
70 | } |
71 | }, {cls: 'PatchSuggest'}) |
72 | |
73 | ratingTypeInput.addEventListener('suggestselect', ev => { |
74 | value.set(ev.detail.value) |
75 | }) |
76 | |
77 | return when(isEditing, suggestWrapper, |
78 | h('span.text', { innerHTML: computed(value, simpleMarkdown) })) |
79 | } |
80 | |
81 | function simpleEdit(isEditing, name, value) { |
82 | // REVIEW - I split out logic into new lines when it's muddying the html areas |
83 | // REVIEW - don't use when + computed (when is just like a special sort of computed... jus use computed and role your own |
84 | const classList = computed([value, isEditing], (v, e) => { |
85 | return v || e |
86 | ? '-expanded' |
87 | : '-contracted' |
88 | }) |
89 | |
90 | return h('div', { classList }, [ |
91 | h('span', name + ':'), |
92 | when(isEditing, |
93 | h('input', {'ev-input': e => value.set(e.target.value), value }), |
94 | h('span', value) |
95 | ) |
96 | ]) |
97 | } |
98 | |
99 | function textEdit(isEditing, name, value) { |
100 | const classList = computed([value, isEditing], (v, e) => { |
101 | return v || e |
102 | ? '-expanded' |
103 | : '-contracted' |
104 | }) |
105 | |
106 | return h('div', { classList }, [ |
107 | h('div', name + ':'), |
108 | when(isEditing, |
109 | h('textarea', {'ev-input': e => value.set(e.target.value), value }), |
110 | computed(value, api.message.html.markdown) |
111 | ) |
112 | ]) |
113 | // REVIEW - refactored this to follow same pattern as function above |
114 | } |
115 | |
116 | function bookLayout (msg, opts) { |
117 | if (!(opts.layout === undefined || opts.layout === 'detail')) return |
118 | |
119 | const { obs, isEditing, isCard } = opts |
120 | |
121 | const { title, authors, description, images } = api.book.html |
122 | |
123 | let isEditingSubjective = Value(false) |
124 | let originalSubjective = {} |
125 | let originalBook = {} |
126 | let reviews = [] |
127 | |
128 | return [h('Message -book-detail', [ |
129 | title({ title: obs.title, msg, isEditing, onUpdate: obs.title.set }), |
130 | authors({authors: obs.authors, isEditing, onUpdate: obs.authors.set}), |
131 | h('section.content', [ |
132 | images({images: obs.images, isEditing, onUpdate: obs.images.add }), |
133 | h('section.description', |
134 | description({description: obs.description, isEditing, onUpdate: obs.description.set})), |
135 | ]), |
136 | h('section.actions', [ |
137 | h('button.edit', { 'ev-click': () => { |
138 | if (isEditing()) { // cancel |
139 | Object.keys(originalBook).forEach((v) => { |
140 | obs[v].set(originalBook[v]) |
141 | }) |
142 | } else |
143 | originalBook = JSON.parse(JSON.stringify(obs())) |
144 | |
145 | isEditing.set(!isEditing()) |
146 | } }, |
147 | when(isEditing, 'Cancel', 'Edit book')), |
148 | when(isEditing, h('button', {'ev-click': () => saveBook(obs)}, 'Update book')) |
149 | ]), |
150 | h('section.subjective', [ |
151 | computed(obs.subjective, subjectives => { |
152 | let i = 0; |
153 | Object.keys(subjectives).forEach(user => { |
154 | if (i++ < reviews.length) return |
155 | let subjective = obs.subjective.get(user) |
156 | let isMe = Value(api.keys.sync.id() == user) |
157 | let isOwnEditingSubj = computed([isEditingSubjective, isMe], |
158 | (e, me) => { return e && me }) |
159 | reviews.push([ |
160 | // REVIEW - this section is hard to read, I'd get rid of the when computed and change in indenting (but that could be my personal taste) |
161 | h('section', [api.about.html.image(user), |
162 | when(computed([subjective.rating, isEditingSubjective], |
163 | (v, e) => { return v || e }), |
164 | h('span.text', [api.about.obs.name(user), ' rated '])), |
165 | ratingEdit(isOwnEditingSubj, subjective.rating), |
166 | ratingTypeEdit(isOwnEditingSubj, subjective.ratingType)]), |
167 | simpleEdit(isOwnEditingSubj, 'Shelve', subjective.shelve), |
168 | simpleEdit(isOwnEditingSubj, 'Genre', subjective.genre), |
169 | textEdit(isOwnEditingSubj, 'Review', subjective.review) |
170 | ]) |
171 | }) |
172 | |
173 | return reviews |
174 | }) |
175 | ]), |
176 | h('section.actions', [ |
177 | h('button.subjective', { |
178 | 'ev-click': () => { |
179 | if (isEditingSubjective()) { // cancel |
180 | let subj = obs.subjective.get(api.keys.sync.id()) |
181 | Object.keys(originalSubjective).forEach((v) => { |
182 | subj[v].set(originalSubjective[v]) |
183 | }) |
184 | } else |
185 | originalSubjective = JSON.parse(JSON.stringify(obs.subjective.get(api.keys.sync.id())())) |
186 | |
187 | isEditingSubjective.set(!isEditingSubjective()) |
188 | } |
189 | }, |
190 | when(isEditingSubjective, 'Cancel', 'Edit rating')), |
191 | when(isEditingSubjective, h('button', { 'ev-click': () => saveSubjective(obs) }, 'Update rating')) |
192 | ]), |
193 | ])] |
194 | |
195 | function saveBook(obs) { |
196 | obs.amend() |
197 | |
198 | isEditing.set(false) |
199 | } |
200 | |
201 | function saveSubjective(obs) { |
202 | obs.updateSubjective() |
203 | |
204 | isEditingSubjective.set(false) |
205 | } |
206 | } |
207 | } |
208 |
Built with git-ssb-web