Visual editor - throwing away "[[no class]]" marker
[fnpeditor.git] / modules / visualEditor.js
1 rng.modules.visualEditor = function(sandbox) {\r
2     var transformations = rng.modules.visualEditor.transformations;\r
3 \r
4     var view = {\r
5         node: $(sandbox.getTemplate('main')()),\r
6         currentNode: null,\r
7         setup: function() {\r
8             var view = this;\r
9
10             this.node.find('#rng-visualEditor-content').on('keyup', function() {\r
11                 isDirty = true;\r
12             });\r
13             \r
14             this.node.find('#rng-visualEditor-meta').on('keyup', function() {\r
15                 isDirty = true;\r
16             });\r
17
18             this.node.on('mouseover', '[wlxml-tag]', function(e) { mediator.nodeHovered($(e.target));});\r
19             this.node.on('mouseout', '[wlxml-tag]', function(e) { mediator.nodeBlured($(e.target));});\r
20             this.node.on('click', '[wlxml-tag]', function(e) {\r
21                 console.log('clicked node type: '+e.target.nodeType);\r
22                 view._markSelected($(e.target));\r
23             });\r
24 \r
25             this.node.on('keyup', '#rng-visualEditor-contentWrapper', function(e) {\r
26                 var anchor = $(window.getSelection().anchorNode);\r
27                 if(anchor[0].nodeType === Node.TEXT_NODE)\r
28                     anchor = anchor.parent();\r
29                 if(!anchor.is('[wlxml-tag]'))\r
30                     return;\r
31                 view._markSelected(anchor);\r
32             });\r
33             \r
34             this.node.on('keydown', '#rng-visualEditor-contentWrapper', function(e) {\r
35                 if(e.which === 13) { \r
36                     e.preventDefault();\r
37                     view.insertNewNode(null, null);\r
38                 }\r
39             });\r
40             \r
41             \r
42             var metaTable = this.metaTable = this.node.find('#rng-visualEditor-meta table');\r
43             \r
44             this.node.find('.rng-visualEditor-metaAddBtn').click(function() {\r
45                 var newRow = view._addMetaRow('', '');\r
46                 $(newRow.find('td div')[0]).focus();\r
47                 isDirty = true;\r
48             });\r
49             \r
50             this.metaTable.on('click', '.rng-visualEditor-metaRemoveBtn', function(e) {\r
51                 $(e.target).closest('tr').remove();\r
52                 isDirty = true;\r
53             });\r
54             \r
55             this.metaTable.on('keydown', '[contenteditable]', function(e) {\r
56                 console.log(e.which);\r
57                 if(e.which === 13) { \r
58                     if($(document.activeElement).hasClass('rng-visualEditor-metaItemKey')) {\r
59                         metaTable.find('.rng-visualEditor-metaItemValue').focus();\r
60                     } else {\r
61                         var input = $('<input>');\r
62                         input.appendTo('body').focus()\r
63                         view.node.find('.rng-visualEditor-metaAddBtn').focus();\r
64                         input.remove();\r
65                     }\r
66                     e.preventDefault();\r
67                 }\r
68                 \r
69             });\r
70             \r
71             \r
72             var observer = new MutationObserver(function(mutations) {\r
73               mutations.forEach(function(mutation) {\r
74                 _.each(mutation.addedNodes, function(node) {\r
75                     node = $(node);\r
76                     node.parent().find('[wlxml-tag]').each(function() {\r
77                         tag = $(this);\r
78                         if(!tag.attr('id'))\r
79                             tag.attr('id', 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {var r = Math.random()*16|0,v=c=='x'?r:r&0x3|0x8;return v.toString(16);}));\r
80                     });\r
81                 });\r
82               });    \r
83             });\r
84             var config = { attributes: true, childList: true, characterData: true, subtree: true };\r
85             observer.observe(this.node.find('#rng-visualEditor-contentWrapper')[0], config);\r
86             \r
87             this.gridToggled = false;
88         },\r
89         _createNode: function(wlxmlTag, wlxmlClass) {\r
90             var toBlock = ['div', 'document', 'section', 'header'];\r
91             var htmlTag = _.contains(toBlock, wlxmlTag) ? 'div' : 'span';\r
92             var toret = $('<' + htmlTag + '>');\r
93             toret.attr('wlxml-tag', wlxmlTag);\r
94             if(wlxmlClass)\r
95                 toret.attr('wlxml-class', wlxmlClass);\r
96             return toret;\r
97         },\r
98         insertNewNode: function(wlxmlTag, wlxmlClass) {\r
99             //TODO: Insert inline\r
100             var anchor = $(window.getSelection().anchorNode);\r
101             var anchorOffset = window.getSelection().anchorOffset;\r
102             if(anchor[0].nodeType === Node.TEXT_NODE)\r
103                 anchor = anchor.parent();\r
104             if(anchor.text() === '') {\r
105                 var todel = anchor;\r
106                 anchor = anchor.parent();\r
107                 todel.remove();\r
108             }\r
109             if(anchorOffset > 0 && anchorOffset < anchor.text().length) {\r
110                 if(wlxmlTag === null && wlxmlClass === null) {\r
111                     return this.splitWithNewNode(anchor);\r
112                 }\r
113                 return this.wrapSelectionWithNewNode(wlxmlTag, wlxmlClass);\r
114             }\r
115             var newNode = this._createNode(wlxmlTag || anchor.attr('wlxml-tag'), wlxmlClass || anchor.attr('wlxml-class'));\r
116             if(anchorOffset === 0)\r
117                 anchor.before(newNode)\r
118             else\r
119                 anchor.after(newNode);\r
120             mediator.nodeCreated(newNode);\r
121             isDirty = true;\r
122         },\r
123         wrapSelectionWithNewNode: function(wlxmlTag, wlxmlClass) {\r
124             var selection = window.getSelection();\r
125             if(selection.anchorNode === selection.focusNode && selection.anchorNode.nodeType === Node.TEXT_NODE) {\r
126                 var startOffset = selection.anchorOffset;\r
127                 var endOffset = selection.focusOffset;\r
128                 if(startOffset > endOffset) {\r
129                     var tmp = startOffset;\r
130                     startOffset = endOffset;\r
131                     endOffset = tmp;\r
132                 }\r
133                 var node = selection.anchorNode;\r
134                 var prefix = node.data.substr(0, startOffset);\r
135                 var suffix = node.data.substr(endOffset);\r
136                 var core = node.data.substr(startOffset, endOffset - startOffset);\r
137                 var newNode = this._createNode(wlxmlTag, wlxmlClass);\r
138                 newNode.text(core || 'test');\r
139                 $(node).replaceWith(newNode);\r
140                 newNode.before(prefix);\r
141                 newNode.after(suffix);\r
142                 mediator.nodeCreated(newNode);\r
143                 isDirty = true;\r
144             }\r
145         },\r
146         splitWithNewNode: function(node) {\r
147             var selection = window.getSelection();\r
148             if(selection.anchorNode === selection.focusNode && selection.anchorNode.nodeType === Node.TEXT_NODE) {\r
149                 var startOffset = selection.anchorOffset;\r
150                 var endOffset = selection.focusOffset;\r
151                 if(startOffset > endOffset) {\r
152                     var tmp = startOffset;\r
153                     startOffset = endOffset;\r
154                     endOffset = tmp;\r
155                 }\r
156                 var anchor = selection.anchorNode;\r
157                 var prefix = anchor.data.substr(0, startOffset);\r
158                 var suffix = anchor.data.substr(endOffset);\r
159                 var prefixNode = this._createNode(node.attr('wlxml-tag'), node.attr('wlxml-class'));\r
160                 var newNode = this._createNode(node.attr('wlxml-tag'), node.attr('wlxml-class'));\r
161                 var suffixNode = this._createNode(node.attr('wlxml-tag'), node.attr('wlxml-class'));\r
162                 prefixNode.text(prefix);\r
163                 suffixNode.text(suffix);\r
164                 node.replaceWith(newNode);\r
165                 newNode.before(prefixNode);\r
166                 newNode.after(suffixNode);\r
167                 mediator.nodeCreated(newNode);\r
168                 isDirty = true;\r
169             }\r
170         },\r
171         getMetaData: function() {\r
172             var toret = {};\r
173             this.metaTable.find('tr').each(function() {\r
174                 var tr = $(this);\r
175                 var inputs = $(this).find('td [contenteditable]');\r
176                 var key = $(inputs[0]).text();\r
177                 var value = $(inputs[1]).text();\r
178                 toret[key] = value;\r
179             });\r
180             console.log(toret);\r
181             return toret;\r
182         },\r
183         setMetaData: function(metadata) {\r
184             var view = this;\r
185             this.metaTable.find('tr').remove();\r
186             _.each(_.keys(metadata), function(key) {    \r
187                 view._addMetaRow(key, metadata[key]);\r
188             });\r
189         },\r
190         setBody: function(HTMLTree) {\r
191             this.node.find('#rng-visualEditor-content').html(HTMLTree);\r
192         },\r
193         getBody: function() {\r
194             return this.node.find('#rng-visualEditor-content').html();\r
195         }, \r
196         _markSelected: function(node) {\r
197             this.dimNode(node);\r
198             \r
199             this.node.find('.rng-current').removeClass('rng-current');\r
200             \r
201             node.addClass('rng-current');\r
202 \r
203             this.currentNode = node;\r
204             mediator.nodeSelected(node);\r
205         },\r
206         selectNode: function(node) {\r
207             view._markSelected(node);\r
208             var range = document.createRange();\r
209             range.selectNodeContents(node[0]);\r
210             range.collapse(false);\r
211 \r
212             var selection = document.getSelection();\r
213             selection.removeAllRanges()\r
214             selection.addRange(range);\r
215         },\r
216         selectNodeById: function(id) {\r
217             var node = this.node.find('#'+id);\r
218             if(node)\r
219                 this.selectNode(node);\r
220         },\r
221         highlightNode: function(node) {\r
222             if(!this.gridToggled) {\r
223                 node.addClass('rng-hover');\r
224                 var label = node.attr('wlxml-tag');\r
225                 if(node.attr('wlxml-class'))\r
226                     label += ' / ' + node.attr('wlxml-class');\r
227                 var tag = $('<div>').addClass('rng-visualEditor-nodeHoverTag').text(label);\r
228                 node.append(tag);\r
229             }\r
230         },\r
231         dimNode: function(node) {\r
232             if(!this.gridToggled) {\r
233                 node.removeClass('rng-hover');\r
234                 node.find('.rng-visualEditor-nodeHoverTag').remove();\r
235             }\r
236         },\r
237         highlightNodeById: function(id) {\r
238             var node = this.node.find('#'+id);\r
239             if(node)\r
240                 this.highlightNode(node);\r
241         },\r
242         dimNodeById: function(id) {\r
243             var node = this.node.find('#'+id);\r
244             if(node)\r
245                 this.dimNode(node);\r
246         },\r
247         selectFirstNode: function() {\r
248             var firstNodeWithText = this.node.find('[wlxml-tag]').filter(function() {\r
249                 return $(this).clone().children().remove().end().text().trim() !== '';\r
250             }).first();\r
251             var node;\r
252             if(firstNodeWithText.length)\r
253                 node = $(firstNodeWithText[0])\r
254             else {\r
255                 node = this.node.find('[wlxml-class|="p"]')\r
256             }\r
257             this.selectNode(node);\r
258         },\r
259         _addMetaRow: function(key, value) {\r
260             var newRow = $(sandbox.getTemplate('metaItem')({key: key || '', value: value || ''}));\r
261             newRow.appendTo(this.metaTable);\r
262             return newRow;\r
263         },\r
264         toggleGrid: function(toggle) {\r
265             this.node.find('[wlxml-tag]').toggleClass('rng-hover', toggle);\r
266             this.gridToggled = toggle;\r
267         },\r
268         toggleTags: function(toggle) {\r
269         \r
270         }\r
271     };\r
272     \r
273     \r
274     var sideBarView = {\r
275         node: view.node.find('#rng-visualEditor-sidebar'),\r
276         setup: function() {\r
277             var view = this;\r
278             this.node.find('#rng-visualEditor-sidebarButtons a').click(function(e) {\r
279                 e.preventDefault();\r
280                 var target = $(e.currentTarget);\r
281                 if(!target.attr('data-content-id'))\r
282                     return;\r
283                 view.selectTab(target.attr('data-content-id'));\r
284             });\r
285             view.selectTab('rng-visualEditor-edit');\r
286             \r
287             view.node.on('change', '.rng-visualEditor-editPaneNodeForm select', function(e) {\r
288                 var target = $(e.target);\r
289                 var attr = target.attr('id').split('-')[2].split('editPane')[1].substr(0,3) === 'Tag' ? 'tag' : 'class';\r
290                 mediator.getCurrentNode().attr('wlxml-'+attr, target.val());\r
291                 isDirty = true;\r
292             });\r
293                        \r
294             view.node.on('click', '.rng-visualEditor-editPaneSurrouding a', function(e) {\r
295                 var target = $(e.target);\r
296                 mediator.nodeDimmedById(target.attr('data-id'));\r
297                 mediator.nodeSelectedById(target.attr('data-id'));\r
298             });\r
299             \r
300             view.node.on('mouseenter', '.rng-visualEditor-editPaneSurrouding a', function(e) {\r
301                 var target = $(e.target);\r
302                 mediator.nodeHighlightedById(target.attr('data-id')); \r
303             });\r
304             view.node.on('mouseleave', '.rng-visualEditor-editPaneSurrouding a', function(e) {\r
305                 var target = $(e.target);\r
306                 mediator.nodeDimmedById(target.attr('data-id')); \r
307             });\r
308         },\r
309         selectTab: function(id) {\r
310            this.node.find('.rng-visualEditor-sidebarContentItem').hide();\r
311            this.node.find('#'+id).show();\r
312            this.node.find('#rng-visualEditor-sidebarButtons li').removeClass('active');\r
313            this.node.find('#rng-visualEditor-sidebarButtons li a[data-content-id=' + id + ']').parent().addClass('active');\r
314         \r
315         },\r
316         updateEditPane: function(node) {\r
317             var pane = this.node.find('#rng-visualEditor-edit');\r
318             var parentClass = node.parent().attr('wlxml-class');\r
319             pane.html( $(sandbox.getTemplate('editPane')({tag: node.attr('wlxml-tag'), klass: node.attr('wlxml-class')})));\r
320             \r
321             var parent = node.parent('[wlxml-tag]').length ? {\r
322                 repr: node.parent().attr('wlxml-tag') + (parentClass ? ' / ' + parentClass : ''),\r
323                 id: node.parent().attr('id')\r
324             } : undefined;\r
325             var children = [];\r
326             node.children('[wlxml-tag]').each(function() {\r
327                 var child = $(this);\r
328                 var childClass = child.attr('wlxml-class');\r
329                 children.push({repr: child.attr('wlxml-tag') + (childClass ? ' / ' + childClass : ''), id: child.attr('id')});\r
330             });\r
331             var naviTemplate = sandbox.getTemplate('editPaneNavigation')({parent: parent, children: children});\r
332             pane.find('.rng-visualEditor-editPaneSurrouding > div').html($(naviTemplate));\r
333         },\r
334         highlightNode: function(id) {\r
335             var pane = this.node.find('#rng-visualEditor-edit');\r
336             pane.find('a[data-id="'+id+'"]').addClass('rng-hover');\r
337         },\r
338         dimNode: function(id) {\r
339             var pane = this.node.find('#rng-visualEditor-edit');\r
340             pane.find('a[data-id="' +id+'"]').removeClass('rng-hover');\r
341         }\r
342     }\r
343     \r
344     var toolbarView = {\r
345         node: view.node.find('#rng-visualEditor-toolbar'),\r
346         setup: function() {\r
347             var view = this;\r
348             \r
349             view.node.find('button').click(function(e) {\r
350                 var btn = $(e.currentTarget);\r
351                 if(btn.attr('data-btn-type') === 'toggle') {\r
352                     btn.toggleClass('active')\r
353                     mediator.toolbarButtonToggled(btn.attr('data-btn'), btn.hasClass('active'));\r
354                 }\r
355                 if(btn.attr('data-btn-type') === 'cmd') {\r
356                     mediator.toolbarButtonCmd(btn.attr('data-btn'));\r
357                 }\r
358             });\r
359         },\r
360         getOption: function(option) {\r
361             return this.node.find('.rng-visualEditor-toolbarOption[data-option=' + option +']').val();\r
362         }\r
363     }\r
364     \r
365     var statusBarView = {\r
366         node: view.node.find('#rng-visualEditor-statusbar'),\r
367         setup: function() {\r
368             var view = this;\r
369             view.node.on('mouseenter', 'a', function(e) {\r
370                 var target = $(e.target);\r
371                 mediator.nodeHighlightedById(target.attr('data-id')); \r
372             });\r
373             view.node.on('mouseleave', 'a', function(e) {\r
374                 var target = $(e.target);\r
375                 mediator.nodeDimmedById(target.attr('data-id')); \r
376             });\r
377             view.node.on('click', 'a', function(e) {\r
378                 e.preventDefault();\r
379                 mediator.nodeSelectedById($(e.target).attr('data-id'));\r
380             });\r
381         },\r
382         \r
383         showNode: function(node) {\r
384             this.node.empty();\r
385             this.node.html(sandbox.getTemplate('statusBarNodeDisplay')({node: node, parents: node.parents('[wlxml-tag]')}));\r
386             //node.parents('[wlxml-tag]')\r
387         },\r
388         \r
389         highlightNode: function(id) {\r
390             this.node.find('a[data-id="'+id+'"]').addClass('rng-hover');\r
391         },\r
392         dimNode: function(id) {\r
393             this.node.find('a[data-id="' +id+'"]').removeClass('rng-hover');\r
394         }\r
395     }\r
396     \r
397     view.setup();\r
398     sideBarView.setup();\r
399     toolbarView.setup();\r
400     statusBarView.setup();\r
401     \r
402     var mediator = {\r
403         getCurrentNode: function() {\r
404             return view.currentNode;\r
405         },\r
406         nodeCreated: function(node) {\r
407             view.selectNode(node);\r
408         },\r
409         nodeSelected: function(node) {\r
410             sideBarView.updateEditPane(node);\r
411             statusBarView.showNode(node);\r
412         },\r
413         nodeSelectedById: function(id) {\r
414             view.selectNodeById(id);\r
415         },\r
416         nodeHighlightedById: function(id) {\r
417             view.highlightNodeById(id);\r
418         },\r
419         nodeDimmedById: function(id) {\r
420             view.dimNodeById(id);\r
421         },\r
422         toolbarButtonToggled: function(btn, toggle) {\r
423             if(btn === 'grid')\r
424                 view.toggleGrid(toggle);\r
425             if(btn === 'tags')\r
426                 view.toggleTags(toggle);\r
427         },\r
428         toolbarButtonCmd: function(btn) {\r
429             if(btn === 'new-node') {\r
430                 if(window.getSelection().isCollapsed)\r
431                     view.insertNewNode(toolbarView.getOption('newTag-tag'), toolbarView.getOption('newTag-class'));\r
432                 else {\r
433                     this.wrapWithNodeRequest(toolbarView.getOption('newTag-tag'), toolbarView.getOption('newTag-class'));\r
434                 }\r
435                     \r
436                     \r
437             }\r
438         },\r
439         nodeHovered: function(node) {\r
440             view.highlightNode(node);\r
441             sideBarView.highlightNode(node.attr('id'));\r
442             statusBarView.highlightNode(node.attr('id'));\r
443         },\r
444         nodeBlured: function(node) {\r
445             view.dimNode(node);\r
446             sideBarView.dimNode(node.attr('id'));\r
447             statusBarView.dimNode(node.attr('id'));\r
448         },\r
449         wrapWithNodeRequest: function(wlxmlTag, wlxmlClass) {\r
450             view.wrapSelectionWithNewNode(wlxmlTag, wlxmlClass);\r
451         }\r
452         \r
453     }\r
454     \r
455     var isDirty = false;\r
456     var wasShownAlready = false;\r
457     \r
458     \r
459     return {\r
460         start: function() {\r
461             sandbox.publish('ready');\r
462         },\r
463         getView: function() {\r
464             return view.node;\r
465         },\r
466         setDocument: function(xml) {\r
467             var transformed = transformations.fromXML.getDocumentDescription(xml);\r
468             view.setBody(transformed.HTMLTree);\r
469             view.setMetaData(transformed.metadata);\r
470             isDirty = false;\r
471         },\r
472         getDocument: function() {\r
473             return transformations.toXML.getXML({HTMLTree: view.getBody(), metadata: view.getMetaData()});\r
474         },\r
475         isDirty: function() {\r
476             return isDirty;\r
477         },\r
478         setDirty: function(dirty) {\r
479             isDirty = dirty;\r
480         },\r
481         onShowed: function() {\r
482             if(!wasShownAlready) {\r
483                 wasShownAlready = true;\r
484                 view.selectFirstNode();\r
485             }\r
486         }\r
487     \r
488     }\r
489 };