Link — это штатный диалог, который используется в CKeditor для редактирования ссылок. И выглядит он как показано на картинке сверху. Он хорошо выполняет свою задачу, но неудобным для редакторов является то, что ссылку на какой-либо статью предварительно приходится искать на самом сайте. А было бы здорово, если б поле предлагало варианты ссылок на материалы сайта, т.н. suggestions list.
Вот и займемся реализацией этой фичи.
Помимо того, что искать и копировать ссылки не очень удобно, еще и результат действий редактора в этом случае не всегда тот, что требуется. Обычно редакторы вставляют так называемые абсолютные URL, вместо требуемых относительных. Имея список «подсказок», это проблему тоже можно разрулить, т.к. ссылка будет требуемого вам формата.
Я буду решать эту задачу применительно к CKeditor v4, в CMS Drupal 7. Предполагается, что все нужные компоненты (модули и настройки) уже установлены.
Suggestions list
Какой бы CMS или Framework вы не использовали, вам нужен endpoint, который возвращает список статей. В Drupal 7 — это реализуется следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/** * Implementation hook menu. */ function MYMODULE_menu() { $items = array(); // request a node url list by requested name $items['MYMODULE/ajax/link_helper'] = array( 'page callback' => '_MYMODULE_node_link_helper', 'access arguments' => array('administer site configuration'), 'type' => MENU_CALLBACK, ); return $items; } /* CALLBACK function to provide node url list for CKEditor */ function _MYMODULE_node_link_helper(string $query = '') { if (mb_strlen($query) < 3) { $data = []; } else { $data = db_select('node', 'n') ->fields('n', ['nid', 'title', 'type']) ->condition('title', '%' . db_like($query) . '%', 'LIKE') ->condition('status', 0,'>') // возможно вам нужны не все типы публикаций // ограничьте их следующим условием ->condition('type', ['news', 'event'], 'IN') ->orderBy('type, title') ->range(0, 10) ->execute() ->fetchAll(PDO::FETCH_ASSOC); } foreach($data as &$item) { $item['path'] = drupal_get_path_alias('node/' . $item['nid']); } drupal_json_output($data); drupal_exit(); } |
Доступ к endpoint ограничен ‘administer site configuration‘, т.к. мы используем данные только на бек-енд. Требуется ввести не менее 3 символов для запроса.
Настройки CKEditor для работы с suggestion list
Нам нужно как то зацепиться за элемент формы диалога LINK редактора CKEditor. В API такая возможность предусмотрена (обработка события dialogDefinition).
Мы будет изменять конфигурацию диалога, добавив обработчик к нужному полю. Единственная проблема заключается в том, что обработчик может быть только один. Потому я повторю работу стандартного обработчика в своей функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
(function ($) { if(typeof(CKEDITOR) !== 'undefined') { CKEDITOR.on( 'dialogDefinition', function( ev ) { // получим диалог и его имя из данных события var dialog = ev.data; var dialogName = dialog.name; var dialogDefinition = dialog.definition; // нам нужен LINK диалог if ( dialogName == 'link' ) { // получим ссылку на нужную вкладку (а там их несколько) var infoTab = dialogDefinition.getContents( 'info' ); // получим элемент формы - поле ввода URL var urlField = infoTab.get( 'url' ); // переопределим обработчик события // добавив запрос к созданному нами endpoint urlField[ 'onKeyUp' ] = (event) => { if (typeof(event) !== 'undefined' && typeof(event.data) !== 'undefined') { let urlField = event.data.$.target; // этот код реализует переопределенный обработчик // его основная задача - вычленить протокол ссылки // в отдельном поле var a = $($(urlField).parents('table').get(0)).find('select'), b = $(urlField).val(), c = /^((javascript:)|[#\/\.\?])/i, f = /^(http|https|ftp|news):\/\/(?=.)/i.exec(b); f ? ($(urlField).val(b.substr(f[0].length)), a.val(f[0].toLowerCase())) : c.test(b) && a.val(""); // запрос к endpoint MYMODULE/ajax/link_helper let value = urlField.value; fetch('/MYMODULE/ajax/link_helper/' + encodeURIComponent(value), { credentials: 'same-origin' }) .then((response) => { // распаковываем присланный массив из json return response.json(); }) .then((data) => { // рендерим список ссылок // и обвязываем его обработчиками if (data.length > 0) { let elm = $(event.data.$.target); let optionLayer = elm.siblings('.optionLayer'); if (!optionLayer.length) { elm.after('<div class="optionLayer"></div>'); optionLayer = elm.siblings('.optionLayer'); } else { optionLayer.empty(); } optionLayer.append('<strong>Найдены статьи:</strong>'); $(data).each((index, item) => { optionLayer.append('<div class="item" data-value="/' + item.path + '">' + item.type + ': ' + item.title + '</div>'); }); optionLayer.children().click((ev) => { elm.val($(ev.target).attr('data-value')); }); } }); } } } }); } })(jQuery); |
В результате, по мере ввода названия статьи (вместо URL) в диалоге будет показан примерно такой список:
При клике на элемент, будет подставлена ссылка.