const nest = require('depnest') const { h, when, computed, Value } = require('mutant') var htmlEscape = require('html-escape') const addSuggest = require('suggest-box') exports.needs = nest({ 'book.obs.book': 'first', 'about.html.image': 'first', 'about.obs.name': 'first', 'keys.sync.id': 'first', 'emoji.async.suggest': 'first', 'emoji.sync.url': 'first', 'message.html': { 'markdown': 'first' }, 'book.html': { 'title': 'first', 'authors': 'first', 'description': 'first', 'images': 'first' } }) exports.gives = nest('book.html.layout') exports.create = (api) => { return nest('book.html.layout', bookLayout) function renderEmoji (emoji, url) { if (!url) return ':' + emoji + ':' return ` :${htmlEscape(emoji)}: ` } function simpleMarkdown(text) { if (text.startsWith(':')) return renderEmoji(text, api.emoji.sync.url(text.match(/:([^:]*)/)[1])) else return text } function ratingEdit(isEditing, value) { return when(isEditing, h('input', { 'ev-input': e => value.set(e.target.value), value, placeholder: 'your rating' // REVIEW - I spread the keys over several lines to make this easier to read }), h('span.text', { innerHTML: computed(value, simpleMarkdown) }) ) } function ratingTypeEdit(isEditing, value) { let getEmojiSuggestions = api.emoji.async.suggest() let ratingTypeInput = h('input', {'ev-input': e => value.set(e.target.value), value: value, placeholder: 'rating type' }) let suggestWrapper = h('span.ratingType', ratingTypeInput) addSuggest(ratingTypeInput, (inputText, cb) => { if (inputText[0] === ':') { cb(null, getEmojiSuggestions(inputText.slice(1))) } }, {cls: 'PatchSuggest'}) ratingTypeInput.addEventListener('suggestselect', ev => { value.set(ev.detail.value) }) return when(isEditing, suggestWrapper, h('span.text', { innerHTML: computed(value, simpleMarkdown) })) } function simpleEdit(isEditing, name, value) { // REVIEW - I split out logic into new lines when it's muddying the html areas // REVIEW - don't use when + computed (when is just like a special sort of computed... jus use computed and role your own const classList = computed([value, isEditing], (v, e) => { return v || e ? '-expanded' : '-contracted' }) return h('div', { classList }, [ h('span', name + ':'), when(isEditing, h('input', {'ev-input': e => value.set(e.target.value), value }), h('span', value) ) ]) } function textEdit(isEditing, name, value) { const classList = computed([value, isEditing], (v, e) => { return v || e ? '-expanded' : '-contracted' }) return h('div', { classList }, [ h('div', name + ':'), when(isEditing, h('textarea', {'ev-input': e => value.set(e.target.value), value }), computed(value, api.message.html.markdown) ) ]) // REVIEW - refactored this to follow same pattern as function above } function bookLayout (msg, opts) { if (!(opts.layout === undefined || opts.layout === 'detail')) return const { obs, isEditing, isCard } = opts const { title, authors, description, images } = api.book.html let isEditingSubjective = Value(false) let originalSubjective = {} let originalBook = {} let reviews = [] return [h('Message -book-detail', [ title({ title: obs.title, msg, isEditing, onUpdate: obs.title.set }), authors({authors: obs.authors, isEditing, onUpdate: obs.authors.set}), h('section.content', [ images({images: obs.images, isEditing, onUpdate: obs.images.add }), h('section.description', description({description: obs.description, isEditing, onUpdate: obs.description.set})), ]), h('section.actions', [ h('button.edit', { 'ev-click': () => { if (isEditing()) { // cancel Object.keys(originalBook).forEach((v) => { obs[v].set(originalBook[v]) }) } else originalBook = JSON.parse(JSON.stringify(obs())) isEditing.set(!isEditing()) } }, when(isEditing, 'Cancel', 'Edit book')), when(isEditing, h('button', {'ev-click': () => saveBook(obs)}, 'Update book')) ]), h('section.subjective', [ computed(obs.subjective, subjectives => { let i = 0; Object.keys(subjectives).forEach(user => { if (i++ < reviews.length) return let subjective = obs.subjective.get(user) let isMe = Value(api.keys.sync.id() == user) let isOwnEditingSubj = computed([isEditingSubjective, isMe], (e, me) => { return e && me }) reviews.push([ // 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) h('section', [api.about.html.image(user), when(computed([subjective.rating, isEditingSubjective], (v, e) => { return v || e }), h('span.text', [api.about.obs.name(user), ' rated '])), ratingEdit(isOwnEditingSubj, subjective.rating), ratingTypeEdit(isOwnEditingSubj, subjective.ratingType)]), simpleEdit(isOwnEditingSubj, 'Shelve', subjective.shelve), simpleEdit(isOwnEditingSubj, 'Genre', subjective.genre), textEdit(isOwnEditingSubj, 'Review', subjective.review) ]) }) return reviews }) ]), h('section.actions', [ h('button.subjective', { 'ev-click': () => { if (isEditingSubjective()) { // cancel let subj = obs.subjective.get(api.keys.sync.id()) Object.keys(originalSubjective).forEach((v) => { subj[v].set(originalSubjective[v]) }) } else originalSubjective = JSON.parse(JSON.stringify(obs.subjective.get(api.keys.sync.id())())) isEditingSubjective.set(!isEditingSubjective()) } }, when(isEditingSubjective, 'Cancel', 'Edit rating')), when(isEditingSubjective, h('button', { 'ev-click': () => saveSubjective(obs) }, 'Update rating')) ]), ])] function saveBook(obs) { obs.amend() isEditing.set(false) } function saveSubjective(obs) { obs.updateSubjective() isEditingSubjective.set(false) } } }