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:
parent
bedd30d021
commit
457bc0b15d
|
@ -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')
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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('')
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -42,6 +42,9 @@
|
|||
//@import "bootstrap/tooltip"; // we do our own
|
||||
@import "popovers";
|
||||
|
||||
// Components: Misc
|
||||
@import "bootstrap/thumbnails";
|
||||
|
||||
|
||||
// Components: Misc
|
||||
@import "bootstrap/thumbnails";
|
||||
|
|
|
@ -116,4 +116,4 @@ iframe#tool_content {
|
|||
|
||||
blockquote p {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<ul role="tree" class="folderTree"></ul>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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}}, '')
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () {
|
||||
|
|
53
public/javascripts/tinymce/jscripts/tiny_mce/plugins/instructure_image/editor_plugin.js
vendored
Normal file
53
public/javascripts/tinymce/jscripts/tiny_mce/plugins/instructure_image/editor_plugin.js
vendored
Normal 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);
|
||||
});
|
Before Width: | Height: | Size: 164 B After Width: | Height: | Size: 164 B |
|
@ -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
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue