mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #3347 from mateusz/plat-61
Fix the anchor selector to load anchors from other internal pages.
This commit is contained in:
commit
94f70a24cb
@ -190,7 +190,8 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
private static $allowed_actions = array(
|
||||
'LinkForm',
|
||||
'MediaForm',
|
||||
'viewfile'
|
||||
'viewfile',
|
||||
'getanchors'
|
||||
);
|
||||
|
||||
/**
|
||||
@ -518,6 +519,41 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
))->renderWith($this->templateViewFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all anchors available on the given page.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getanchors() {
|
||||
$id = (int)$this->request->getVar('PageID');
|
||||
$anchors = array();
|
||||
|
||||
if (($page = Page::get()->byID($id)) && !empty($page)) {
|
||||
if (!$page->canView()) {
|
||||
throw new SS_HTTPResponse_Exception(
|
||||
_t(
|
||||
'HtmlEditorField.ANCHORSCANNOTACCESSPAGE',
|
||||
'You are not permitted to access the content of the target page.'
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
// Similar to the regex found in HtmlEditorField.js / getAnchors method.
|
||||
if (preg_match_all("/name=\"([^\"]+?)\"|name='([^']+?)'/im", $page->Content, $matches)) {
|
||||
$anchors = $matches[1];
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new SS_HTTPResponse_Exception(
|
||||
_t('HtmlEditorField.ANCHORSPAGENOTFOUND', 'Target page not found.'),
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
return json_encode($anchors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link File->getCMSFields()}, but only returns fields
|
||||
* for manipulating the instance of the file as inserted into the HTML content,
|
||||
|
@ -74,7 +74,7 @@ ss.editorWrappers.tinyMCE = (function() {
|
||||
this.statusKeyboardNavigation.destroy();
|
||||
this.statusKeyboardNavigation = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ss.editorWrappers.tinyMCE.patched = true;
|
||||
}
|
||||
@ -543,6 +543,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
* 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();
|
||||
@ -558,7 +559,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
redraw: function() {
|
||||
this._super();
|
||||
|
||||
var linkType = this.find(':input[name=LinkType]:checked').val(), list = ['internal', 'external', 'file', 'email'];
|
||||
var linkType = this.find(':input[name=LinkType]:checked').val();
|
||||
|
||||
this.addAnchorSelector();
|
||||
|
||||
@ -568,10 +569,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
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#AnchorRefresh').show();
|
||||
}
|
||||
if(linkType == 'anchor') this.find('.field#AnchorSelector').show();
|
||||
this.find('.field#Description').show();
|
||||
},
|
||||
/**
|
||||
@ -613,8 +611,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
}
|
||||
|
||||
return {
|
||||
href : href,
|
||||
target : target,
|
||||
href : href,
|
||||
target : target,
|
||||
title : this.find(':input[name=Description]').val()
|
||||
};
|
||||
},
|
||||
@ -630,63 +628,135 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
});
|
||||
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, anchorSelector;
|
||||
var self = this;
|
||||
var anchorSelector = $(
|
||||
'<select id="Form_EditorToolbarLinkForm_AnchorSelector" name="AnchorSelector"></select>'
|
||||
);
|
||||
this.find(':input[name=Anchor]').parent().append(anchorSelector);
|
||||
|
||||
// refresh the anchor selector on click, or in case of IE - button click
|
||||
if( !$.browser.ie ) {
|
||||
anchorSelector = $('<select id="Form_EditorToolbarLinkForm_AnchorSelector" name="AnchorSelector"></select>');
|
||||
this.find(':input[name=Anchor]').parent().append(anchorSelector);
|
||||
|
||||
anchorSelector.focus(function(e) {
|
||||
self.refreshAnchors();
|
||||
});
|
||||
} else {
|
||||
var buttonRefresh = $('<a id="Form_EditorToolbarLinkForm_AnchorRefresh" title="Refresh the anchor list" alt="Refresh the anchor list" class="buttonRefresh"><span></span></a>');
|
||||
anchorSelector = $('<select id="Form_EditorToolbarLinkForm_AnchorSelector" class="hasRefreshButton" name="AnchorSelector"></select>');
|
||||
this.find(':input[name=Anchor]').parent().append(buttonRefresh).append(anchorSelector);
|
||||
|
||||
buttonRefresh.click(function(e) {
|
||||
self.refreshAnchors();
|
||||
});
|
||||
}
|
||||
|
||||
// initialization
|
||||
self.refreshAnchors();
|
||||
// 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());
|
||||
});
|
||||
},
|
||||
// this function collects the anchors in the currently active editor and regenerates the dropdown
|
||||
refreshAnchors: function() {
|
||||
var selector = this.find(':input[name=AnchorSelector]'), anchors = [], 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++) {
|
||||
anchors.push(raw[i].substr(6).replace(/"$/, ''));
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.SelectAnchor') +
|
||||
ss.i18n._t('HtmlEditorField.LOOKINGFORANCHORS', 'Looking for anchors...') +
|
||||
'</option>'
|
||||
));
|
||||
for (var j = 0; j < anchors.length; j++) {
|
||||
selector.append($('<option value="'+anchors[j]+'">'+anchors[j]+'</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.
|
||||
@ -711,103 +781,123 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 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";
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -881,7 +971,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
this.find('.Actions .media-update')[editingSelected ? 'show' : 'hide']();
|
||||
this.find('.ss-uploadfield-item-editform').toggleEditForm(editingSelected);
|
||||
},
|
||||
resetFields: function() {
|
||||
resetFields: function() {
|
||||
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
|
||||
@ -1118,7 +1208,9 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
* Logic similar to TinyMCE 'advimage' plugin, insertAndClose() method.
|
||||
*/
|
||||
insertHTML: function(ed) {
|
||||
var form = this.closest('form'), node = form.getSelection(), ed = form.getEditor();
|
||||
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();
|
||||
@ -1259,7 +1351,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
imgEl = $('<img />').attr(attrs).addClass('ss-htmleditorfield-file embed');
|
||||
|
||||
$.each(extraData, function (key, value) {
|
||||
imgEl.attr('data-' + key, value)
|
||||
imgEl.attr('data-' + key, value);
|
||||
});
|
||||
|
||||
if(extraData.CaptionText) {
|
||||
@ -1354,10 +1446,10 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
this.height(0);
|
||||
itemInfo.find('.toggle-details-icon').removeClass('opened');
|
||||
if(!this.hasClass('edited')){
|
||||
text = ss.i18n._t('UploadField.NOCHANGES', 'No Changes')
|
||||
text = ss.i18n._t('UploadField.NOCHANGES', 'No Changes');
|
||||
status.addClass('ui-state-success-text');
|
||||
}else{
|
||||
text = ss.i18n._t('UploadField.CHANGESSAVED', 'Changes Made')
|
||||
text = ss.i18n._t('UploadField.CHANGESSAVED', 'Changes Made');
|
||||
this.removeClass('edited');
|
||||
status.addClass('ui-state-success-text');
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user