kiteditor 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (229) hide show
  1. data/LICENSE +25 -0
  2. data/app/controllers/mercury/images_controller.rb +5 -0
  3. data/app/controllers/mercury_controller.rb +34 -0
  4. data/app/models/mercury/image.rb +17 -0
  5. data/app/views/layouts/mercury.html.erb +33 -0
  6. data/app/views/layouts/popup.html.haml +9 -0
  7. data/app/views/mercury/images/_list.html.haml +22 -0
  8. data/app/views/mercury/images/index.html.haml +4 -0
  9. data/app/views/mercury/images/index.js.erb +2 -0
  10. data/app/views/mercury/lightviews/about.html +11 -0
  11. data/app/views/mercury/modals/character.html +255 -0
  12. data/app/views/mercury/modals/htmleditor.html +13 -0
  13. data/app/views/mercury/modals/link.html +94 -0
  14. data/app/views/mercury/modals/media.html +1 -0
  15. data/app/views/mercury/modals/table.html +84 -0
  16. data/app/views/mercury/palettes/backcolor.html +73 -0
  17. data/app/views/mercury/palettes/forecolor.html +73 -0
  18. data/app/views/mercury/panels/history.html +3 -0
  19. data/app/views/mercury/panels/notes.html +3 -0
  20. data/app/views/mercury/panels/snippets.html +12 -0
  21. data/app/views/mercury/selects/formatblock.html +11 -0
  22. data/app/views/mercury/selects/style.html +5 -0
  23. data/app/views/mercury/snippets/example/options.html.erb +34 -0
  24. data/app/views/mercury/snippets/example/preview.html.erb +1 -0
  25. data/config/engine.rb +44 -0
  26. data/db/migrate/20110526035601_create_mercury_images.rb +11 -0
  27. data/features/loading/loading.feature +22 -0
  28. data/features/loading/navigating.feature +77 -0
  29. data/features/loading/user_interface.feature +67 -0
  30. data/features/regions/editable/advanced_editing.feature +0 -0
  31. data/features/regions/editable/basic_editing.feature +195 -0
  32. data/features/regions/editable/inserting_links.feature +98 -0
  33. data/features/regions/editable/inserting_media.feature +110 -0
  34. data/features/regions/editable/inserting_snippets.feature +102 -0
  35. data/features/regions/editable/inserting_special_characters.feature +24 -0
  36. data/features/regions/editable/inserting_tables.feature +109 -0
  37. data/features/regions/editable/pasting.feature +0 -0
  38. data/features/regions/editable/uploading_images.feature +0 -0
  39. data/features/regions/markupable/advanced_editing.feature +0 -0
  40. data/features/regions/markupable/basic_editing.feature +0 -0
  41. data/features/regions/markupable/inserting_links.feature +0 -0
  42. data/features/regions/markupable/inserting_media.feature +0 -0
  43. data/features/regions/markupable/inserting_snippets.feature +0 -0
  44. data/features/regions/markupable/inserting_special_characters.feature +0 -0
  45. data/features/regions/markupable/inserting_tables.feature +0 -0
  46. data/features/regions/markupable/uploading_images.feature +0 -0
  47. data/features/regions/snippetable/advanced_editing.feature +0 -0
  48. data/features/regions/snippetable/basic_editing.feature +0 -0
  49. data/features/regions/snippetable/inserting_snippets.feature +0 -0
  50. data/features/saving/saving.feature +33 -0
  51. data/features/step_definitions/debug_steps.rb +14 -0
  52. data/features/step_definitions/mercury_steps.rb +438 -0
  53. data/features/step_definitions/web_steps.rb +211 -0
  54. data/features/support/env.rb +46 -0
  55. data/features/support/mercury_contents.rb +25 -0
  56. data/features/support/mercury_selectors.rb +148 -0
  57. data/features/support/paths.rb +38 -0
  58. data/features/support/selectors.rb +44 -0
  59. data/lib/generators/mercury/install/install_generator.rb +49 -0
  60. data/lib/generators/mercury/install/templates/mongoid_paperclip_image.rb +17 -0
  61. data/lib/mercury/authentication.rb +8 -0
  62. data/lib/mercury-rails.rb +3 -0
  63. data/spec/javascripts/mercury/dialog_spec.js.coffee +281 -0
  64. data/spec/javascripts/mercury/dialogs/backcolor_spec.js.coffee +37 -0
  65. data/spec/javascripts/mercury/dialogs/forecolor_spec.js.coffee +37 -0
  66. data/spec/javascripts/mercury/dialogs/formatblock_spec.js.coffee +25 -0
  67. data/spec/javascripts/mercury/dialogs/snippetpanel_spec.js.coffee +30 -0
  68. data/spec/javascripts/mercury/dialogs/style_spec.js.coffee +25 -0
  69. data/spec/javascripts/mercury/history_buffer_spec.js.coffee +76 -0
  70. data/spec/javascripts/mercury/lightview_spec.js.coffee +497 -0
  71. data/spec/javascripts/mercury/mercury_spec.js.coffee +132 -0
  72. data/spec/javascripts/mercury/modal_spec.js.coffee +504 -0
  73. data/spec/javascripts/mercury/modals/htmleditor_spec.js.coffee +30 -0
  74. data/spec/javascripts/mercury/modals/insertcharacter_spec.js.coffee +29 -0
  75. data/spec/javascripts/mercury/modals/insertlink_spec.js.coffee +220 -0
  76. data/spec/javascripts/mercury/modals/insertmedia_spec.js.coffee +167 -0
  77. data/spec/javascripts/mercury/modals/insertsnippet_spec.js.coffee +52 -0
  78. data/spec/javascripts/mercury/modals/inserttable_spec.js.coffee +160 -0
  79. data/spec/javascripts/mercury/native_extensions_spec.js.coffee +60 -0
  80. data/spec/javascripts/mercury/page_editor_spec.js.coffee +750 -0
  81. data/spec/javascripts/mercury/palette_spec.js.coffee +49 -0
  82. data/spec/javascripts/mercury/panel_spec.js.coffee +185 -0
  83. data/spec/javascripts/mercury/region_spec.js.coffee +298 -0
  84. data/spec/javascripts/mercury/regions/editable_spec.js.coffee +561 -0
  85. data/spec/javascripts/mercury/regions/markupable_spec.js.coffee +367 -0
  86. data/spec/javascripts/mercury/regions/snippetable_spec.js.coffee +370 -0
  87. data/spec/javascripts/mercury/select_spec.js.coffee +49 -0
  88. data/spec/javascripts/mercury/snippet_spec.js.coffee +245 -0
  89. data/spec/javascripts/mercury/snippet_toolbar_spec.js.coffee +184 -0
  90. data/spec/javascripts/mercury/statusbar_spec.js.coffee +150 -0
  91. data/spec/javascripts/mercury/table_editor_spec.js.coffee +194 -0
  92. data/spec/javascripts/mercury/toolbar.button_group_spec.js.coffee +90 -0
  93. data/spec/javascripts/mercury/toolbar.button_spec.js.coffee +360 -0
  94. data/spec/javascripts/mercury/toolbar.expander_spec.js.coffee +118 -0
  95. data/spec/javascripts/mercury/toolbar_spec.js.coffee +222 -0
  96. data/spec/javascripts/mercury/tooltip_spec.js.coffee +186 -0
  97. data/spec/javascripts/mercury/uploader_spec.js.coffee +514 -0
  98. data/spec/javascripts/spec_helper.js +513 -0
  99. data/spec/javascripts/templates/mercury/dialog.html +2 -0
  100. data/spec/javascripts/templates/mercury/dialogs/backcolor.html +5 -0
  101. data/spec/javascripts/templates/mercury/dialogs/forecolor.html +5 -0
  102. data/spec/javascripts/templates/mercury/dialogs/formatblock.html +3 -0
  103. data/spec/javascripts/templates/mercury/dialogs/snippetpanel.html +16 -0
  104. data/spec/javascripts/templates/mercury/dialogs/style.html +3 -0
  105. data/spec/javascripts/templates/mercury/lightview.html +13 -0
  106. data/spec/javascripts/templates/mercury/modal.html +13 -0
  107. data/spec/javascripts/templates/mercury/modals/htmleditor.html +5 -0
  108. data/spec/javascripts/templates/mercury/modals/insertcharacter.html +5 -0
  109. data/spec/javascripts/templates/mercury/modals/insertlink.html +30 -0
  110. data/spec/javascripts/templates/mercury/modals/insertmedia.html +35 -0
  111. data/spec/javascripts/templates/mercury/modals/insertsnippet.html +6 -0
  112. data/spec/javascripts/templates/mercury/modals/inserttable.html +27 -0
  113. data/spec/javascripts/templates/mercury/page_editor.html +35 -0
  114. data/spec/javascripts/templates/mercury/palette.html +16 -0
  115. data/spec/javascripts/templates/mercury/panel.html +16 -0
  116. data/spec/javascripts/templates/mercury/region.html +2 -0
  117. data/spec/javascripts/templates/mercury/regions/editable.html +3 -0
  118. data/spec/javascripts/templates/mercury/regions/snippetable.html +4 -0
  119. data/spec/javascripts/templates/mercury/select.html +16 -0
  120. data/spec/javascripts/templates/mercury/snippet.html +1 -0
  121. data/spec/javascripts/templates/mercury/snippet_toolbar.html +16 -0
  122. data/spec/javascripts/templates/mercury/statusbar.html +8 -0
  123. data/spec/javascripts/templates/mercury/table_editor.html +65 -0
  124. data/spec/javascripts/templates/mercury/toolbar.button.html +64 -0
  125. data/spec/javascripts/templates/mercury/toolbar.button_group.html +9 -0
  126. data/spec/javascripts/templates/mercury/toolbar.expander.html +18 -0
  127. data/spec/javascripts/templates/mercury/toolbar.html +11 -0
  128. data/spec/javascripts/templates/mercury/tooltip.html +12 -0
  129. data/spec/javascripts/templates/mercury/uploader.html +11 -0
  130. data/vendor/assets/images/mercury/button.png +0 -0
  131. data/vendor/assets/images/mercury/close.png +0 -0
  132. data/vendor/assets/images/mercury/default-snippet.png +0 -0
  133. data/vendor/assets/images/mercury/loading-dark.gif +0 -0
  134. data/vendor/assets/images/mercury/loading-light.gif +0 -0
  135. data/vendor/assets/images/mercury/missing-image.png +0 -0
  136. data/vendor/assets/images/mercury/search-icon.png +0 -0
  137. data/vendor/assets/images/mercury/temp-logo.png +0 -0
  138. data/vendor/assets/images/mercury/toolbar/editable/buttons.png +0 -0
  139. data/vendor/assets/images/mercury/toolbar/primary/_expander.png +0 -0
  140. data/vendor/assets/images/mercury/toolbar/primary/_pressed.png +0 -0
  141. data/vendor/assets/images/mercury/toolbar/primary/historypanel.png +0 -0
  142. data/vendor/assets/images/mercury/toolbar/primary/insertcharacter.png +0 -0
  143. data/vendor/assets/images/mercury/toolbar/primary/insertlink.png +0 -0
  144. data/vendor/assets/images/mercury/toolbar/primary/insertmedia.png +0 -0
  145. data/vendor/assets/images/mercury/toolbar/primary/inserttable.png +0 -0
  146. data/vendor/assets/images/mercury/toolbar/primary/inspectorpanel.png +0 -0
  147. data/vendor/assets/images/mercury/toolbar/primary/notespanel.png +0 -0
  148. data/vendor/assets/images/mercury/toolbar/primary/preview.png +0 -0
  149. data/vendor/assets/images/mercury/toolbar/primary/redo.png +0 -0
  150. data/vendor/assets/images/mercury/toolbar/primary/save.png +0 -0
  151. data/vendor/assets/images/mercury/toolbar/primary/snippetpanel.png +0 -0
  152. data/vendor/assets/images/mercury/toolbar/primary/undo.png +0 -0
  153. data/vendor/assets/images/mercury/toolbar/snippetable/buttons.png +0 -0
  154. data/vendor/assets/javascripts/mercury/dependencies/jquery-1.7.js +9300 -0
  155. data/vendor/assets/javascripts/mercury/dependencies/jquery-ui-1.8.13.custom.js +1328 -0
  156. data/vendor/assets/javascripts/mercury/dependencies/jquery-ui-1.8.18.custom.min.js +356 -0
  157. data/vendor/assets/javascripts/mercury/dependencies/jquery-ui.1.8.13.custom.min.js +356 -0
  158. data/vendor/assets/javascripts/mercury/dependencies/jquery.additions.js +206 -0
  159. data/vendor/assets/javascripts/mercury/dependencies/jquery.htmlClean.js +527 -0
  160. data/vendor/assets/javascripts/mercury/dependencies/liquidmetal.js +88 -0
  161. data/vendor/assets/javascripts/mercury/dependencies/showdown.js +1340 -0
  162. data/vendor/assets/javascripts/mercury/dialog.js.coffee +159 -0
  163. data/vendor/assets/javascripts/mercury/dialogs/backcolor.js.coffee +6 -0
  164. data/vendor/assets/javascripts/mercury/dialogs/forecolor.js.coffee +6 -0
  165. data/vendor/assets/javascripts/mercury/dialogs/formatblock.js.coffee +4 -0
  166. data/vendor/assets/javascripts/mercury/dialogs/snippetpanel.js.coffee +10 -0
  167. data/vendor/assets/javascripts/mercury/dialogs/style.js.coffee +4 -0
  168. data/vendor/assets/javascripts/mercury/finalize.js.coffee +3 -0
  169. data/vendor/assets/javascripts/mercury/history_buffer.js.coffee +30 -0
  170. data/vendor/assets/javascripts/mercury/lightview.js.coffee +205 -0
  171. data/vendor/assets/javascripts/mercury/locales/da.locale.js.coffee +0 -0
  172. data/vendor/assets/javascripts/mercury/locales/de.locale.js.coffee +206 -0
  173. data/vendor/assets/javascripts/mercury/locales/es.locale.js.coffee +211 -0
  174. data/vendor/assets/javascripts/mercury/locales/example.local.js.coffee +211 -0
  175. data/vendor/assets/javascripts/mercury/locales/fr.locale.js.coffee +211 -0
  176. data/vendor/assets/javascripts/mercury/locales/it.locale.js.coffee +208 -0
  177. data/vendor/assets/javascripts/mercury/locales/ko.local.js.coffee +206 -0
  178. data/vendor/assets/javascripts/mercury/locales/nl.locale.js.coffee +206 -0
  179. data/vendor/assets/javascripts/mercury/locales/pt.locale.js.coffee +211 -0
  180. data/vendor/assets/javascripts/mercury/locales/sv.local.js.coffee +209 -0
  181. data/vendor/assets/javascripts/mercury/locales/swedish_chef.locale.js.coffee +213 -0
  182. data/vendor/assets/javascripts/mercury/mercury.js.coffee +109 -0
  183. data/vendor/assets/javascripts/mercury/modal.js.coffee +204 -0
  184. data/vendor/assets/javascripts/mercury/modals/htmleditor.js.coffee +11 -0
  185. data/vendor/assets/javascripts/mercury/modals/insertcharacter.js.coffee +4 -0
  186. data/vendor/assets/javascripts/mercury/modals/insertlink.js.coffee +95 -0
  187. data/vendor/assets/javascripts/mercury/modals/insertmedia.js.coffee +107 -0
  188. data/vendor/assets/javascripts/mercury/modals/insertsnippet.js.coffee +12 -0
  189. data/vendor/assets/javascripts/mercury/modals/inserttable.js.coffee +54 -0
  190. data/vendor/assets/javascripts/mercury/native_extensions.js.coffee +55 -0
  191. data/vendor/assets/javascripts/mercury/page_editor.js.coffee +241 -0
  192. data/vendor/assets/javascripts/mercury/palette.js.coffee +29 -0
  193. data/vendor/assets/javascripts/mercury/panel.js.coffee +115 -0
  194. data/vendor/assets/javascripts/mercury/plugins/save_as_xml/mercury/page_editor.js.coffee +28 -0
  195. data/vendor/assets/javascripts/mercury/plugins/save_as_xml/plugin.js +9 -0
  196. data/vendor/assets/javascripts/mercury/region.js.coffee +107 -0
  197. data/vendor/assets/javascripts/mercury/regions/editable.js.coffee +600 -0
  198. data/vendor/assets/javascripts/mercury/regions/markupable.js.coffee +398 -0
  199. data/vendor/assets/javascripts/mercury/regions/simple.js.coffee +339 -0
  200. data/vendor/assets/javascripts/mercury/regions/snippetable.js.coffee +124 -0
  201. data/vendor/assets/javascripts/mercury/select.js.coffee +44 -0
  202. data/vendor/assets/javascripts/mercury/snippet.js.coffee +104 -0
  203. data/vendor/assets/javascripts/mercury/snippet_toolbar.js.coffee +72 -0
  204. data/vendor/assets/javascripts/mercury/statusbar.js.coffee +51 -0
  205. data/vendor/assets/javascripts/mercury/support/history.js +1 -0
  206. data/vendor/assets/javascripts/mercury/table_editor.js.coffee +265 -0
  207. data/vendor/assets/javascripts/mercury/toolbar.button.js.coffee +173 -0
  208. data/vendor/assets/javascripts/mercury/toolbar.button_group.js.coffee +42 -0
  209. data/vendor/assets/javascripts/mercury/toolbar.expander.js.coffee +56 -0
  210. data/vendor/assets/javascripts/mercury/toolbar.js.coffee +86 -0
  211. data/vendor/assets/javascripts/mercury/tooltip.js.coffee +74 -0
  212. data/vendor/assets/javascripts/mercury/uploader.js.coffee +227 -0
  213. data/vendor/assets/javascripts/mercury.js +479 -0
  214. data/vendor/assets/javascripts/mercury_loader.js +193 -0
  215. data/vendor/assets/javascripts/mercury_overrides.js +6 -0
  216. data/vendor/assets/javascripts/popup.js +8 -0
  217. data/vendor/assets/stylesheets/mercury/all_images.css.erb +89 -0
  218. data/vendor/assets/stylesheets/mercury/dialog.css +208 -0
  219. data/vendor/assets/stylesheets/mercury/lightview.css +151 -0
  220. data/vendor/assets/stylesheets/mercury/mercury.css +151 -0
  221. data/vendor/assets/stylesheets/mercury/modal.css +183 -0
  222. data/vendor/assets/stylesheets/mercury/statusbar.css +32 -0
  223. data/vendor/assets/stylesheets/mercury/toolbar.css +304 -0
  224. data/vendor/assets/stylesheets/mercury/tooltip.css +26 -0
  225. data/vendor/assets/stylesheets/mercury/uploader.css +111 -0
  226. data/vendor/assets/stylesheets/mercury.css +28 -0
  227. data/vendor/assets/stylesheets/mercury_overrides.css +17 -0
  228. data/vendor/assets/stylesheets/popup.css.erb +37 -0
  229. metadata +634 -0
