Created
July 20, 2015 12:42
-
-
Save gordonbanderson/1349bce77cc51177d829 to your computer and use it in GitHub Desktop.
Modified version of HtmlEditorField.js, SilverStripe 3.13, with addition of console logging. See https://github.com/silverstripe/silverstripe-cms/issues/1166
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Functions for HtmlEditorFields in the back end. | |
* Includes the JS for the ImageUpload forms. | |
* | |
* Relies on the jquery.form.js plugin to power the | |
* ajax / iframe submissions | |
*/ | |
var ss = ss || {}; | |
/** | |
* Wrapper for HTML WYSIWYG libraries, which abstracts library internals | |
* from interface concerns like inserting and editing links. | |
* Caution: Incomplete and unstable API. | |
*/ | |
ss.editorWrappers = {}; | |
ss.editorWrappers.tinyMCE = (function() { | |
var instance; | |
return { | |
init: function(config) { | |
if(!ss.editorWrappers.tinyMCE.initialized) { | |
tinyMCE.init(config); | |
ss.editorWrappers.tinyMCE.initialized = true; | |
} | |
}, | |
/** | |
* @return Mixed Implementation specific object | |
*/ | |
getInstance: function() { | |
return this.instance; | |
}, | |
/** | |
* Invoked when a content-modifying UI is opened. | |
*/ | |
onopen: function() { | |
}, | |
/** | |
* Invoked when a content-modifying UI is closed. | |
*/ | |
onclose: function() { | |
}, | |
/** | |
* Write the HTML back to the original text area field. | |
*/ | |
save: function() { | |
tinyMCE.triggerSave(); | |
}, | |
/** | |
* Create a new instance based on a textarea field. | |
* | |
* Please proxy the events from your editor implementation into JS events | |
* on the textarea field. For events that do not map directly, use the | |
* following naming scheme: editor<event>. | |
* | |
* @param String | |
* @param Object Implementation specific configuration | |
* @param Function | |
*/ | |
create: function(domID, config) { | |
this.instance = new tinymce.Editor(domID, config); | |
// Patch TinyMCE events into underlying textarea field. | |
this.instance.onInit.add(function(ed) { | |
if(!ss.editorWrappers.tinyMCE.patched) { | |
// Not ideal, but there's a memory leak we need to patch | |
var originalDestroy = tinymce.themes.AdvancedTheme.prototype.destroy; | |
tinymce.themes.AdvancedTheme.prototype.destroy = function() { | |
originalDestroy.apply(this, arguments); | |
if (this.statusKeyboardNavigation) { | |
this.statusKeyboardNavigation.destroy(); | |
this.statusKeyboardNavigation = null; | |
} | |
}; | |
ss.editorWrappers.tinyMCE.patched = true; | |
} | |
jQuery(ed.getElement()).trigger('editorinit'); | |
// Periodically check for inline changes when focused, | |
// since TinyMCE's onChange only fires on certain actions | |
// like inserting a new paragraph, as opposed to any user input. | |
// This also works around an issue where the "save" button | |
// wouldn't trigger if the click is the cause of a "blur" event | |
// after an (undetected) inline change. This "blur" causes onChange | |
// to trigger, which will change the button markup to show "alternative" styles, | |
// effectively cancelling the original click event. | |
if(ed.settings.update_interval) { | |
var interval; | |
jQuery(ed.getBody()).on('focus', function() { | |
interval = setInterval(function() { | |
// Update underlying element as necessary | |
var element = jQuery(ed.getElement()); | |
if(ed.isDirty()) { | |
// Set content without triggering editor content cleanup | |
element.val(ed.getContent({format : 'raw', no_events : 1})); | |
} | |
}, ed.settings.update_interval); | |
}); | |
jQuery(ed.getBody()).on('blur', function() { | |
clearInterval(interval); | |
}); | |
} | |
}); | |
this.instance.onChange.add(function(ed, l) { | |
// Update underlying textarea on every change, so external handlers | |
// such as changetracker have a chance to trigger properly. | |
ed.save(); | |
jQuery(ed.getElement()).trigger('change'); | |
}); | |
// Add more events here as needed. | |
this.instance.render(); | |
}, | |
/** | |
* Redraw the editor contents | |
*/ | |
repaint: function() { | |
tinyMCE.execCommand("mceRepaint"); | |
}, | |
/** | |
* @return boolean | |
*/ | |
isDirty: function() { | |
return this.getInstance().isDirty(); | |
}, | |
/** | |
* HTML representation of the edited content. | |
* | |
* Returns: {String} | |
*/ | |
getContent: function() { | |
return this.getInstance().getContent(); | |
}, | |
/** | |
* DOM tree of the edited content | |
* | |
* Returns: DOMElement | |
*/ | |
getDOM: function() { | |
return this.getInstance().dom; | |
}, | |
/** | |
* Returns: DOMElement | |
*/ | |
getContainer: function() { | |
return this.getInstance().getContainer(); | |
}, | |
/** | |
* Get the closest node matching the current selection. | |
* | |
* Returns: {jQuery} DOMElement | |
*/ | |
getSelectedNode: function() { | |
return this.getInstance().selection.getNode(); | |
}, | |
/** | |
* Select the given node within the editor DOM | |
* | |
* Parameters: {DOMElement} | |
*/ | |
selectNode: function(node) { | |
this.getInstance().selection.select(node); | |
}, | |
/** | |
* Replace entire content | |
* | |
* @param String HTML | |
* @param Object opts | |
*/ | |
setContent: function(html, opts) { | |
this.getInstance().execCommand('mceSetContent', false, html, opts); | |
}, | |
/** | |
* Insert content at the current caret position | |
* | |
* @param String HTML | |
*/ | |
insertContent: function(html, opts) { | |
this.getInstance().execCommand('mceInsertContent', false, html, opts); | |
}, | |
/** | |
* Replace currently selected content | |
* | |
* @param {String} html | |
*/ | |
replaceContent: function(html, opts) { | |
this.getInstance().execCommand('mceReplaceContent', false, html, opts); | |
}, | |
/** | |
* Insert or update a link in the content area (based on current editor selection) | |
* | |
* Parameters: {Object} attrs | |
*/ | |
insertLink: function(attrs, opts) { | |
this.getInstance().execCommand("mceInsertLink", false, attrs, opts); | |
}, | |
/** | |
* Remove the link from the currently selected node (if any). | |
*/ | |
removeLink: function() { | |
this.getInstance().execCommand('unlink', false); | |
}, | |
/** | |
* Strip any editor-specific notation from link in order to make it presentable in the UI. | |
* | |
* Parameters: | |
* {Object} | |
* {DOMElement} | |
*/ | |
cleanLink: function(href, node) { | |
var cb = tinyMCE.settings['urlconverter_callback']; | |
if(cb) href = eval(cb + "(href, node, true);"); | |
// Turn into relative | |
if(href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) { | |
href = RegExp.$1; | |
} | |
// Get rid of TinyMCE's temporary URLs | |
if(href.match(/^javascript:\s*mctmp/)) href = ''; | |
return href; | |
}, | |
/** | |
* Creates a bookmark for the currently selected range, | |
* which can be used to reselect this range at a later point. | |
* @return {mixed} | |
*/ | |
createBookmark: function() { | |
return this.getInstance().selection.getBookmark(); | |
}, | |
/** | |
* Selects a bookmarked range previously saved through createBookmark(). | |
* @param {mixed} bookmark | |
*/ | |
moveToBookmark: function(bookmark) { | |
this.getInstance().selection.moveToBookmark(bookmark); | |
this.getInstance().focus(); | |
}, | |
/** | |
* Removes any selection & de-focuses this editor | |
*/ | |
blur: function() { | |
this.getInstance().selection.collapse(); | |
}, | |
/** | |
* Add new undo point with the current DOM content. | |
*/ | |
addUndo: function() { | |
this.getInstance().undoManager.add(); | |
} | |
}; | |
}); | |
// Override this to switch editor wrappers | |
ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; | |
(function($) { | |
$.entwine('ss', function($) { | |
/** | |
* Class: textarea.htmleditor | |
* | |
* Add tinymce to HtmlEditorFields within the CMS. Works in combination | |
* with a TinyMCE.init() call which is prepopulated with the used HTMLEditorConfig settings, | |
* and included in the page as an inline <script> tag. | |
*/ | |
$('textarea.htmleditor').entwine({ | |
Editor: null, | |
/** | |
* Constructor: onmatch | |
*/ | |
onadd: function() { | |
var edClass = this.data('editor') || 'default', ed = ss.editorWrappers[edClass](); | |
this.setEditor(ed); | |
// Using a global config (generated through HTMLEditorConfig PHP logic). | |
// Depending on browser cache load behaviour, entwine's DOMMaybeChanged | |
// can be called before the bottom-most inline script tag is executed, | |
// which defines the global. If that's the case, wait for the window load. | |
if(typeof ssTinyMceConfig != 'undefined') this.redraw(); | |
this._super(); | |
}, | |
onremove: function() { | |
var ed = tinyMCE.get(this.attr('id')); | |
if (ed) { | |
try { | |
ed.remove(); | |
} catch(ex) {} | |
try { | |
ed.destroy(); | |
} catch(ex) {} | |
// Remove any residual tinyMCE editor element | |
this.next('.mceEditor').remove(); | |
// TinyMCE leaves behind events. We should really fix TinyMCE, but lets brute force it for now | |
$.each(jQuery.cache, function(){ | |
var source = this.handle && this.handle.elem; | |
if (!source) return; | |
var parent = source; | |
while (parent && parent.nodeType == 1) parent = parent.parentNode; | |
if (!parent) $(source).unbind().remove(); | |
}); | |
} | |
this._super(); | |
}, | |
getContainingForm: function(){ | |
return this.closest('form'); | |
}, | |
fromWindow: { | |
onload: function(){ | |
this.redraw(); | |
} | |
}, | |
redraw: function() { | |
// Using a global config (generated through HTMLEditorConfig PHP logic) | |
var config = ssTinyMceConfig, self = this, ed = this.getEditor(); | |
ed.init(config); | |
// Create editor instance and render it. | |
// Similar logic to adapter/jquery/jquery.tinymce.js, but doesn't rely on monkey-patching | |
// jQuery methods, and avoids replicate the script lazyloading which is already in place with jQuery.ondemand. | |
ed.create(this.attr('id'), config); | |
this._super(); | |
}, | |
/** | |
* Make sure the editor has flushed all it's buffers before the form is submitted. | |
*/ | |
'from .cms-edit-form': { | |
onbeforesubmitform: function(e) { | |
this.getEditor().save(); | |
this._super(); | |
} | |
}, | |
oneditorinit: function() { | |
// Delayed show because TinyMCE calls hide() via setTimeout on removing an element, | |
// which is called in quick succession with adding a new editor after ajax loading new markup | |
//storing the container object before setting timeout | |
var redrawObj = $(this.getEditor().getInstance().getContainer()); | |
setTimeout(function() { | |
redrawObj.show(); | |
}, 10); | |
}, | |
'from .cms-container': { | |
onbeforestatechange: function(){ | |
this.css('visibility', 'hidden'); | |
var ed = this.getEditor(), container = (ed && ed.getInstance()) ? ed.getContainer() : null; | |
if(container && container.length) container.remove(); | |
} | |
}, | |
isChanged: function() { | |
var ed = this.getEditor(); | |
return (ed && ed.getInstance() && ed.isDirty()); | |
}, | |
resetChanged: function() { | |
var ed = this.getEditor(); | |
if(typeof tinyMCE == 'undefined') return; | |
// TODO Abstraction layer | |
var inst = tinyMCE.getInstanceById(this.attr('id')); | |
if (inst) inst.startContent = tinymce.trim(inst.getContent({format : 'raw', no_events : 1})); | |
}, | |
openLinkDialog: function() { | |
this.openDialog('link'); | |
}, | |
openMediaDialog: function() { | |
this.openDialog('media'); | |
}, | |
openDialog: function(type) { | |
var capitalize = function(text) { | |
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); | |
}; | |
var self = this, url = $('#cms-editor-dialogs').data('url' + capitalize(type) + 'form'), | |
dialog = $('.htmleditorfield-' + type + 'dialog'); | |
if(dialog.length) { | |
dialog.getForm().setElement(this); | |
dialog.open(); | |
} else { | |
// Show a placeholder for instant feedback. Will be replaced with actual | |
// form dialog once its loaded. | |
dialog = $('<div class="htmleditorfield-dialog htmleditorfield-' + type + 'dialog loading">'); | |
$('body').append(dialog); | |
$.ajax({ | |
url: url, | |
complete: function() { | |
dialog.removeClass('loading'); | |
}, | |
success: function(html) { | |
dialog.html(html); | |
dialog.getForm().setElement(self); | |
dialog.trigger('ssdialogopen'); | |
} | |
}); | |
} | |
} | |
}); | |
$('.htmleditorfield-dialog').entwine({ | |
onadd: function() { | |
// Create jQuery dialog | |
if (!this.is('.ui-dialog-content')) { | |
this.ssdialog({autoOpen: true}); | |
} | |
this._super(); | |
}, | |
getForm: function() { | |
return this.find('form'); | |
}, | |
open: function() { | |
this.ssdialog('open'); | |
}, | |
close: function() { | |
this.ssdialog('close'); | |
}, | |
toggle: function(bool) { | |
if(this.is(':visible')) this.close(); | |
else this.open(); | |
} | |
}); | |
/** | |
* Base form implementation for interactions with an editor instance, | |
* mostly geared towards modification and insertion of content. | |
*/ | |
$('form.htmleditorfield-form').entwine({ | |
Selection: null, | |
// Implementation-dependent serialization of the current editor selection state | |
Bookmark: null, | |
// DOMElement pointing to the currently active textarea | |
Element: null, | |
setSelection: function(node) { | |
return this._super($(node)); | |
}, | |
onadd: function() { | |
// Move title from headline to (jQuery compatible) title attribute | |
var titleEl = this.find(':header:first'); | |
this.getDialog().attr('title', titleEl.text()); | |
this._super(); | |
}, | |
onremove: function() { | |
this.setSelection(null); | |
this.setBookmark(null); | |
this.setElement(null); | |
this._super(); | |
}, | |
getDialog: function() { | |
// TODO Refactor to listen to form events to remove two-way coupling | |
return this.closest('.htmleditorfield-dialog'); | |
}, | |
fromDialog: { | |
onssdialogopen: function(){ | |
var ed = this.getEditor(); | |
ed.onopen(); | |
this.setSelection(ed.getSelectedNode()); | |
this.setBookmark(ed.createBookmark()); | |
ed.blur(); | |
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:enabled').eq(0).focus(); | |
this.updateFromEditor(); | |
this.redraw(); | |
}, | |
onssdialogclose: function(){ | |
var ed = this.getEditor(); | |
ed.onclose(); | |
ed.moveToBookmark(this.getBookmark()); | |
this.setSelection(null); | |
this.setBookmark(null); | |
this.resetFields(); | |
} | |
}, | |
/** | |
* @return Object ss.editorWrapper instance | |
*/ | |
getEditor: function(){ | |
return this.getElement().getEditor(); | |
}, | |
modifySelection: function(callback) { | |
var ed = this.getEditor(); | |
ed.moveToBookmark(this.getBookmark()); | |
callback.call(this, ed); | |
this.setSelection(ed.getSelectedNode()); | |
this.setBookmark(ed.createBookmark()); | |
ed.blur(); | |
}, | |
updateFromEditor: function() { | |
/* NOP */ | |
}, | |
redraw: function() { | |
/* NOP */ | |
}, | |
resetFields: function() { | |
// Flush the tree drop down fields, as their content might get changed in other parts of the CMS, ie in Files and images | |
this.find('.tree-holder').empty(); | |
} | |
}); | |
/** | |
* Inserts and edits links in an html editor, including internal/external web links, | |
* links to files on the webserver, email addresses, and anchors in the existing html content. | |
* Every variation has its own fields (e.g. a "target" attribute doesn't make sense for an email link), | |
* which are toggled through a type dropdown. Variations share fields, so there's only one "title" field in the form. | |
*/ | |
$('form.htmleditorfield-linkform').entwine({ | |
// TODO Entwine doesn't respect submits triggered by ENTER key | |
onsubmit: function(e) { | |
this.insertLink(); | |
this.getDialog().close(); | |
return false; | |
}, | |
resetFields: function() { | |
this._super(); | |
// Reset the form using a native call. This will also correctly reset checkboxes and radio buttons. | |
this[0].reset(); | |
}, | |
redraw: function() { | |
this._super(); | |
var linkType = this.find(':input[name=LinkType]:checked').val(); | |
this.addAnchorSelector(); | |
// Toggle field visibility depending on the link type. | |
this.find('div.content .field').hide(); | |
this.find('.field#LinkType').show(); | |
this.find('.field#' + linkType).show(); | |
if(linkType == 'internal' || linkType == 'anchor') this.find('.field#Anchor').show(); | |
if(linkType !== 'email') this.find('.field#TargetBlank').show(); | |
if(linkType == 'anchor') this.find('.field#AnchorSelector').show(); | |
this.find('.field#Description').show(); | |
}, | |
/** | |
* @return Object Keys: 'href', 'target', 'title' | |
*/ | |
getLinkAttributes: function() { | |
var href, target = null, anchor = this.find(':input[name=Anchor]').val(); | |
// Determine target | |
if(this.find(':input[name=TargetBlank]').is(':checked')) target = '_blank'; | |
// All other attributes | |
switch(this.find(':input[name=LinkType]:checked').val()) { | |
case 'internal': | |
href = '[sitetree_link,id=' + this.find(':input[name=internal]').val() + ']'; | |
if(anchor) href += '#' + anchor; | |
break; | |
case 'anchor': | |
href = '#' + anchor; | |
break; | |
case 'file': | |
href = '[file_link,id=' + this.find(':input[name=file]').val() + ']'; | |
target = '_blank'; | |
break; | |
case 'email': | |
href = 'mailto:' + this.find(':input[name=email]').val(); | |
target = null; | |
break; | |
// case 'external': | |
default: | |
href = this.find(':input[name=external]').val(); | |
// Prefix the URL with "http://" if no prefix is found | |
if(href.indexOf('://') == -1) href = 'http://' + href; | |
break; | |
} | |
return { | |
href : href, | |
target : target, | |
title : this.find(':input[name=Description]').val() | |
}; | |
}, | |
insertLink: function() { | |
this.modifySelection(function(ed){ | |
ed.insertLink(this.getLinkAttributes()); | |
}); | |
this.updateFromEditor(); | |
}, | |
removeLink: function() { | |
this.modifySelection(function(ed){ | |
ed.removeLink(); | |
}); | |
this.close(); | |
}, | |
/** | |
* Builds an anchor selector element and injects it into the DOM next to the anchor field. | |
*/ | |
addAnchorSelector: function() { | |
// Avoid adding twice | |
if(this.find(':input[name=AnchorSelector]').length) return; | |
var self = this; | |
var anchorSelector = $( | |
'<select id="Form_EditorToolbarLinkForm_AnchorSelector" name="AnchorSelector"></select>' | |
); | |
this.find(':input[name=Anchor]').parent().append(anchorSelector); | |
// Initialise the anchor dropdown. | |
this.updateAnchorSelector(); | |
// copy the value from dropdown to the text field | |
anchorSelector.change(function(e) { | |
self.find(':input[name="Anchor"]').val($(this).val()); | |
}); | |
}, | |
/** | |
* Fetch relevant anchors, depending on the link type. | |
* | |
* @return $.Deferred A promise of an anchor array, or an error message. | |
*/ | |
getAnchors: function() { | |
var linkType = this.find(':input[name=LinkType]:checked').val(); | |
var dfdAnchors = $.Deferred(); | |
switch (linkType) { | |
case 'anchor': | |
// Fetch from the local editor. | |
var collectedAnchors = []; | |
var ed = this.getEditor(); | |
// name attribute is defined as CDATA, should accept all characters and entities | |
// http://www.w3.org/TR/1999/REC-html401-19991224/struct/links.html#h-12.2 | |
if(ed) { | |
var raw = ed.getContent().match(/name="([^"]+?)"|name='([^']+?)'/gim); | |
if (raw && raw.length) { | |
for(var i = 0; i < raw.length; i++) { | |
collectedAnchors.push(raw[i].substr(6).replace(/"$/, '')); | |
} | |
} | |
} | |
dfdAnchors.resolve(collectedAnchors); | |
break; | |
case 'internal': | |
// Fetch available anchors from the target internal page. | |
var pageId = this.find(':input[name=internal]').val(); | |
if (pageId) { | |
$.ajax({ | |
url: $.path.addSearchParams( | |
this.attr('action').replace('LinkForm', 'getanchors'), | |
{'PageID': parseInt(pageId)} | |
), | |
success: function(body, status, xhr) { | |
dfdAnchors.resolve($.parseJSON(body)); | |
}, | |
error: function(xhr, status) { | |
dfdAnchors.reject(xhr.responseText); | |
} | |
}); | |
} else { | |
dfdAnchors.resolve([]); | |
} | |
break; | |
default: | |
// This type does not support anchors at all. | |
dfdAnchors.reject(ss.i18n._t( | |
'HtmlEditorField.ANCHORSNOTSUPPORTED', | |
'Anchors are not supported for this link type.' | |
)); | |
break; | |
} | |
return dfdAnchors.promise(); | |
}, | |
/** | |
* Update the anchor list in the dropdown. | |
*/ | |
updateAnchorSelector: function() { | |
var self = this; | |
var selector = this.find(':input[name=AnchorSelector]'); | |
var dfdAnchors = this.getAnchors(); | |
// Inform the user we are loading. | |
selector.empty(); | |
selector.append($( | |
'<option value="" selected="1">' + | |
ss.i18n._t('HtmlEditorField.LOOKINGFORANCHORS', 'Looking for anchors...') + | |
'</option>' | |
)); | |
dfdAnchors.done(function(anchors) { | |
selector.empty(); | |
selector.append($( | |
'<option value="" selected="1">' + | |
ss.i18n._t('HtmlEditorField.SelectAnchor') + | |
'</option>' | |
)); | |
if (anchors) { | |
for (var j = 0; j < anchors.length; j++) { | |
selector.append($('<option value="'+anchors[j]+'">'+anchors[j]+'</option>')); | |
} | |
} | |
}).fail(function(message) { | |
selector.empty(); | |
selector.append($( | |
'<option value="" selected="1">' + | |
message + | |
'</option>' | |
)); | |
}); | |
// Poke the selector for IE8, otherwise the changes won't be noticed. | |
if ($.browser.msie) selector.hide().show(); | |
}, | |
/** | |
* Updates the state of the dialog inputs to match the editor selection. | |
* If selection does not contain a link, resets the fields. | |
*/ | |
updateFromEditor: function() { | |
var htmlTagPattern = /<\S[^><]*>/g, fieldName, data = this.getCurrentLink(); | |
if(data) { | |
for(fieldName in data) { | |
var el = this.find(':input[name=' + fieldName + ']'), selected = data[fieldName]; | |
// Remove html tags in the selected text that occurs on IE browsers | |
if(typeof(selected) == 'string') selected = selected.replace(htmlTagPattern, ''); | |
// Set values and invoke the triggers (e.g. for TreeDropdownField). | |
if(el.is(':checkbox')) { | |
el.prop('checked', selected).change(); | |
} else if(el.is(':radio')) { | |
el.val([selected]).change(); | |
} else { | |
el.val(selected).change(); | |
} | |
} | |
} | |
}, | |
/** | |
* Return information about the currently selected link, suitable for population of the link form. | |
* | |
* Returns null if no link was currently selected. | |
*/ | |
getCurrentLink: function() { | |
var selectedEl = this.getSelection(), | |
href = "", target = "", title = "", action = "insert", style_class = ""; | |
// We use a separate field for linkDataSource from tinyMCE.linkElement. | |
// If we have selected beyond the range of an <a> element, then use use that <a> element to get the link data source, | |
// but we don't use it as the destination for the link insertion | |
var linkDataSource = null; | |
if(selectedEl.length) { | |
if(selectedEl.is('a')) { | |
// Element is a link | |
linkDataSource = selectedEl; | |
// TODO Limit to inline elements, otherwise will also apply to e.g. paragraphs which already contain one or more links | |
// } else if((selectedEl.find('a').length)) { | |
// // Element contains a link | |
// var firstLinkEl = selectedEl.find('a:first'); | |
// if(firstLinkEl.length) linkDataSource = firstLinkEl; | |
} else { | |
// Element is a child of a link | |
linkDataSource = selectedEl = selectedEl.parents('a:first'); | |
} | |
} | |
if(linkDataSource && linkDataSource.length) this.modifySelection(function(ed){ | |
ed.selectNode(linkDataSource[0]); | |
}); | |
// Is anchor not a link | |
if (!linkDataSource.attr('href')) linkDataSource = null; | |
if (linkDataSource) { | |
href = linkDataSource.attr('href'); | |
target = linkDataSource.attr('target'); | |
title = linkDataSource.attr('title'); | |
style_class = linkDataSource.attr('class'); | |
href = this.getEditor().cleanLink(href, linkDataSource); | |
action = "update"; | |
} | |
if(href.match(/^mailto:(.*)$/)) { | |
return { | |
LinkType: 'email', | |
email: RegExp.$1, | |
Description: title | |
}; | |
} else if(href.match(/^(assets\/.*)$/) || href.match(/^\[file_link\s*(?:\s*|%20|,)?id=([0-9]+)\]?(#.*)?$/)) { | |
return { | |
LinkType: 'file', | |
file: RegExp.$1, | |
Description: title, | |
TargetBlank: target ? true : false | |
}; | |
} else if(href.match(/^#(.*)$/)) { | |
return { | |
LinkType: 'anchor', | |
Anchor: RegExp.$1, | |
Description: title, | |
TargetBlank: target ? true : false | |
}; | |
} else if(href.match(/^\[sitetree_link(?:\s*|%20|,)?id=([0-9]+)\]?(#.*)?$/i)) { | |
return { | |
LinkType: 'internal', | |
internal: RegExp.$1, | |
Anchor: RegExp.$2 ? RegExp.$2.substr(1) : '', | |
Description: title, | |
TargetBlank: target ? true : false | |
}; | |
} else if(href) { | |
return { | |
LinkType: 'external', | |
external: href, | |
Description: title, | |
TargetBlank: target ? true : false | |
}; | |
} else { | |
// No link/invalid link selected. | |
return null; | |
} | |
} | |
}); | |
$('form.htmleditorfield-linkform input[name=LinkType]').entwine({ | |
onclick: function(e) { | |
this.parents('form:first').redraw(); | |
this._super(); | |
}, | |
onchange: function() { | |
this.parents('form:first').redraw(); | |
// Update if a anchor-supporting link type is selected. | |
var linkType = this.parent().find(':checked').val(); | |
if (linkType==='anchor' || linkType==='internal') { | |
this.parents('form.htmleditorfield-linkform').updateAnchorSelector(); | |
} | |
this._super(); | |
} | |
}); | |
$('form.htmleditorfield-linkform input[name=internal]').entwine({ | |
/** | |
* Update the anchor dropdown if a different page is selected in the "internal" dropdown. | |
*/ | |
onvalueupdated: function() { | |
this.parents('form.htmleditorfield-linkform').updateAnchorSelector(); | |
this._super(); | |
} | |
}); | |
$('form.htmleditorfield-linkform :submit[name=action_remove]').entwine({ | |
onclick: function(e) { | |
this.parents('form:first').removeLink(); | |
this._super(); | |
return false; | |
} | |
}); | |
/** | |
* Responsible for inserting media files, although only images are supported so far. | |
* Allows to select one or more files, and load form fields for each file via ajax. | |
* This allows us to tailor the form fields to the file type (e.g. different ones for images and flash), | |
* as well as add new form fields via framework extensions. | |
* The inputs on each of those files are used for constructing the HTML to insert into | |
* the rich text editor. Also allows editing the properties of existing files if any are selected in the editor. | |
* Note: Not each file has a representation on the webserver filesystem, supports insertion and editing | |
* of remove files as well. | |
*/ | |
$('form.htmleditorfield-mediaform').entwine({ | |
toggleCloseButton: function(){ | |
console.log('Toggle close button'); | |
var updateExisting = Boolean(this.find('.ss-htmleditorfield-file').length); | |
this.find('.overview .action-delete')[updateExisting ? 'hide' : 'show'](); | |
}, | |
onsubmit: function() { | |
console.log('----------------'); | |
console.log('Submission of insert media'); | |
this.modifySelection(function(ed){ | |
this.find('.ss-htmleditorfield-file').each(function() { | |
console.log("Inserting", ed, this); | |
$(this).insertHTML(ed); | |
}); | |
console.log('Repaint'); | |
ed.repaint(); | |
}); | |
console.log('Close dialog'); | |
this.getDialog().close(); | |
return false; | |
}, | |
updateFromEditor: function() { | |
console.log('Update from editor'); | |
var self = this, node = this.getSelection(); | |
// TODO Depends on managed mime type | |
if(node.is('img')) { | |
this.showFileView(node.data('url') || node.attr('src')).done(function(filefield) { | |
filefield.updateFromNode(node); | |
self.toggleCloseButton(); | |
self.redraw(); | |
}); | |
} | |
this.redraw(); | |
}, | |
redraw: function(updateExisting) { | |
console.log('Redraw'); | |
this._super(); | |
var node = this.getSelection(), | |
hasItems = Boolean(this.find('.ss-htmleditorfield-file').length), | |
editingSelected = node.is('img'), | |
header = this.find('.header-edit'); | |
console.log('Has items', hasItems); | |
var nfiles = this.find('.ss-htmleditorfield-file').length; | |
console.log('Number of files', nfiles); | |
// Only show second step if files are selected | |
header[(hasItems) ? 'show' : 'hide'](); | |
// Disable "insert" button if no files are selected | |
console.log('Enable/Disable insert button', 'ui state disabled', !hasItems); | |
// NOTE line 3 of this only visually disables the button, not physically disable it | |
// SCRATCH THAT, enable/disable does the job | |
// CAN RECREATE BUG LOCALLY BY MULTI CLICKING | |
this.find('.Actions :submit') | |
.button(hasItems ? 'enable' : 'disable') | |
.toggleClass('ui-state-disabled', !hasItems); | |
// Hide file selection and step labels when editing an existing file | |
this.find('#MediaFormInsertMediaTabs,.header-edit')[editingSelected ? 'hide' : 'show'](); | |
// TODO Way too much knowledge on UploadField internals, use viewfile URL directly instead | |
console.log('cleaning UI allegedly'); | |
this.find('.htmleditorfield-mediaform-heading.insert')[editingSelected ? 'hide' : 'show'](); | |
this.find('.ss-uploadfield-item-actions')[editingSelected ? 'hide' : 'show'](); | |
this.find('.ss-uploadfield-item-name')[editingSelected ? 'hide' : 'show'](); | |
this.find('.ss-uploadfield-item-preview')[editingSelected ? 'hide' : 'show'](); | |
this.find('.Actions .media-insert')[editingSelected ? 'hide' : 'show'](); | |
this.find('.htmleditorfield-mediaform-heading.update')[editingSelected ? 'show' : 'hide'](); | |
this.find('.Actions .media-update')[editingSelected ? 'show' : 'hide'](); | |
this.find('.ss-uploadfield-item-editform').toggleEditForm(editingSelected); | |
}, | |
resetFields: function() { | |
console.log('Reset fields'); | |
this.find('.ss-htmleditorfield-file').remove(); // Remove any existing views | |
this.find('.ss-gridfield-items .ui-selected').removeClass('ui-selected'); // Unselect all items | |
this.find('li.ss-uploadfield-item').remove(); // Remove all selected items | |
this.redraw(); | |
this._super(); | |
}, | |
getFileView: function(idOrUrl) { | |
console.log('Get file view'); | |
return this.find('.ss-htmleditorfield-file[data-id=' + idOrUrl + ']'); | |
}, | |
showFileView: function(idOrUrl) { | |
console.log('show file view'); | |
var self = this, params = (Number(idOrUrl) == idOrUrl) ? {ID: idOrUrl} : {FileURL: idOrUrl}; | |
var item = $('<div class="ss-htmleditorfield-file loading" />'); | |
this.find('.content-edit').prepend(item); | |
var dfr = $.Deferred(); | |
// does the actual upload? | |
$.ajax({ | |
url: $.path.addSearchParams(this.attr('action').replace(/MediaForm/, 'viewfile'), params), | |
success: function(html, status, xhr) { | |
console.log('T1 AJAX SUCCESS REDRAW UPLOAD FIELD'); | |
var newItem = $(html).filter('.ss-htmleditorfield-file'); | |
console.log('T2 AJAX SUCCESS REDRAW UPLOAD FIELD'); | |
item.replaceWith(newItem); | |
console.log('T3 AJAX SUCCESS REDRAW UPLOAD FIELD'); | |
self.redraw(); | |
console.log('T4 AJAX SUCCESS REDRAW UPLOAD FIELD'); | |
dfr.resolve(newItem); | |
console.log('T5 AJAX SUCCESS REDRAW UPLOAD FIELD'); | |
}, | |
error: function() { | |
item.remove(); | |
dfr.reject(); | |
} | |
}); | |
return dfr.promise(); | |
} | |
}); | |
$('form.htmleditorfield-mediaform .ss-gridfield-items').entwine({ | |
onselectableselected: function(e, ui) { | |
var form = this.closest('form'), item = $(ui.selected); | |
if(!item.is('.ss-gridfield-item')) return; | |
form.closest('form').showFileView(item.data('id')); | |
form.redraw(); | |
}, | |
onselectableunselected: function(e, ui) { | |
var form = this.closest('form'), item = $(ui.unselected); | |
if(!item.is('.ss-gridfield-item')) return; | |
form.getFileView(item.data('id')).remove(); | |
form.redraw(); | |
} | |
}); | |
/** | |
* Show the second step after uploading an image | |
*/ | |
$('form.htmleditorfield-form.htmleditorfield-mediaform div.ss-assetuploadfield').entwine({ | |
onfileuploaddone: function(e) { | |
console.log('file upload done'); | |
}, | |
onfileuploadcomplete: function(e) { | |
console.log('file upload complete'); | |
}, | |
//the UploadField div.ss-uploadfield-editandorganize is hidden in CSS, | |
// because we use the detail view for each individual file instead | |
onfileuploadstop: function(e) { | |
console.log("File upload stop"); | |
var form = this.closest('form'); | |
//update the editFields to show those Files that are newly uploaded | |
var editFieldIDs = []; | |
form.find('div.content-edit').find('div.ss-htmleditorfield-file').each(function(){ | |
//get the uploaded file ID when this event triggers, signaling the upload has compeleted successfully | |
editFieldIDs.push($(this).data('id')); | |
}); | |
// NOTE timing of this seems to be an issue | |
// we only want this .ss-uploadfield-files - else we get all ss-uploadfield-files wich include the ones not related to #tinymce insertmedia | |
var uploadedFiles = $('.ss-uploadfield-files', this).children('.ss-uploadfield-item'); | |
uploadedFiles.each(function(){ | |
console.log("Uploaded file", $(this).getHTML()); | |
var uploadedID = $(this).data('fileid'); | |
if (uploadedID && $.inArray(uploadedID, editFieldIDs) == -1) { | |
//trigger the detail view for filling out details about the file we are about to insert into TinyMCE | |
$(this).remove(); // Remove successfully added item from the queue | |
form.showFileView(uploadedID); | |
} | |
}); | |
console.log('About to redraw form'); | |
form.redraw(); | |
} | |
}); | |
$('form.htmleditorfield-form.htmleditorfield-mediaform input.remoteurl').entwine({ | |
onadd: function() { | |
this._super(); | |
this.validate(); | |
}, | |
onkeyup: function() { | |
this.validate(); | |
}, | |
onchange: function() { | |
this.validate(); | |
}, | |
getAddButton: function() { | |
return this.closest('.CompositeField').find('button.add-url'); | |
}, | |
validate: function() { | |
var val = this.val(), orig = val; | |
val = $.trim(val); | |
val = val.replace(/^https?:\/\//i, ''); | |
if (orig !== val) this.val(val); | |
this.getAddButton().button(!!val ? 'enable' : 'disable'); | |
return !!val; | |
} | |
}); | |
/** | |
* Show the second step after adding a URL | |
*/ | |
$('form.htmleditorfield-form.htmleditorfield-mediaform .add-url').entwine({ | |
getURLField: function() { | |
return this.closest('.CompositeField').find('input.remoteurl'); | |
}, | |
onclick: function(e) { | |
var urlField = this.getURLField(), container = this.closest('.CompositeField'), form = this.closest('form'); | |
if (urlField.validate()) { | |
container.addClass('loading'); | |
form.showFileView('http://' + urlField.val()).done(function() { | |
container.removeClass('loading'); | |
}); | |
form.redraw(); | |
} | |
return false; | |
} | |
}); | |
/** | |
* Represents a single selected file, together with a set of form fields to edit its properties. | |
* Overload this based on the media type to determine how the HTML should be created. | |
*/ | |
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file').entwine({ | |
/** | |
* @return {Object} Map of HTML attributes which can be set on the created DOM node. | |
*/ | |
getAttributes: function() { | |
}, | |
/** | |
* @return {Object} Map of additional properties which can be evaluated | |
* by the specific media type. | |
*/ | |
getExtraData: function() { | |
}, | |
// FIXME HERE | |
/** | |
* @return {String} HTML suitable for insertion into the rich text editor | |
*/ | |
getHTML: function() { | |
console.log('**** GET HTML ENTWINE ****'); | |
// Assumes UploadField markup structure | |
return $('<div>').append( | |
$('<a/>').attr({href: this.data('url')}).text(this.find('.name').text()) | |
).html(); | |
}, | |
/** | |
* Insert updated HTML content into the rich text editor | |
*/ | |
insertHTML: function(ed) { | |
// Insert content | |
console.log('**** INSERT HTML ENTWINE ****'); | |
console.log(this.getHTML()); | |
ed.replaceContent(this.getHTML()); | |
}, | |
/** | |
* Updates the form values from an existing node in the editor. | |
* | |
* @param {DOMElement} | |
*/ | |
updateFromNode: function(node) { | |
}, | |
/** | |
* Transforms values set on the dimensions form fields based on two constraints: | |
* An aspect ration, and max width/height values. Writes back to the field properties as required. | |
* | |
* @param {String} The dimension to constrain the other value by, if any ("Width" or "Height") | |
* @param {Int} Optional max width | |
* @param {Int} Optional max height | |
*/ | |
updateDimensions: function(constrainBy, maxW, maxH) { | |
var widthEl = this.find(':input[name=Width]'), | |
heightEl = this.find(':input[name=Height]'), | |
w = widthEl.val(), | |
h = heightEl.val(), | |
aspect; | |
// Proportionate updating of heights, using the original values | |
if(w && h) { | |
if(constrainBy) { | |
aspect = heightEl.getOrigVal() / widthEl.getOrigVal(); | |
// Uses floor() and ceil() to avoid both fields constantly lowering each other's values in rounding situations | |
if(constrainBy == 'Width') { | |
if(maxW && w > maxW) w = maxW; | |
h = Math.floor(w * aspect); | |
} else if(constrainBy == 'Height') { | |
if(maxH && h > maxH) h = maxH; | |
w = Math.ceil(h / aspect); | |
} | |
} else { | |
if(maxW && w > maxW) w = maxW; | |
if(maxH && h > maxH) h = maxH; | |
} | |
widthEl.val(w); | |
heightEl.val(h); | |
} | |
} | |
}); | |
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file.image').entwine({ | |
getAttributes: function() { | |
var width = this.find(':input[name=Width]').val(), | |
height = this.find(':input[name=Height]').val(); | |
return { | |
'src' : this.find(':input[name=URL]').val(), | |
'alt' : this.find(':input[name=AltText]').val(), | |
'width' : width ? parseInt(width, 10) : null, | |
'height' : height ? parseInt(height, 10) : null, | |
'title' : this.find(':input[name=Title]').val(), | |
'class' : this.find(':input[name=CSSClass]').val() | |
}; | |
}, | |
getExtraData: function() { | |
return { | |
'CaptionText': this.find(':input[name=CaptionText]').val() | |
}; | |
}, | |
getHTML: function() { | |
/* NOP */ | |
}, | |
/** | |
* Logic similar to TinyMCE 'advimage' plugin, insertAndClose() method. | |
*/ | |
insertHTML: function(ed) { | |
console.log('IMAGE INSERT HTML FN'); | |
var form = this.closest('form'); | |
var node = form.getSelection(); | |
if (!ed) ed = form.getEditor(); | |
// Get the attributes & extra data | |
var attrs = this.getAttributes(), extraData = this.getExtraData(); | |
// Find the element we are replacing - either the img, it's wrapper parent, or nothing (if creating) | |
var replacee = (node && node.is('img')) ? node : null; | |
if (replacee && replacee.parent().is('.captionImage')) replacee = replacee.parent(); | |
// Find the img node - either the existing img or a new one, and update it | |
var img = (node && node.is('img')) ? node : $('<img />'); | |
img.attr(attrs); | |
// Any existing figure or caption node | |
var container = img.parent('.captionImage'), caption = container.find('.caption'); | |
// If we've got caption text, we need a wrapping div.captionImage and sibling p.caption | |
if (extraData.CaptionText) { | |
if (!container.length) { | |
container = $('<div></div>'); | |
} | |
container.attr('class', 'captionImage '+attrs['class']).css('width', attrs.width); | |
if (!caption.length) { | |
caption = $('<p class="caption"></p>').appendTo(container); | |
} | |
caption.attr('class', 'caption '+attrs['class']).text(extraData.CaptionText); | |
} | |
// Otherwise forget they exist | |
else { | |
container = caption = null; | |
} | |
// The element we are replacing the replacee with | |
var replacer = container ? container : img; | |
// If we're replacing something, and it's not with itself, do so | |
if (replacee && replacee.not(replacer).length) { | |
replacee.replaceWith(replacer); | |
} | |
// If we have a wrapper element, make sure the img is the first child - img might be the | |
// replacee, and the wrapper the replacer, and we can't do this till after the replace has happened | |
if (container) { | |
container.prepend(img); | |
} | |
// If we don't have a replacee, then we need to insert the whole HTML | |
if (!replacee) { | |
// Otherwise insert the whole HTML content | |
ed.repaint(); | |
ed.insertContent($('<div />').append(replacer).html(), {skip_undo : 1}); | |
} | |
ed.addUndo(); | |
ed.repaint(); | |
}, | |
updateFromNode: function(node) { | |
console.log('IMAGE UPDATE FROM NODE'); | |
this.find(':input[name=AltText]').val(node.attr('alt')); | |
this.find(':input[name=Title]').val(node.attr('title')); | |
this.find(':input[name=CSSClass]').val(node.attr('class')); | |
this.find(':input[name=Width]').val(node.width()); | |
this.find(':input[name=Height]').val(node.height()); | |
this.find(':input[name=CaptionText]').val(node.siblings('.caption:first').text()); | |
} | |
}); | |
/** | |
* Insert a flash object tag into the content. | |
* Requires the 'media' plugin for serialization of tags into <img> placeholders. | |
*/ | |
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file.flash').entwine({ | |
getAttributes: function() { | |
var width = this.find(':input[name=Width]').val(), | |
height = this.find(':input[name=Height]').val(); | |
return { | |
'src' : this.find(':input[name=URL]').val(), | |
'width' : width ? parseInt(width, 10) : null, | |
'height' : height ? parseInt(height, 10) : null | |
}; | |
}, | |
getHTML: function() { | |
var attrs = this.getAttributes(); | |
// Emulate serialization from 'media' plugin | |
var el = tinyMCE.activeEditor.plugins.media.dataToImg({ | |
'type': 'flash', | |
'width': attrs.width, | |
'height': attrs.height, | |
'params': {'src': attrs.src}, | |
'video': {'sources': []} | |
}); | |
return $('<div />').append(el).html(); // Little hack to get outerHTML string | |
}, | |
updateFromNode: function(node) { | |
// TODO Not implemented | |
} | |
}); | |
/** | |
* Insert an oembed object tag into the content. | |
* Requires the 'media' plugin for serialization of tags into <img> placeholders. | |
*/ | |
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file.embed').entwine({ | |
getAttributes: function() { | |
var width = this.find(':input[name=Width]').val(), | |
height = this.find(':input[name=Height]').val(); | |
return { | |
'src' : this.find('.thumbnail-preview').attr('src'), | |
'width' : width ? parseInt(width, 10) : null, | |
'height' : height ? parseInt(height, 10) : null, | |
'class' : this.find(':input[name=CSSClass]').val(), | |
'alt' : this.find(':input[name=AltText]').val(), | |
'title' : this.find(':input[name=Title]').val() | |
}; | |
}, | |
getExtraData: function() { | |
var width = this.find(':input[name=Width]').val(), | |
height = this.find(':input[name=Height]').val(); | |
return { | |
'CaptionText': this.find(':input[name=CaptionText]').val(), | |
'Url': this.find(':input[name=URL]').val(), | |
'thumbnail': this.find('.thumbnail-preview').attr('src'), | |
'width' : width ? parseInt(width, 10) : null, | |
'height' : height ? parseInt(height, 10) : null, | |
'cssclass': this.find(':input[name=CSSClass]').val() | |
}; | |
}, | |
getHTML: function() { | |
var el, | |
attrs = this.getAttributes(), | |
extraData = this.getExtraData(), | |
// imgEl = $('<img id="_ss_tmp_img" />'); | |
imgEl = $('<img />').attr(attrs).addClass('ss-htmleditorfield-file embed'); | |
$.each(extraData, function (key, value) { | |
imgEl.attr('data-' + key, value); | |
}); | |
if(extraData.CaptionText) { | |
el = $('<div style="width: ' + attrs['width'] + 'px;" class="captionImage ' + attrs['class'] + '"><p class="caption">' + extraData.CaptionText + '</p></div>').prepend(imgEl); | |
} else { | |
el = imgEl; | |
} | |
return $('<div />').append(el).html(); // Little hack to get outerHTML string | |
}, | |
updateFromNode: function(node) { | |
this.find(':input[name=AltText]').val(node.attr('alt')); | |
this.find(':input[name=Title]').val(node.attr('title')); | |
this.find(':input[name=Width]').val(node.width()); | |
this.find(':input[name=Height]').val(node.height()); | |
this.find(':input[name=Title]').val(node.attr('title')); | |
this.find(':input[name=CSSClass]').val(node.data('cssclass')); | |
} | |
}); | |
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file .dimensions :input').entwine({ | |
OrigVal: null, | |
onmatch: function () { | |
this._super(); | |
this.setOrigVal(parseInt(this.val(), 10)); | |
}, | |
onunmatch: function() { | |
this._super(); | |
}, | |
onfocusout: function(e) { | |
this.closest('.ss-htmleditorfield-file').updateDimensions(this.attr('name')); | |
} | |
}); | |
/** | |
* Deselect item and remove the 'edit' view | |
*/ | |
$('form.htmleditorfield-mediaform .ss-uploadfield-item .ss-uploadfield-item-cancel').entwine({ | |
onclick: function(e) { | |
var form = this.closest('form'), file = this.closest('ss-uploadfield-item'); | |
form.find('.ss-gridfield-item[data-id=' + file.data('id') + ']').removeClass('ui-selected'); | |
this.closest('.ss-uploadfield-item').remove(); | |
form.redraw(); | |
e.preventDefault(); | |
} | |
}); | |
$('div.ss-assetuploadfield .ss-uploadfield-item-edit, div.ss-assetuploadfield .ss-uploadfield-item-name').entwine({ | |
getEditForm: function() { | |
return this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform'); | |
}, | |
fromEditForm: { | |
onchange: function(e){ | |
var form = $(e.target); | |
form.removeClass('edited'); //so edited class is only there once | |
form.addClass('edited'); | |
} | |
}, | |
onclick: function(e) { | |
var editForm = this.getEditForm(); | |
// Make sure we're in an HtmlEditorField here, or fall-back to _super(). HtmlEditorField with | |
// AssetUploadField doesn't use iframes, so needs its own toggleEditForm() logic | |
if (this.closest('.ss-uploadfield-item').hasClass('ss-htmleditorfield-file')) { | |
editForm.parent('ss-uploadfield-item').removeClass('ui-state-warning'); | |
editForm.toggleEditForm(); | |
e.preventDefault(); // Avoid a form submit | |
return false; // Avoid duplication from button | |
} | |
this._super(e); | |
} | |
}); | |
$('div.ss-assetuploadfield .ss-uploadfield-item-editform').entwine({ | |
toggleEditForm: function(bool) { | |
var itemInfo = this.prev('.ss-uploadfield-item-info'), status = itemInfo.find('.ss-uploadfield-item-status'); | |
var text=""; | |
if(bool === true || (bool !== false && this.height() === 0)) { | |
text = ss.i18n._t('UploadField.Editing', "Editing ..."); | |
this.height('auto'); | |
itemInfo.find('.toggle-details-icon').addClass('opened'); | |
status.removeClass('ui-state-success-text').removeClass('ui-state-warning-text'); | |
} else { | |
this.height(0); | |
itemInfo.find('.toggle-details-icon').removeClass('opened'); | |
if(!this.hasClass('edited')){ | |
text = ss.i18n._t('UploadField.NOCHANGES', 'No Changes'); | |
status.addClass('ui-state-success-text'); | |
}else{ | |
text = ss.i18n._t('UploadField.CHANGESSAVED', 'Changes Made'); | |
this.removeClass('edited'); | |
status.addClass('ui-state-success-text'); | |
} | |
} | |
status.attr('title',text).text(text); | |
} | |
}); | |
$('form.htmleditorfield-mediaform #ParentID .TreeDropdownField').entwine({ | |
onadd: function() { | |
this._super(); | |
// TODO Custom event doesn't fire in IE if registered through object literal | |
var self = this; | |
this.bind('change', function() { | |
var fileList = self.closest('form').find('.ss-gridfield'); | |
fileList.setState('ParentID', self.getValue()); | |
fileList.reload(); | |
}); | |
} | |
}); | |
}); | |
})(jQuery); | |
/** | |
* These callback globals hook it into tinymce. They need to be referenced in the TinyMCE config. | |
*/ | |
function sapphiremce_cleanup(type, value) { | |
if(type == 'get_from_editor') { | |
// replace indented text with a <blockquote> | |
value = value.replace(/<p [^>]*margin-left[^>]*>([^\n|\n\015|\015\n]*)<\/p>/ig,"<blockquote><p>$1</p></blockquote>"); | |
// replace VML pixel image references with image tags - experimental | |
value = value.replace(/<[a-z0-9]+:imagedata[^>]+src="?([^> "]+)"?[^>]*>/ig,"<img src=\"$1\">"); | |
// Word comments | |
value = value.replace(new RegExp('<(!--)([^>]*)(--)>', 'g'), ""); | |
// kill class=mso??? and on mouse* tags | |
value = value.replace(/([ \f\r\t\n\'\"])class=mso[a-z0-9]+[^ >]+/ig, "$1"); | |
value = value.replace(/([ \f\r\t\n\'\"]class=")mso[a-z0-9]+[^ ">]+ /ig, "$1"); | |
value = value.replace(/([ \f\r\t\n\'\"])class="mso[a-z0-9]+[^">]+"/ig, "$1"); | |
value = value.replace(/([ \f\r\t\n\'\"])on[a-z]+=[^ >]+/ig, "$1"); | |
value = value.replace(/ >/ig, ">"); | |
// remove everything that's in a closing tag | |
value = value.replace(/<(\/[A-Za-z0-9]+)[ \f\r\t\n]+[^>]*>/ig,"<$1>"); | |
} | |
if(type == 'get_from_editor_dom') { | |
jQuery(value).find('img').each(function() { | |
this.onresizestart = null; | |
this.onresizeend = null; | |
this.removeAttribute('onresizestart'); | |
this.removeAttribute('onresizeend'); | |
}); | |
} | |
return value; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Update from editor | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items false | |
HtmlEditorField.js?m=1437392823:973 Number of files 0 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled true | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items false | |
HtmlEditorField.js?m=1437392823:973 Number of files 0 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled true | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
HtmlEditorField.js?m=1437392823:1068 file upload done | |
HtmlEditorField.js?m=1437392823:1077 File upload stop | |
HtmlEditorField.js?m=1437392823:1091 Uploaded file undefined | |
HtmlEditorField.js?m=1437392823:1015 show file view | |
HtmlEditorField.js?m=1437392823:1100 About to redraw form | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items true | |
HtmlEditorField.js?m=1437392823:973 Number of files 1 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled false | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
jquery.js?m=1432796351:8206 Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check http://xhr.spec.whatwg.org/. | |
HtmlEditorField.js?m=1437392823:1027 T1 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:1029 T2 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:1031 T3 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items true | |
HtmlEditorField.js?m=1437392823:973 Number of files 1 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled false | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
HtmlEditorField.js?m=1437392823:1033 T4 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:1035 T5 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:929 ---------------- | |
HtmlEditorField.js?m=1437392823:930 Submission of insert media | |
HtmlEditorField.js?m=1437392823:933 Inserting Object {instance: k.c…e.Editor} <div class="ss-uploadfield-item image ss-htmleditorfield-file template-upload" data-id="164" data-url="assets/Uploads/screenshot746.png">…</div> | |
HtmlEditorField.js?m=1437392823:1267 IMAGE INSERT HTML FN | |
HtmlEditorField.js?m=1437392823:937 Repaint | |
HtmlEditorField.js?m=1437392823:942 Close dialog | |
HtmlEditorField.js?m=1437392823:1002 Reset fields | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items false | |
HtmlEditorField.js?m=1437392823:973 Number of files 0 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled true | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedl |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Update from editor | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items false | |
HtmlEditorField.js?m=1437392823:973 Number of files 0 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled true | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items false | |
HtmlEditorField.js?m=1437392823:973 Number of files 0 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled true | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
HtmlEditorField.js?m=1437392823:1068 file upload done | |
HtmlEditorField.js?m=1437392823:1077 File upload stop | |
HtmlEditorField.js?m=1437392823:1091 Uploaded file undefined | |
HtmlEditorField.js?m=1437392823:1015 show file view | |
HtmlEditorField.js?m=1437392823:1100 About to redraw form | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items true | |
HtmlEditorField.js?m=1437392823:973 Number of files 1 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled false | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
HtmlEditorField.js?m=1437392823:929 ---------------- | |
HtmlEditorField.js?m=1437392823:930 Submission of insert media | |
HtmlEditorField.js?m=1437392823:933 Inserting Object {instance: k.c…e.Editor} <div class="ss-htmleditorfield-file loading"></div> | |
HtmlEditorField.js?m=1437392823:1193 **** INSERT HTML ENTWINE **** | |
HtmlEditorField.js?m=1437392823:1182 **** GET HTML ENTWINE **** | |
HtmlEditorField.js?m=1437392823:1194 <a></a> | |
HtmlEditorField.js?m=1437392823:1182 **** GET HTML ENTWINE **** | |
HtmlEditorField.js?m=1437392823:937 Repaint | |
HtmlEditorField.js?m=1437392823:942 Close dialog | |
HtmlEditorField.js?m=1437392823:1002 Reset fields | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items false | |
HtmlEditorField.js?m=1437392823:973 Number of files 0 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled true | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
jquery.js?m=1432796351:8206 Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check http://xhr.spec.whatwg.org/. | |
HtmlEditorField.js?m=1437392823:1027 T1 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:1029 T2 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:1031 T3 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:962 Redraw | |
HtmlEditorField.js?m=1437392823:970 Has items false | |
HtmlEditorField.js?m=1437392823:973 Number of files 0 | |
HtmlEditorField.js?m=1437392823:979 Enable/Disable insert button ui state disabled true | |
HtmlEditorField.js?m=1437392823:991 cleaning UI allegedly | |
HtmlEditorField.js?m=1437392823:1033 T4 AJAX SUCCESS REDRAW UPLOAD FIELD | |
HtmlEditorField.js?m=1437392823:1035 T5 AJAX SUCCESS REDRAW UPLOAD FIELD |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment