【Summernote v0.8.8】画像にリンクをつける
WYSIWYGエディタのSummenoteで画像にリンクをつけたり、外したりできるようカスタマイズしました。
デフォルトで画像アップロードの際にリンクをつける機能はあるのですが、
ファインダーを別途つけており、カスタマイズする必要がありました。
summernote.jsを直接編集しています。カスタマイズは慎重に。自己責任でお願いします。
今回は、アップロードした画像をクリックした時に、センタリングなどのボタンと合わせて「リンク」と「リンクを外す」アイコンを追加したいと思います。
環境
summernote v0.8.8。
bootstrap v4.0.0
Summernoteで画像にリンクさせる簡単な流れ
ボタンを作る流れ
- uiオブジェクトにリンクボタンとリンク解除ボタンを追加
- 画像をクリックした時に表示されるポップオーバーに作ったボタンを追加
- ポップオーバーの動作を定義した変数を作る
- その変数をSummernoteのモジュールに追加
画像にリンクをつける流れ
- リンクボタンをクリックした時のダイアログの変数を作る
- それをSummernoteのモジュールに追加
- 画像リンク処理、リンク解除処理をEditorクラスに追加
- ボタンと処理を紐付け
「リンク」ボタンを追加する
ボタンを作成
uiオブジェクトにに新しいボタンを定義します。
var ui = {
editor: editor,
〜〜〜〜〜〜〜〜〜〜〜
$.extend($.summernote.lang, {
'en-US': {
〜〜〜〜〜〜〜〜〜〜〜
image: {
〜〜〜〜〜〜〜〜〜〜〜
maximumFileSizeError: 'Maximum file size exceeded.',
//ここにリンクボタンを追加します-----
//新たにinsertLinkというボタンを作って、リンクのアイコンを使います
insertLink: 'Link Image',
//unlinkというボタンを作って、アンリンクのアイコンを使います。
unlink: 'Unlink',
//ここにリンクボタンを追加します-----
url: 'Image URL',
remove: 'Remove Image'
},
Summernoteのポップオーバーに追加
ポップオーバーはSummernoteにもともと付いている機能です。
uiオブジェクトで新しく作成したボタンを、そのポップオーバーで表示するように追加します。
$.summernote = $.extend($.summernote, {
version: '0.8.8',
ui: ui,
dom: dom,
〜〜〜〜〜
// popover
popover: {
image: [
['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
['float', ['floatLeft', 'floatRight', 'floatNone']],
//作ったボタンを追加--------
['image', ['insertLink','unlink' ]],
//作ったボタンを追加--------
['remove', ['removeMedia']]
],
〜〜〜〜〜
ポップオーバーの処理をする変数を作成
ポップオーバーでの振る舞いを処理する変数を作成します。
var ImagePopover = function (context) {
var self = this;
var ui = $.summernote.ui;
var $editable = context.layoutInfo.editable;
var editable = $editable[0];
var options = context.options;
this.events = {
'summernote.disable': function () {
self.hide();
}
};
this.shouldInitialize = function () {
return !list.isEmpty(options.popover.image);
};
this.initialize = function () {
this.$popover = ui.popover({
className: 'note-image-popover'
}).render().appendTo('body');
var $content = this.$popover.find('.popover-content,.note-popover-content');
context.invoke('buttons.build', $content, options.popover.image);
};
this.destroy = function () {
this.$popover.remove();
};
this.update = function (target) {
if (dom.isImg(target)) {
var pos = dom.posFromPlaceholder(target);
var posEditor = dom.posFromPlaceholder(editable);
this.$popover.css({
display: 'block',
left: pos.left,
top: Math.min(pos.top, posEditor.top)
});
} else {
this.hide();
}
};
this.hide = function () {
this.$popover.hide();
};
};
それをsummernoteのモジュールに追加
$.summernote = $.extend($.summernote, {
version: '0.8.8',
ui: ui,
dom: dom,
plugins: {},
options: {
modules: {
〜〜
'linkDialog': LinkDialog,
'linkPopover': LinkPopover,
//作った変数を追加-------
'imagePopover': ImagePopover,
//作った変数を追加-------
〜〜〜〜
},
リンクボタンをクリックした時に、ダイアログを表示させる
次に、リンクボタンをクリックした時に、ダイアログを表示させてURLを入力できるようにします。
ダイアログを作成
ダイアログを処理する変数を作ります。ダイアログのhtml、振る舞い、全てここに入っています。
var ImageLinkDialog = function (context) {
var self = this;
var ui = $.summernote.ui;
var $editor = context.layoutInfo.editor;
var options = context.options;
var lang = options.langInfo;
this.initialize = function () {
var $container = options.dialogsInBody ? $(document.body) : $editor;
var body = '<div class="form-group note-form-group">' +
'<label class="note-form-label">' + lang.link.url + '</label>' +
'<input class="note-link-url form-control note-form-control ' +
'note-input" type="text" value="http://" />' +
'</div>' +
(!options.disableLinkTarget ?
$('<div/>').append(ui.checkbox({ id: 'sn-checkbox-open-in-new-window', text: lang.link.openInNewWindow, checked: true }).render())
.html()
: '');
var footer = '<button href="#" class="btn btn-primary note-btn note-btn-primary ' +
'note-link-btn disabled" disabled>' + lang.link.insert + '</button>';
this.$dialog = ui.dialog({
className: 'note-image-link-dialog',
title: lang.link.insert,
fade: options.dialogsFade,
body: body,
footer: footer
}).render().appendTo($container);
};
this.destroy = function () {
ui.hideDialog(this.$dialog);
this.$dialog.remove();
};
this.bindEnterKey = function ($input, $btn) {
$input.on('keypress', function (event) {
if (event.keyCode === key.code.ENTER) {
$btn.trigger('click');
}
});
};
/**
* toggle update button
*/
this.toggleLinkBtn = function ($linkBtn, $linkText, $linkUrl) {
ui.toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
};
/**
* Show link dialog and set event handlers on dialog controls.
*
* @param {Object} linkInfo
* @return {Promise}
*/
/**
* Show link dialog and set event handlers on dialog controls.
*
* @param {jQuery} $dialog
* @param {Object} linkInfo
* @return {Promise}
*/
this.showImageLinkDialog = function (linkInfo) {
return $.Deferred(function (deferred) {
var $linkUrl = self.$dialog.find('.note-link-url'),
$linkBtn = self.$dialog.find('.note-link-btn'),
$openInNewWindow = self.$dialog.find('input[type=checkbox]');
ui.onDialogShown(self.$dialog, function () {
context.triggerEvent('dialog.shown');
var handleLinkUrlUpdate = function () {
ui.toggleBtn($linkBtn, $linkUrl.val());
};
$linkUrl.on('input', handleLinkUrlUpdate).on('paste', function () {
setTimeout(handleLinkUrlUpdate, 0);
}).val(linkInfo.url).trigger('focus');
ui.toggleBtn($linkBtn, $linkUrl.val());
self.bindEnterKey($linkUrl, $linkBtn);
var isChecked = linkInfo.isNewWindow !== undefined ?
linkInfo.isNewWindow : context.options.linkTargetBlank;
$openInNewWindow.prop('checked', isChecked);
$linkBtn.one('click', function (event) {
event.preventDefault();
deferred.resolve({
range: linkInfo.range,
url: $linkUrl.val(),
isNewWindow: $openInNewWindow.is(':checked')
});
ui.hideDialog(self.$dialog);
});
});
ui.onDialogHidden(self.$dialog, function () {
// detach events
$linkUrl.off('input paste keypress');
$linkBtn.off('click');
if (deferred.state() === 'pending') {
deferred.reject();
}
});
ui.showDialog(self.$dialog);
}).promise();
};
/**
* @param {Object} layoutInfo
*/
this.show = function () {
var linkInfo = context.invoke('editor.getLinkInfo');
context.invoke('editor.saveRange');
this.showImageLinkDialog(linkInfo).then(function (linkInfo) {
context.invoke('editor.restoreRange');
context.invoke('editor.createImageLink', linkInfo);
}).fail(function () {
context.invoke('editor.restoreRange');
});
};
context.memo('help.linkDialog.show', options.langInfo.help['linkDialog.show']);
};
2020/1/1追記 リンク情報取得を修正
リンクを編集する際に、カーソルが画像の前後にないと、リンク情報(hrefなど)が取得できていませんでした。
そこで、リンク情報取得する関数を追加します。
var Editor = function (context) {
〜〜〜〜〜〜〜〜〜〜〜
//画像をクリックした際に、画像に貼ってあるリンク情報を取得する
/**
* returns Link link info
*
* @return {Object}
* @return {WrappedRange} return.range
* @return {String} return.text
* @return {Boolean} [return.isNewWindow=true]
* @return {String} [return.url=""]
*/
this.getImgLinkInfo = function () {
var $handle = context.layoutInfo.editingArea,
target = $handle.find('.note-control-selection').data('target'),
$target = $(target),
$target_anchor = $target.parent('a');
var startRange = range.createFromNodeBefore($target.get(0));
var startPoint = startRange.getStartPoint();
var endRange = range.createFromNodeAfter($target.get(0));
var endPoint = endRange.getEndPoint();
var rng = range.create(
startPoint.node,
startPoint.offset,
endPoint.node,
endPoint.offset
).select();
var linkInfo = {
range: rng,
url: $target_anchor.length ? $target_anchor.attr('href') : ''
};
// Define isNewWindow when anchor exists.
if ($target_anchor.length) {
linkInfo.isNewWindow = $target_anchor.attr('target') === '_blank';
}
return linkInfo;
};
〜〜〜〜〜〜〜〜〜〜〜
}
var ImageLinkDialogのshow()でリンク情報を取得しているので、そこを差し替えます。
this.showImageLinkDialog = function (linkInfo) {
〜〜〜〜〜〜〜〜〜〜〜
this.show = function () {
//ここのgetLinkInfoを作成したgetImgLinkInfoへ差し替え
var linkInfo = context.invoke('editor.getLinkInfo');
//↓↓↓↓↓↓↓これに差し替え↓↓↓↓↓↓↓↓
var linkInfo = context.invoke('editor.getImgLinkInfo');
//↑↑↑↑↑↑↑/これに差し替え↑↑↑↑↑↑↑↑
context.invoke('editor.saveRange');
this.showImageLinkDialog(linkInfo).then(function (linkInfo) {
context.invoke('editor.restoreRange');
context.invoke('editor.createImageLink', linkInfo);
}).fail(function () {
context.invoke('editor.restoreRange');
});
};
〜〜〜〜〜〜〜〜〜〜〜
}
summernoteのモジュールに追加
$.summernote = $.extend($.summernote, {
version: '0.8.8',
ui: ui,
dom: dom,
plugins: {},
options: {
modules: {
〜〜
'linkDialog': LinkDialog,
'linkPopover': LinkPopover,
'imagePopover': ImagePopover,
//作った変数を追加-------
'imageLinkDialog':ImageLinkDialog,
//作った変数を追加-------
〜〜〜〜
},
これでリンクボタンをクリックした時に、URLを入れるダイアログが表示されるようになりました。
画像にリンクを貼る処理を行う
Editorクラスの中に、画像にリンクを貼る処理を入れます。
これらの処理は、前項で作成したダイアログ(var ImageLinkDialog)の処理の中で発火させます。
var ImageLinkDialogの中にある、context.invoke(‘editor.createImageLink’, linkInfo);がそれです。
/**
* @class Editor
*/
var Editor = function (context) {
var self = this;
〜〜〜〜〜〜〜〜〜〜〜
//ダイアログでsubmitを押した後の処理を入れます。
//ここで、画像にリンクを張っています。
this.createImageLink = this.wrapCommand(function (linkInfo) {
var linkUrl = linkInfo.url;
var isNewWindow = linkInfo.isNewWindow;
var rng = linkInfo.range || this.createRange();
var $handle = context.layoutInfo.editingArea,
target = $handle.find('.note-control-selection').data('target'),
$target = $(target);
if (typeof linkUrl === 'string') {
linkUrl = linkUrl.trim();
}
if (options.onCreateLink) {
linkUrl = options.onCreateLink(linkUrl);
} else {
// if url doesn't match an URL schema, set http:// as default
linkUrl = /^[A-Za-z][A-Za-z0-9+-.]*\:[\/\/]?/.test(linkUrl) ?
linkUrl : 'http://' + linkUrl;
}
//2020/1/1修正
rng = rng.deleteContents();
var anchors = [];
var anchor = rng.insertNode($('<A>' + $target.prop('outerHTML')+ '</A>')[0]);
anchors.push(anchor);
$.each(anchors, function (idx, anchor) {
$(anchor).attr('href', linkUrl);
if (isNewWindow) {
$(anchor).attr('target', '_blank');
} else {
$(anchor).removeAttr('target');
}
});
//2020/1/1修正
var startRange = range.createFromNodeBefore(list.head(anchors));
var startPoint = startRange.getStartPoint();
var endRange = range.createFromNodeAfter(list.last(anchors));
var endPoint = endRange.getEndPoint();
range.create(
startPoint.node,
startPoint.offset,
endPoint.node,
endPoint.offset
).select();
});
〜〜〜〜〜〜〜〜〜〜〜
2020/1/1
※Nodeを追加する、リンクをつける箇所を修正しています。
リンク解除の処理
同じくEditorクラスの中にリンク解除の処理も入れます。
this.unlinkImageLink = function () {
var rng = this.createRange();
if (rng.isOnAnchor()) {
var anchor = dom.ancestor(rng.sc, dom.isAnchor);
rng = range.createFromNode(anchor);
rng.select();
beforeCommand();
document.execCommand('unlink');
afterCommand();
}
};
最後に、ボタンと動作を紐付けます。
this.addToolbarButtons = function () {
〜〜〜〜〜〜〜〜〜〜〜
//ポップオーバーのボタンを追加-----
this.addLinkPopoverButtons = function () {
//リンクをつけるリンクボタンとその処理
context.memo('button.insertLink', function () {
return ui.button({
contents: ui.icon(options.icons.link),
tooltip: lang.link.edit,
click: context.createInvokeHandler('imageLinkDialog.show')
}).render();
});
//リンクを解除するアイコンボタンとその処理
context.memo('button.unlink', function () {
return ui.button({
contents: ui.icon(options.icons.unlink),
tooltip: lang.link.unlink,
click: context.createInvokeHandler('editor.unlinkImageLink')
}).render();
});
};
〜〜〜〜〜〜〜〜〜〜〜
これで、画像をクリックした時にリンクボタンが表示されて、そこからリンクをつける事ができます。
ご参考にさせて頂きました
https://github.com/summernote/summernote/commit/55773561371b13788b60e36dd0dd3bda79ab3b25
この方のコードがなかったら、達成できていなかったです。感謝します。