@@ -0,0 +1,600 @@
1
+ class @Mercury.Regions.Editable extends Mercury.Region
2
+ # No IE support yet because it doesn't follow the W3C standards for HTML5 contentEditable (aka designMode).
3
+ @supported: document.designMode && !jQuery.browser.konqueror && !jQuery.browser.msie
4
+ @supportedText: "Chrome 10+, Firefox 4+, Safari 5+"
5
+
6
+ type = 'editable'
7
+
8
+ constructor: (@element, @window, @options = {}) ->
9
+ @type = 'editable'
10
+ super
11
+
12
+
13
+ build: ->
14
+ # mozilla: set some initial content so everything works correctly
15
+ @content(' ') if jQuery.browser.mozilla && @content() == ''
16
+
17
+ # set overflow just in case
18
+ @element.data({originalOverflow: @element.css('overflow')})
19
+ @element.css({overflow: 'auto'})
20
+
21
+ # mozilla: there's some weird behavior when the element isn't a div
22
+ @specialContainer = jQuery.browser.mozilla && @element.get(0).tagName != 'DIV'
23
+
24
+ # make it editable
25
+ # mozilla: this makes double clicking in textareas fail: https://bugzilla.mozilla.org/show_bug.cgi?id=490367
26
+ @element.get(0).contentEditable = true
27
+
28
+ # make all snippets not editable, and set their versions to 1
29
+ for element in @element.find('.mercury-snippet')
30
+ element.contentEditable = false
31
+ jQuery(element).attr('data-version', '1')
32
+
33
+ # add the basic editor settings to the document (only once)
34
+ unless @document.mercuryEditing
35
+ @document.mercuryEditing = true
36
+ try
37
+ @document.execCommand('styleWithCSS', false, false)
38
+ @document.execCommand('insertBROnReturn', false, true)
39
+ @document.execCommand('enableInlineTableEditing', false, false)
40
+ @document.execCommand('enableObjectResizing', false, false)
41
+ catch e
42
+ # intentionally do nothing if any of these fail, to broaden support for Opera
43
+
44
+
45
+ bindEvents: ->
46
+ super
47
+
48
+ Mercury.on 'region:update', =>
49
+ return if @previewing || Mercury.region != @
50
+ setTimeout(1, => @selection().forceSelection(@element.get(0)))
51
+ currentElement = @currentElement()
52
+ if currentElement.length
53
+ # setup the table editor if we're inside a table
54
+ table = currentElement.closest('table', @element)
55
+ Mercury.tableEditor(table, currentElement.closest('tr, td'), '<br/>') if table.length
56
+ # display a tooltip if we're in an anchor
57
+ anchor = currentElement.closest('a', @element)
58
+ if anchor.length && anchor.attr('href')
59
+ Mercury.tooltip(anchor, "#{anchor.attr('href')}: <a class='internal_link' href=\"#{anchor.attr('href')}\" >Follow Link</a>", {position: 'below'})
60
+ else
61
+ Mercury.tooltip.hide()
62
+
63
+ @element.on 'dragenter', (event) =>
64
+ return if @previewing
65
+ event.preventDefault() unless Mercury.snippet
66
+ event.originalEvent.dataTransfer.dropEffect = 'copy'
67
+
68
+ @element.on 'dragover', (event) =>
69
+ return if @previewing
70
+ event.preventDefault() unless Mercury.snippet
71
+ event.originalEvent.dataTransfer.dropEffect = 'copy'
72
+ if jQuery.browser.webkit
73
+ clearTimeout(@dropTimeout)
74
+ @dropTimeout = setTimeout(10, => @element.trigger('possible:drop'))
75
+
76
+ @element.on 'drop', (event) =>
77
+ return if @previewing
78
+
79
+ # handle dropping snippets
80
+ clearTimeout(@dropTimeout)
81
+ @dropTimeout = setTimeout(1, => @element.trigger('possible:drop'))
82
+
83
+ # handle any files that were dropped
84
+ return unless event.originalEvent.dataTransfer.files.length
85
+ event.preventDefault()
86
+ @focus()
87
+ Mercury.uploader(event.originalEvent.dataTransfer.files[0])
88
+
89
+ # possible:drop custom event: we have to do this because webkit doesn't fire the drop event unless both dragover and
90
+ # dragstart default behaviors are canceled.. but when we do that and observe the drop event, the default behavior
91
+ # isn't handled (eg, putting the image where it was dropped,) so to allow the browser to do it's thing, and also do
92
+ # our thing we have this little hack. *sigh*
93
+ # read: http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html
94
+ @element.on 'possible:drop', =>
95
+ return if @previewing
96
+ if snippet = @element.find('img[data-snippet]').get(0)
97
+ @focus()
98
+ Mercury.Snippet.displayOptionsFor(jQuery(snippet).data('snippet'))
99
+ @document.execCommand('undo', false, null)
100
+
101
+ # custom paste handling: we have to do some hackery to get the pasted content since it's not exposed normally
102
+ # through a clipboard in firefox (heaven forbid), and to keep the behavior across all browsers, we manually detect
103
+ # what was pasted by focusing a different (hidden) region, letting it paste there, making our adjustments, and then
104
+ # inserting the content where it was intended. This is possible, so it doesn't make sense why it wouldn't be
105
+ # exposed in a sensible way. *sigh*
106
+ @element.on 'paste', (event) =>
107
+ return if @previewing || Mercury.region != @
108
+ if @specialContainer
109
+ event.preventDefault()
110
+ return
111
+ return if @pasting
112
+ Mercury.changes = true
113
+ @handlePaste(event.originalEvent)
114
+
115
+ @element.on 'focus', =>
116
+ return if @previewing
117
+ Mercury.region = @
118
+ setTimeout(1, => @selection().forceSelection(@element.get(0)))
119
+ Mercury.trigger('region:focused', {region: @})
120
+
121
+ @element.on 'blur', =>
122
+ return if @previewing
123
+ Mercury.trigger('region:blurred', {region: @})
124
+ Mercury.tooltip.hide()
125
+
126
+ @element.on 'click', (event) =>
127
+ jQuery(event.target).closest('a').attr('target', '_parent') if @previewing
128
+
129
+ @element.on 'dblclick', (event) =>
130
+ return if @previewing
131
+ image = jQuery(event.target).closest('img', @element)
132
+ if image.length
133
+ @selection().selectNode(image.get(0), true)
134
+ Mercury.trigger('button', {action: 'insertMedia'})
135
+
136
+ @element.on 'mouseup', =>
137
+ return if @previewing
138
+ @pushHistory()
139
+ Mercury.trigger('region:update', {region: @})
140
+
141
+ @element.on 'keydown', (event) =>
142
+ return if @previewing
143
+ switch event.keyCode
144
+ when 90 # undo / redo
145
+ return unless event.metaKey
146
+ event.preventDefault()
147
+ if event.shiftKey then @execCommand('redo') else @execCommand('undo')
148
+ return
149
+
150
+ when 13 # enter
151
+ if jQuery.browser.webkit && @selection().commonAncestor().closest('li, ul, ol', @element).length == 0
152
+ event.preventDefault()
153
+ @document.execCommand('insertParagraph', false, null)
154
+ else if @specialContainer || jQuery.browser.opera
155
+ # mozilla: pressing enter in any element besides a div handles strangely
156
+ event.preventDefault()
157
+ @document.execCommand('insertHTML', false, '<br/>')
158
+
159
+ when 9 # tab
160
+ event.preventDefault()
161
+ container = @selection().commonAncestor()
162
+
163
+ # indent when inside of an li
164
+ if container.closest('li', @element).length
165
+ unless event.shiftKey
166
+ @execCommand('indent')
167
+ # do not outdent on last ul/ol parent, or we break out of the list
168
+ else if container.parents('ul, ol').length > 1
169
+ @execCommand('outdent')
170
+ else
171
+ @execCommand('insertHTML', {value: '&nbsp;&nbsp;'})
172
+
173
+ if event.metaKey
174
+ switch event.keyCode
175
+ when 66 # b
176
+ @execCommand('bold')
177
+ event.preventDefault()
178
+
179
+ when 73 # i
180
+ @execCommand('italic')
181
+ event.preventDefault()
182
+
183
+ when 85 # u
184
+ @execCommand('underline')
185
+ event.preventDefault()
186
+
187
+ @pushHistory(event.keyCode)
188
+
189
+ @element.on 'keyup', =>
190
+ return if @previewing
191
+ Mercury.trigger('region:update', {region: @})
192
+ Mercury.changes = true
193
+
194
+
195
+ focus: ->
196
+ if Mercury.region != @
197
+ setTimeout(10, => @element.focus())
198
+ try
199
+ @selection().selection.collapseToStart()
200
+ catch e
201
+ # intentially do nothing
202
+ else
203
+ setTimeout(10, => @element.focus())
204
+
205
+ Mercury.trigger('region:focused', {region: @})
206
+ Mercury.trigger('region:update', {region: @})
207
+
208
+
209
+ content: (value = null, filterSnippets = true, includeMarker = false) ->
210
+ if value != null
211
+ # sanitize the html before we insert it
212
+ container = jQuery('<div>').appendTo(@document.createDocumentFragment())
213
+ container.html(value)
214
+
215
+ # fill in the snippet contents
216
+ for element in container.find('[data-snippet]')
217
+ element.contentEditable = false
218
+ element = jQuery(element)
219
+ if snippet = Mercury.Snippet.find(element.data('snippet'))
220
+ unless element.data('version')
221
+ try
222
+ version = parseInt(element.html().match(/\/(\d+)\]/)[1])
223
+ if version
224
+ snippet.setVersion(version)
225
+ element.attr({'data-version': version})
226
+ element.html(snippet.data)
227
+ catch error
228
+
229
+ # set the html
230
+ @element.html(container.html())
231
+
232
+ # create a selection if there's markers
233
+ @selection().selectMarker(@element)
234
+ else
235
+ # remove any meta tags
236
+ @element.find('meta').remove()
237
+
238
+ # place markers for the selection
239
+ if includeMarker
240
+ selection = @selection()
241
+ selection.placeMarker()
242
+
243
+ # sanitize the html before we return it
244
+ container = jQuery('<div>').appendTo(@document.createDocumentFragment())
245
+ container.html(@element.html().replace(/^\s+|\s+$/g, ''))
246
+
247
+ # replace snippet contents to be an identifier
248
+ if filterSnippets then for element, index in container.find('[data-snippet]')
249
+ element = jQuery(element)
250
+ if snippet = Mercury.Snippet.find(element.data("snippet"))
251
+ snippet.data = element.html()
252
+ element.html("[#{element.data("snippet")}/#{element.data("version")}]")
253
+ element.attr({contenteditable: null, 'data-version': null})
254
+
255
+ # get the html before removing the markers
256
+ content = container.html()
257
+
258
+ # remove the markers from the dom
259
+ selection.removeMarker() if includeMarker
260
+
261
+ return content
262
+
263
+
264
+ togglePreview: ->
265
+ if @previewing
266
+ @element.get(0).contentEditable = true
267
+ @element.css({overflow: 'auto'})
268
+ else
269
+ @content(@content())
270
+ @element.get(0).contentEditable = false
271
+ @element.css({overflow: @element.data('originalOverflow')})
272
+ @element.blur()
273
+ super
274
+
275
+
276
+ execCommand: (action, options = {}) ->
277
+ super
278
+
279
+ # use a custom handler if there's one, otherwise use execCommand
280
+ if handler = Mercury.config.behaviors[action] || Mercury.Regions.Editable.actions[action]
281
+ handler.call(@, @selection(), options)
282
+ else
283
+ sibling = @element.get(0).previousSibling if action == 'indent'
284
+ options.value = jQuery('<div>').html(options.value).html() if action == 'insertHTML' && options.value && options.value.get
285
+ try
286
+ @document.execCommand(action, false, options.value)
287
+ catch error
288
+ # mozilla: indenting when there's no br tag handles strangely
289
+ # mozilla: trying to justify the first line of any contentEditable fails
290
+ @element.prev().remove() if action == 'indent' && @element.prev() != sibling
291
+
292
+ # handle any broken images by replacing the source with an alert image
293
+ @element.find('img').one 'error', -> jQuery(@).attr({src: '/assets/mercury/missing-image.png', title: 'Image not found'})
294
+
295
+
296
+ pushHistory: (keyCode) ->
297
+ # when pressing return, delete or backspace it should push to the history
298
+ # all other times it should store if there's a 1 second pause
299
+ keyCodes = [13, 46, 8]
300
+ waitTime = 2.5
301
+ knownKeyCode = keyCodes.indexOf(keyCode) if keyCode
302
+
303
+ # clear any pushes to the history
304
+ clearTimeout(@historyTimeout)
305
+
306
+ # if the key code was return, delete, or backspace store now -- unless it was the same as last time
307
+ if knownKeyCode >= 0 && knownKeyCode != @lastKnownKeyCode # || !keyCode
308
+ @history.push(@content(null, false, true))
309
+ else if keyCode
310
+ # set a timeout for pushing to the history
311
+ @historyTimeout = setTimeout(waitTime * 1000, => @history.push(@content(null, false, true)))
312
+ else
313
+ # push to the history immediately
314
+ @history.push(@content(null, false, true))
315
+
316
+ @lastKnownKeyCode = knownKeyCode
317
+
318
+
319
+ selection: ->
320
+ return new Mercury.Regions.Editable.Selection(@window.getSelection(), @document)
321
+
322
+
323
+ path: ->
324
+ container = @selection().commonAncestor()
325
+ return [] unless container
326
+ return if container.get(0) == @element.get(0) then [] else container.parentsUntil(@element)
327
+
328
+
329
+ currentElement: ->
330
+ element = []
331
+ selection = @selection()
332
+ if selection.range
333
+ element = selection.commonAncestor()
334
+ element = element.parent() if element.get(0).nodeType == 3
335
+ return element
336
+
337
+
338
+ handlePaste: (event) ->
339
+ # get the text content from the clipboard and fall back to using the sanitizer if unavailable
340
+ if Mercury.config.pasting.sanitize == 'text' && event.clipboardData
341
+ @execCommand('insertHTML', {value: event.clipboardData.getData('text/plain')})
342
+ event.preventDefault()
343
+ return
344
+ else
345
+ # get current selection & range
346
+ selection = @selection()
347
+ selection.placeMarker()
348
+
349
+ sanitizer = jQuery('#mercury_sanitizer', @document).focus()
350
+
351
+ # set 1ms timeout to allow paste event to complete
352
+ setTimeout 1, =>
353
+ # sanitize the content
354
+ content = @sanitize(sanitizer)
355
+
356
+ # move cursor back to original element & position
357
+ selection.selectMarker(@element)
358
+ selection.removeMarker()
359
+
360
+ # paste sanitized content
361
+ @element.focus()
362
+ @execCommand('insertHTML', {value: content})
363
+
364
+
365
+ sanitize: (sanitizer) ->
366
+ # always remove nested regions
367
+ sanitizer.find(".#{Mercury.config.regions.className}").remove()
368
+
369
+ if Mercury.config.pasting.sanitize
370
+ switch Mercury.config.pasting.sanitize
371
+ when 'blacklist'
372
+ # todo: finish writing black list functionality
373
+ sanitizer.find('[style]').removeAttr('style')
374
+ sanitizer.find('[class="Apple-style-span"]').removeClass('Apple-style-span')
375
+ content = sanitizer.html()
376
+ when 'whitelist'
377
+ for element in sanitizer.find('*')
378
+ allowed = false
379
+ for allowedTag, allowedAttributes of Mercury.config.pasting.whitelist
380
+ if element.tagName.toLowerCase() == allowedTag.toLowerCase()
381
+ allowed = true
382
+ for attr in jQuery(element.attributes)
383
+ jQuery(element).removeAttr(attr.name) unless attr.name in allowedAttributes
384
+ break
385
+ jQuery(element).replaceWith(jQuery(element).contents()) unless allowed
386
+ content = sanitizer.html()
387
+ else content = sanitizer.text()
388
+ else
389
+ # force text if it looks like it's from word/pages, even if there's no sanitizing requested
390
+ content = sanitizer.html()
391
+ if content.indexOf('<!--StartFragment-->') > -1 || content.indexOf('="mso-') > -1 || content.indexOf('<o:') > -1 || content.indexOf('="Mso') > -1
392
+ content = sanitizer.text()
393
+
394
+ sanitizer.html('')
395
+ return content
396
+
397
+
398
+ # Custom actions (eg. things that execCommand doesn't do, or doesn't do well)
399
+ @actions: {
400
+
401
+ insertRowBefore: -> Mercury.tableEditor.addRow('before')
402
+
403
+ insertRowAfter: -> Mercury.tableEditor.addRow('after')
404
+
405
+ insertColumnBefore: -> Mercury.tableEditor.addColumn('before')
406
+
407
+ insertColumnAfter: -> Mercury.tableEditor.addColumn('after')
408
+
409
+ deleteColumn: -> Mercury.tableEditor.removeColumn()
410
+
411
+ deleteRow: -> Mercury.tableEditor.removeRow()
412
+
413
+ increaseColspan: -> Mercury.tableEditor.increaseColspan()
414
+
415
+ decreaseColspan: -> Mercury.tableEditor.decreaseColspan()
416
+
417
+ increaseRowspan: -> Mercury.tableEditor.increaseRowspan()
418
+
419
+ decreaseRowspan: -> Mercury.tableEditor.decreaseRowspan()
420
+
421
+ undo: -> @content(@history.undo())
422
+
423
+ redo: -> @content(@history.redo())
424
+
425
+ horizontalRule: -> this.execCommand('insertHorizontalRule')
426
+
427
+ removeFormatting: (selection) -> selection.insertTextNode(selection.textContent())
428
+
429
+ backColor: (selection, options) -> selection.wrap("<span style=\"background-color:#{options.value.toHex()}\">", true)
430
+
431
+ overline: (selection) -> selection.wrap('<span style="text-decoration:overline">', true)
432
+
433
+ style: (selection, options) -> selection.wrap("<span class=\"#{options.value}\">", true)
434
+
435
+ replaceHTML: (selection, options) -> @content(options.value)
436
+
437
+ insertImage: (selection, options) -> @execCommand('insertHTML', {value: jQuery('<img/>', options.value)})
438
+
439
+ insertTable: (selection, options) -> @execCommand('insertHTML', {value: options.value})
440
+
441
+ insertLink: (selection, options) ->
442
+ anchor = jQuery("<#{options.value.tagName}>", @document).attr(options.value.attrs).html(options.value.content)
443
+ selection.insertNode(anchor)
444
+
445
+ replaceLink: (selection, options) ->
446
+ anchor = jQuery("<#{options.value.tagName}>", @document).attr(options.value.attrs).html(options.value.content)
447
+ selection.selectNode(options.node)
448
+ html = jQuery('<div>').html(selection.content()).find('a').html()
449
+ selection.replace(jQuery(anchor, selection.context).html(html))
450
+
451
+ insertSnippet: (selection, options) ->
452
+ snippet = options.value
453
+ if (existing = @element.find("[data-snippet=#{snippet.identity}]")).length
454
+ selection.selectNode(existing.get(0))
455
+ selection.insertNode(snippet.getHTML(@document))
456
+
457
+ editSnippet: ->
458
+ return unless @snippet
459
+ snippet = Mercury.Snippet.find(@snippet.data('snippet'))
460
+ snippet.displayOptions()
461
+
462
+ removeSnippet: ->
463
+ @snippet.remove() if @snippet
464
+ Mercury.trigger('hide:toolbar', {type: 'snippet', immediately: true})
465
+ }
466
+
467
+
468
+ # Helper class for managing selection and getting information from it
469
+ class Mercury.Regions.Editable.Selection
470
+
471
+ constructor: (@selection, @context) ->
472
+ return unless @selection.rangeCount >= 1
473
+ @range = @selection.getRangeAt(0)
474
+ @fragment = @range.cloneContents()
475
+ @clone = @range.cloneRange()
476
+ @collapsed = @selection.isCollapsed
477
+
478
+
479
+ commonAncestor: (onlyTag = false) ->
480
+ return null unless @range
481
+ ancestor = @range.commonAncestorContainer
482
+ ancestor = ancestor.parentNode if ancestor.nodeType == 3 && onlyTag
483
+ return jQuery(ancestor)
484
+
485
+
486
+ wrap: (element, replace = false) ->
487
+ element = jQuery(element, @context).html(@fragment)
488
+ @replace(element) if replace
489
+ return element
490
+
491
+
492
+ textContent: ->
493
+ return @range.cloneContents().textContent
494
+
495
+
496
+ content: ->
497
+ return @range.cloneContents()
498
+
499
+
500
+ is: (elementType) ->
501
+ content = @content()
502
+ return jQuery(content.firstChild) if jQuery(content).length == 1 && jQuery(content.firstChild).is(elementType)
503
+ return false
504
+
505
+
506
+ forceSelection: (element) ->
507
+ return unless jQuery.browser.webkit
508
+ range = @context.createRange()
509
+
510
+ if @range
511
+ if @commonAncestor(true).closest('.mercury-snippet').length
512
+ lastChild = @context.createTextNode('\x00')
513
+ element.appendChild(lastChild)
514
+ else
515
+ if element.lastChild && element.lastChild.nodeType == 3 && element.lastChild.textContent.replace(/^[\s+|\n+]|[\s+|\n+]$/, '') == ''
516
+ lastChild = element.lastChild
517
+ element.lastChild.textContent = '\x00'
518
+ else
519
+ lastChild = @context.createTextNode('\x00')
520
+ element.appendChild(lastChild)
521
+
522
+ if lastChild
523
+ range.setStartBefore(lastChild)
524
+ range.setEndBefore(lastChild)
525
+ @selection.addRange(range)
526
+
527
+
528
+ selectMarker: (context) ->
529
+ markers = context.find('em.mercury-marker')
530
+ return unless markers.length
531
+
532
+ range = @context.createRange()
533
+ range.setStartBefore(markers.get(0))
534
+ range.setEndBefore(markers.get(1)) if markers.length >= 2
535
+
536
+ markers.remove()
537
+
538
+ @selection.removeAllRanges()
539
+ @selection.addRange(range)
540
+
541
+
542
+ placeMarker: ->
543
+ return unless @range
544
+
545
+ @startMarker = jQuery('<em class="mercury-marker"/>', @context).get(0)
546
+ @endMarker = jQuery('<em class="mercury-marker"/>', @context).get(0)
547
+
548
+ # put a single marker (the end)
549
+ rangeEnd = @range.cloneRange()
550
+ rangeEnd.collapse(false)
551
+ rangeEnd.insertNode(@endMarker)
552
+
553
+ unless @range.collapsed
554
+ # put a start marker
555
+ rangeStart = @range.cloneRange()
556
+ rangeStart.collapse(true)
557
+ rangeStart.insertNode(@startMarker)
558
+
559
+ @selection.removeAllRanges()
560
+ @selection.addRange(@range)
561
+
562
+
563
+ removeMarker: ->
564
+ jQuery(@startMarker).remove()
565
+ jQuery(@endMarker).remove()
566
+
567
+
568
+ insertTextNode: (string) ->
569
+ node = @context.createTextNode(string)
570
+ @range.extractContents()
571
+ @range.insertNode(node)
572
+ @range.selectNodeContents(node)
573
+ @selection.addRange(@range)
574
+
575
+
576
+ insertNode: (element) ->
577
+ element = element.get(0) if element.get
578
+ element = jQuery(element, @context).get(0) if jQuery.type(element) == 'string'
579
+
580
+ @range.deleteContents()
581
+ @range.insertNode(element)
582
+ @range.selectNodeContents(element)
583
+ @selection.addRange(@range)
584
+
585
+
586
+ selectNode: (node, removeExisting = false) ->
587
+ @range.selectNode(node)
588
+ @selection.removeAllRanges() if removeExisting
589
+ @selection.addRange(@range)
590
+
591
+
592
+ replace: (element, collapse) ->
593
+ element = element.get(0) if element.get
594
+ element = jQuery(element, @context).get(0) if jQuery.type(element) == 'string'
595
+
596
+ @range.deleteContents()
597
+ @range.insertNode(element)
598
+ @range.selectNodeContents(element)
599
+ @selection.addRange(@range)
600
+ @range.collapse(false) if collapse