a new [image] button in tinyMCE's toolbar

fixes CNVS_5151

test plan:
by using the new image button in the tinymce toolbar, you
should be able to:
 * insert an image from Canvas content (course or group files;
   whatever the context for the editor is)
   * test in wikis, discussions, quizzes, eportfolios...
     anywhere you can find a rich content editor
   * if you're in a course or group context, you should be
     able to add course/group files.  otherwise (in account
     context, for instance) you will only see "my files".
   * also, pls to test that subfolders work
 * single-click an image to select it (and set size/alt text etc.
   before pressing Update)
 * double-click an image to select it and insert with the
   default alt-text and size
   * note that the size is constrained to the image's aspect ratio
 * insert an image from the user's own files
 * insert an image by URL
 * insert an image from Flickr via search
   * images inserted from flickr should link to the source flickr page
     (this is part of flickr TOS, and is not a new behavior, but should
     be tested explicitly)
   * make sure if you change to a different flickr image, the link
     is updated
   * also test that if you change a flickr image to a canvas image
     or url image that the flickr link goes away
 * NOTE: also test the old flickr search dialog on the wiki sidebar
   (the blue magnifying glass thing) for possible regressions.
   (the tinymce plugin that powers this thing was modified.)
 * create or edit alt text for any image type

(note, it does not add uploading new files, that will come in another commit)

Change-Id: I2d5f8ca9f2301168f442955fda791631ee886636
Reviewed-on: https://gerrit.instructure.com/14391
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
Product-Review: Bracken Mosbacker <bracken@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Adam Phillipps <adam@instructure.com>
This commit is contained in:
Jeremy Stanley 2012-10-05 10:29:14 -06:00
parent bedd30d021
commit 457bc0b15d
34 changed files with 760 additions and 35 deletions

View File

@ -0,0 +1,65 @@
define [
'Backbone'
'underscore'
], (Backbone, _) ->
class Folder extends Backbone.Model
defaults:
'name' : ''
initialize: ->
@setUpFilesAndFoldersIfNeeded()
super
parse: (response) ->
@setUpFilesAndFoldersIfNeeded()
@folders.url = response.folders_url
@files.url = response.files_url
super
setUpFilesAndFoldersIfNeeded: ->
unless @folders
@folders = new Backbone.Collection
@folders.model = Folder
unless @files
@files = new Backbone.Collection
expand: (force=false) ->
@isExpanded = true
@trigger 'expanded'
unless @expandDfd || force
@isExpanding = true
@trigger 'beginexpanding'
@expandDfd = $.Deferred().done =>
@isExpanding = false
@trigger 'endexpanding'
selfHasntBeenFetched = @folders.url is @folders.constructor::url or @files.url is @files.constructor::url
fetchDfd = @fetch() if selfHasntBeenFetched || force
$.when(fetchDfd).done =>
foldersDfd = @folders.fetch() unless @get('folders_count') is 0
filesDfd = @files.fetch() unless @get('files_count') is 0
$.when(foldersDfd, filesDfd).done(@expandDfd.resolve)
collapse: ->
@isExpanded = false
@trigger 'collapsed'
toggle: ->
if @isExpanded
@collapse()
else
@expand()
contents: ->
_(@files.models.concat(@folders.models)).sortBy (model) ->
(model.get('name') || model.get('display_name') || '').toLowerCase()
previewUrlForFile: (file) ->
if @get('context_type') in ['Course', 'Group']
"/#{ @get('context_type').toLowerCase() + 's' }/#{ @get('context_id') }/files/#{ file.get('id') }/preview"
else
# we need the full path with verifier for user files
file.get('url')

View File

