From e31d991778266d48383abf23cfe02db291f82f9c Mon Sep 17 00:00:00 2001 From: lpaulsen93 Date: Sun, 4 Oct 2020 15:35:59 +0200 Subject: [PATCH 1/4] Implemented 'Edit Tags' dialog This adds an 'Edit tags' menu item. Clicking on it opens a jQuery dialog in which the user can add and remove single tags: - the tag list is updated after each change (new ajax request) - closing the dialog refreshes the browser page - entered tag name is validated on keypress --- MenuItem.php | 79 +++++++++++++++ action.php | 158 ++++++++++++++++++++++++++++++ lang/en/lang.php | 7 +- script.js | 243 +++++++++++++++++++++++++++++++++++++++++++++++ style.less | 40 ++++++++ 5 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 MenuItem.php diff --git a/MenuItem.php b/MenuItem.php new file mode 100644 index 0000000..ee27fa1 --- /dev/null +++ b/MenuItem.php @@ -0,0 +1,79 @@ +params['rev'] = $REV; + } + + /** + * Get label from plugin language file + * + * @return string + */ + public function getLabel() { + $hlp = plugin_load('helper', 'tagging'); + return $hlp->getLang('edit_tags_button'); + } + + /** + * Return the link this item links to + * + * @return string + */ + public function getLink() { + return 'javascript:void(0);'; + } + + /** + * Convenience method to get the attributes for constructing an element + * + * @see buildAttributes() + * @return array + */ + public function getLinkAttributes($classprefix = 'menuitemtagging ') { + global $INFO; + global $lang; + + $attr = array( + 'href' => $this->getLink(), + 'title' => $this->getTitle(), + ); + $attr['rel'] = 'nofollow'; + + /** @var helper_plugin_tagging $hlp */ + $hlp = plugin_load('helper', 'tagging'); + + $filter = array('pid' => $INFO['id']); + if ($hlp->getConf('singleusermode')) { + $filter['tagger'] = 'auto'; + } + + $tags = $hlp->findItems($filter, 'tag'); + $attr['data-tags'] = implode(', ', array_keys($tags)); + + return $attr; + } +} diff --git a/action.php b/action.php index ec7c26e..9be374a 100644 --- a/action.php +++ b/action.php @@ -69,6 +69,10 @@ function register(Doku_Event_Handler $controller) { 'PLUGIN_MOVE_PAGE_RENAME', 'AFTER', $this, 'update_moved_page' ); + + $controller->register_hook( + 'MENU_ITEMS_ASSEMBLY', 'AFTER', $this, + 'add_menu', array()); } /** @@ -103,6 +107,12 @@ function handle_ajax_call_unknown(Doku_Event &$event, $param) { $this->deleteTag(); } elseif ($event->data === 'plugin_tagging_rename') { $this->renameTag(); + } elseif ($event->data === 'plugin_tagging_get') { + $this->getTags(); + } elseif ($event->data === 'plugin_tagging_add_tag') { + $this->addTag(); + } elseif ($event->data === 'plugin_tagging_remove_tag') { + $this->removeTag(); } else { $handled = false; } @@ -602,4 +612,152 @@ protected function restoreSearchQuery() global $QUERY; $QUERY = $this->originalQuery; } + + /** + * Add tagging button to page tools menu + * + * @param Doku_Event $event + */ + public function add_menu(Doku_Event $event) { + if($event->data['view'] != 'page') return; + // ToDo: only insert button if configured and if logged in + // user has got the required permissions + array_splice($event->data['items'], -1, 0, [new \dokuwiki\plugin\tagging\MenuItem()]); + } + + protected function getTags() { + global $INFO; + + /** @var helper_plugin_tagging $hlp */ + $hlp = plugin_load('helper', 'tagging'); + + $filter = array('pid' => $INFO['id']); + if ($hlp->getConf('singleusermode')) { + $filter['tagger'] = 'auto'; + } + + $tags = $hlp->findItems($filter, 'tag'); + $tags = implode(', ', array_keys($tags)); + $tags = array('tags' => $tags); + + header('Content-Type: application/json'); + echo json_encode($tags); + } + + /** + * Add single tag, return current tags + */ + function addTag() { + global $INPUT; + global $INFO; + + $error = false; + + /** @var helper_plugin_tagging $hlp */ + $hlp = plugin_load('helper', 'tagging'); + + $data = $INPUT->arr('tagging'); + $id = $INPUT->str('id'); + $tag = $INPUT->str('tag'); + $user = $hlp->getUser(); + $INFO['writable'] = auth_quickaclcheck($id) >= AUTH_EDIT; // we also need this in findItems + + if (empty($id) || empty($tag) || empty($user)) { + $error = true; + $msg = 'missing parameters'; + } else if ($INFO['writable'] && $hlp->getUser()) { + // Get saved tags + $filter = array('pid' => $INFO['id']); + if ($hlp->getConf('singleusermode')) { + $filter['tagger'] = 'auto'; + } + + $tags = $hlp->findItems($filter, 'tag'); + $tags = array_keys($tags); + + // Add new tag, if not yet existing + if (array_search($tag, $tags) === false) { + array_push($tags, $tag); + $hlp->replaceTags($id, $user, $tags); + $msg = 'added \''.$tag.'\' to page \''.$id.'\''; + } else { + $error = true; + $msg = 'tag already exists'; + } + } else { + $error = true; + $msg = 'permission denied'; + } + + // Return JSON encoded list of current tags + $tags = $hlp->findItems($filter, 'tag'); + $tags = implode(', ', array_keys($tags)); + if ($error) { + $result = array('tags' => $tags, 'error' => $error, 'message' => $msg); + } else { + $result = array('tags' => $tags, 'error' => $error, 'message' => $msg); + } + + header('Content-Type: application/json'); + echo json_encode($result); + } + + /** + * Remove single tag, return current tags + */ + function removeTag() { + global $INPUT; + global $INFO; + + $error = false; + + /** @var helper_plugin_tagging $hlp */ + $hlp = plugin_load('helper', 'tagging'); + + $data = $INPUT->arr('tagging'); + $id = $INPUT->str('id'); + $tag = $INPUT->str('tag'); + $user = $hlp->getUser(); + $INFO['writable'] = auth_quickaclcheck($id) >= AUTH_EDIT; // we also need this in findItems + + if (empty($id) || empty($tag) || empty($user)) { + $error = true; + $msg = 'missing parameters'; + } else if ($INFO['writable'] && $hlp->getUser()) { + // Get saved tags + $filter = array('pid' => $INFO['id']); + if ($hlp->getConf('singleusermode')) { + $filter['tagger'] = 'auto'; + } + + $tags = $hlp->findItems($filter, 'tag'); + $tags = array_keys($tags); + + // Remove tag, if existing + $key = array_search($tag, $tags); + if ($key !== false) { + unset($tags[$key]); + $hlp->replaceTags($id, $user, $tags); + $msg = 'removed \''.$tag.'\' from page \''.$id.'\''; + } else { + $error = true; + $msg = 'tag does not exist'; + } + } else { + $error = true; + $msg = 'permission denied'; + } + + // Return JSON encoded list of current tags + $tags = $hlp->findItems($filter, 'tag'); + $tags = implode(', ', array_keys($tags)); + if ($error) { + $result = array('tags' => $tags, 'error' => $error, 'message' => $msg); + } else { + $result = array('tags' => $tags, 'error' => $error, 'message' => $msg); + } + + header('Content-Type: application/json'); + echo json_encode($result); + } } diff --git a/lang/en/lang.php b/lang/en/lang.php index 8434ca1..b550535 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -28,6 +28,7 @@ $lang['admin no invalid'] = 'No invalid tags found.'; $lang['admin clean'] = 'Irrevocably delete all invalid taggings'; $lang['toggle admin mode'] = 'Tag Admin'; +$lang['edit_tags_button'] = 'Edit tags'; $lang['no_admin'] = 'Error. Only admins can modify tags.'; @@ -50,6 +51,10 @@ $lang['js']['admin_sure'] = 'Are you sure?'; $lang['js']['admin_newtags'] = 'Please enter the new tag name. You can provide multiple comma separated tags to split the tag into multiple.'; $lang['js']['search_nofilter'] = 'nothing found'; - +$lang['js']['edit_dialog_title'] = 'Edit Tags'; +$lang['js']['edit_dialog_text_list'] = 'The following tags are set:'; +$lang['js']['edit_dialog_button_delete'] = 'Delete'; +$lang['js']['edit_dialog_button_add'] = 'Add'; +$lang['js']['edit_dialog_label_add'] = 'Add a new tag:'; $lang['tagjmp_error'] = 'Could not find any page tagged with %s'; diff --git a/script.js b/script.js index 3d61d86..e0c8f04 100644 --- a/script.js +++ b/script.js @@ -4,6 +4,39 @@ jQuery(function () { + const url = DOKU_BASE + 'lib/exe/ajax.php'; + const requestParams = { + 'id': JSINFO.id, + 'sectok': JSINFO.sectok + }; + + /** + * Trigger a backend action via AJAX + * + * @param {object} params Required "call" is the DokuWiki event name, plus optional data object(s) + * @returns {*} + */ + const callBackend = function(params, successCallback, failureCallback) { + const mergedParams = jQuery.extend( + {}, + requestParams, + params + ); + + return jQuery.ajax({ + url : url, + data : mergedParams, + type : 'POST' + }) + .done(jQuery.proxy(function(response) { + successCallback(response); + }, this)) + .fail(jQuery.proxy(function(xhr) { + var msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error'; + failureCallback(msg); + }, this)); + }; + /** * Add JavaScript confirmation to the User Delete button */ @@ -139,4 +172,214 @@ jQuery(function () { return false; } }); + + /** + * Below follows code for the edit dialog + */ + jQuery('.plugin_tagging__edit').click(plugin_tagging_edit); + + /** + * Menu item on-click function: + * Gets the tag list and creates the edit dialog. + */ + function plugin_tagging_edit() { + jQuery.ajax({ + url: DOKU_BASE + 'lib/exe/ajax.php', + type: 'POST', + data: { + call: 'plugin_tagging_get' + }, + dataType: 'json', + + success: function (data) { + tags = get_tags_from_response(data); + plugin_tagging_show_edit_dialog(tags); + }, + + error: function (xhr, status, error) { + } + }); + + } + + /** + * Build HTML code for the tags list. + * + * @param {array} tags Array of tags + * @returns {string} HTML code + */ + function edit_dialog_build_tag_list(tags) { + var table = '
'; + if (tags && tags.length > 0) { + tags.forEach(function (tag) { + var htmlTag = jQuery(tag).html(); + var button = '' + + LANG.plugins.tagging.edit_dialog_button_delete + ''; + table += ''; + }); + } else { + table += ''; + } + table += '
' + tag + '' + button + '
' + LANG.plugins.tagging.notags + '
'; + + return table; + } + + /** + * Create and show the edit tags dialog. + * Tags can be added and removed from the current page. + * Closing the dialog refreshes the browser page. + * + * @param {array} tags Array of tags + * @returns {string} HTML code + */ + function plugin_tagging_show_edit_dialog(tags) { + var content = '
'; + + dialog = jQuery(content).dialog({ + resizable: false, + width: 480, + height: 'auto', + modal: true, + buttons: { + Close: function() { + jQuery(this).dialog('close'); + } + }, + close: function( event, ui ) { + jQuery(this).dialog('destroy'); + location.reload(); + } + }); + jQuery(dialog).append('

' + LANG.plugins.tagging.edit_dialog_text_list + '

'); + + table = edit_dialog_build_tag_list(tags); + jQuery(dialog).append(table); + + jQuery('.tagging_delete_button').button({ + icon: "ui-icon-trash" + }); + + var button = '' + + LANG.plugins.tagging.edit_dialog_button_add + ''; + var input = '
' + + '' + button; + jQuery(dialog).append(input); + + jQuery('.tagging_add_button').button({ + icon: "ui-icon-plus" + }); + + jQuery('.tagging_add_button').click(edit_dialog_add_tag); + jQuery('.tagging_delete_button').click(edit_dialog_delete_tag); + + jQuery('#new_tag_name').keyup(function (event) { + if (event.which === 13) { + edit_dialog_add_tag(); + } else { + setTimeout(edit_dialog_validate_input, 250); + } + }); + + return dialog; + } + + /** + * Callback function for validation of the input field '#new_tag_name'. + * + * @returns {boolean} true if valid, false otherwise + */ + function edit_dialog_validate_input() { + var tag = jQuery('#new_tag_name').val(), + valid = true; + + if (tag.length > 0) { + var $cells = jQuery('#tag_list td:first-child'); + for (var cell of $cells) { + if (tag === cell.textContent) { + // Ignore duplicates. + valid = false; + break; + } + } + } else { + valid = false; + } + + var input = jQuery('#new_tag_name'); + if (valid) { + input.addClass('valid_input'); + input.removeClass('invalid_input'); + } else { + input.removeClass('valid_input'); + input.addClass('invalid_input'); + } + + return valid; + } + + /** + * The function updates the tag list in the edit dialog. + * + * @param {array} tags Array of tags + */ + function edit_dialog_update_tags(tags) { + table = edit_dialog_build_tag_list(tags); + jQuery('#tag_list').replaceWith(table); + jQuery('.tagging_add_button').click(edit_dialog_add_tag); + jQuery('.tagging_delete_button').click(edit_dialog_delete_tag); + } + + /** + * Reads tags from the given Jquery ajax response and returns + * them as an array (might be empty). + * + * @param {object} response Ajax response object + * @returns {array} Array of tags + */ + function get_tags_from_response(response) { + if (response.tags && response.tags.length > 0) { + tags = response.tags.split(/,\s*/); + } else { + tags = []; + } + return tags; + } + + /** + * Callback function for the add button. + * Adds a new tag. + */ + function edit_dialog_add_tag() { + var tag = jQuery('#new_tag_name').val(); + + if (edit_dialog_validate_input()) { + // Clear input field + jQuery('#new_tag_name').val(''); + + result = callBackend({call: 'plugin_tagging_add_tag', tag: tag}, + function (response) { + tags = get_tags_from_response(response); + edit_dialog_update_tags(tags); + }, + function (error) { + }); + } + } + + /** + * Callback function for the delete button. + * Removes the clicked tag. + */ + function edit_dialog_delete_tag() { + var tag = jQuery(this).closest("td").prev().html(); + + result = callBackend({call: 'plugin_tagging_remove_tag', tag: tag}, + function (response) { + tags = get_tags_from_response(response); + edit_dialog_update_tags(tags); + }, + function (error) { + }); + } }); diff --git a/style.less b/style.less index 4dcca6a..ba0fe88 100644 --- a/style.less +++ b/style.less @@ -135,3 +135,43 @@ table.plugin_tagging { width: 300px; } } + +#tagging__edit_dialog { + p, table { + margin-top: 1em; + margin-bottm: 1em; + } + + table { + width: 100%; + margin-left: auto; + margin-right: auto; + border-collapse: collapse; + border: none; + } + + tr { + background-color: gray; + } + + td { + background-color: white; + margin: 5px; + text-align: center; + vertical-align: middle; + border: solid #ddd; + border-width: 2px 0px; + } + + input { + margin: 5px; + } + + .invalid_input { + background-color: LightPink; + } + + .valid_input { + background-color: LightGreen; + } +} From 5081e951a3da6642a7444d628746acadeefd585f Mon Sep 17 00:00:00 2001 From: lpaulsen93 Date: Mon, 5 Oct 2020 19:57:03 +0200 Subject: [PATCH 2/4] Make "Edit tags" menu button configurable The administrator can switch the button on or off. The default is off, so the button will not be shown. --- action.php | 4 +++- conf/default.php | 9 +++++---- conf/metadata.php | 1 + lang/en/settings.php | 9 +++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/action.php b/action.php index 9be374a..3a53692 100644 --- a/action.php +++ b/action.php @@ -622,7 +622,9 @@ public function add_menu(Doku_Event $event) { if($event->data['view'] != 'page') return; // ToDo: only insert button if configured and if logged in // user has got the required permissions - array_splice($event->data['items'], -1, 0, [new \dokuwiki\plugin\tagging\MenuItem()]); + if($this->getConf('showedittagsbutton')) { + array_splice($event->data['items'], -1, 0, [new \dokuwiki\plugin\tagging\MenuItem()]); + } } protected function getTags() { diff --git a/conf/default.php b/conf/default.php index c36c685..8cb57ec 100644 --- a/conf/default.php +++ b/conf/default.php @@ -1,5 +1,6 @@ Date: Tue, 6 Oct 2020 21:36:34 +0200 Subject: [PATCH 3/4] Fixed sometimes not working on-click callback. Also removed useless HTML attributes. --- script.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/script.js b/script.js index e0c8f04..499ac47 100644 --- a/script.js +++ b/script.js @@ -10,6 +10,9 @@ jQuery(function () { 'sectok': JSINFO.sectok }; + // Set on-click callback for edit dialog. + jQuery('.plugin_tagging__edit').click(plugin_tagging_edit); + /** * Trigger a backend action via AJAX * @@ -176,7 +179,6 @@ jQuery(function () { /** * Below follows code for the edit dialog */ - jQuery('.plugin_tagging__edit').click(plugin_tagging_edit); /** * Menu item on-click function: @@ -212,10 +214,9 @@ jQuery(function () { var table = '
'; if (tags && tags.length > 0) { tags.forEach(function (tag) { - var htmlTag = jQuery(tag).html(); - var button = '' + var button = '' + LANG.plugins.tagging.edit_dialog_button_delete + ''; - table += ''; + table += ''; }); } else { table += ''; From cb6c430ae7cb7893eb0ba4bd8ed9a9574c1bbfa1 Mon Sep 17 00:00:00 2001 From: lpaulsen93 Date: Tue, 6 Oct 2020 22:27:11 +0200 Subject: [PATCH 4/4] Always use 'callBackend()' for sending ajax requests --- script.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/script.js b/script.js index 499ac47..d431ddb 100644 --- a/script.js +++ b/script.js @@ -185,23 +185,13 @@ jQuery(function () { * Gets the tag list and creates the edit dialog. */ function plugin_tagging_edit() { - jQuery.ajax({ - url: DOKU_BASE + 'lib/exe/ajax.php', - type: 'POST', - data: { - call: 'plugin_tagging_get' - }, - dataType: 'json', - - success: function (data) { - tags = get_tags_from_response(data); + callBackend({call: 'plugin_tagging_get'}, + function (response) { + tags = get_tags_from_response(response); plugin_tagging_show_edit_dialog(tags); }, - - error: function (xhr, status, error) { - } - }); - + function (error) { + }); } /**
' + tag + '' + button + '
' + tag + '' + button + '
' + LANG.plugins.tagging.notags + '