git ssb

2+

mixmix / ticktack



Tree: cc9b1a7a5f4f79d8e0f7c4a2491a50f440ac9153

Files: cc9b1a7a5f4f79d8e0f7c4a2491a50f440ac9153 / app / page / blogNew.js

8008 bytesRaw
1const nest = require('depnest')
2const { h, Struct, Value, when } = require('mutant')
3const addSuggest = require('suggest-box')
4const pull = require('pull-stream')
5const marksum = require('markdown-summary')
6const MediumEditor = require('medium-editor').MediumEditor
7const MediumToMD = require('medium-editor-markdown')
8// const CustomHtml = require('medium-editor-custom-async')
9
10exports.gives = nest('app.page.blogNew')
11
12exports.needs = nest({
13 'app.html.sideNav': 'first',
14 'blob.html.input': 'first',
15 'channel.async.suggest': 'first',
16 'history.sync.push': 'first',
17 'message.html.compose': 'first',
18 'translations.sync.strings': 'first',
19 'sbot.async.addBlob': 'first'
20})
21
22exports.create = (api) => {
23 return nest('app.page.blogNew', blogNew)
24
25 function blogNew (location) {
26 const strings = api.translations.sync.strings()
27 const getChannelSuggestions = api.channel.async.suggest()
28
29 const meta = Struct({
30 type: 'blog',
31 channel: Value(),
32 title: Value(),
33 summary: Value(),
34 text: Value('')
35 })
36
37 const mediumComposer = h('Markdown.editor')
38 var filesById = {}
39 const composer = initialiseDummyComposer({ filesById, meta, api })
40 // NOTE we are bootstrapping off the message.html.compose logic
41 // - the mediumComposer feeds content into the composer, which the provides publishing
42 // - this works, but should be refactorer after more thought about generalised composer
43 // componentes have been done
44
45 const channelInput = h('input', {
46 'ev-input': e => meta.channel.set(e.target.value),
47 value: meta.channel,
48 placeholder: strings.channel
49 })
50 const updateTitle = e => {
51 if (e.target.childElementCount) {
52 // the title h1 is contenteditable, meaning people can paste html elements in here!
53 // this is designed to strip down to heading content text
54 // - I went with contenteditable because it handles wrapping of long titles,
55 // whereas a styled input field just pushes content off the page!
56 e.target.innerHTML = e.target.innerText
57 }
58 meta.title.set(e.target.innerText)
59 }
60
61 var page = h('Page -blogNew', [
62 api.app.html.sideNav(location),
63 h('div.content', [
64 h('div.container', [
65 h('div.field -title', [
66 h('h1.input', {
67 attributes: {
68 contenteditable: true,
69 'data-placeholder': strings.blogNew.field.title
70 },
71 'ev-input': updateTitle,
72 className: when(meta.title, '', '-empty')
73 })
74 ]),
75 mediumComposer,
76 h('div.field -attachment',
77 AddFileButton({ api, filesById, meta, textArea: mediumComposer })
78 ),
79 h('div.field -channel', [
80 h('div.label', strings.channel),
81 channelInput
82 ]),
83 h('div.field -summary', [
84 h('div.label', strings.blogNew.field.summary),
85 h('input', {
86 'ev-input': e => meta.summary.set(e.target.value),
87 placeholder: strings.blogNew.field.summary
88 })
89 ]),
90 composer
91 ])
92 ])
93 ])
94
95 initialiseMedium({ page, el: mediumComposer, meta })
96 initialiseChannelSuggests({ input: channelInput, suggester: getChannelSuggestions, meta })
97
98 return page
99 }
100}
101
102function AddFileButton ({ api, filesById, meta, textArea }) {
103 // var textRaw = meta.text
104 //
105
106 const fileInput = api.blob.html.input(file => {
107 filesById[file.link] = file
108
109 const isImage = file.type.match(/^image/)
110
111 var content
112
113 if (isImage) {
114 content = h('img', {
115 src: `http://localhost:8989/blobs/get/${encodeURIComponent(file.link)}`,
116 alt: file.name
117 })
118 } else {
119 content = h('a', { href: file.link }, file.name)
120 }
121 // TODO - insert where the mouse is yo
122 var editor = MediumEditor.getEditorFromElement(textArea)
123 textArea.insertBefore(
124 h('p', content),
125 editor.currentEl || null
126 )
127
128 console.log('added:', file)
129 })
130
131 return fileInput
132}
133
134function initialiseDummyComposer ({ meta, api, filesById }) {
135 return api.message.html.compose(
136 {
137 meta,
138 // placeholder: strings.blogNew.actions.writeBlog,
139 shrink: false,
140 canAttach: false,
141 canPreview: false,
142 publishString: api.translations.sync.strings().publishBlog,
143 filesById,
144 prepublish: function (content, cb) {
145 var m = /\!\[[^]+\]\(([^\)]+)\)/.exec(marksum.image(content.text))
146 content.thumbnail = m && m[1]
147 // content.summary = marksum.summary(content.text) // TODO Need a summary which doesn't trim the start
148
149 var stream = pull.values([content.text])
150 api.sbot.async.addBlob(stream, function (err, hash) {
151 if (err) return cb(err)
152 if (!hash) throw new Error('missing hash')
153 content.blog = hash
154 delete content.text
155 cb(null, content)
156 })
157 }
158 },
159 (err, msg) => api.history.sync.push(err || { page: 'blogIndex' })
160 )
161}
162
163function initialiseChannelSuggests ({ input, suggester, meta }) {
164 addSuggest(input, (inputText, cb) => {
165 inputText = inputText.replace(/^#/, '')
166 var suggestions = suggester(inputText)
167 .map(s => {
168 s.value = s.value.replace(/^#/, '') // strip the defualt # prefix here
169 return s
170 })
171 .map(s => {
172 if (s.subtitle === 'subscribed') { s.subtitle = h('i.fa.fa-heart') } // TODO - translation-friendly subscribed
173 return s
174 })
175
176 // HACK add the input text if it's not an option already
177 if (!suggestions.some(s => s.title === inputText)) {
178 suggestions.push({
179 title: inputText,
180 subtitle: h('i.fa.fa-plus-circle'),
181 value: inputText
182 })
183 }
184
185 cb(null, suggestions)
186 }, {cls: 'PatchSuggest.-channelHorizontal'}) // WARNING hacking suggest-box cls
187
188 input.addEventListener('suggestselect', (e) => {
189 meta.channel.set(e.detail.value)
190 })
191}
192
193function initialiseMedium ({ page, el, meta }) {
194 var editor = new MediumEditor(el, {
195 elementsContainer: page,
196 // autoLink: true,
197 buttonLabels: 'fontawesome',
198 imageDragging: true,
199 toolbar: {
200 allowMultiParagraphSelection: true,
201 buttons: [
202 'bold',
203 'italic',
204 'anchor',
205 {
206 name: 'h2',
207 contentFA: '<i class="fa fa-header" />',
208 classList: ['custom-button-h2']
209 },
210 {
211 name: 'h3',
212 contentFA: '<i class="fa fa-header" />',
213 classList: ['custom-button-h3']
214 },
215 'quote'
216 ],
217 diffLeft: 0,
218 diffTop: 10,
219 firstButtonClass: 'medium-editor-button-first',
220 lastButtonClass: 'medium-editor-button-last',
221 relativeContainer: null,
222 standardizeSelectionStart: false,
223 static: false,
224 /* options which only apply when static is true */
225 align: 'center',
226 sticky: false,
227 updateOnEmptySelection: false
228 },
229 extensions: {
230 markdown: new MediumToMD(
231 {
232 toMarkdownOptions: {
233 converters: [{
234 filter: 'img',
235 replacement: (content, node) => {
236 var blob = decodeURIComponent(node.src.replace('http://localhost:8989/blobs/get/', ''))
237 return `![${node.alt}](${blob})`
238 }
239 }, {
240 filter: 'span',
241 replacement: (content, node) => content
242 }]
243 },
244 events: ['input', 'change', 'DOMNodeInserted']
245 },
246 md => meta.text.set(md)
247 )
248 }
249 })
250
251 editor.on(el, 'keyup', setCurrentEl)
252 editor.on(el, 'click', setCurrentEl)
253
254 function setCurrentEl (ev) {
255 var sel = window.getSelection()
256 var container = sel.getRangeAt(0).commonAncestorContainer
257 editor.currentEl = container.textContent === '' // NOTE this could be a brittle check
258 ? container
259 : container.parentElement
260 }
261
262 return editor
263}
264

Built with git-ssb-web