mercury-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.rdoc +152 -0
- data/VERSION +1 -0
- data/app/assets/images/mercury/button.png +0 -0
- data/app/assets/images/mercury/clippy.png +0 -0
- data/app/assets/images/mercury/default-snippet.png +0 -0
- data/app/assets/images/mercury/loading-dark.gif +0 -0
- data/app/assets/images/mercury/loading-light.gif +0 -0
- data/app/assets/images/mercury/search-icon.png +0 -0
- data/app/assets/images/mercury/toolbar/editable/buttons.png +0 -0
- data/app/assets/images/mercury/toolbar/markupable/buttons.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/_expander.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/_pressed.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/historypanel.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/insertcharacter.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/insertlink.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/insertmedia.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/inserttable.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/inspectorpanel.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/notespanel.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/objectspanel.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/preview.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/redo.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/save.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/todospanel.png +0 -0
- data/app/assets/images/mercury/toolbar/primary/undo.png +0 -0
- data/app/assets/images/mercury/toolbar/snippetable/buttons.png +0 -0
- data/app/assets/javascripts/mercury.js +30 -0
- data/app/assets/javascripts/mercury/dialog.js.coffee +75 -0
- data/app/assets/javascripts/mercury/dialogs/backcolor.js.coffee +6 -0
- data/app/assets/javascripts/mercury/dialogs/forecolor.js.coffee +6 -0
- data/app/assets/javascripts/mercury/dialogs/formatblock.js.coffee +4 -0
- data/app/assets/javascripts/mercury/dialogs/objectspanel.js.coffee +10 -0
- data/app/assets/javascripts/mercury/dialogs/style.js.coffee +4 -0
- data/app/assets/javascripts/mercury/history_buffer.js.coffee +30 -0
- data/app/assets/javascripts/mercury/mercury.js.coffee +293 -0
- data/app/assets/javascripts/mercury/modal.js.coffee +177 -0
- data/app/assets/javascripts/mercury/modals/htmleditor.js.coffee +10 -0
- data/app/assets/javascripts/mercury/modals/insertcharacter.js.coffee +4 -0
- data/app/assets/javascripts/mercury/modals/insertlink.js.coffee +92 -0
- data/app/assets/javascripts/mercury/modals/insertmedia.js.coffee +72 -0
- data/app/assets/javascripts/mercury/modals/insertsnippet.js.coffee +11 -0
- data/app/assets/javascripts/mercury/modals/inserttable.js.coffee +56 -0
- data/app/assets/javascripts/mercury/native_extensions.js.coffee +47 -0
- data/app/assets/javascripts/mercury/page_editor.js.coffee +139 -0
- data/app/assets/javascripts/mercury/palette.js.coffee +29 -0
- data/app/assets/javascripts/mercury/panel.js.coffee +97 -0
- data/app/assets/javascripts/mercury/region.js.coffee +103 -0
- data/app/assets/javascripts/mercury/regions/editable.js.coffee +546 -0
- data/app/assets/javascripts/mercury/regions/markupable.js.coffee +380 -0
- data/app/assets/javascripts/mercury/regions/snippetable.js.coffee +127 -0
- data/app/assets/javascripts/mercury/select.js.coffee +40 -0
- data/app/assets/javascripts/mercury/snippet.js.coffee +92 -0
- data/app/assets/javascripts/mercury/snippet_toolbar.js.coffee +69 -0
- data/app/assets/javascripts/mercury/statusbar.js.coffee +25 -0
- data/app/assets/javascripts/mercury/table_editor.js.coffee +266 -0
- data/app/assets/javascripts/mercury/toolbar.button.js.coffee +152 -0
- data/app/assets/javascripts/mercury/toolbar.button_group.js.coffee +42 -0
- data/app/assets/javascripts/mercury/toolbar.expander.js.coffee +56 -0
- data/app/assets/javascripts/mercury/toolbar.js.coffee +72 -0
- data/app/assets/javascripts/mercury/tooltip.js.coffee +67 -0
- data/app/assets/javascripts/mercury/uploader.js.coffee +213 -0
- data/app/assets/javascripts/mercury/websocket.js.coffee +34 -0
- data/app/assets/stylesheets/mercury.css +31 -0
- data/app/assets/stylesheets/mercury/dialog.scss +178 -0
- data/app/assets/stylesheets/mercury/mercury.scss +119 -0
- data/app/assets/stylesheets/mercury/modal.scss +192 -0
- data/app/assets/stylesheets/mercury/statusbar.scss +23 -0
- data/app/assets/stylesheets/mercury/toolbar.scss +417 -0
- data/app/assets/stylesheets/mercury/tooltip.scss +26 -0
- data/app/assets/stylesheets/mercury/uploader.scss +109 -0
- data/app/controllers/images_controller.rb +19 -0
- data/app/controllers/mercury_controller.rb +20 -0
- data/app/models/image.rb +14 -0
- data/app/views/layouts/mercury.html.haml +12 -0
- data/app/views/mercury/modals/character.html.haml +252 -0
- data/app/views/mercury/modals/htmleditor.html.haml +8 -0
- data/app/views/mercury/modals/link.html.haml +31 -0
- data/app/views/mercury/modals/media.html.haml +33 -0
- data/app/views/mercury/modals/sanitizer.html.haml +4 -0
- data/app/views/mercury/modals/table.html.haml +49 -0
- data/app/views/mercury/palettes/backcolor.html.haml +79 -0
- data/app/views/mercury/palettes/forecolor.html.haml +79 -0
- data/app/views/mercury/panels/history.html.haml +0 -0
- data/app/views/mercury/panels/notes.html.haml +0 -0
- data/app/views/mercury/panels/snippets.html.haml +10 -0
- data/app/views/mercury/selects/formatblock.html.haml +10 -0
- data/app/views/mercury/selects/style.html.haml +4 -0
- data/app/views/mercury/snippets/example.html.haml +2 -0
- data/app/views/mercury/snippets/example_options.html.haml +16 -0
- data/config/engine.rb +6 -0
- data/config/routes.rb +15 -0
- data/db/migrate/20110526035601_create_images.rb +11 -0
- data/features/editing/basic.feature +11 -0
- data/features/step_definitions/debug_steps.rb +14 -0
- data/features/step_definitions/web_steps.rb +211 -0
- data/features/support/env.rb +46 -0
- data/features/support/paths.rb +35 -0
- data/features/support/selectors.rb +42 -0
- data/lib/mercury-rails.rb +4 -0
- data/log/.gitkeep +0 -0
- data/mercury-rails.gemspec +230 -0
- data/spec/javascripts/mercury/dialog_spec.js.coffee +258 -0
- data/spec/javascripts/mercury/history_buffer_spec.js.coffee +79 -0
- data/spec/javascripts/mercury/mercury_spec.js.coffee +52 -0
- data/spec/javascripts/mercury/native_extensions_spec.js.coffee +66 -0
- data/spec/javascripts/mercury/page_editor_spec.js.coffee +435 -0
- data/spec/javascripts/mercury/palette_spec.js.coffee +51 -0
- data/spec/javascripts/mercury/panel_spec.js.coffee +147 -0
- data/spec/javascripts/mercury/region_spec.js.coffee +261 -0
- data/spec/javascripts/mercury/regions/_editable_.js.coffee +0 -0
- data/spec/javascripts/mercury/regions/_markupable_.js.coffee +0 -0
- data/spec/javascripts/mercury/regions/snippetable_spec.js.coffee +368 -0
- data/spec/javascripts/mercury/select_spec.js.coffee +51 -0
- data/spec/javascripts/mercury/snippet_spec.js.coffee +246 -0
- data/spec/javascripts/mercury/snippet_toolbar_spec.js.coffee +186 -0
- data/spec/javascripts/mercury/statusbar_spec.js.coffee +78 -0
- data/spec/javascripts/mercury/table_editor_spec.js.coffee +192 -0
- data/spec/javascripts/mercury/toolbar.button_group_spec.js.coffee +92 -0
- data/spec/javascripts/mercury/toolbar.button_spec.js.coffee +341 -0
- data/spec/javascripts/mercury/toolbar.expander_spec.js.coffee +120 -0
- data/spec/javascripts/mercury/toolbar_spec.js.coffee +152 -0
- data/spec/javascripts/mercury/tooltip_spec.js.coffee +188 -0
- data/spec/javascripts/mercury/uploader_spec.js.coffee +512 -0
- data/spec/javascripts/responses/blank.html +1 -0
- data/spec/javascripts/spec_helper.js +513 -0
- data/spec/javascripts/templates/mercury/dialog.html +2 -0
- data/spec/javascripts/templates/mercury/page_editor.html +24 -0
- data/spec/javascripts/templates/mercury/palette.html +16 -0
- data/spec/javascripts/templates/mercury/panel.html +16 -0
- data/spec/javascripts/templates/mercury/region.html +2 -0
- data/spec/javascripts/templates/mercury/regions/snippetable.html +4 -0
- data/spec/javascripts/templates/mercury/select.html +16 -0
- data/spec/javascripts/templates/mercury/snippet.html +1 -0
- data/spec/javascripts/templates/mercury/snippet_toolbar.html +16 -0
- data/spec/javascripts/templates/mercury/statusbar.html +7 -0
- data/spec/javascripts/templates/mercury/table_editor.html +65 -0
- data/spec/javascripts/templates/mercury/toolbar.button.html +64 -0
- data/spec/javascripts/templates/mercury/toolbar.button_group.html +9 -0
- data/spec/javascripts/templates/mercury/toolbar.expander.html +18 -0
- data/spec/javascripts/templates/mercury/toolbar.html +10 -0
- data/spec/javascripts/templates/mercury/tooltip.html +12 -0
- data/spec/javascripts/templates/mercury/uploader.html +11 -0
- data/vendor/assets/javascripts/jquery-1.6.js +8865 -0
- data/vendor/assets/javascripts/jquery-ui-1.8.13.custom.min.js +249 -0
- data/vendor/assets/javascripts/jquery-ui-1.8.13.sortable.custom.js +1078 -0
- data/vendor/assets/javascripts/jquery.easing.js +173 -0
- data/vendor/assets/javascripts/jquery.json2.js +178 -0
- data/vendor/assets/javascripts/jquery.serialize_object.js +16 -0
- data/vendor/assets/javascripts/jquery.ujs.js +289 -0
- data/vendor/assets/javascripts/liquidmetal.js +88 -0
- data/vendor/assets/javascripts/showdown.js +1362 -0
- metadata +364 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
class @Mercury.PageEditor
|
2
|
+
|
3
|
+
# options
|
4
|
+
# saveStyle: 'form', or 'json' (defaults to json)
|
5
|
+
# ignoredLinks: an array containing classes for links to ignore (eg. lightbox or accordian controls)
|
6
|
+
constructor: (@saveUrl = null, @options = {}) ->
|
7
|
+
throw "Mercury.PageEditor is unsupported in this client. Supported browsers are chrome 10+, firefix 4+, and safari 5+." unless Mercury.supported
|
8
|
+
throw "Mercury.PageEditor can only be instantiated once." if window.mercuryInstance
|
9
|
+
|
10
|
+
window.mercuryInstance = @
|
11
|
+
@regions = []
|
12
|
+
@initializeInterface()
|
13
|
+
Mercury.csrfToken = token if token = $('meta[name="csrf-token"]').attr('content')
|
14
|
+
|
15
|
+
|
16
|
+
initializeInterface: ->
|
17
|
+
@focusableElement = $('<input>', {type: 'text', style: 'position:absolute;opacity:0'}).appendTo(@options.appendTo ? 'body')
|
18
|
+
@iframe = $('<iframe>', {class: 'mercury-iframe', seamless: 'true', frameborder: '0', src: 'about:blank', style: 'position:absolute;top:0;width:100%;visibility:hidden'})
|
19
|
+
@iframe.load => @initializeFrame()
|
20
|
+
@iframe.attr('src', @iframeSrc())
|
21
|
+
@iframe.appendTo($(@options.appendTo).get(0) ? 'body')
|
22
|
+
|
23
|
+
@toolbar = new Mercury.Toolbar(@options)
|
24
|
+
@statusbar = new Mercury.Statusbar(@options)
|
25
|
+
|
26
|
+
|
27
|
+
initializeFrame: ->
|
28
|
+
try
|
29
|
+
return if @iframe.data('loaded')
|
30
|
+
@iframe.data('loaded', true)
|
31
|
+
@document = $(@iframe.get(0).contentWindow.document)
|
32
|
+
$("<style mercury-styles=\"true\">").html(Mercury.config.injectedStyles).appendTo(@document.find('head'))
|
33
|
+
|
34
|
+
@bindEvents()
|
35
|
+
@initializeRegions()
|
36
|
+
@finalizeInterface()
|
37
|
+
|
38
|
+
@iframe.css({visibility: 'visible'})
|
39
|
+
catch error
|
40
|
+
alert("Mercury.PageEditor failed to load: #{error}\n\nPlease try refreshing.")
|
41
|
+
|
42
|
+
|
43
|
+
initializeRegions: ->
|
44
|
+
@buildRegion($(region)) for region in $('.mercury-region', @document)
|
45
|
+
for region in @regions
|
46
|
+
if region.focus
|
47
|
+
region.focus()
|
48
|
+
break
|
49
|
+
|
50
|
+
|
51
|
+
buildRegion: (region) ->
|
52
|
+
try
|
53
|
+
type = region.data('type').titleize()
|
54
|
+
@regions.push(new Mercury.Regions[type](region, @iframe.get(0).contentWindow))
|
55
|
+
catch error
|
56
|
+
alert(error) if Mercury.debug
|
57
|
+
alert("Region type is malformed, no data-type provided, or \"#{type}\" is unknown.")
|
58
|
+
|
59
|
+
|
60
|
+
finalizeInterface: ->
|
61
|
+
@snippetToolbar = new Mercury.SnippetToolbar(@document)
|
62
|
+
|
63
|
+
@hijackLinks()
|
64
|
+
@resize()
|
65
|
+
|
66
|
+
|
67
|
+
bindEvents: ->
|
68
|
+
Mercury.bind 'initialize:frame', => setTimeout(@initializeFrame, 100)
|
69
|
+
Mercury.bind 'focus:frame', => @iframe.focus()
|
70
|
+
Mercury.bind 'focus:window', => setTimeout((=> @focusableElement.focus()), 10)
|
71
|
+
|
72
|
+
Mercury.bind 'action', (event, options) =>
|
73
|
+
@save() if options.action == 'save'
|
74
|
+
|
75
|
+
@document.mousedown (event) ->
|
76
|
+
Mercury.trigger('hide:dialogs')
|
77
|
+
Mercury.trigger('unfocus:regions') unless $(event.target).closest('.mercury-region').get(0) == Mercury.region.element.get(0)
|
78
|
+
|
79
|
+
$(window).resize => @resize()
|
80
|
+
window.onbeforeunload = @beforeUnload
|
81
|
+
|
82
|
+
|
83
|
+
resize: ->
|
84
|
+
width = $(window).width()
|
85
|
+
height = $(window).height()
|
86
|
+
toolbarHeight = @toolbar.height()
|
87
|
+
statusbarHeight = @statusbar.height()
|
88
|
+
|
89
|
+
Mercury.displayRect = {top: toolbarHeight, left: 0, width: width, height: height - statusbarHeight - toolbarHeight}
|
90
|
+
|
91
|
+
@iframe.css {
|
92
|
+
top: toolbarHeight,
|
93
|
+
width: width,
|
94
|
+
height: height - statusbarHeight - toolbarHeight
|
95
|
+
}
|
96
|
+
|
97
|
+
Mercury.trigger('resize')
|
98
|
+
|
99
|
+
|
100
|
+
iframeSrc: (url = null) ->
|
101
|
+
(url ? window.location.href).replace(/([http|https]:\/\/.[^\/]*)\/edit\/?(.*)/i, "$1/$2")
|
102
|
+
|
103
|
+
|
104
|
+
hijackLinks: ->
|
105
|
+
for link in $('a', @document)
|
106
|
+
ignored = false
|
107
|
+
for classname in @options.ignoredLinks || []
|
108
|
+
if $(link).hasClass(classname)
|
109
|
+
ignored = true
|
110
|
+
continue
|
111
|
+
if !ignored && (link.target == '' || link.target == '_self') && !$(link).closest('.mercury-region').length
|
112
|
+
$(link).attr('target', '_top')
|
113
|
+
|
114
|
+
|
115
|
+
beforeUnload: ->
|
116
|
+
if Mercury.changes && !Mercury.silent
|
117
|
+
return "You have unsaved changes. Are you sure you want to leave without saving them first?"
|
118
|
+
return null
|
119
|
+
|
120
|
+
|
121
|
+
save: ->
|
122
|
+
url = @saveUrl ? @iframeSrc()
|
123
|
+
data = @serialize()
|
124
|
+
Mercury.log('saving', data)
|
125
|
+
data = $.toJSON(data) unless @options.saveStyle == 'form'
|
126
|
+
$.ajax url, {
|
127
|
+
type: 'POST'
|
128
|
+
data: {content: data}
|
129
|
+
success: =>
|
130
|
+
Mercury.changes = false
|
131
|
+
error: =>
|
132
|
+
alert("Mercury was unable to save to the url: #{url}")
|
133
|
+
}
|
134
|
+
|
135
|
+
|
136
|
+
serialize: ->
|
137
|
+
serialized = {}
|
138
|
+
serialized[region.name] = region.serialize() for region in @regions
|
139
|
+
return serialized
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class @Mercury.Palette extends Mercury.Dialog
|
2
|
+
|
3
|
+
constructor: (@url, @name, @options = {}) ->
|
4
|
+
super
|
5
|
+
|
6
|
+
|
7
|
+
build: ->
|
8
|
+
@element = $('<div>', {class: "mercury-palette mercury-#{@name}-palette loading", style: 'display:none'})
|
9
|
+
@element.appendTo($(@options.appendTo).get(0) ? 'body')
|
10
|
+
|
11
|
+
|
12
|
+
bindEvents: ->
|
13
|
+
Mercury.bind 'hide:dialogs', (event, dialog) => @hide() unless dialog == @
|
14
|
+
super
|
15
|
+
|
16
|
+
|
17
|
+
position: (keepVisible) ->
|
18
|
+
@element.css({top: 0, left: 0, display: 'block', visibility: 'hidden'})
|
19
|
+
position = @button.offset()
|
20
|
+
width = @element.width()
|
21
|
+
|
22
|
+
position.left = position.left - width + @button.width() if position.left + width > $(window).width()
|
23
|
+
|
24
|
+
@element.css {
|
25
|
+
top: position.top + @button.height(),
|
26
|
+
left: position.left,
|
27
|
+
display: if keepVisible then 'block' else 'none',
|
28
|
+
visibility: 'visible'
|
29
|
+
}
|
@@ -0,0 +1,97 @@
|
|
1
|
+
class @Mercury.Panel extends Mercury.Dialog
|
2
|
+
|
3
|
+
constructor: (@url, @name, @options = {}) ->
|
4
|
+
super
|
5
|
+
|
6
|
+
|
7
|
+
build: ->
|
8
|
+
@element = $('<div>', {class: 'mercury-panel loading', style: 'display:none;'})
|
9
|
+
@titleElement = $("<h1>#{@options.title}</h1>").appendTo(@element)
|
10
|
+
@paneElement = $('<div>', {class: 'mercury-panel-pane'}).appendTo(@element)
|
11
|
+
|
12
|
+
@element.appendTo($(@options.appendTo).get(0) ? 'body');
|
13
|
+
|
14
|
+
|
15
|
+
bindEvents: ->
|
16
|
+
Mercury.bind 'resize', => @position(@visible)
|
17
|
+
Mercury.bind 'hide:panels', (event, panel) =>
|
18
|
+
unless panel == @
|
19
|
+
@button.removeClass('pressed')
|
20
|
+
@hide()
|
21
|
+
|
22
|
+
@element.mousedown (event) -> event.stopPropagation()
|
23
|
+
|
24
|
+
super
|
25
|
+
|
26
|
+
|
27
|
+
show: ->
|
28
|
+
Mercury.trigger('hide:panels', @)
|
29
|
+
super
|
30
|
+
|
31
|
+
|
32
|
+
resize: ->
|
33
|
+
@paneElement.css({display: 'none'})
|
34
|
+
preWidth = @element.width()
|
35
|
+
|
36
|
+
@paneElement.css({visibility: 'hidden', width: 'auto', display: 'block'})
|
37
|
+
postWidth = @element.width()
|
38
|
+
|
39
|
+
@paneElement.css({visibility: 'visible', display: 'none'})
|
40
|
+
position = @element.offset()
|
41
|
+
@element.animate {left: position.left - (postWidth - preWidth), width: postWidth}, 200, 'easeInOutSine', =>
|
42
|
+
@paneElement.css({display: 'block', width: postWidth})
|
43
|
+
@makeDraggable()
|
44
|
+
|
45
|
+
@hide() unless @visible
|
46
|
+
|
47
|
+
|
48
|
+
position: (keepVisible) ->
|
49
|
+
@element.css({display: 'block', visibility: 'hidden'})
|
50
|
+
offset = @element.offset()
|
51
|
+
elementWidth = @element.width()
|
52
|
+
height = Mercury.displayRect.height - 16
|
53
|
+
|
54
|
+
paneHeight = height - @titleElement.outerHeight()
|
55
|
+
@paneElement.css({height: paneHeight, overflowY: if paneHeight < 30 then 'hidden' else 'auto'})
|
56
|
+
|
57
|
+
left = Mercury.displayRect.width - elementWidth - 20 unless @moved
|
58
|
+
left = 8 if left <= 8
|
59
|
+
|
60
|
+
if @pinned || elementWidth + offset.left > Mercury.displayRect.width - 20
|
61
|
+
left = Mercury.displayRect.width - elementWidth - 20
|
62
|
+
|
63
|
+
@element.css {
|
64
|
+
top: Mercury.displayRect.top + 8,
|
65
|
+
left: left,
|
66
|
+
height: height,
|
67
|
+
display: if keepVisible then 'block' else 'none',
|
68
|
+
visibility: 'visible'
|
69
|
+
}
|
70
|
+
|
71
|
+
@makeDraggable()
|
72
|
+
@element.hide() unless keepVisible
|
73
|
+
|
74
|
+
|
75
|
+
loadContent: (data) ->
|
76
|
+
@loaded = true
|
77
|
+
@element.removeClass('loading')
|
78
|
+
@paneElement.css({visibility: 'hidden'})
|
79
|
+
@paneElement.html(data)
|
80
|
+
|
81
|
+
|
82
|
+
makeDraggable: ->
|
83
|
+
elementWidth = @element.width()
|
84
|
+
@element.draggable {
|
85
|
+
handle: 'h1',
|
86
|
+
axis: 'x',
|
87
|
+
opacity: 0.70
|
88
|
+
scroll: false,
|
89
|
+
addClasses: false,
|
90
|
+
iframeFix: true,
|
91
|
+
containment: [8, 0, Mercury.displayRect.width - elementWidth - 20, 0] #[x1, y1, x2, y2]
|
92
|
+
stop: =>
|
93
|
+
left = @element.offset().left
|
94
|
+
@moved = true
|
95
|
+
@pinned = if left > Mercury.displayRect.width - elementWidth - 30 then true else false
|
96
|
+
return true
|
97
|
+
}
|
@@ -0,0 +1,103 @@
|
|
1
|
+
class @Mercury.Region
|
2
|
+
type = 'region'
|
3
|
+
|
4
|
+
constructor: (@element, @window, @options = {}) ->
|
5
|
+
@type = 'region' unless @type
|
6
|
+
Mercury.log("building #{@type}", @element, @options)
|
7
|
+
|
8
|
+
@document = @window.document
|
9
|
+
@name = @element.attr('id')
|
10
|
+
@history = new Mercury.HistoryBuffer()
|
11
|
+
@build()
|
12
|
+
@bindEvents()
|
13
|
+
@pushHistory()
|
14
|
+
|
15
|
+
|
16
|
+
build: ->
|
17
|
+
|
18
|
+
|
19
|
+
focus: ->
|
20
|
+
|
21
|
+
|
22
|
+
bindEvents: ->
|
23
|
+
Mercury.bind 'mode', (event, options) =>
|
24
|
+
@togglePreview() if options.mode == 'preview'
|
25
|
+
|
26
|
+
Mercury.bind 'focus:frame', =>
|
27
|
+
return if @previewing
|
28
|
+
return unless Mercury.region == @
|
29
|
+
@focus()
|
30
|
+
|
31
|
+
Mercury.bind 'action', (event, options) =>
|
32
|
+
return if @previewing
|
33
|
+
return unless Mercury.region == @
|
34
|
+
@execCommand(options.action, options) if options.action
|
35
|
+
|
36
|
+
@element.mousemove (event) =>
|
37
|
+
return if @previewing
|
38
|
+
return unless Mercury.region == @
|
39
|
+
snippet = $(event.target).closest('.mercury-snippet')
|
40
|
+
if snippet.length
|
41
|
+
@snippet = snippet
|
42
|
+
Mercury.trigger('show:toolbar', {type: 'snippet', snippet: @snippet})
|
43
|
+
|
44
|
+
@element.mouseout (event) =>
|
45
|
+
return if @previewing
|
46
|
+
Mercury.trigger('hide:toolbar', {type: 'snippet', immediately: false})
|
47
|
+
|
48
|
+
|
49
|
+
html: (value = null, filterSnippets = false) ->
|
50
|
+
if value != null
|
51
|
+
@element.html(value)
|
52
|
+
else
|
53
|
+
# sanitize the html before we return it
|
54
|
+
container = $('<div>').appendTo(@document.createDocumentFragment())
|
55
|
+
container.html(@element.html().replace(/^\s+|\s+$/g, ''))
|
56
|
+
|
57
|
+
# replace snippet contents to be an identifier
|
58
|
+
if filterSnippets then for snippet, index in container.find('.mercury-snippet')
|
59
|
+
snippet = $(snippet)
|
60
|
+
snippet.attr({contenteditable: null, 'data-version': null})
|
61
|
+
snippet.html("[#{snippet.data('snippet')}]")
|
62
|
+
|
63
|
+
return container.html()
|
64
|
+
|
65
|
+
|
66
|
+
togglePreview: ->
|
67
|
+
if @previewing
|
68
|
+
@previewing = false
|
69
|
+
@element.addClass('mercury-region').removeClass('mercury-region-preview')
|
70
|
+
@focus() if Mercury.region == @
|
71
|
+
else
|
72
|
+
@previewing = true
|
73
|
+
@element.addClass('mercury-region-preview').removeClass('mercury-region')
|
74
|
+
Mercury.trigger('region:blurred', {region: @})
|
75
|
+
|
76
|
+
|
77
|
+
execCommand: (action, options = {}) ->
|
78
|
+
@focus()
|
79
|
+
@pushHistory() unless action == 'redo'
|
80
|
+
|
81
|
+
Mercury.log('execCommand', action, options.value)
|
82
|
+
Mercury.changes = true
|
83
|
+
|
84
|
+
|
85
|
+
pushHistory: ->
|
86
|
+
@history.push(@html())
|
87
|
+
|
88
|
+
|
89
|
+
snippets: ->
|
90
|
+
snippets = {}
|
91
|
+
for element in @element.find('[data-snippet]')
|
92
|
+
snippet = Mercury.Snippet.find($(element).data('snippet'))
|
93
|
+
snippet.setVersion($(element).data('version'))
|
94
|
+
snippets[snippet.identity] = snippet.serialize()
|
95
|
+
return snippets
|
96
|
+
|
97
|
+
|
98
|
+
serialize: ->
|
99
|
+
return {
|
100
|
+
type: @type,
|
101
|
+
value: @html(null, true)
|
102
|
+
snippets: @snippets()
|
103
|
+
}
|
@@ -0,0 +1,546 @@
|
|
1
|
+
class @Mercury.Regions.Editable extends Mercury.Region
|
2
|
+
type = 'editable'
|
3
|
+
|
4
|
+
constructor: (@element, @window, @options = {}) ->
|
5
|
+
@type = 'editable'
|
6
|
+
super
|
7
|
+
|
8
|
+
|
9
|
+
build: ->
|
10
|
+
# mozilla: set some initial content so everything works correctly
|
11
|
+
@html(' ') if $.browser.mozilla && @html() == ''
|
12
|
+
|
13
|
+
# set overflow just in case
|
14
|
+
@element.data({originalOverflow: @element.css('overflow')})
|
15
|
+
@element.css({overflow: 'auto'})
|
16
|
+
|
17
|
+
# mozilla: there's some weird behavior when the element isn't a div
|
18
|
+
@specialContainer = $.browser.mozilla && @element.get(0).tagName != 'DIV'
|
19
|
+
|
20
|
+
# make it editable
|
21
|
+
# gecko: in this makes double clicking in textareas fail: https://bugzilla.mozilla.org/show_bug.cgi?id=490367
|
22
|
+
@element.get(0).contentEditable = true
|
23
|
+
|
24
|
+
# make all snippets not editable, and set their versions to 1
|
25
|
+
for element in @element.find('.mercury-snippet')
|
26
|
+
element.contentEditable = false
|
27
|
+
$(element).attr('data-version', '1')
|
28
|
+
|
29
|
+
# add the basic editor settings to the document (only once)
|
30
|
+
unless @document.mercuryEditing
|
31
|
+
@document.execCommand('styleWithCSS', false, false)
|
32
|
+
@document.execCommand('insertBROnReturn', false, true)
|
33
|
+
@document.execCommand('enableInlineTableEditing', false, false)
|
34
|
+
@document.execCommand('enableObjectResizing', false, false)
|
35
|
+
@document.mercuryEditing = true
|
36
|
+
|
37
|
+
|
38
|
+
bindEvents: ->
|
39
|
+
super
|
40
|
+
|
41
|
+
Mercury.bind 'region:update', =>
|
42
|
+
return if @previewing
|
43
|
+
return unless Mercury.region == @
|
44
|
+
setTimeout((=> @selection().forceSelection(@element.get(0))), 1)
|
45
|
+
currentElement = @currentElement()
|
46
|
+
if currentElement.length
|
47
|
+
# setup the table editor if we're inside a table
|
48
|
+
table = currentElement.closest('table', @element)
|
49
|
+
Mercury.tableEditor(table, currentElement) if table.length
|
50
|
+
# display a tooltip if we're in an anchor
|
51
|
+
anchor = currentElement.closest('a', @element)
|
52
|
+
if anchor.length && anchor.attr('href')
|
53
|
+
Mercury.tooltip(anchor, "<a href=\"#{anchor.attr('href')}\" target=\"_blank\">#{anchor.attr('href')}</a>", {position: 'below'})
|
54
|
+
else
|
55
|
+
Mercury.tooltip.hide()
|
56
|
+
|
57
|
+
@element.bind 'dragenter', (event) =>
|
58
|
+
return if @previewing
|
59
|
+
event.preventDefault() if event.shiftKey
|
60
|
+
event.originalEvent.dataTransfer.dropEffect = 'copy'
|
61
|
+
|
62
|
+
@element.bind 'dragover', (event) =>
|
63
|
+
return if @previewing
|
64
|
+
event.preventDefault() if event.shiftKey
|
65
|
+
event.originalEvent.dataTransfer.dropEffect = 'copy'
|
66
|
+
if $.browser.webkit
|
67
|
+
clearTimeout(@dropTimeout)
|
68
|
+
@dropTimeout = setTimeout((=> @element.trigger('possible:drop')), 10)
|
69
|
+
|
70
|
+
@element.bind 'drop', (event) =>
|
71
|
+
return if @previewing
|
72
|
+
|
73
|
+
# handle dropping snippets
|
74
|
+
clearTimeout(@dropTimeout)
|
75
|
+
@dropTimeout = setTimeout((=> @element.trigger('possible:drop')), 1)
|
76
|
+
|
77
|
+
# handle any files that were dropped
|
78
|
+
return unless event.originalEvent.dataTransfer.files.length
|
79
|
+
event.preventDefault()
|
80
|
+
@focus()
|
81
|
+
Mercury.uploader(event.originalEvent.dataTransfer.files[0])
|
82
|
+
|
83
|
+
# possible:drop custom event: we have to do this because webkit doesn't fire the drop event unless both dragover and
|
84
|
+
# dragstart default behaviors are canceled.. but when we do that and observe the drop event, the default behavior
|
85
|
+
# isn't handled (eg, putting the image where it was dropped,) so to allow the browser to do it's thing, and also do
|
86
|
+
# our thing we have this little hack. *sigh*
|
87
|
+
# read: http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html
|
88
|
+
@element.bind 'possible:drop', (event) =>
|
89
|
+
return if @previewing
|
90
|
+
if snippet = @element.find('img[data-snippet]').get(0)
|
91
|
+
@focus()
|
92
|
+
Mercury.Snippet.displayOptionsFor($(snippet).data('snippet'))
|
93
|
+
@document.execCommand('undo', false, null)
|
94
|
+
|
95
|
+
# custom paste handling: we have to do some hackery to get the pasted content since it's not exposed normally
|
96
|
+
# through a clipboard in firefox (heaven forbid), and to keep the behavior across all browsers, we manually detect
|
97
|
+
# what was pasted by running a quick diff, removing it by calling undo, making our adjustments, and then putting the
|
98
|
+
# content back. This is possible, so it doesn't make sense why it wouldn't be exposed in a sensible way. *sigh*
|
99
|
+
@element.bind 'paste', =>
|
100
|
+
return if @previewing
|
101
|
+
return unless Mercury.region == @
|
102
|
+
Mercury.changes = true
|
103
|
+
html = @html()
|
104
|
+
event.preventDefault() if @specialContainer
|
105
|
+
setTimeout((=> @handlePaste(html)), 1)
|
106
|
+
|
107
|
+
@element.focus =>
|
108
|
+
return if @previewing
|
109
|
+
Mercury.region = @
|
110
|
+
setTimeout((=> @selection().forceSelection(@element.get(0))), 1)
|
111
|
+
Mercury.trigger('region:focused', {region: @})
|
112
|
+
|
113
|
+
@element.blur =>
|
114
|
+
return if @previewing
|
115
|
+
Mercury.trigger('region:blurred', {region: @})
|
116
|
+
Mercury.tooltip.hide()
|
117
|
+
|
118
|
+
@element.click (event) =>
|
119
|
+
$(event.target).closest('a').attr('target', '_top') if @previewing
|
120
|
+
|
121
|
+
@element.dblclick (event) =>
|
122
|
+
return if @previewing
|
123
|
+
image = $(event.target).closest('img', @element)
|
124
|
+
if image.length
|
125
|
+
@selection().selectNode(image.get(0), true)
|
126
|
+
Mercury.trigger('button', {action: 'insertmedia'})
|
127
|
+
|
128
|
+
@element.mouseup =>
|
129
|
+
return if @previewing
|
130
|
+
@pushHistory()
|
131
|
+
Mercury.trigger('region:update', {region: @})
|
132
|
+
|
133
|
+
@element.keydown (event) =>
|
134
|
+
return if @previewing
|
135
|
+
Mercury.changes = true
|
136
|
+
switch event.keyCode
|
137
|
+
|
138
|
+
when 90 # undo / redo
|
139
|
+
return unless event.metaKey
|
140
|
+
event.preventDefault()
|
141
|
+
if event.shiftKey then @execCommand('redo') else @execCommand('undo')
|
142
|
+
return
|
143
|
+
|
144
|
+
when 13 # enter
|
145
|
+
if $.browser.webkit && @selection().commonAncestor().closest('li, ul', @element).length == 0
|
146
|
+
event.preventDefault()
|
147
|
+
@document.execCommand('insertlinebreak', false, null)
|
148
|
+
else if @specialContainer
|
149
|
+
# mozilla: pressing enter in any elemeny besides a div handles strangely
|
150
|
+
event.preventDefault()
|
151
|
+
@document.execCommand('insertHTML', false, '<br/>')
|
152
|
+
|
153
|
+
when 9 # tab
|
154
|
+
event.preventDefault()
|
155
|
+
container = @selection().commonAncestor()
|
156
|
+
handled = false
|
157
|
+
|
158
|
+
# indent when inside of an li
|
159
|
+
if container.closest('li', @element).length
|
160
|
+
handled = true
|
161
|
+
if event.shiftKey then @execCommand('outdent') else @execCommand('indent')
|
162
|
+
|
163
|
+
@execCommand('insertHTML', {value: ' '}) unless handled
|
164
|
+
|
165
|
+
if event.metaKey
|
166
|
+
switch event.keyCode
|
167
|
+
|
168
|
+
when 66 # b
|
169
|
+
@execCommand('bold')
|
170
|
+
event.preventDefault()
|
171
|
+
|
172
|
+
when 73 # i
|
173
|
+
@execCommand('italic')
|
174
|
+
event.preventDefault()
|
175
|
+
|
176
|
+
when 85 # u
|
177
|
+
@execCommand('underline')
|
178
|
+
event.preventDefault()
|
179
|
+
|
180
|
+
@pushHistory(event.keyCode)
|
181
|
+
|
182
|
+
@element.keyup =>
|
183
|
+
return if @previewing
|
184
|
+
Mercury.trigger('region:update', {region: @})
|
185
|
+
|
186
|
+
|
187
|
+
focus: ->
|
188
|
+
@element.focus()
|
189
|
+
setTimeout((=> @selection().forceSelection(@element.get(0))), 1)
|
190
|
+
Mercury.trigger('region:update', {region: @})
|
191
|
+
|
192
|
+
|
193
|
+
html: (value = null, filterSnippets = true, includeMarker = false) ->
|
194
|
+
if value != null
|
195
|
+
# sanitize the html before we insert it
|
196
|
+
container = $('<div>').appendTo(@document.createDocumentFragment())
|
197
|
+
container.html(value)
|
198
|
+
|
199
|
+
# fill in the snippet contents
|
200
|
+
for element in container.find('[data-snippet]')
|
201
|
+
element.contentEditable = false
|
202
|
+
element = $(element)
|
203
|
+
if snippet = Mercury.Snippet.find(element.data('snippet'))
|
204
|
+
unless element.data('version')
|
205
|
+
try
|
206
|
+
version = parseInt(element.html().match(/\/(\d+)\]/)[1])
|
207
|
+
if version
|
208
|
+
snippet.setVersion(version)
|
209
|
+
element.attr({'data-version': version})
|
210
|
+
element.html(snippet.data)
|
211
|
+
catch error
|
212
|
+
|
213
|
+
# set the html
|
214
|
+
@element.html(container.html())
|
215
|
+
|
216
|
+
# create a selection if there's markers
|
217
|
+
@selection().selectMarker(@element)
|
218
|
+
else
|
219
|
+
# remove any meta tags
|
220
|
+
@element.find('meta').remove()
|
221
|
+
|
222
|
+
# place markers for the selection
|
223
|
+
if includeMarker
|
224
|
+
selection = @selection()
|
225
|
+
selection.placeMarker()
|
226
|
+
|
227
|
+
# sanitize the html before we return it
|
228
|
+
container = $('<div>').appendTo(@document.createDocumentFragment())
|
229
|
+
container.html(@element.html().replace(/^\s+|\s+$/g, ''))
|
230
|
+
|
231
|
+
# replace snippet contents to be an identifier
|
232
|
+
if filterSnippets then for element, index in container.find('[data-snippet]')
|
233
|
+
element = $(element)
|
234
|
+
if snippet = Mercury.Snippet.find(element.data("snippet"))
|
235
|
+
snippet.data = element.html()
|
236
|
+
element.html("[#{element.data("snippet")}/#{element.data("version")}]")
|
237
|
+
element.attr({contenteditable: null, 'data-version': null})
|
238
|
+
|
239
|
+
# get the html before removing the markers
|
240
|
+
html = container.html()
|
241
|
+
|
242
|
+
# remove the markers from the dom
|
243
|
+
selection.removeMarker() if includeMarker
|
244
|
+
|
245
|
+
return html
|
246
|
+
|
247
|
+
|
248
|
+
togglePreview: ->
|
249
|
+
if @previewing
|
250
|
+
@element.get(0).contentEditable = true
|
251
|
+
@element.css({overflow: 'auto'})
|
252
|
+
else
|
253
|
+
@html(@html())
|
254
|
+
@element.get(0).contentEditable = false
|
255
|
+
@element.css({overflow: @element.data('originalOverflow')})
|
256
|
+
@element.blur()
|
257
|
+
super
|
258
|
+
|
259
|
+
|
260
|
+
execCommand: (action, options = {}) ->
|
261
|
+
super
|
262
|
+
|
263
|
+
# use a custom handler if there's one, otherwise use execCommand
|
264
|
+
if handler = Mercury.config.behaviors[action] || Mercury.Regions.Editable.actions[action]
|
265
|
+
handler.call(@, @selection(), options)
|
266
|
+
else
|
267
|
+
sibling = @element.get(0).previousSibling if action == 'indent'
|
268
|
+
options.value = $('<div>').html(options.value).html() if action == 'insertHTML' && options.value && options.value.get
|
269
|
+
try
|
270
|
+
@document.execCommand(action, false, options.value)
|
271
|
+
catch error
|
272
|
+
# mozilla: indenting when there's no br tag handles strangely
|
273
|
+
@element.prev().remove() if action == 'indent' && @element.prev() != sibling
|
274
|
+
|
275
|
+
|
276
|
+
pushHistory: (keyCode) ->
|
277
|
+
# when pressing return, delete or backspace it should push to the history
|
278
|
+
# all other times it should store if there's a 1 second pause
|
279
|
+
keyCodes = [13, 46, 8]
|
280
|
+
waitTime = 2.5
|
281
|
+
knownKeyCode = keyCodes.indexOf(keyCode) if keyCode
|
282
|
+
|
283
|
+
# clear any pushes to the history
|
284
|
+
clearTimeout(@historyTimeout)
|
285
|
+
|
286
|
+
# if the key code was return, delete, or backspace store now -- unless it was the same as last time
|
287
|
+
if knownKeyCode >= 0 && knownKeyCode != @lastKnownKeyCode # || !keyCode
|
288
|
+
@history.push(@html(null, false, true))
|
289
|
+
else if keyCode
|
290
|
+
# set a timeout for pushing to the history
|
291
|
+
@historyTimeout = setTimeout((=> @history.push(@html(null, false, true))), waitTime * 1000)
|
292
|
+
else
|
293
|
+
# push to the history immediately
|
294
|
+
@history.push(@html(null, false, true))
|
295
|
+
|
296
|
+
@lastKnownKeyCode = knownKeyCode
|
297
|
+
|
298
|
+
|
299
|
+
selection: ->
|
300
|
+
return new Mercury.Regions.Editable.Selection(@window.getSelection(), @document)
|
301
|
+
|
302
|
+
|
303
|
+
path: ->
|
304
|
+
container = @selection().commonAncestor()
|
305
|
+
return [] unless container
|
306
|
+
return if container.get(0) == @element.get(0) then [] else container.parentsUntil(@element)
|
307
|
+
|
308
|
+
|
309
|
+
currentElement: ->
|
310
|
+
element = []
|
311
|
+
selection = @selection()
|
312
|
+
if selection.range
|
313
|
+
element = selection.commonAncestor()
|
314
|
+
element = element.parent() if element.get(0).nodeType == 3
|
315
|
+
return element
|
316
|
+
|
317
|
+
|
318
|
+
handlePaste: (prePasteHTML) ->
|
319
|
+
prePasteHTML = prePasteHTML.replace(/^\<br\>/, '')
|
320
|
+
|
321
|
+
# remove any regions that might have been pasted
|
322
|
+
@element.find('.mercury-region').remove()
|
323
|
+
|
324
|
+
# handle pasting from ms office etc
|
325
|
+
html = @html()
|
326
|
+
if html.indexOf('<!--StartFragment-->') > -1 || html.indexOf('="mso-') > -1 || html.indexOf('<o:') > -1 || html.indexOf('="Mso') > -1
|
327
|
+
# clean out all the tags from the pasted contents
|
328
|
+
cleaned = prePasteHTML.singleDiff(@html()).sanitizeHTML()
|
329
|
+
try
|
330
|
+
# try to undo and put the cleaned html where the selection was
|
331
|
+
@document.execCommand('undo', false, null)
|
332
|
+
@execCommand('insertHTML', {value: cleaned})
|
333
|
+
catch error
|
334
|
+
# remove the pasted html and load up the cleaned contents into a modal
|
335
|
+
@html(prePasteHTML)
|
336
|
+
Mercury.modal '/mercury/modals/sanitizer', {
|
337
|
+
title: 'HTML Sanitizer (Starring Clippy)',
|
338
|
+
afterLoad: -> @element.find('textarea').val(cleaned.replace(/<br\/>/g, '\n'))
|
339
|
+
}
|
340
|
+
else if Mercury.config.cleanStylesOnPaste
|
341
|
+
# strip styles
|
342
|
+
pasted = prePasteHTML.singleDiff(@html())
|
343
|
+
|
344
|
+
container = $('<div>').appendTo(@document.createDocumentFragment()).html(pasted)
|
345
|
+
container.find('[style]').attr({style: null})
|
346
|
+
|
347
|
+
@document.execCommand('undo', false, null)
|
348
|
+
@execCommand('insertHTML', {value: container.html()})
|
349
|
+
|
350
|
+
|
351
|
+
# Custom actions (eg. things that execCommand doesn't do, or doesn't do well)
|
352
|
+
@actions: {
|
353
|
+
insertrowbefore: -> Mercury.tableEditor.addRow('before')
|
354
|
+
|
355
|
+
insertrowafter: -> Mercury.tableEditor.addRow('after')
|
356
|
+
|
357
|
+
insertcolumnbefore: -> Mercury.tableEditor.addColumn('before')
|
358
|
+
|
359
|
+
insertcolumnafter: -> Mercury.tableEditor.addColumn('after')
|
360
|
+
|
361
|
+
deletecolumn: -> Mercury.tableEditor.removeColumn()
|
362
|
+
|
363
|
+
deleterow: -> Mercury.tableEditor.removeRow()
|
364
|
+
|
365
|
+
increasecolspan: -> Mercury.tableEditor.increaseColspan()
|
366
|
+
|
367
|
+
decreasecolspan: -> Mercury.tableEditor.decreaseColspan()
|
368
|
+
|
369
|
+
increaserowspan: -> Mercury.tableEditor.increaseRowspan()
|
370
|
+
|
371
|
+
decreaserowspan: -> Mercury.tableEditor.decreaseRowspan()
|
372
|
+
|
373
|
+
undo: -> @html(@history.undo())
|
374
|
+
|
375
|
+
redo: -> @html(@history.redo())
|
376
|
+
|
377
|
+
removeformatting: (selection) -> selection.insertTextNode(selection.textContent())
|
378
|
+
|
379
|
+
backcolor: (selection, options) -> selection.wrap("<span style=\"background-color:#{options.value.toHex()}\">", true)
|
380
|
+
|
381
|
+
overline: (selection) -> selection.wrap('<span style="text-decoration:overline">', true)
|
382
|
+
|
383
|
+
style: (selection, options) -> selection.wrap("<span class=\"#{options.value}\">", true)
|
384
|
+
|
385
|
+
replaceHTML: (selection, options) -> @html(options.value)
|
386
|
+
|
387
|
+
insertImage: (selection, options) -> @execCommand('insertHTML', {value: $('<img/>', options.value)})
|
388
|
+
|
389
|
+
insertLink: (selection, options) ->
|
390
|
+
anchor = $("<#{options.value.tagName}>").attr(options.value.attrs).html(options.value.content)
|
391
|
+
selection.insertNode(anchor)
|
392
|
+
|
393
|
+
replaceLink: (selection, options) ->
|
394
|
+
anchor = $("<#{options.value.tagName}>").attr(options.value.attrs).html(options.value.content)
|
395
|
+
selection.selectNode(options.node)
|
396
|
+
html = $('<div>').html(selection.content()).find('a').html()
|
397
|
+
selection.replace($(anchor, selection.context).html(html))
|
398
|
+
|
399
|
+
insertsnippet: (selection, options) ->
|
400
|
+
snippet = options.value
|
401
|
+
if (existing = @element.find("[data-snippet=#{snippet.identity}]")).length
|
402
|
+
selection.selectNode(existing.get(0))
|
403
|
+
selection.insertNode(snippet.getHTML(@document))
|
404
|
+
|
405
|
+
editsnippet: ->
|
406
|
+
return unless @snippet
|
407
|
+
snippet = Mercury.Snippet.find(@snippet.data('snippet'))
|
408
|
+
snippet.displayOptions()
|
409
|
+
|
410
|
+
removesnippet: ->
|
411
|
+
@snippet.remove() if @snippet
|
412
|
+
Mercury.trigger('hide:toolbar', {type: 'snippet', immediately: true})
|
413
|
+
}
|
414
|
+
|
415
|
+
|
416
|
+
# Helper class for managing selection and getting information from it
|
417
|
+
class Mercury.Regions.Editable.Selection
|
418
|
+
|
419
|
+
constructor: (@selection, @context) ->
|
420
|
+
return unless @selection.rangeCount >= 1
|
421
|
+
@range = @selection.getRangeAt(0)
|
422
|
+
@fragment = @range.cloneContents()
|
423
|
+
@clone = @range.cloneRange()
|
424
|
+
|
425
|
+
|
426
|
+
commonAncestor: (onlyTag = false) ->
|
427
|
+
return null unless @range
|
428
|
+
ancestor = @range.commonAncestorContainer
|
429
|
+
ancestor = ancestor.parentNode if ancestor.nodeType == 3 && onlyTag
|
430
|
+
return $(ancestor)
|
431
|
+
|
432
|
+
|
433
|
+
wrap: (element, replace = false) ->
|
434
|
+
element = $(element, @context).html(@fragment)
|
435
|
+
@replace(element) if replace
|
436
|
+
return element
|
437
|
+
|
438
|
+
|
439
|
+
textContent: ->
|
440
|
+
return @range.cloneContents().textContent
|
441
|
+
|
442
|
+
|
443
|
+
content: ->
|
444
|
+
return @range.cloneContents()
|
445
|
+
|
446
|
+
|
447
|
+
is: (elementType) ->
|
448
|
+
content = @content()
|
449
|
+
return $(content.firstChild) if content.childNodes.length == 1 && $(content.firstChild).is(elementType)
|
450
|
+
return false
|
451
|
+
|
452
|
+
|
453
|
+
forceSelection: (element) ->
|
454
|
+
return unless $.browser.webkit
|
455
|
+
range = @context.createRange()
|
456
|
+
|
457
|
+
if @range
|
458
|
+
if @commonAncestor(true).closest('.mercury-snippet').length
|
459
|
+
lastChild = @context.createTextNode('\00')
|
460
|
+
element.appendChild(lastChild)
|
461
|
+
else
|
462
|
+
if element.lastChild && element.lastChild.nodeType == 3 && element.lastChild.textContent.replace(/^[\s+|\n+]|[\s+|\n+]$/, '') == ''
|
463
|
+
lastChild = element.lastChild
|
464
|
+
element.lastChild.textContent = '\00'
|
465
|
+
else
|
466
|
+
lastChild = @context.createTextNode('\00')
|
467
|
+
element.appendChild(lastChild)
|
468
|
+
|
469
|
+
if lastChild
|
470
|
+
range.setStartBefore(lastChild)
|
471
|
+
range.setEndBefore(lastChild)
|
472
|
+
@selection.addRange(range)
|
473
|
+
|
474
|
+
|
475
|
+
selectMarker: (context) ->
|
476
|
+
markers = context.find('em.mercury-marker')
|
477
|
+
return unless markers.length
|
478
|
+
|
479
|
+
range = @context.createRange()
|
480
|
+
range.setStartBefore(markers.get(0))
|
481
|
+
range.setEndBefore(markers.get(1)) if markers.length >= 2
|
482
|
+
|
483
|
+
markers.remove()
|
484
|
+
|
485
|
+
@selection.removeAllRanges()
|
486
|
+
@selection.addRange(range)
|
487
|
+
|
488
|
+
|
489
|
+
placeMarker: ->
|
490
|
+
return unless @range
|
491
|
+
|
492
|
+
@startMarker = $('<em class="mercury-marker"/>', @context).get(0)
|
493
|
+
@endMarker = $('<em class="mercury-marker"/>', @context).get(0)
|
494
|
+
|
495
|
+
# put a single marker (the end)
|
496
|
+
rangeEnd = @range.cloneRange()
|
497
|
+
rangeEnd.collapse(false)
|
498
|
+
rangeEnd.insertNode(@endMarker)
|
499
|
+
|
500
|
+
unless @range.collapsed
|
501
|
+
# put a start marker
|
502
|
+
rangeStart = @range.cloneRange()
|
503
|
+
rangeStart.collapse(true)
|
504
|
+
rangeStart.insertNode(@startMarker)
|
505
|
+
|
506
|
+
@selection.removeAllRanges()
|
507
|
+
@selection.addRange(@range)
|
508
|
+
|
509
|
+
|
510
|
+
removeMarker: ->
|
511
|
+
$(@startMarker).remove()
|
512
|
+
$(@endMarker).remove()
|
513
|
+
|
514
|
+
|
515
|
+
insertTextNode: (string) ->
|
516
|
+
node = @context.createTextNode(string)
|
517
|
+
@range.extractContents()
|
518
|
+
@range.insertNode(node)
|
519
|
+
@range.selectNodeContents(node)
|
520
|
+
@selection.addRange(@range)
|
521
|
+
|
522
|
+
|
523
|
+
insertNode: (element) ->
|
524
|
+
element = element.get(0) if element.get
|
525
|
+
element = $(element, @context).get(0) if $.type(element) == 'string'
|
526
|
+
|
527
|
+
@range.deleteContents()
|
528
|
+
@range.insertNode(element)
|
529
|
+
@range.selectNodeContents(element)
|
530
|
+
@selection.addRange(@range)
|
531
|
+
|
532
|
+
|
533
|
+
selectNode: (node, removeExisting = false) ->
|
534
|
+
@range.selectNode(node)
|
535
|
+
@selection.removeAllRanges() if removeExisting
|
536
|
+
@selection.addRange(@range)
|
537
|
+
|
538
|
+
|
539
|
+
replace: (element) ->
|
540
|
+
element = element.get(0) if element.get
|
541
|
+
element = $(element, @context).get(0) if $.type(element) == 'string'
|
542
|
+
|
543
|
+
@range.deleteContents()
|
544
|
+
@range.insertNode(element)
|
545
|
+
@range.selectNodeContents(element)
|
546
|
+
@selection.addRange(@range)
|