@ -8,6 +8,7 @@ define [
# instructure plugins
'tinymce/jscripts/tiny_mce/plugins/instructure_contextmenu/editor_plugin'
'tinymce/jscripts/tiny_mce/plugins/instructure_embed/editor_plugin'
'tinymce/jscripts/tiny_mce/plugins/instructure_image/editor_plugin'
'tinymce/jscripts/tiny_mce/plugins/instructure_equation/editor_plugin'
'tinymce/jscripts/tiny_mce/plugins/instructure_equella/editor_plugin'
'tinymce/jscripts/tiny_mce/plugins/instructure_external_tools/editor_plugin'
@ -19,6 +20,7 @@ define [
markScriptsLoaded [
'plugins/instructure_contextmenu/editor_plugin'
'plugins/instructure_embed/editor_plugin'
'plugins/instructure_image/editor_plugin'
'plugins/instructure_equation/editor_plugin'
'plugins/instructure_equella/editor_plugin'
'plugins/instructure_external_tools/editor_plugin'

View File

@ -0,0 +1,46 @@
define [
'i18n!filebrowserview'
'Backbone'
'underscore'
'jst/FileBrowserView'
'compiled/views/FolderTreeView'
'compiled/models/Folder'
'compiled/str/splitAssetString'
], (I18n, Backbone, _, template, FolderTreeView, Folder, splitAssetString) ->
class FileBrowserView extends Backbone.View
template: template
rootFolders: ->
# purposely sharing these across instances of FileBrowserView
# use a 'custom_name' to set I18n'd names for the root folders (the actual names are hard-coded)
FileBrowserView.rootFolders ||= do ->
contextFiles = null
contextTypeAndId = splitAssetString(ENV.context_asset_string || '')
if contextTypeAndId && contextTypeAndId.length == 2 && (contextTypeAndId[0] == 'courses' || contextTypeAndId[0] == 'groups')
contextFiles = new Folder
contextFiles.set 'custom_name', if contextTypeAndId[0] is 'courses' then I18n.t('course_files', 'Course files') else I18n.t('group_files', 'Group files')
contextFiles.url = "/api/v1/#{contextTypeAndId[0]}/#{contextTypeAndId[1]}/folders/root"
contextFiles.fetch()
myFiles = new Folder
myFiles.set 'custom_name', I18n.t('my_files', 'My files')
myFiles.url = '/api/v1/users/self/folders/root'
myFiles.fetch()
result = []
result.push contextFiles if contextFiles
result.push myFiles
result
initialize: ->
@contentTypes = @options?.contentTypes
super
afterRender: ->
@$folderTree = @$el.children('.folderTree')
for folder in @rootFolders()
new FolderTreeView({model: folder, contentTypes: @contentTypes}).$el.appendTo(@$folderTree)
super

View File

@ -0,0 +1,45 @@
define [
'Backbone'
'underscore'
'str/htmlEscape'
'jst/FindFlickrImageView'
'jst/FindFlickrImageResult'
], (Backbone, _, h, template, resultTemplate) ->
class FindFlickrImageView extends Backbone.View
tagName: 'form'
attributes:
'class': 'bootstrap-form form-horizontal FindFlickrImageView'
template: template
events:
'submit' : 'searchFlickr'
'change .flickrSearchTerm' : 'hideResultsIfEmptySearch'
'input .flickrSearchTerm' : 'hideResultsIfEmptySearch'
hideResultsIfEmptySearch: ->
@renderResults([]) unless @$('.flickrSearchTerm').val()
searchFlickr: (event) ->
event?.preventDefault()
return unless query = @$('.flickrSearchTerm').val()
flickrUrl = 'https://secure.flickr.com/services/rest/?method=flickr.photos.search&format=json' +
'&api_key=734839aadcaa224c4e043eaf74391e50&sort=relevance&license=1,2,3,4,5,6' +
"&text=#{query}&per_page=150&jsoncallback=?"
@request?.abort()
@$('.flickrResults').show().disableWhileLoading @request = $.getJSON flickrUrl, (data) =>
photos = data.photos.photo
@renderResults(photos)
renderResults: (photos) ->
html = _.map photos, (photo) ->
resultTemplate
thumb: "https://farm#{photo.farm}.static.flickr.com/#{photo.server}/#{photo.id}_#{photo.secret}_s.jpg"
fullsize: "https://farm#{photo.farm}.static.flickr.com/#{photo.server}/#{photo.id}_#{photo.secret}.jpg"
source: "https://secure.flickr.com/photos/#{photo.owner}/#{photo.id}"
title: photo.title
@$('.flickrResults').showIf(!!photos.length).html html.join('')

View File

@ -0,0 +1,72 @@
define [
'Backbone'
'underscore'
'compiled/fn/preventDefault'
'compiled/models/Folder'
'jst/FolderTreeItem'
], (Backbone, _, preventDefault, Folder, treeItemTemplate) ->
class FolderTreeView extends Backbone.View
tagName: 'li'
attributes: ->
'role': 'treeitem'
'aria-expanded': "#{!!@model.isExpanded}"
events:
'click .folderLabel': 'toggle'
# you can set an optional `@options.contentTypes` attribute with an array of
# content-types files that you want to show
initialize: ->
@model.on 'all', @render, this
@model.files.on 'all', @render, this
@model.folders.on 'all', @render, this
@render()
super
render: ->
$focusedChild = @$(document.activeElement)
@renderSelf()
@renderContents()
# restore focus for keyboard users
@$el.find($focusedChild).focus() if $focusedChild.length
toggle: (event) ->
# prevent it from bubbling up to parents and from following link
event.preventDefault()
event.stopPropagation()
@model.toggle()
@$el.attr(@attributes())
title_text: ->
@model.get('custom_name') || @model.get('name')
renderSelf: ->
@$label ||= $("<a class='folderLabel' href='#' title='#{@title_text()}'/>").prependTo(@$el)
@$label
.text(@title_text())
.toggleClass('expanded', !!@model.isExpanded)
.toggleClass('loading after', !!@model.isExpanding)
renderContents: ->
@$folderContents?.detach()
if @model.isExpanded
@$folderContents = $("<ul role='group' />").appendTo(@$el)
_.each @model.contents(), (model) =>
node = @["viewFor_#{model.cid}"] ||=
if model.constructor is Folder
# recycle DOM nodes to prevent zombies that still respond to model events,
# sad that I have to attach something to the model though
new FolderTreeView(
model: model
contentTypes: @options.contentTypes
).el
else if !@options.contentTypes || (model.get('content-type') in @options.contentTypes)
$ treeItemTemplate
title: model.get 'display_name'
thumbnail_url: model.get 'thumbnail_url'
preview_url: @model.previewUrlForFile(model)
@$folderContents.append node if node

View File

@ -0,0 +1,141 @@
define [
'i18n!editor'
'jquery'
'underscore'
'str/htmlEscape'
'compiled/fn/preventDefault'
'compiled/views/DialogBaseView'
'jst/tinymce/InsertUpdateImageView'
], (I18n, $, _, h, preventDefault, DialogBaseView, template) ->
class InsertUpdateImageView extends DialogBaseView
template: template
events:
'change [name="image[width]"]' : 'constrainProportions'
'change [name="image[height]"]' : 'constrainProportions'
'click .flickrImageResult, .treeFile' : 'onFileLinkClick'
'change [name="image[src]"]' : 'onImageUrlChange'
'tabsshow .imageSourceTabs': 'onTabsshow'
'dblclick .flickrImageResult, .treeFile' : 'onFileLinkDblclick'
dialogOptions:
width: 625
title: I18n.t 'titles.insert_edit_image', 'Insert / Edit Image'
initialize: (@editor, selectedNode) ->
@$editor = $("##{@editor.id}")
@prevSelection = @editor.selection.getBookmark()
@$selectedNode = $(selectedNode)
super
@render()
@show()
if @$selectedNode.prop('nodeName') is 'IMG'
@setSelectedImage
src: @$selectedNode.attr('src')
alt: @$selectedNode.attr('alt')
width: @$selectedNode.width()
height: @$selectedNode.height()
afterRender: ->
@$('.imageSourceTabs').tabs()
onTabsshow: (event, ui) ->
loadTab = (fn) =>
return if @["#{ui.panel.id}IsLoaded"]
@["#{ui.panel.id}IsLoaded"] = true
loadingDfd = $.Deferred()
$(ui.panel).disableWhileLoading loadingDfd
fn(loadingDfd.resolve)
switch ui.panel.id
when 'tabUploaded'
loadTab (done) =>
require [
'compiled/views/FileBrowserView',
'compiled/util/mimeClass'
], (FileBrowserView, mimeClass) =>
contentTypes = _.compact _.map mimeClass.mimeClasses, (val, key) -> key if val is 'image'
new FileBrowserView({contentTypes}).render().$el.appendTo(ui.panel)
done()
when 'tabFlickr'
loadTab (done) =>
require ['compiled/views/FindFlickrImageView'], (FindFlickrImageView) =>
new FindFlickrImageView().render().$el.appendTo(ui.panel)
done()
setAspectRatio: ->
width = Number @$("[name='image[width]']").val()
height = Number @$("[name='image[height]']").val()
if width && height
@aspectRatio = width / height
else
delete @aspectRatio
constrainProportions: (event) =>
val = Number $(event.target).val()
if @aspectRatio && (val or (val is 0))
if $(event.target).is('[name="image[height]"]')
@$('[name="image[width]"]').val Math.round(val * @aspectRatio)
else
@$('[name="image[height]"]').val Math.round(val / @aspectRatio)
setSelectedImage: (attributes = {}) ->
# set given attributes immediately; update width and height after image loads
@$("[name='image[#{key}]']").val(value) for key, value of attributes
dfd = $.Deferred()
onLoad = ({target: img}) =>
newAttributes = _.defaults attributes,
width: img.width
height: img.height
@$("[name='image[#{key}]']").val(value) for key, value of newAttributes
isValidImage = newAttributes.width && newAttributes.height
@setAspectRatio()
dfd.resolve newAttributes
onError = ({target: img}) =>
newAttributes =
width: ''
height: ''
@$("[name='image[#{key}]']").val(value) for key, value of newAttributes
@$img = $('<img>', attributes).load(onLoad).error(onError)
dfd
getAttributes: ->
res = {}
for key in ['width', 'height']
val = Number @$("[name='image[#{key}]']").val()
res[key] = val if val && val > 0
for key in ['src', 'alt']
val = @$("[name='image[#{key}]']").val()
res[key] = val if val
res
onFileLinkClick: (event) ->
event.preventDefault()
@$('.active').removeClass('active').parent().removeAttr('aria-selected')
$a = $(event.currentTarget).addClass('active')
$a.parent().attr('aria-selected', true)
@flickr_link = $a.attr('data-linkto')
@setSelectedImage
src: $a.attr('data-fullsize')
alt: $a.attr('title')
onFileLinkDblclick: (event) =>
# click event is handled on the first click
@update()
onImageUrlChange: (event) ->
@flickr_link = null
@setSelectedImage src: $(event.currentTarget).val()
generateImageHtml: ->
img_tag = @editor.dom.createHTML("img", @getAttributes())
if @flickr_link
img_tag = "<a href='#{@flickr_link}'>#{img_tag}</a>"
img_tag
update: =>
@editor.selection.moveToBookmark(@prevSelection)
@$editor.editorBox 'insert_code', @generateImageHtml()
@editor.focus()
@close()

View File

@ -28,6 +28,7 @@ $section_tabs_border_bottom_color: lighten($section_tabs_border_top_color, 4%)
$section_tabs_to_be_hidden_color: #888
$hintTextColor: #888
$button-text-color: #525252
$ui-state-default-gradient-top: #ededed
$ui-state-default-gradient-bottom: #c4c4c4

View File

@ -79,7 +79,6 @@ sub {
img {
/* Responsive images (ensure images don't scale beyond their parents) */
max-width: 100%; /* Part 1: Set a maxium relative to the parent */
width: auto\9; /* IE7-8 need help adjusting responsive images */
height: auto; /* Part 2: Scale the height according to the width, otherwise you get stretching */
vertical-align: middle;

View File

@ -42,6 +42,9 @@
//@import "bootstrap/tooltip"; // we do our own
@import "popovers";
// Components: Misc
@import "bootstrap/thumbnails";
// Components: Misc
@import "bootstrap/thumbnails";

View File

@ -116,4 +116,4 @@ iframe#tool_content {
blockquote p {
font-size: inherit;
}
}

View File

@ -0,0 +1,42 @@
@import 'environment';
$triangle-edge-size: 5px;
.folderTree {
&, ul {
@include reset-list;
}
ul { margin-left: 8px; }
li a {
padding: 1px 7px 1px 35px;
display: block;
background: url(/images/inst_tree/file_types/page_white.png) no-repeat 13px 3px;
position: relative;
&.folderLabel {
background-image: url(/images/inst_tree/folder.png);
&:before{
position: absolute;
top: $triangle-edge-size;
left: 4px;
border: solid transparent;
border-width: $triangle-edge-size;
border-left-color: $button-text-color;
content: '';
}
&.expanded:before{
left: 0;
top: 7px;
border-left-color: transparent;
border-top-color: $button-text-color;
}
}
&.active { background-color: $activeBG; }
}
.preview-thumbnail {
margin-left: -23px;
max-width: 200px;
height: 30px;
vertical-align: middle;
}
}

View File

@ -0,0 +1,30 @@
@import 'environment';
.FindFlickrImageView {
.flickrResults {
padding: 0;
max-height: 200px;
overflow: auto;
margin-left: 0px;
margin-top: 10px;
li {
margin-bottom: 10px;
margin-left: 10px;
a.active { background-color: darken($activeBG, 25%); }
}
}
}
.WikiSidebarView {
.flickrResults {
max-height: 130px;
li {
margin-bottom: 10px;
margin-left: 10px;
}
img {
width: 40px;
height: 40px;
}
}
}

View File

@ -0,0 +1,15 @@
.insertUpdateImage {
.insertUpdateImageTabpane {
height: 200px;
overflow: auto;
}
.checkbox.inline { white-space: nowrap; }
// fix safari legend margin issue
legend {
margin-bottom: 0px;
}
legend + * {
margin-top: 20px;
-webkit-margin-collapse: separate;
}
}

50
app/stylesheets/tree.scss Normal file
View File

@ -0,0 +1,50 @@
@import 'environment';
$triangle-edge-size: 5px;
.folderTree {
&, ul {
@include reset-list;
}
ul { margin-left: 8px; }
li a {
padding: 1px 7px 1px 35px;
display: block;
background: url(/images/inst_tree/file_types/page_white.png) no-repeat 13px 3px;
position: relative;
&.folderLabel {
background-image: url(/images/inst_tree/folder.png);
&:before{
position: absolute;
top: $triangle-edge-size;
left: 4px;
border: solid transparent;
border-width: $triangle-edge-size;
border-left-color: $button-text-color;
content: '';
}
&.expanded:before{
left: 0;
top: 7px;
border-left-color: transparent;
border-top-color: $button-text-color;
}
&.loading:after {
width: image-width('ajax-loader-linear.gif');
height: image-height('ajax-loader-linear.gif');
background: url(/images/ajax-loader-linear.gif);
content: '';
display: inline-block;
margin-left: 7px;
}
}
&:focus { background-color: #f2fafd; }
}
.preview-thumbnail {
margin-left: -23px;
max-width: 200px;
height: 30px;
vertical-align: middle;
}
}

View File

@ -0,0 +1 @@
<ul role="tree" class="folderTree"></ul>

View File

@ -0,0 +1,10 @@
<li>
<a
href="#"
class="thumbnail flickrImageResult"
data-fullsize="{{fullsize}}"
title="{{title}}"
data-linkto="{{source}}">
<img src="{{thumb}}" alt="{{title}}" width="75" height="75" />
</a>
</li>

View File

@ -0,0 +1,10 @@
<div class="input-append">
<input class="input-xxlarge flickrSearchTerm"
placeholder="{{#t "find_cc_on_flickr"}}Find Creative Commons images on Flickr{{/t}}"
type="search"
style="position: relative; z-index: 1200"
><button class="btn flickrSearchButton"
title="{{#t "search"}}Search{{/t}}"
type="submit"><i class="icon-search"></i></button>
</div>
<ul class="flickrResults thumbnails insertUpdateImageTabpane" style="display: none;"></ul>

View File

@ -0,0 +1,6 @@
<li role="treeitem">
<a href="#" data-fullsize="{{preview_url}}" class="treeFile ellipsis" title="{{title}}">
<img class="preview-thumbnail" src="{{thumbnail_url}}">
{{title}}
</a>
</li>

View File

@ -0,0 +1,52 @@
<div class="insertUpdateImage bootstrap-form form-horizontal" >
<fieldset style="max-width: 597px;">
<legend>{{#t "image_source"}}Image Source{{/t}}</legend>
<div class="ui-tabs-minimal imageSourceTabs">
<ul>
<li><a href="#tabUrl">{{#t "url"}}URL{{/t}}</a></li>
<li><a href="#tabUploaded">{{#t "canvas"}}Canvas{{/t}}</a></li>
<li><a href="#tabFlickr">{{#t "flickr"}}Flickr{{/t}}</a></li>
</ul>
<div id="tabUrl">
<input type="url"
name="image[src]"
class="input-xxlarge"
placeholder="http://example.com/image.png"
style="margin-bottom: 20px;">
</div>
<ul role="tree" class="folderTree insertUpdateImageTabpane" id="tabUploaded"></ul>
<div id="tabFlickr">
</div>
</div>
</fieldset>
<fieldset>
<legend>{{#t "attributes"}}Attributes{{/t}}</legend>
<div class="control-group">
<label class="control-label" for="image_alt">{{#t "alt_text"}}Alt text{{/t}}</label>
<div class="controls">
<input type="text"
class="input-xlarge"
name="image[alt]"
id="image_alt"
aria-describedby="alt_text_description">
<span><p class="help-block" id="alt_text_description">{{#t "alt_help_text"}}Describe the image to improve accessibility{{/t}}</p></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="dimensions_controls">{{#t "dimensions"}}Dimensions{{/t}}</label>
<div class="controls" id="dimensions_controls" aria-describedby="aspect_ratio_note">
<input class="span1"
name="image[width]"
type="text"
aria-label="{{#t "image_width"}}Image Width{{/t}}">
x
<input class="span1"
name="image[height]"
type="text"
aria-label="{{#t "image_height"}}Image Height{{/t}}">
<span><p class="help-block" id="aspect_ratio_note">{{#t "dimension_help_text"}}Aspect ratio will be preserved{{/t}}</p></span>
</div>
</div>
</fieldset>
</div>

View File

@ -9,7 +9,7 @@
require = {
translate: <%= include_js_translations? %>,
baseUrl: '<%= js_base_url %>',
paths: <%= raw Canvas::RequireJs.paths %>,
paths: <%= raw Canvas::RequireJs.paths(true) %>,
use: <%= raw Canvas::RequireJs.shims %>
};
</script>

View File

@ -51,13 +51,13 @@ module Canvas
bundle == '*' ? result : (result[bundle.to_s] || [])
end
def paths
def paths(cache_busting = false)
@paths ||= {
:common => 'compiled/bundles/common',
:jqueryui => 'vendor/jqueryui',
:use => 'vendor/use',
:uploadify => '../flash/uploadify/jquery.uploadify-3.1.min'
}.update(plugin_paths).update(Canvas::RequireJs::PluginExtension.paths).to_json.gsub(/([,{])/, "\\1\n ")
}.update(cache_busting ? cache_busting_paths : {}).update(plugin_paths).update(Canvas::RequireJs::PluginExtension.paths).to_json.gsub(/([,{])/, "\\1\n ")
end
def plugin_paths
@ -70,6 +70,10 @@ module Canvas
end
end
def cache_busting_paths
{ 'compiled/tinymce' => 'compiled/tinymce.js?v2' } # hack: increment to purge browser cached bundles after tiny change
end
def shims
<<-JS.gsub(%r{\A +|^ {8}}, '')
{

View File

@ -1702,7 +1702,7 @@ define([
tinyOptions: {
valid_elements: '*[*]',
extended_valid_elements: '*[*]',
plugins: "autolink,instructure_external_tools,instructure_contextmenu,instructure_links,instructure_embed,instructure_equation,instructure_equella,media,paste,table,inlinepopups"
plugins: "autolink,instructure_external_tools,instructure_contextmenu,instructure_links,instructure_image,instructure_equation,instructure_equella,media,paste,table,inlinepopups"
}
});
$textarea.data('tinyIsVisible', !tinyIsVisible);

View File

@ -22,9 +22,9 @@ define([
'jquery' /* $ */
], function(INST, $) {
$.originalGetJSON = $.getJSON;
var _getJSON = $.getJSON;
$.getJSON = function(url, data, callback) {
var xhr = $.originalGetJSON(url, data, callback);
var xhr = _getJSON.apply($, arguments);
$.ajaxJSON.storeRequest(xhr, url, 'GET', data);
return xhr;
};

View File

@ -113,7 +113,7 @@ define([
if(width == 0) {
width = $textarea.closest(":visible").width();
}
var instructure_buttons = ",instructure_embed,instructure_equation";
var instructure_buttons = ",instructure_image,instructure_equation";
for(var idx in INST.editorButtons) {
// maxVisibleEditorButtons should be the max number of external tool buttons
// that are visible, INCLUDING the catchall "more external tools" button that
@ -150,7 +150,7 @@ define([
elements: id,
theme : "advanced",
plugins: "autolink,instructure_external_tools,instructure_contextmenu,instructure_links," +
"instructure_embed,instructure_equation,instructure_record,instructure_equella," +
"instructure_embed,instructure_image,instructure_equation,instructure_record,instructure_equella," +
"media,paste,table,inlinepopups",
dialog_type: 'modal',
language_load: false,

View File

@ -149,12 +149,15 @@ define([
if (search === 'flickr') $flickrLink.click();
});
/* replaced by instructure_image button
but this plugin is still used by the wiki sidebar (for now)
editor.addButton('instructure_embed', {
title: TRANSLATIONS.embed_image,
cmd: 'instructureEmbed',
image: url + '/img/button.gif'
});
*/
},
getInfo: function () {

View File

@ -0,0 +1,53 @@
define([
'compiled/editor/stocktiny',
'i18n!editor',
'jquery',
'str/htmlEscape',
'jqueryui/dialog'
], function(tinymce, I18n, $, htmlEscape) {
tinymce.create('tinymce.plugins.InstructureImagePlugin', {
init : function(ed, url) {
// Register commands
ed.addCommand('mceInstructureImage', function() {
var selectedNode = ed.selection.getNode();
// Internal image object like a flash placeholder
if (ed.dom.getAttrib(selectedNode, 'class', '').indexOf('mceItem') != -1) return;
require(['compiled/views/tinymce/InsertUpdateImageView'], function(InsertUpdateImageView){
new InsertUpdateImageView(ed, selectedNode);
});
});
// Register buttons
ed.addButton('instructure_image', {
title : htmlEscape(I18n.t('embed_image', 'Embed Image')),
cmd : 'mceInstructureImage',
image : url + '/img/button.gif'
});
// highlight our button when an image is selected
ed.onNodeChange.add(function(ed, cm, e) {
if(e.nodeName == 'IMG' && e.className != 'equation_image') {
cm.setActive('instructure_image', true);
} else {
cm.setActive('instructure_image', false);
}
});
},
getInfo : function() {
return {
longname : 'Instructure image',
author : 'Instructure',
authorurl : 'http://instructure.com',
infourl : 'http://instructure.com',
version : '1'
};
}
});
// Register plugin
tinymce.PluginManager.add('instructure_image', tinymce.plugins.InstructureImagePlugin);
});

View File

@ -53,3 +53,8 @@ end
def stub_png_data(filename = 'test my file? hai!&.png', data = nil)
stub_file_data(filename, data, 'image/png')
end
def jpeg_data_frd
fixture_path = File.expand_path(File.dirname(__FILE__) + '/../fixtures/test_image.jpg')
ActionController::TestUploadedFile.new(fixture_path, 'image/jpeg', true)
end

BIN
spec/fixtures/test_image.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -824,6 +824,17 @@ shared_examples_for "all selenium tests" do
temp_file = open(element.attribute('src'))
temp_file.size.should > 0
end
def check_element_attrs(element, attrs)
element.should be_displayed
attrs.each do |k, v|
if v.is_a? Regexp
element.attribute(k).should match v
else
element.attribute(k).should == v
end
end
end
def check_file(element)
require 'open-uri'

View File

@ -90,9 +90,9 @@ describe "eportfolios" do
edit_link.click
f('.add_content_link.add_rich_content_link').click
wait_for_tiny(f('textarea.edit_section'))
f("img[alt='Embed Image']").click
f(".flickr_search_link").click
f("#instructure_image_search").should be_displayed
f('a.mce_instructure_image').click
f('a[href="#tabFlickr"]').click
f('form.FindFlickrImageView').should be_displayed
end

View File

@ -91,15 +91,16 @@ require File.expand_path(File.dirname(__FILE__) + '/../common')
def add_flickr_image(el)
require 'open-uri'
el.find_element(:css, '.mce_instructure_embed').click
f('.flickr_search_link').click
f('#image_search_form > input').send_keys('angel')
submit_form('#image_search_form')
el.find_element(:css, '.mce_instructure_image').click
dialog = ff('.ui-dialog').reverse.detect(&:displayed?)
f('a[href="#tabFlickr"]', dialog).click
f('.FindFlickrImageView .flickrSearchTerm', dialog).send_keys('angel')
submit_form(f('.FindFlickrImageView', dialog))
wait_for_ajax_requests
keep_trying_until { f('.image_link').should be_displayed }
keep_trying_until { f('.flickrImageResult', dialog).should be_displayed }
# sometimes flickr has broken images; choose the first one that works
image = ff('.image_link').detect do |image|
image = ff('.flickrImageResult img', dialog).detect do |image|
begin
temp_file = open(image.attribute('src'))
temp_file.size > 0
@ -109,6 +110,35 @@ require File.expand_path(File.dirname(__FILE__) + '/../common')
end
raise "Couldn't find an image on flickr!" unless image
image.click
f('.ui-dialog-buttonset .btn-primary', dialog).click
wait_for_ajaximations
end
def add_canvas_image(el, folder, filename)
el.find_element(:css, '.mce_instructure_image').click
dialog = ff('.ui-dialog').reverse.detect(&:displayed?)
f('a[href="#tabUploaded"]', dialog).click
keep_trying_until { f('.folderLabel', dialog).displayed? }
folder_el = ff('.folderLabel', dialog).detect { |el| el.text == folder }
folder_el.should_not be_nil
folder_el.click
keep_trying_until { f('.treeFile', dialog).displayed? }
file_el = f(".treeFile[title=\"#{filename}\"]", dialog)
file_el.should_not be_nil
file_el.click
wait_for_ajaximations
f('.ui-dialog-buttonset .btn-primary', dialog).click
wait_for_ajaximations
end
def add_url_image(el, url, alt_text)
el.find_element(:css, '.mce_instructure_image').click
dialog = ff('.ui-dialog').reverse.detect(&:displayed?)
f('a[href="#tabUrl"]', dialog).click
f('[name="image[src]"]', dialog).send_keys(url)
f('[name="image[alt]"]', dialog).send_keys(alt_text)
f('.ui-dialog-buttonset .btn-primary', dialog).click
wait_for_ajaximations
end
def add_image_to_rce

View File

@ -9,6 +9,7 @@ describe "Wiki pages and Tiny WYSIWYG editor Images" do
before (:each) do
course_with_teacher_logged_in
@blank_page = @course.wiki.wiki_pages.create! :title => 'blank'
end
after(:each) do
@ -142,30 +143,59 @@ describe "Wiki pages and Tiny WYSIWYG editor Images" do
end
it "should add image from flickr" do
get "/courses/#{@course.id}/wiki"
#add image from flickr to rce
f('.wiki_switch_views_link').click
clear_wiki_rce
f('.wiki_switch_views_link').click
get "/courses/#{@course.id}/wiki/blank"
wait_for_ajaximations
f('.edit_link').click
add_flickr_image(driver)
in_frame "wiki_page_body_ifr" do
f('#tinymce img').should be_displayed
end
submit_form('#new_wiki_page')
wait_for_ajax_requests
get "/courses/#{@course.id}/wiki" #can't just wait for the dom, for some reason it stays in edit mode
wait_for_ajax_requests
submit_form("#edit_wiki_page_#{@blank_page.id}")
keep_trying_until { f('#wiki_body').displayed? }
check_image(f('#wiki_body img'))
end
it "should add image via url" do
get "/courses/#{@course.id}/wiki/blank"
wait_for_ajaximations
f('.edit_link').click
add_url_image(driver, 'http://example.com/image.png', 'alt text')
submit_form("#edit_wiki_page_#{@blank_page.id}")
keep_trying_until { f('#wiki_body').displayed? }
check_element_attrs(f('#wiki_body img'), :src => 'http://example.com/image.png', :alt => 'alt text')
end
describe "canvas images" do
before do
@course_root = Folder.root_folders(@course).first
@course_attachment = @course.attachments.create! :uploaded_data => jpeg_data_frd, :filename => 'course.jpg', :display_name => 'course.jpg', :folder => @course_root
@teacher_root = Folder.root_folders(@teacher).first
@teacher_attachment = @teacher.attachments.create! :uploaded_data => jpeg_data_frd, :filename => 'teacher.jpg', :display_name => 'teacher.jpg', :folder => @teacher_root
get "/courses/#{@course.id}/wiki/blank"
wait_for_ajaximations
f('.edit_link').click
end
it "should add a course image" do
add_canvas_image(driver, 'Course files', 'course.jpg')
submit_form("#edit_wiki_page_#{@blank_page.id}")
keep_trying_until { f('#wiki_body').displayed? }
check_element_attrs(f('#wiki_body img'), :src => /\/files\/#{@course_attachment.id}/, :alt => 'course.jpg')
end
it "should add a user image" do
add_canvas_image(driver, 'My files', 'teacher.jpg')
submit_form("#edit_wiki_page_#{@blank_page.id}")
keep_trying_until { f('#wiki_body').displayed? }
check_element_attrs(f('#wiki_body img'), :src => /\/files\/#{@teacher_attachment.id}/, :alt => 'teacher.jpg')
end
end
it "should put flickr images into the right editor" do
get "/courses/#{@course.id}/quizzes"
wait_for_ajaximations
f(".new-quiz-link").click
keep_trying_until { f(".mce_instructure_embed").should be_displayed }
keep_trying_until { f(".mce_instructure_image").displayed? }
add_flickr_image(driver)
click_questions_tab
@ -181,4 +211,3 @@ describe "Wiki pages and Tiny WYSIWYG editor Images" do
end
end
end