From edc7cb0e3e3b5ae4c77fb757596bba129e61f4f9 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 8 Jul 2024 13:11:50 +0200 Subject: [PATCH 01/12] optionally move media with page rename #222 A new checkbox allows to move referenced page media along with a page rename. This happens only if * the checkbox is ticked * the page actually moves to a new namespace * the page was not in the root namespace * the referenced media is in the same namespace as the page # Conflicts: # action/rename.php --- action/rename.php | 58 ++++++++++++++++++++++++++++++++++++++++------- lang/en/lang.php | 1 + script/rename.js | 10 ++++++-- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/action/rename.php b/action/rename.php index 861af95..814767b 100644 --- a/action/rename.php +++ b/action/rename.php @@ -94,6 +94,11 @@ public function addsvgbutton(Doku_Event $event) { /** * Rename a single page + * + * This creates a plan and executes it right away. If the user selected to move media with the page, + * all media files used in the original page that are located in the same namespace are moved with the page + * to the new namespace. + * */ public function handle_ajax(Doku_Event $event) { if($event->data != 'plugin_move_rename') return; @@ -105,22 +110,59 @@ public function handle_ajax(Doku_Event $event) { $src = cleanID($INPUT->str('id')); $dst = cleanID($INPUT->str('newid')); - - /** @var helper_plugin_move_op $MoveOperator */ - $MoveOperator = plugin_load('helper', 'move_op'); + $doMedia = $INPUT->bool('media'); header('Content-Type: application/json'); - if($this->renameOkay($src) && $MoveOperator->movePage($src, $dst)) { - // all went well, redirect + if(!$this->renameOkay($src)) { + echo json_encode(['error' => $this->getLang('cantrename')]); + return; + } + + /** @var helper_plugin_move_plan $plan */ + $plan = plugin_load('helper', 'move_plan'); + if($plan->isCommited()) { + echo json_encode(['error' => $this->getLang('cantrename')]); + return; + } + $plan->setOption('autorewrite', true); + $plan->addPageMove($src, $dst); // add the page move to the plan + + if($doMedia) { // move media with the page? + $srcNS = getNS($src); + $dstNS = getNS($dst); + $srcNSLen = strlen($srcNS); + // we don't do this for root namespace or if namespace hasn't changed + if ($srcNS != '' && $srcNS != $dstNS) { + $media = p_get_metadata($src, 'relation media'); + if (is_array($media)) { + foreach ($media as $file => $exists) { + if(!$exists) continue; + $mediaNS = getNS($file); + if ($mediaNS == $srcNS) { + $plan->addMediaMove($file, $dstNS . substr($file, $srcNSLen)); + } + } + } + } + } + + try { + // commit and execute the plan + $plan->commit(); + do { + $next = $plan->nextStep(); + if ($next === false) throw new \Exception('Move plan failed'); + } while ($next > 0); echo json_encode(array('redirect_url' => wl($dst, '', true, '&'))); - } else { + } catch (\Exception $e) { + // error should be in $MSG if(isset($MSG[0])) { $error = $MSG[0]; // first error } else { - $error = $this->getLang('cantrename'); + $error = $this->getLang('cantrename') . ' ' . $e->getMessage(); } - echo json_encode(array('error' => $error)); + echo json_encode(['error' => $error]); } } diff --git a/lang/en/lang.php b/lang/en/lang.php index 48a81d6..202ab3b 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -72,6 +72,7 @@ $lang['js']['rename'] = 'Rename'; $lang['js']['cancel'] = 'Cancel'; $lang['js']['newname'] = 'New name:'; +$lang['js']['rename_media'] = 'Move referenced media files along with the page'; $lang['js']['inprogress'] = 'renaming page and adjusting links...'; $lang['js']['complete'] = 'Move operation finished.'; diff --git a/script/rename.js b/script/rename.js index 03a8e5f..cf4d916 100644 --- a/script/rename.js +++ b/script/rename.js @@ -14,6 +14,9 @@ '' + + '' + '' + '' ); @@ -26,6 +29,8 @@ const newid = $dialog.find('input[name=id]').val(); if (!newid) return false; + const doMedia = $dialog.find('input[name=media]').is(':checked'); + // remove buttons and show throbber $dialog.html( ' ' + @@ -39,12 +44,13 @@ { call: 'plugin_move_rename', id: JSINFO.id, - newid: newid + newid: newid, + media: doMedia ? 1 : 0, }, // redirect or display error function (result) { if (result.error) { - $dialog.html(result.error.msg); + $dialog.html(result.error); } else { window.location.href = result.redirect_url; } From 91abc83a2627a937e5f9a25adc76a9e825f52e63 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Tue, 16 Jul 2024 14:47:15 +0200 Subject: [PATCH 02/12] Rewrite of the tree manager This rewrite drops all dependencies on jQuery for the tree manager and uses modern JavaScript for the drag'n'drop operations. It introduces the posibility to view the tree as a merged tree of page and media namespaces (similar to what we do in the ACL manager). This merged view is often advantageous in wikis where both trees are very similar. A default view can be set in the configuration but users can switch between the views on a case by case basis. This does not change any functionality in any of the other scripts or in the way how the move is executed. It's a GUI refresh only. Other parts of the plugin could use a GUI refresh as well (eg. use the same SVG based icons and drop jQuery dependencies). --- action/tree.php | 11 +- admin/tree.php | 134 ++++++-- conf/default.php | 1 + conf/metadata.php | 1 + lang/en/lang.php | 8 +- lang/en/settings.php | 1 + lang/en/tree.txt | 7 +- script.js | 14 +- script/tree.js | 783 ++++++++++++++++++++++++++++++------------- script/tree_old.js | 260 ++++++++++++++ style.less | 117 ++++--- 11 files changed, 1024 insertions(+), 313 deletions(-) create mode 100644 script/tree_old.js diff --git a/action/tree.php b/action/tree.php index 644c978..ac5b0cf 100644 --- a/action/tree.php +++ b/action/tree.php @@ -51,13 +51,10 @@ public function handle_ajax_call(Doku_Event $event, $params) { $type = admin_plugin_move_tree::TYPE_PAGES; } - $data = $plugin->tree($type, $ns, $ns); + header('Content-Type: application/json'); - echo html_buildlist( - $data, 'tree_list', - array($plugin, 'html_list'), - array($plugin, 'html_li') - ); + $data = $plugin->tree($type, $ns, $ns); + echo json_encode($data); } -} \ No newline at end of file +} diff --git a/admin/tree.php b/admin/tree.php index a3482dc..acf034a 100644 --- a/admin/tree.php +++ b/admin/tree.php @@ -1,6 +1,7 @@ locale_xhtml('tree'); + $dual = $INPUT->bool('dual', $this->getConf('dual')); + + /** @var helper_plugin_move_plan $plan */ + $plan = plugin_load('helper', 'move_plan'); + if ($plan->isCommited()) { + echo '
' . $this->getLang('moveinprogress') . '
'; + } else { + echo ''; + + echo ''; + + echo '
'; + echo '
'; + if ($dual) { + echo '
    '; + echo '
      '; + } else { + echo '
        '; + } + echo '
        '; + + + $form = new dokuwiki\Form\Form(['method' => 'post']); + $form->setHiddenField('page', 'move_main'); + + $cb = $form->addCheckbox('autoskip', $this->getLang('autoskip')); + if ($this->getConf('autoskip')) $cb->attr('checked', 'checked'); + + $cb = $form->addCheckbox('autorewrite', $this->getLang('autorewrite')); + if ($this->getConf('autorewrite')) $cb->attr('checked', 'checked'); + + $form->addButton('submit', $this->getLang('btn_start')); + echo $form->toHTML(); + echo '
        '; + } + } + + public function htmlOld() + { + global $ID; + + echo $this->locale_xhtml('tree'); echo ''; echo '
        '; @@ -51,7 +113,7 @@ public function html() { /** @var helper_plugin_move_plan $plan */ $plan = plugin_load('helper', 'move_plan'); echo '
        '; - if($plan->isCommited()) { + if ($plan->isCommited()) { echo '
        ' . $this->getLang('moveinprogress') . '
        '; } else { $form = new Doku_Form(array('action' => wl($ID), 'id' => 'plugin_move__tree_execute')); @@ -76,15 +138,16 @@ public function html() { * * @param int $type */ - protected function htmlTree($type = self::TYPE_PAGES) { + protected function htmlTree($type = self::TYPE_PAGES) + { $data = $this->tree($type); // wrap a list with the root level around the other namespaces array_unshift( $data, array( - 'level' => 0, 'id' => '*', 'type' => 'd', - 'open' => 'true', 'label' => $this->getLang('root') - ) + 'level' => 0, 'id' => '*', 'type' => 'd', + 'open' => 'true', 'label' => $this->getLang('root') + ) ); echo html_buildlist( $data, 'tree_list idx', @@ -96,31 +159,32 @@ protected function htmlTree($type = self::TYPE_PAGES) { /** * Build a tree info structure from media or page directories * - * @param int $type + * @param int $type * @param string $open The hierarchy to open FIXME not supported yet * @param string $base The namespace to start from * @return array */ - public function tree($type = self::TYPE_PAGES, $open = '', $base = '') { + public function tree($type = self::TYPE_PAGES, $open = '', $base = '') + { global $conf; $opendir = utf8_encodeFN(str_replace(':', '/', $open)); $basedir = utf8_encodeFN(str_replace(':', '/', $base)); $opts = array( - 'pagesonly' => ($type == self::TYPE_PAGES), - 'listdirs' => true, - 'listfiles' => true, - 'sneakyacl' => $conf['sneaky_index'], - 'showmsg' => false, - 'depth' => 1, + 'pagesonly' => ($type == self::TYPE_PAGES), + 'listdirs' => true, + 'listfiles' => true, + 'sneakyacl' => $conf['sneaky_index'], + 'showmsg' => false, + 'depth' => 1, 'showhidden' => true ); $data = array(); - if($type == self::TYPE_PAGES) { + if ($type == self::TYPE_PAGES) { search($data, $conf['datadir'], 'search_universal', $opts, $basedir); - } elseif($type == self::TYPE_MEDIA) { + } elseif ($type == self::TYPE_MEDIA) { search($data, $conf['mediadir'], 'search_universal', $opts, $basedir); } @@ -134,24 +198,25 @@ public function tree($type = self::TYPE_PAGES, $open = '', $base = '') { * * @author Andreas Gohr */ - function html_list($item) { + function html_list($item) + { $ret = ''; // what to display - if(!empty($item['label'])) { + if (!empty($item['label'])) { $base = $item['label']; } else { $base = ':' . $item['id']; $base = substr($base, strrpos($base, ':') + 1); } - if($item['id'] == '*') $item['id'] = ''; + if ($item['id'] == '*') $item['id'] = ''; if ($item['id']) { $ret .= ' '; } // namespace or page? - if($item['type'] == 'd') { + if ($item['type'] == 'd') { $ret .= ''; $ret .= $base; $ret .= ''; @@ -161,7 +226,7 @@ function html_list($item) { $ret .= ''; } - if($item['id']) $ret .= ''; + if ($item['id']) $ret .= ''; else $ret .= ''; return $ret; @@ -173,17 +238,18 @@ function html_list($item) { * @param array $item * @return string */ - function html_li($item) { - if($item['id'] == '*') $item['id'] = ''; + function html_li($item) + { + if ($item['id'] == '*') $item['id'] = ''; - $params = array(); + $params = array(); $params['class'] = ' type-' . $item['type']; - if($item['type'] == 'd') $params['class'] .= ' ' . ($item['open'] ? 'open' : 'closed'); - $params['data-name'] = noNS($item['id']); - $params['data-id'] = $item['id']; - $attr = buildAttributes($params); + if ($item['type'] == 'd') $params['class'] .= ' ' . ($item['open'] ? 'open' : 'closed'); + $params['data-name'] = noNS($item['id']); + $params['data-id'] = $item['id']; + $attr = buildAttributes($params); - return "
      • "; + return "
      • "; } } diff --git a/conf/default.php b/conf/default.php index cc1cbe7..efeafc5 100644 --- a/conf/default.php +++ b/conf/default.php @@ -2,6 +2,7 @@ $conf['allowrename'] = '@user'; $conf['minor'] = 1; +$conf['dual'] = 1; $conf['autoskip'] = 0; $conf['autorewrite'] = 1; $conf['pagetools_integration'] = 1; diff --git a/conf/metadata.php b/conf/metadata.php index 35e1476..3323029 100644 --- a/conf/metadata.php +++ b/conf/metadata.php @@ -2,6 +2,7 @@ $meta['allowrename'] = array('string'); $meta['minor'] = array('onoff'); +$meta['dual'] = array('onoff'); $meta['autoskip'] = array('onoff'); $meta['autorewrite'] = array('onoff'); $meta['pagetools_integration'] = array('onoff'); diff --git a/lang/en/lang.php b/lang/en/lang.php index 202ab3b..c020a0e 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -80,9 +80,15 @@ $lang['root'] = '[Root namespace]'; $lang['noscript'] = 'This feature requires JavaScript'; $lang['moveinprogress'] = 'There is another move operation in progress currently, you can\'t use this tool right now.'; +$lang['dual0'] = 'Pages and Media combined'; +$lang['dual1'] = 'Pages and Media separated'; $lang['js']['renameitem'] = 'Rename this item'; $lang['js']['add'] = 'Create a new namespace'; -$lang['js']['duplicate'] = 'Sorry, "%s" already exists in this namespace.'; +$lang['js']['duplicate'] = 'Sorry, item "%s" already exists.'; + +$lang['js']['select'] = 'Select for moving'; +$lang['js']['extchange'] = 'You can not change the extension of a media file'; + // Media Manager $lang['js']['moveButton'] = 'Move file'; diff --git a/lang/en/settings.php b/lang/en/settings.php index 659f2e1..2aea7cc 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -2,6 +2,7 @@ $lang['allowrename'] = 'Allow renaming of pages and media to these groups and users (comma separated).'; $lang['minor'] = 'Mark link adjustments as minor? Minor changes will not be listed in RSS feeds and subscription mails.'; +$lang['dual'] = 'Should pages and media files be shown separatly? If disabled, their namespaces are shown as if they are one tree.'; $lang['autoskip'] = 'Enable automatic skipping of errors in namespace moves by default.'; $lang['autorewrite'] = 'Enable automatic link rewriting after namespace moves by default.'; $lang['pagetools_integration'] = 'Add renaming button to pagetools'; diff --git a/lang/en/tree.txt b/lang/en/tree.txt index 621abf8..e704c4b 100644 --- a/lang/en/tree.txt +++ b/lang/en/tree.txt @@ -2,6 +2,7 @@ This interface allows you to rearrange your wiki's namespaces, pages and media files via Drag'n'Drop. -In order to move many namespaces, pages or media files to the same destination, you can use the checkboxes as follows: - * check the namespaces, pages or media files you want to move; - * move one of the checked items to the desired destination, all selected items will be moved to this destination. +Click namespace names to open them, click the icons to select items. All selected items will be moved together. Click the names to rename individual items. Use the edit icon to rename a namespace, page or file. + +Click the "Start" button at the bottom to start the move process. You'll have the chance to review all changes resulting from the move before they are applied. + diff --git a/script.js b/script.js index 9fba837..9c4604a 100644 --- a/script.js +++ b/script.js @@ -7,9 +7,19 @@ /* DOKUWIKI:include_once script/json2.js */ /* DOKUWIKI:include script/MoveMediaManager.js */ -jQuery(function() { +jQuery(function () { /* DOKUWIKI:include script/form.js */ /* DOKUWIKI:include script/progress.js */ - /* DOKUWIKI:include script/tree.js */ /* DOKUWIKI:include script/rename.js */ + + + // lazy load the tree manager + const $tree = jQuery('#plugin_move__tree'); + if ($tree.length) { + jQuery.getScript( + DOKU_BASE + 'lib/plugins/move/script/tree.js', + () => new PluginMoveTree($tree.get(0)) + ); + } + }); diff --git a/script/tree.js b/script/tree.js index 6dcd650..8a34994 100644 --- a/script/tree.js +++ b/script/tree.js @@ -1,260 +1,587 @@ /** - * Script for the tree management interface + * The Tree Move Manager + * + * This script handles the move tree and all its interactions. + * + * The script supports combined and separate page/media trees. Items have their orignal ID in data-orig and their + * current ID in data-id. + * + * This is pure vanilla JavaScript without any dependencies to jQuery. It is lazy loaded by the main script. */ +class PluginMoveTree { + #ENDPOINT = DOKU_BASE + 'lib/exe/ajax.php?call=plugin_move_tree'; -var $GUI = jQuery('#plugin_move__tree'); + icons = { + 'close': 'M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z', + 'open': 'M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z', + 'page': 'M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M15,18V16H6V18H15M18,14V12H6V14H18Z', + 'media': 'M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z', + 'rename': 'M18,4V3A1,1 0 0,0 17,2H5A1,1 0 0,0 4,3V7A1,1 0 0,0 5,8H17A1,1 0 0,0 18,7V6H19V10H9V21A1,1 0 0,0 10,22H12A1,1 0 0,0 13,21V12H21V4H18Z', + 'drag': 'M4 4V22H20V24H4C2.9 24 2 23.1 2 22V4H4M15 7H20.5L15 1.5V7M8 0H16L22 6V18C22 19.11 21.11 20 20 20H8C6.89 20 6 19.1 6 18V2C6 .89 6.89 0 8 0M17 16V14H8V16H17M20 12V10H8V12H20Z', + }; -$GUI.show(); -jQuery('#plugin_move__treelink').show(); + #mainElement; + #mediaTree; + #pageTree; + #dragTarget; + #dragIcon; -/** - * Checks if the given list item was moved in the tree - * - * Moved elements are highlighted and a title shows where they came from - * - * @param {jQuery} $li - */ -var checkForMovement = function ($li) { - // we need to check this LI and all previously moved sub LIs - var $all = $li.add($li.find('li.moved')); - $all.each(function () { - var $this = jQuery(this); - var oldid = $this.data('id'); - var newid = determineNewID($this); - - if (newid != oldid && !$this.hasClass('created')) { - $this.addClass('moved'); - $this.children('div').attr('title', oldid + ' -> ' + newid); - } else { - $this.removeClass('moved'); - $this.children('div').attr('title', ''); + /** + * Initialize the base tree and attach all event handlers + * + * @param {HTMLElement} main + */ + constructor(main) { + this.#mainElement = main; + this.#mediaTree = this.#mainElement.querySelector('.move-media'); + this.#pageTree = this.#mainElement.querySelector('.move-pages'); + + this.loadSubTree('', 'pages'); + this.loadSubTree('', 'media'); + + this.#dragIcon = this.icon('drag'); + this.#dragIcon.classList.add('drag-icon'); + this.#mainElement.appendChild(this.#dragIcon); + + this.#mainElement.addEventListener('click', this.clickHandler.bind(this)); + this.#mainElement.addEventListener('dragstart', this.dragStartHandler.bind(this)); + this.#mainElement.addEventListener('dragover', this.dragOverHandler.bind(this)); + this.#mainElement.addEventListener('drop', this.dragDropHandler.bind(this)); + this.#mainElement.addEventListener('dragend', this.dragEndHandler.bind(this)); + this.#mainElement.querySelector('form').addEventListener('submit', this.submitHandler.bind(this)); + + // make tree visible + this.#mainElement.style.display = 'block'; + } + + /** + * Handle all item clicks + * + * @param {MouseEvent} ev + */ + clickHandler(ev) { + const target = ev.target; + const li = target.closest('li'); + if (!li) return; + + // we want to handle clicks on these elements only + const clicked = target.closest('i,button,span'); + + // icon click selects the item + if (clicked.tagName.toLowerCase() === 'i') { + ev.stopPropagation(); + li.classList.toggle('selected'); + return; } - }); -}; -/** - * Check if the given name is allowed in the given parent - * - * @param {jQuery} $li the edited or moved LI - * @param {jQuery} $parent the (new) parent of the edited or moved LI - * @param {string} name the (new) name to check - * @returns {boolean} - */ -var checkNameAllowed = function ($li, $parent, name) { - var ok = true; - $parent.children('li').each(function () { - if (this === $li[0]) return; - var cname = 'type-f'; - if ($li.hasClass('type-d')) cname = 'type-d'; - - var $this = jQuery(this); - if ($this.data('name') == name && $this.hasClass(cname)) ok = false; - }); - return ok; -}; + // button click opens rename dialog + if (clicked.tagName.toLowerCase() === 'button') { + ev.stopPropagation(); + this.renameGui(li); + return; + } -/** - * Returns the new ID of a given list item - * - * @param {jQuery} $li - * @returns {string} - */ -var determineNewID = function ($li) { - var myname = $li.data('name'); - - var $parent = $li.parent().closest('li'); - if ($parent.length) { - return (determineNewID($parent) + ':' + myname).replace(/^:/, ''); - } else { - return myname; + // click on name opens/closes namespace + if (clicked.tagName.toLowerCase() === 'span' && li.classList.contains('move-ns')) { + ev.stopPropagation(); + this.toggleNamespace(li); + } } -}; -/** - * Very simplistic cleanID() in JavaScript - * - * Strips out namespaces - * - * @param {string} id - */ -var cleanID = function (id) { - if (!id) return ''; + /** + * Submit the data for the move operation + * + * @param {FormDataEvent} ev + */ + submitHandler(ev) { + // gather all changed items + const data = []; + this.#mainElement.querySelectorAll('.changed').forEach(li => { + let entry = { + src: li.dataset.orig, + dst: li.dataset.id, + type: this.isItemMedia(li) ? 'media' : 'page', + class: this.isItemNamespace(li) ? 'ns' : 'doc', + }; + data.push(entry); + + // if this is a namspace that is shared between media and pages, add a second entry + if(entry.class === 'ns' && entry.type === 'media' && this.isItemPage(li)) { + entry = {...entry}; // clone + entry.type = 'page'; + data.push(entry); + } + }); - id = id.replace(/[!"#$%§&\'()+,/;<=>?@\[\]^`\{|\}~\\;:\/\*]+/g, '_'); - id = id.replace(/^_+/, ''); - id = id.replace(/_+$/, ''); - id = id.toLowerCase(); + // add JSON data to form, then let the event continue + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'json'; + input.value = JSON.stringify(data); + ev.target.appendChild(input); + } - return id; -}; + /** + * Begin drag operation + * + * @param {DragEvent} ev + */ + dragStartHandler(ev) { + if (!ev.target) return; + const li = ev.target.closest('li'); + if (!li) return; -/** - * Initialize the drag & drop-tree at the given li (must be this). - */ -var initTree = function () { - var $li = jQuery(this); - var my_root = $li.closest('.tree_root')[0]; - $li.draggable({ - revert: true, - revertDuration: 0, - opacity: 0.5, - stop : function(event, ui) { - ui.helper.css({height: "auto", width: "auto"}); + ev.dataTransfer.setData('text/plain', li.dataset.id); // FIXME needed? + ev.dataTransfer.effectAllowed = 'move'; + ev.dataTransfer.setDragImage(this.#dragIcon, -12, -12); + + // the dragged element is always selected + li.classList.add('selected'); + } + + /** + * Higlight drop zone and allow dropping + * + * @param {DragEvent} ev + */ + dragOverHandler(ev) { + if (!ev.target) return; // the element the mouse is over + const ul = ev.target.closest('ul'); + if (!ul) return; + ev.preventDefault(); // allow drop + + if (this.#dragTarget && this.#dragTarget !== ul) { + this.#dragTarget.classList.remove('drop-zone'); } - }).droppable({ - tolerance: 'pointer', - greedy: true, - accept : function(draggable) { - return my_root == draggable.closest('.tree_root')[0]; - }, - drop : function (event, ui) { - var $dropped = ui.draggable; - var $me = jQuery(this); - - if ($dropped.children('div.li').children('input').prop('checked')) { - $dropped = $dropped.add( - jQuery(my_root) - .find('input') - .filter(function() { - return jQuery(this).prop('checked'); - }).parent().parent() - ); + this.#dragTarget = ul; + this.#dragTarget.classList.add('drop-zone'); + } + + /** + * Handle the Drop operation + * + * @param {DragEvent} ev + */ + dragDropHandler(ev) { + if (!ev.target) return; + const dst = ev.target.closest('ul'); + if (!dst) return; + + // move all selected items to the drop target + const elements = this.#mainElement.querySelectorAll('.selected'); + elements.forEach(src => { + const newID = this.getNewId(src.dataset.id, dst.dataset.id); + // ensure that item stays in its own tree, ignore cross-tree moves + if (this.itemTree(src).contains(dst) === false) { + return; } - if ($me.parents().addBack().is($dropped)) { + // same ID? we consider this an abort + if(newID === src.dataset.id) { + src.classList.remove('selected'); return; } - var insert_child = !($me.hasClass("type-f") || $me.hasClass("closed")); - var $new_parent = insert_child ? $me.children('ul') : $me.parent(); - var allowed = true; + // moving into self? ignore + if(dst.contains(src)) { + return; + } - $dropped.each(function () { - var $this = jQuery(this); - allowed &= checkNameAllowed($this, $new_parent, $this.data('name')); - }); + // check if item with same ID already exists + if (this.itemTree(src).querySelector(`li[data-id="${newID}"]`)) { + alert(LANG.plugins.move.duplicate.replace('%s', newID)); + return; + } + dst.append(src); + this.updateMovedItem(src, newID); + }); + this.updatePassiveSubNamespaces(dst); + this.sortList(dst); + } - if (allowed) { - if (insert_child) { - $dropped.prependTo($new_parent); - } else { - $dropped.insertAfter($me); - } + /** + * Clean up after drag'n'drop operation + * + * @param {DragEvent} ev + */ + dragEndHandler(ev) { + if (this.#dragTarget) { + this.#dragTarget.classList.remove('drop-zone'); + } + } + + /** + * Rename an item via a prompt dialog + * + * @param li + */ + renameGui(li) { + const newname = window.prompt(LANG.plugins.move.renameitem, this.getBase(li.dataset.id)); + const clean = this.cleanID(newname); + + if (!clean) { + return; + } + + // avoid extension changes for media items + if (!this.isItemNamespace(li) && this.isItemMedia(li)) { + if (this.getExtension(li.dataset.id) !== this.getExtension(clean)) { + alert(LANG.plugins.move.extchange); + return; } + } - checkForMovement($dropped); + // construct new ID and check for duplicate + const ns = this.getNamespace(li.dataset.id); + const newID = ns ? ns + ':' + clean : clean; + if (this.itemTree(li).querySelector(`li[data-id="${newID}"]`)) { + alert(LANG.plugins.move.duplicate.replace('%s', newID)); + return; } - }) - // add title to rename icon - .find('img.rename').attr('title', LANG.plugins.move.renameitem) - .end() - .find('img.add').attr('title', LANG.plugins.move.add); -}; -var add_template = '
        • '; + // update the item + this.updateMovedItem(li, newID); -/** - * Attach event listeners to the tree - */ -$GUI.find('div.tree_root > ul.tree_list') - .click(function (e) { - var $clicky = jQuery(e.target); - var $li = $clicky.parent().parent(); - - if ($clicky[0].tagName == 'A' && $li.hasClass('type-d')) { // Click on folder - open and close via AJAX - e.stopPropagation(); - if ($li.hasClass('open')) { - $li - .removeClass('open') - .addClass('closed'); - - } else { - $li - .removeClass('closed') - .addClass('open'); - - // if had not been loaded before, load via AJAX - if (!$li.find('ul').length) { - var is_media = $li.closest('div.tree_root').hasClass('tree_media') ? 1 : 0; - jQuery.post( - DOKU_BASE + 'lib/exe/ajax.php', - { - call: 'plugin_move_tree', - ns: $clicky.attr('href'), - is_media: is_media - }, - function (data) { - $li.append(data); - $li.find('li').each(initTree); - } - ); - } + // if this was a namespace, update sub namespaces + if (this.isItemNamespace(li)) { + this.updatePassiveSubNamespaces(li.querySelector('ul')); + } + } + + + /** + * Open or close a namespace + * + * @param li + */ + toggleNamespace(li) { + const isOpen = li.classList.toggle('open'); + + // swap icon + const icon = li.querySelector('i'); + icon.parentNode.insertBefore(this.icon(isOpen ? 'open' : 'close'), icon); + icon.remove(); + + if (isOpen) { + // check if UL already exists and reuse it + let ul = li.querySelector('ul'); + if (ul) { + ul.style.display = ''; + return; + } + + // create new UL + ul = document.createElement('ul'); + ul.classList = li.classList; + ul.dataset.id = li.dataset.id; + ul.dataset.orig = li.dataset.orig; + li.appendChild(ul); + + if (li.classList.contains('move-pages')) { + this.loadSubTree(li.dataset.orig, 'pages'); } - e.preventDefault(); - } else if ($clicky[0].tagName == 'IMG') { // Click on IMG - do rename - e.stopPropagation(); - var $a = $clicky.parent().find('a'); - - if ($clicky.hasClass('rename')) { - var newname = window.prompt(LANG.plugins.move.renameitem, $li.data('name')); - newname = cleanID(newname); - if (newname) { - if (checkNameAllowed($li, $li.parent(), newname)) { - $li.data('name', newname); - $a.text(newname); - checkForMovement($li); - } else { - alert(LANG.plugins.move.duplicate.replace('%s', newname)); - } + if (li.classList.contains('move-media')) { + this.loadSubTree(li.dataset.orig, 'media'); + } + } else { + const ul = li.querySelector('ul'); + if (ul) { + ul.style.display = 'none'; + } + } + } + + /** + * Load the data for a namespace + * + * @param {string} namespace + * @param {string} type + * @returns {Promise} + */ + async loadSubTree(namespace, type) { + + const data = new FormData; + data.append('ns', namespace); + data.append('is_media', type === 'media' ? 1 : 0); + + const response = await fetch(this.#ENDPOINT, { + method: 'POST', + body: data + }); + const result = await response.json(); + + this.renderSubTree(namespace, result, type); + } + + /** + * Render the data for a namespace + * + * @param {string} namespace + * @param {object[]} data + * @param {string} type + */ + renderSubTree(namespace, data, type) { + const selector = `ul[data-orig="${namespace}"].move-${type}.move-ns`; + const parent = this.#mainElement.querySelector(selector); + + for (const item of data) { + let li; + // reuse namespace + if (item.type === 'd') { + li = parent.querySelector(`li[data-orig="${item.id}"].move-ns`); + } + // create new item + if (!li) { + li = this.createListItem(item, type); + parent.appendChild(li); + } + // ensure class is added to reused namespaces + li.classList.add(`move-${type}`); + } + + this.sortList(parent); + this.updatePassiveSubNamespaces(parent); // subtree might have been loaded into a renamed namespace + } + + /** + * Sort the children of the given element + * + * namespaces are sorted first, then by ID + * + * @param {HTMLUListElement} parent + */ + sortList(parent) { + [...parent.children] + .sort((a, b) => { + // sort namespaces first + if (a.classList.contains('move-ns') && !b.classList.contains('move-ns')) { + return -1; } - } else { - var newname = window.prompt(LANG.plugins.move.add); - newname = cleanID(newname); - if (newname) { - if (checkNameAllowed($li, $li.children('ul'), newname)) { - var $new_li = jQuery(add_template.replace(/%s/g, newname)); - $li.children('ul').prepend($new_li); - - $new_li.each(initTree); - } else { - alert(LANG.plugins.move.duplicate.replace('%s', newname)); - } + if (!a.classList.contains('move-ns') && b.classList.contains('move-ns')) { + return 1; } + // sort by ID + return a.dataset.id.localeCompare(b.dataset.id); + }) + .forEach(node => parent.appendChild(node)); + } + + /** + * Update the IDs of all sub-namespaces without marking them as moved + * + * The update is not marked as a change, because it will be covered in the move of an upper namespace. + * But updating the ID ensures that all drags that go into this namespace will already reflect the new namespace. + * + * @param {HTMLUListElement} parent + */ + updatePassiveSubNamespaces(parent) { + const ns = parent.dataset.id; // parent is the namespace + + for (const li of parent.children) { + if(!this.isItemNamespace(li)) continue; + + const newID = this.getNewId(li.dataset.id, ns); + li.dataset.id = newID; + + const sub = li.getElementsByTagName('ul'); + if (sub.length) { + sub[0].dataset.id = newID; + this.updatePassiveSubNamespaces(sub[0]); } - e.preventDefault(); } - }).find('li').each(initTree); + } -/** - * Gather all moves from the trees and put them as JSON into the form before submit - * - * @fixme has some duplicate code - */ -jQuery('#plugin_move__tree_execute').submit(function (e) { - var data = []; - - $GUI.find('.tree_pages .moved').each(function (idx, el) { - var $el = jQuery(el); - var newid = determineNewID($el); - - data[data.length] = { - 'class': $el.hasClass('type-d') ? 'ns' : 'doc', - type: 'page', - src: $el.data('id'), - dst: newid - }; - }); - $GUI.find('.tree_media .moved').each(function (idx, el) { - var $el = jQuery(el); - var newid = determineNewID($el); - - data[data.length] = { - 'class': $el.hasClass('type-d') ? 'ns' : 'doc', - type: 'media', - src: $el.data('id'), - dst: newid - }; - }); - - jQuery(this).find('input[name=json]').val(JSON.stringify(data)); -}); + /** + * Get the new ID when moving an item to a new namespace + * + * @param oldId + * @param newNS + * @returns {string} + */ + getNewId(oldId, newNS) { + const base = this.getBase(oldId); + return newNS ? newNS + ':' + base : base; + } + + /** + * Adjust the ID of a moved item + * + * @param {HTMLLIElement} li The item to rename + * @param {string} newID The new ID + */ + updateMovedItem(li, newID) { + const name = li.querySelector('span'); + + if (li.dataset.id !== newID) { + li.dataset.id = newID; + li.classList.add('changed'); + name.textContent = this.getBase(newID); + name.title = li.dataset.orig + ' → ' + newID; + + const ul = li.querySelector('ul'); + if (ul) { + ul.dataset.id = newID; + } + } else { + li.classList.remove('changed'); + name.title = ''; + } + } + + /** + * Check if an item is a namespace item + * + * @param {HTMLLIElement} li + * @returns {boolean} + */ + isItemNamespace(li) { + return li.classList.contains('move-ns'); + } + + /** + * Check if an item is a media item + * + * @param {HTMLLIElement} li + * @returns {boolean} + */ + isItemMedia(li) { + return li.classList.contains('move-media'); + } + + /** + * Check if an item is a page item + * + * @param {HTMLLIElement} li + * @returns {boolean} + */ + isItemPage(li) { + return li.classList.contains('move-pages'); + } + + /** + * Get the tree for the given item + * + * @param li + * @returns {HTMLUListElement} + */ + itemTree(li) { + if (this.isItemMedia(li)) { + return this.#mediaTree; + } else { + return this.#pageTree; + } + } + + /** + * Create a list item + * + * @param {object} item + * @param {string} type + * @returns {HTMLLIElement} + */ + createListItem(item, type) { + const li = document.createElement('li'); + li.dataset.id = item.id; + li.dataset.orig = item.id; // track the original ID + li.classList.add(`move-${type}`); + li.draggable = true; + + const wrapper = document.createElement('div'); + wrapper.classList.add('li'); + li.appendChild(wrapper); + + let icon; + if (item.type === 'd') { + li.classList.add('move-ns'); + icon = this.icon('close'); + } else if (type === 'media') { + icon = this.icon('media'); + } else { + icon = this.icon('page'); + } + icon.title = LANG.plugins.move.select; + wrapper.appendChild(icon); + + const name = document.createElement('span'); + name.textContent = this.getBase(item.id); + wrapper.appendChild(name); + + const renameBtn = document.createElement('button'); + this.icon('rename', renameBtn); + renameBtn.title = LANG.plugins.move.renameitem; + wrapper.appendChild(renameBtn); + + return li; + } + + /** + * Create an icon element + * + * @param {string} type + * @param {HTMLElement} element The element to insert the SVG into, a new if not given + * @returns {HTMLElement} + */ + icon(type, element = null) { + if (!element) { + element = document.createElement('i'); + } + + element.classList.add('icon'); + element.innerHTML = ``; + return element; + } + + /** + * Get the base part (filename) of an ID + * + * @param {string} id + * @returns {string} + */ + getBase(id) { + return id.split(':').slice(-1)[0]; + } + + /** + * Get the extension part of an ID + * + * This isn't perfect, but adds some safety + * + * @param {string} id + * @returns {string} + */ + getExtension(id) { + const parts = id.split('.'); + return parts.length > 1 ? parts.pop() : ''; + } + + /** + * Get the namespace part of an ID + * + * @param {string} id + * @returns {string} + */ + getNamespace(id) { + if (id.includes(':') === false) { + return ''; + } + return id.split(':').slice(0, -1).join(':'); + } + + /** + * Very simplistic cleanID() in JavaScript + * + * Strips out namespaces + * + * @param {string} id + */ + cleanID(id) { + if (!id) return ''; + + id = id.replace(/[!"#$%§&'()+,\/;<=>?@\[\]^`{|}~\\:*\s]+/g, '_'); + id = id.replace(/^_+/, ''); + id = id.replace(/_+$/, ''); + id = id.toLowerCase(); + + return id; + }; +} diff --git a/script/tree_old.js b/script/tree_old.js new file mode 100644 index 0000000..6dcd650 --- /dev/null +++ b/script/tree_old.js @@ -0,0 +1,260 @@ +/** + * Script for the tree management interface + */ + +var $GUI = jQuery('#plugin_move__tree'); + +$GUI.show(); +jQuery('#plugin_move__treelink').show(); + +/** + * Checks if the given list item was moved in the tree + * + * Moved elements are highlighted and a title shows where they came from + * + * @param {jQuery} $li + */ +var checkForMovement = function ($li) { + // we need to check this LI and all previously moved sub LIs + var $all = $li.add($li.find('li.moved')); + $all.each(function () { + var $this = jQuery(this); + var oldid = $this.data('id'); + var newid = determineNewID($this); + + if (newid != oldid && !$this.hasClass('created')) { + $this.addClass('moved'); + $this.children('div').attr('title', oldid + ' -> ' + newid); + } else { + $this.removeClass('moved'); + $this.children('div').attr('title', ''); + } + }); +}; + +/** + * Check if the given name is allowed in the given parent + * + * @param {jQuery} $li the edited or moved LI + * @param {jQuery} $parent the (new) parent of the edited or moved LI + * @param {string} name the (new) name to check + * @returns {boolean} + */ +var checkNameAllowed = function ($li, $parent, name) { + var ok = true; + $parent.children('li').each(function () { + if (this === $li[0]) return; + var cname = 'type-f'; + if ($li.hasClass('type-d')) cname = 'type-d'; + + var $this = jQuery(this); + if ($this.data('name') == name && $this.hasClass(cname)) ok = false; + }); + return ok; +}; + +/** + * Returns the new ID of a given list item + * + * @param {jQuery} $li + * @returns {string} + */ +var determineNewID = function ($li) { + var myname = $li.data('name'); + + var $parent = $li.parent().closest('li'); + if ($parent.length) { + return (determineNewID($parent) + ':' + myname).replace(/^:/, ''); + } else { + return myname; + } +}; + +/** + * Very simplistic cleanID() in JavaScript + * + * Strips out namespaces + * + * @param {string} id + */ +var cleanID = function (id) { + if (!id) return ''; + + id = id.replace(/[!"#$%§&\'()+,/;<=>?@\[\]^`\{|\}~\\;:\/\*]+/g, '_'); + id = id.replace(/^_+/, ''); + id = id.replace(/_+$/, ''); + id = id.toLowerCase(); + + return id; +}; + +/** + * Initialize the drag & drop-tree at the given li (must be this). + */ +var initTree = function () { + var $li = jQuery(this); + var my_root = $li.closest('.tree_root')[0]; + $li.draggable({ + revert: true, + revertDuration: 0, + opacity: 0.5, + stop : function(event, ui) { + ui.helper.css({height: "auto", width: "auto"}); + } + }).droppable({ + tolerance: 'pointer', + greedy: true, + accept : function(draggable) { + return my_root == draggable.closest('.tree_root')[0]; + }, + drop : function (event, ui) { + var $dropped = ui.draggable; + var $me = jQuery(this); + + if ($dropped.children('div.li').children('input').prop('checked')) { + $dropped = $dropped.add( + jQuery(my_root) + .find('input') + .filter(function() { + return jQuery(this).prop('checked'); + }).parent().parent() + ); + } + + if ($me.parents().addBack().is($dropped)) { + return; + } + + var insert_child = !($me.hasClass("type-f") || $me.hasClass("closed")); + var $new_parent = insert_child ? $me.children('ul') : $me.parent(); + var allowed = true; + + $dropped.each(function () { + var $this = jQuery(this); + allowed &= checkNameAllowed($this, $new_parent, $this.data('name')); + }); + + if (allowed) { + if (insert_child) { + $dropped.prependTo($new_parent); + } else { + $dropped.insertAfter($me); + } + } + + checkForMovement($dropped); + } + }) + // add title to rename icon + .find('img.rename').attr('title', LANG.plugins.move.renameitem) + .end() + .find('img.add').attr('title', LANG.plugins.move.add); +}; + +var add_template = '
          • '; + +/** + * Attach event listeners to the tree + */ +$GUI.find('div.tree_root > ul.tree_list') + .click(function (e) { + var $clicky = jQuery(e.target); + var $li = $clicky.parent().parent(); + + if ($clicky[0].tagName == 'A' && $li.hasClass('type-d')) { // Click on folder - open and close via AJAX + e.stopPropagation(); + if ($li.hasClass('open')) { + $li + .removeClass('open') + .addClass('closed'); + + } else { + $li + .removeClass('closed') + .addClass('open'); + + // if had not been loaded before, load via AJAX + if (!$li.find('ul').length) { + var is_media = $li.closest('div.tree_root').hasClass('tree_media') ? 1 : 0; + jQuery.post( + DOKU_BASE + 'lib/exe/ajax.php', + { + call: 'plugin_move_tree', + ns: $clicky.attr('href'), + is_media: is_media + }, + function (data) { + $li.append(data); + $li.find('li').each(initTree); + } + ); + } + } + e.preventDefault(); + } else if ($clicky[0].tagName == 'IMG') { // Click on IMG - do rename + e.stopPropagation(); + var $a = $clicky.parent().find('a'); + + if ($clicky.hasClass('rename')) { + var newname = window.prompt(LANG.plugins.move.renameitem, $li.data('name')); + newname = cleanID(newname); + if (newname) { + if (checkNameAllowed($li, $li.parent(), newname)) { + $li.data('name', newname); + $a.text(newname); + checkForMovement($li); + } else { + alert(LANG.plugins.move.duplicate.replace('%s', newname)); + } + } + } else { + var newname = window.prompt(LANG.plugins.move.add); + newname = cleanID(newname); + if (newname) { + if (checkNameAllowed($li, $li.children('ul'), newname)) { + var $new_li = jQuery(add_template.replace(/%s/g, newname)); + $li.children('ul').prepend($new_li); + + $new_li.each(initTree); + } else { + alert(LANG.plugins.move.duplicate.replace('%s', newname)); + } + } + } + e.preventDefault(); + } + }).find('li').each(initTree); + +/** + * Gather all moves from the trees and put them as JSON into the form before submit + * + * @fixme has some duplicate code + */ +jQuery('#plugin_move__tree_execute').submit(function (e) { + var data = []; + + $GUI.find('.tree_pages .moved').each(function (idx, el) { + var $el = jQuery(el); + var newid = determineNewID($el); + + data[data.length] = { + 'class': $el.hasClass('type-d') ? 'ns' : 'doc', + type: 'page', + src: $el.data('id'), + dst: newid + }; + }); + $GUI.find('.tree_media .moved').each(function (idx, el) { + var $el = jQuery(el); + var newid = determineNewID($el); + + data[data.length] = { + 'class': $el.hasClass('type-d') ? 'ns' : 'doc', + type: 'media', + src: $el.data('id'), + dst: newid + }; + }); + + jQuery(this).find('input[name=json]').val(JSON.stringify(data)); +}); diff --git a/style.less b/style.less index 5a7dfbb..b495d70 100644 --- a/style.less +++ b/style.less @@ -2,60 +2,96 @@ * Tree Manager */ #plugin_move__tree { + // hidden by default + display: none; - display: none; // will be enabled via JavaScript - - .tree_pages, - .tree_media { - width: 49%; - float: left; - overflow-wrap: break-word; - overflow: hidden; - } - - .controls { - clear: left; - display: block; + > div.trees { + display: flex; + gap: 1em; + margin: 1em 0; } - ul.tree_list { - .moved > div, .created > div { - border: 1px dashed lighten(@ini_text, 30%); - border-radius: 3px; - margin-left: -3px; - padding-left: 3px; - margin-top: 1px; - } + // basic list layout + ul { + list-style-type: none; + margin: 0.25em 0 0.25em 2em; + padding: 0; + border: 1px solid transparent; li { - cursor: move; + margin: 0; + padding: 0; - img { - float: right; - cursor: pointer; - display: none; + div.li { + display: flex; + align-items: center; + gap: 0.25em; } } + } - li div:hover { - background-color: @ini_background_alt; + // namespace labels can be clicked to open/close + li.move-ns > div.li span { + cursor: pointer; + } - img { - display: block; - } + // icons are used for selection + li > div.li i svg { + width: 1.5em; + height: 1.5em; + cursor: pointer; + fill: @ini_text_neu; + } + + li.selected > div.li i svg { + fill: @ini_link; + } + + // moved items are highlighted + li.changed > div.li > span { + background-color: @ini_highlight; + } + + li > div.li button { + background: none; + border: none; + padding: 0; + cursor: pointer; + opacity: 0; + + &:hover svg { + fill: @ini_link; } + } - li.closed { - ul { - display: none; - } + li > div.li:hover button { + opacity: 1; + } + + .drop-zone { + border: 1px dashed @ini_border; + } + + .drag-icon { + // hide the element from view, but keep it visible + position: absolute; + left: -9999px; + display: inline-flex; + + svg { + height: 32px; + width: 32px; + fill: @ini_text_neu; } } -} -#plugin_move__treelink { - display: none; // will be enabled via JavaScript + + form label { + display: block; + margin: 0.5em 0; + } } + /** * The progress page */ @@ -76,9 +112,11 @@ li.page { list-style-image: url(images/page.png); } + li.media { list-style-image: url(images/disk.png); } + li.affected { list-style-image: url(images/page_link.png); } @@ -111,15 +149,18 @@ #dokuwiki__pagetools ul li.plugin_move_page a { background-position: right 0; } + #dokuwiki__pagetools ul li.plugin_move_page a:before { content: url(images/sprite.png); margin-top: 0; } + #dokuwiki__pagetools:hover ul li.plugin_move_page a, #dokuwiki__pagetools ul li.plugin_move_page a:focus, #dokuwiki__pagetools ul li.plugin_move_page a:active { background-image: url(images/sprite.png); } + #dokuwiki__pagetools ul li.plugin_move_page a:hover, #dokuwiki__pagetools ul li.plugin_move_page a:active, #dokuwiki__pagetools ul li.plugin_move_page a:focus { From 9c25fa09e2c393e349a9d1082e4fb5d1f94a0a9c Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 18 Jul 2024 11:45:21 +0200 Subject: [PATCH 03/12] remove changed language string The duplicate message now displays a full ID including the namespace, so the string had to be adjusted. This removes it from all language files to have translators retranslate it.  Conflicts:  lang/cs/lang.php  lang/zh/lang.php --- lang/cs/lang.php | 1 - lang/da/lang.php | 1 - lang/de-informal/lang.php | 2 +- lang/de/lang.php | 2 +- lang/el/lang.php | 1 - lang/es/lang.php | 1 - lang/fr/lang.php | 1 - lang/hr/lang.php | 1 - lang/id/lang.php | 1 - lang/ja/lang.php | 1 - lang/ko/lang.php | 1 - lang/nl/lang.php | 1 - lang/no/lang.php | 1 - lang/pt-br/lang.php | 1 - lang/ru/lang.php | 3 +-- lang/sk/lang.php | 4 ++-- lang/sv/lang.php | 1 - lang/vi/lang.php | 1 - lang/zh-tw/lang.php | 1 - lang/zh/lang.php | 1 - 20 files changed, 5 insertions(+), 22 deletions(-) diff --git a/lang/cs/lang.php b/lang/cs/lang.php index 51df5b6..70616bb 100644 --- a/lang/cs/lang.php +++ b/lang/cs/lang.php @@ -64,7 +64,6 @@ $lang['js']['complete'] = 'Přesun byl dokončen.'; $lang['js']['renameitem'] = 'Přejmenovat tuto položku'; $lang['js']['add'] = 'Vytvořit nový jmenný prostor'; -$lang['js']['duplicate'] = 'Lituji, ale \'%s\' již existuje ve jmenném prosoru.'; $lang['js']['moveButton'] = 'Přesunout soubor'; $lang['js']['dialogIntro'] = 'Zadejte nový cíl souboru. Můžete změnit jmenný prostor, ale ne příponu souboru.'; $lang['root'] = '[Kořen]'; diff --git a/lang/da/lang.php b/lang/da/lang.php index 73cf293..f9dd1f4 100644 --- a/lang/da/lang.php +++ b/lang/da/lang.php @@ -59,7 +59,6 @@ $lang['js']['complete'] = 'Flytningsoperation færdig.'; $lang['js']['renameitem'] = 'Omdøb denne'; $lang['js']['add'] = 'Opret et nyt navnerum'; -$lang['js']['duplicate'] = 'Beklager, "%s" findes allerede i dette navnerum.'; $lang['root'] = '[Rod navnerum]'; $lang['noscript'] = 'Denne handling kræver JavaScript'; $lang['moveinprogress'] = 'Der er i øjeblikket en anden flytningsoperation i gang, du kan ikke anvende dette værktøj på dette tidspunkt.'; diff --git a/lang/de-informal/lang.php b/lang/de-informal/lang.php index 2fb0ef3..f04aa0c 100644 --- a/lang/de-informal/lang.php +++ b/lang/de-informal/lang.php @@ -61,7 +61,7 @@ $lang['js']['complete'] = 'Verschieben abgeschlossen.'; $lang['js']['renameitem'] = 'Dieses Element umbenennen'; $lang['js']['add'] = 'Neuen Namensraum erstellen'; -$lang['js']['duplicate'] = 'Entschuldigung, "%s" existiert in diesem Namensraum bereits. '; +$lang['js']['duplicate'] = 'Entschuldigung, "%s" existiert bereits.'; $lang['root'] = '[Oberster Namensraum]'; $lang['noscript'] = 'Dieses Feature benötigt JavaScript.'; $lang['moveinprogress'] = 'Eine andere Verschiebeoperation läuft momentan, du kannst dieses Tool gerade nicht benutzen.'; diff --git a/lang/de/lang.php b/lang/de/lang.php index e6f6479..ccf42f0 100644 --- a/lang/de/lang.php +++ b/lang/de/lang.php @@ -63,7 +63,7 @@ $lang['js']['complete'] = 'Verschieben abgeschlossen.'; $lang['js']['renameitem'] = 'Dieses Element umbenennen'; $lang['js']['add'] = 'Neuen Namensraum erstellen'; -$lang['js']['duplicate'] = 'Entschuldigung, "%s" existiert in diesem Namensraum bereits. '; +$lang['js']['duplicate'] = 'Entschuldigung, "%s" existiert bereits.'; $lang['root'] = '[Oberster Namensraum]'; $lang['noscript'] = 'Dieses Feature benötigt JavaScript.'; $lang['moveinprogress'] = 'Eine andere Verschiebeoperation läuft momentan, Sie können dieses Tool gerade nicht benutzen.'; diff --git a/lang/el/lang.php b/lang/el/lang.php index 739afc1..aae3c20 100644 --- a/lang/el/lang.php +++ b/lang/el/lang.php @@ -59,7 +59,6 @@ $lang['js']['complete'] = 'Η διαδικασία της μετακίνησης ολοκληρώθηκε'; $lang['js']['renameitem'] = 'Μετονομάστε αυτό το τεμάχιο'; $lang['js']['add'] = 'Δημιουργήστε ένα νέο χώρο ονόματος'; -$lang['js']['duplicate'] = 'Λυπάμαι, το "%s" υπάρχει ήδη σε αυτό το χώρο ονόματος'; $lang['root'] = '{Βασικός χώρος ονόματος}'; $lang['noscript'] = 'Αυτό το χαρακτηριστικό χρειάζεται JavaScript'; $lang['moveinprogress'] = 'Υπάρχει μια άλλη διαδικασία μετακίνησης σε εξέλιξη τώρα, δεν μπορείτε να χρησιμοποιήστε αυτό το εργαλείο.'; diff --git a/lang/es/lang.php b/lang/es/lang.php index 0c09498..6747406 100644 --- a/lang/es/lang.php +++ b/lang/es/lang.php @@ -60,7 +60,6 @@ $lang['js']['complete'] = 'La operación de mover ha finalizado.'; $lang['js']['renameitem'] = 'Renombrar este elemento'; $lang['js']['add'] = 'Crear un nuevo espacio de nombres'; -$lang['js']['duplicate'] = 'Lo sentimos, "%s" ya existe en este espacio de nombres.'; $lang['root'] = '[Espacio de nombres raíz]'; $lang['noscript'] = 'Esta función requiere JavaScript'; $lang['moveinprogress'] = 'Hay otra operación de mover actualmente en curso, no se puede usar esta herramienta ahora mismo.'; diff --git a/lang/fr/lang.php b/lang/fr/lang.php index 8e9242a..bfe3c1a 100644 --- a/lang/fr/lang.php +++ b/lang/fr/lang.php @@ -63,7 +63,6 @@ $lang['js']['complete'] = 'Déplacement effectué.'; $lang['js']['renameitem'] = 'Renommer cet élément'; $lang['js']['add'] = 'Créer une nouvelle catégorie'; -$lang['js']['duplicate'] = 'Désolé, "%s" existe dans cette catégorie.'; $lang['js']['moveButton'] = 'Déplacer le fichier.'; $lang['js']['dialogIntro'] = 'Entrez le nouvel emplacement du fichier. Vous pouvez changer la catégorie, mais pas l\'extension.'; $lang['root'] = '[Catégorie racine]'; diff --git a/lang/hr/lang.php b/lang/hr/lang.php index ad9f222..5be84cb 100644 --- a/lang/hr/lang.php +++ b/lang/hr/lang.php @@ -59,7 +59,6 @@ $lang['js']['complete'] = 'Operacija premještanja završila.'; $lang['js']['renameitem'] = 'Preimenuj ovu stavku'; $lang['js']['add'] = 'Kreiraj novi imenski prostor'; -$lang['js']['duplicate'] = 'Isprika ali "%s" već postoji u ovom imenskom prostoru'; $lang['root'] = '[Korijen imenskog prostora]'; $lang['noscript'] = 'Ova osobina zahtijeva JavaScript'; $lang['moveinprogress'] = 'Trenutno je druga operacija premještanja u tijeku, zasada ne možete koristiti ovaj alat.'; diff --git a/lang/id/lang.php b/lang/id/lang.php index 55a9df3..40f1a53 100644 --- a/lang/id/lang.php +++ b/lang/id/lang.php @@ -56,7 +56,6 @@ $lang['js']['complete'] = 'Pemindahan selesai.'; $lang['js']['renameitem'] = 'Ubah nama item ini'; $lang['js']['add'] = 'Buat ruangnama baru'; -$lang['js']['duplicate'] = 'Maaf, %s telah ada di ruangnama ini.'; $lang['root'] = '[Ruang nama root]'; $lang['noscript'] = 'Fitur ini membutuhkan JavaScript'; $lang['moveinprogress'] = 'Ada operasi pemindahan lain yang belum selesai, Anda tidak dapat menggunakan alat ini sekarang.'; diff --git a/lang/ja/lang.php b/lang/ja/lang.php index 8b3adf8..791dbd8 100644 --- a/lang/ja/lang.php +++ b/lang/ja/lang.php @@ -59,7 +59,6 @@ $lang['js']['complete'] = '名称変更操作が完了しました。'; $lang['js']['renameitem'] = 'この項目を名称変更します。'; $lang['js']['add'] = '新しい名前空間の作成'; -$lang['js']['duplicate'] = '"%s" はこの名前空間内に既に存在します。'; $lang['root'] = '[ルート名前空間]'; $lang['noscript'] = 'この機能には JavaScriptが必要です。'; $lang['moveinprogress'] = '別の移動操作を処理中なので、今はこのツールを使用できません。'; diff --git a/lang/ko/lang.php b/lang/ko/lang.php index 3396dcd..00e2f80 100644 --- a/lang/ko/lang.php +++ b/lang/ko/lang.php @@ -61,7 +61,6 @@ $lang['js']['complete'] = '이동 작업이 완료되었습니다.'; $lang['js']['renameitem'] = '이 항목 이름 바꾸기'; $lang['js']['add'] = '새 이름공간 만들기'; -$lang['js']['duplicate'] = '죄송하지만, "%s" 문서는 이미 이 이름공간에 존재합니다.'; $lang['root'] = '[루트 이름공간]'; $lang['noscript'] = '이 기능은 자바스크립트가 필요합니다'; $lang['moveinprogress'] = '현재 진행 중인 다른 이동 작업이 있으므로, 지금 바로 이 도구를 사용할 수 없습니다.'; diff --git a/lang/nl/lang.php b/lang/nl/lang.php index 8a11125..4fef32e 100644 --- a/lang/nl/lang.php +++ b/lang/nl/lang.php @@ -62,7 +62,6 @@ $lang['js']['complete'] = 'Verplaatsing compleet.'; $lang['js']['renameitem'] = 'Hernoem dit item'; $lang['js']['add'] = 'Maak een nieuwe namespace'; -$lang['js']['duplicate'] = 'Sorry, "%s" bestaat al in deze namespace.'; $lang['root'] = '[Hoofdnamespace]'; $lang['noscript'] = 'Deze mogelijkheid vereist Javascript'; $lang['moveinprogress'] = 'Er is een andere verplaatsingsactie gaande, gebruik van deze tool is momenteel niet mogelijk.'; diff --git a/lang/no/lang.php b/lang/no/lang.php index 4fd7235..d645494 100644 --- a/lang/no/lang.php +++ b/lang/no/lang.php @@ -63,7 +63,6 @@ $lang['js']['complete'] = 'Flytting avsluttet'; $lang['js']['renameitem'] = 'Endre navn '; $lang['js']['add'] = 'Lag et nytt navnerom'; -$lang['js']['duplicate'] = 'Beklager, "%s" finnes allerede i dette navnerommet.'; $lang['root'] = '[Rot navnerom]'; $lang['noscript'] = 'Denne funksjonen krever Javascript'; $lang['moveinprogress'] = 'En annen flyttingsjobb pågår for øyeblikket så denne funksjonen kan ikke brukes akkurat nå. '; diff --git a/lang/pt-br/lang.php b/lang/pt-br/lang.php index f68c2dd..5338887 100644 --- a/lang/pt-br/lang.php +++ b/lang/pt-br/lang.php @@ -60,7 +60,6 @@ $lang['js']['complete'] = 'Operação de movimentação concluída.'; $lang['js']['renameitem'] = 'Renomear este item'; $lang['js']['add'] = 'Criar um novo domínio'; -$lang['js']['duplicate'] = 'Desculpe, "%s" já existe neste domínio.'; $lang['root'] = '[Domínio raiz]'; $lang['noscript'] = 'Este recurso requer JavaScript'; $lang['moveinprogress'] = 'Há outra operação de movimentação em andamento no momento, você não pode usar esta ferramenta agora.'; diff --git a/lang/ru/lang.php b/lang/ru/lang.php index 508ca58..8830d24 100644 --- a/lang/ru/lang.php +++ b/lang/ru/lang.php @@ -1,7 +1,7 @@ + * @author Viktor Kristian */ $lang['menu'] = 'Presun/premenovanie stránky'; @@ -69,4 +69,4 @@ $lang['moveinprogress'] = 'Práve prebieha iná operácia presunu, tento nástroj momentálne nemôžete použiť.'; $lang['js']['renameitem'] = 'Premenovať túto položku'; $lang['js']['add'] = 'Vytvoriť nový menný priestor'; -$lang['js']['duplicate'] = 'Ľutujeme, \'%s\' už v tomto mennom priestore existuje.'; + diff --git a/lang/sv/lang.php b/lang/sv/lang.php index 281223f..d599a27 100644 --- a/lang/sv/lang.php +++ b/lang/sv/lang.php @@ -46,6 +46,5 @@ $lang['js']['complete'] = 'Flytt/Namnbyte avklarat.'; $lang['js']['renameitem'] = 'Ändra namn på denna post'; $lang['js']['add'] = 'Skapa en ny namnrymd'; -$lang['js']['duplicate'] = 'Tyvärr, "%s" existerar redan i denna namnrymd.'; $lang['root'] = '[Rotnamnrymd]'; $lang['noscript'] = 'Denna funktion kräver JavaScript.'; diff --git a/lang/vi/lang.php b/lang/vi/lang.php index ad97f75..9984b40 100644 --- a/lang/vi/lang.php +++ b/lang/vi/lang.php @@ -59,7 +59,6 @@ $lang['js']['complete'] = 'Đã hoàn thành hoạt động di chuyển.'; $lang['js']['renameitem'] = 'Đổi tên mục này'; $lang['js']['add'] = 'Tạo không gian tên mới'; -$lang['js']['duplicate'] = 'Xin lỗi, đã tồn tại "%s" trong không gian tên này.'; $lang['root'] = '[Không gian tên Gốc]'; $lang['noscript'] = 'Tính năng này yêu cầu JavaScript'; $lang['moveinprogress'] = 'Hiện tại đang diễn ra một hoạt động di chuyển khác, bạn không thể sử dụng công cụ này ngay bây giờ.'; diff --git a/lang/zh-tw/lang.php b/lang/zh-tw/lang.php index ce4a660..58a02ec 100644 --- a/lang/zh-tw/lang.php +++ b/lang/zh-tw/lang.php @@ -60,7 +60,6 @@ $lang['js']['complete'] = '移動操作完畢。'; $lang['js']['renameitem'] = '重新命名該項'; $lang['js']['add'] = '產生新的目錄'; -$lang['js']['duplicate'] = '抱歉,"%s"在該目錄已存在'; $lang['root'] = '[根目錄]'; $lang['noscript'] = '此功能需要JavaScript'; $lang['moveinprogress'] = '另一個移動操作正在進行,您現在無法使用該工具'; diff --git a/lang/zh/lang.php b/lang/zh/lang.php index 2752eb1..2502c28 100644 --- a/lang/zh/lang.php +++ b/lang/zh/lang.php @@ -65,7 +65,6 @@ $lang['js']['complete'] = '移动操作完毕。'; $lang['js']['renameitem'] = '重命名该项'; $lang['js']['add'] = '创建一个新的名称空间'; -$lang['js']['duplicate'] = '抱歉,"%s"在该目录已存在'; $lang['js']['moveButton'] = '文件移动'; $lang['js']['dialogIntro'] = '输入新文件的目标位置。您可以更改命名空间,但无法更改文件扩展名'; $lang['root'] = '[跟目录]'; From 2959f1ba8cc11f6ef34ab8866069cfbf693dbbef Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 18 Jul 2024 11:49:12 +0200 Subject: [PATCH 04/12] better contrast between pages and media files --- script/tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tree.js b/script/tree.js index 8a34994..039b876 100644 --- a/script/tree.js +++ b/script/tree.js @@ -14,7 +14,7 @@ class PluginMoveTree { icons = { 'close': 'M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z', 'open': 'M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z', - 'page': 'M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M15,18V16H6V18H15M18,14V12H6V14H18Z', + 'page': 'M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z', 'media': 'M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z', 'rename': 'M18,4V3A1,1 0 0,0 17,2H5A1,1 0 0,0 4,3V7A1,1 0 0,0 5,8H17A1,1 0 0,0 18,7V6H19V10H9V21A1,1 0 0,0 10,22H12A1,1 0 0,0 13,21V12H21V4H18Z', 'drag': 'M4 4V22H20V24H4C2.9 24 2 23.1 2 22V4H4M15 7H20.5L15 1.5V7M8 0H16L22 6V18C22 19.11 21.11 20 20 20H8C6.89 20 6 19.1 6 18V2C6 .89 6.89 0 8 0M17 16V14H8V16H17M20 12V10H8V12H20Z', From 67ef951e2e40ae3d35551b44ea845d7df885e37c Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 18 Jul 2024 11:49:39 +0200 Subject: [PATCH 05/12] remove accidentally checked in backup file --- script/tree_old.js | 260 --------------------------------------------- 1 file changed, 260 deletions(-) delete mode 100644 script/tree_old.js diff --git a/script/tree_old.js b/script/tree_old.js deleted file mode 100644 index 6dcd650..0000000 --- a/script/tree_old.js +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Script for the tree management interface - */ - -var $GUI = jQuery('#plugin_move__tree'); - -$GUI.show(); -jQuery('#plugin_move__treelink').show(); - -/** - * Checks if the given list item was moved in the tree - * - * Moved elements are highlighted and a title shows where they came from - * - * @param {jQuery} $li - */ -var checkForMovement = function ($li) { - // we need to check this LI and all previously moved sub LIs - var $all = $li.add($li.find('li.moved')); - $all.each(function () { - var $this = jQuery(this); - var oldid = $this.data('id'); - var newid = determineNewID($this); - - if (newid != oldid && !$this.hasClass('created')) { - $this.addClass('moved'); - $this.children('div').attr('title', oldid + ' -> ' + newid); - } else { - $this.removeClass('moved'); - $this.children('div').attr('title', ''); - } - }); -}; - -/** - * Check if the given name is allowed in the given parent - * - * @param {jQuery} $li the edited or moved LI - * @param {jQuery} $parent the (new) parent of the edited or moved LI - * @param {string} name the (new) name to check - * @returns {boolean} - */ -var checkNameAllowed = function ($li, $parent, name) { - var ok = true; - $parent.children('li').each(function () { - if (this === $li[0]) return; - var cname = 'type-f'; - if ($li.hasClass('type-d')) cname = 'type-d'; - - var $this = jQuery(this); - if ($this.data('name') == name && $this.hasClass(cname)) ok = false; - }); - return ok; -}; - -/** - * Returns the new ID of a given list item - * - * @param {jQuery} $li - * @returns {string} - */ -var determineNewID = function ($li) { - var myname = $li.data('name'); - - var $parent = $li.parent().closest('li'); - if ($parent.length) { - return (determineNewID($parent) + ':' + myname).replace(/^:/, ''); - } else { - return myname; - } -}; - -/** - * Very simplistic cleanID() in JavaScript - * - * Strips out namespaces - * - * @param {string} id - */ -var cleanID = function (id) { - if (!id) return ''; - - id = id.replace(/[!"#$%§&\'()+,/;<=>?@\[\]^`\{|\}~\\;:\/\*]+/g, '_'); - id = id.replace(/^_+/, ''); - id = id.replace(/_+$/, ''); - id = id.toLowerCase(); - - return id; -}; - -/** - * Initialize the drag & drop-tree at the given li (must be this). - */ -var initTree = function () { - var $li = jQuery(this); - var my_root = $li.closest('.tree_root')[0]; - $li.draggable({ - revert: true, - revertDuration: 0, - opacity: 0.5, - stop : function(event, ui) { - ui.helper.css({height: "auto", width: "auto"}); - } - }).droppable({ - tolerance: 'pointer', - greedy: true, - accept : function(draggable) { - return my_root == draggable.closest('.tree_root')[0]; - }, - drop : function (event, ui) { - var $dropped = ui.draggable; - var $me = jQuery(this); - - if ($dropped.children('div.li').children('input').prop('checked')) { - $dropped = $dropped.add( - jQuery(my_root) - .find('input') - .filter(function() { - return jQuery(this).prop('checked'); - }).parent().parent() - ); - } - - if ($me.parents().addBack().is($dropped)) { - return; - } - - var insert_child = !($me.hasClass("type-f") || $me.hasClass("closed")); - var $new_parent = insert_child ? $me.children('ul') : $me.parent(); - var allowed = true; - - $dropped.each(function () { - var $this = jQuery(this); - allowed &= checkNameAllowed($this, $new_parent, $this.data('name')); - }); - - if (allowed) { - if (insert_child) { - $dropped.prependTo($new_parent); - } else { - $dropped.insertAfter($me); - } - } - - checkForMovement($dropped); - } - }) - // add title to rename icon - .find('img.rename').attr('title', LANG.plugins.move.renameitem) - .end() - .find('img.add').attr('title', LANG.plugins.move.add); -}; - -var add_template = '
            • '; - -/** - * Attach event listeners to the tree - */ -$GUI.find('div.tree_root > ul.tree_list') - .click(function (e) { - var $clicky = jQuery(e.target); - var $li = $clicky.parent().parent(); - - if ($clicky[0].tagName == 'A' && $li.hasClass('type-d')) { // Click on folder - open and close via AJAX - e.stopPropagation(); - if ($li.hasClass('open')) { - $li - .removeClass('open') - .addClass('closed'); - - } else { - $li - .removeClass('closed') - .addClass('open'); - - // if had not been loaded before, load via AJAX - if (!$li.find('ul').length) { - var is_media = $li.closest('div.tree_root').hasClass('tree_media') ? 1 : 0; - jQuery.post( - DOKU_BASE + 'lib/exe/ajax.php', - { - call: 'plugin_move_tree', - ns: $clicky.attr('href'), - is_media: is_media - }, - function (data) { - $li.append(data); - $li.find('li').each(initTree); - } - ); - } - } - e.preventDefault(); - } else if ($clicky[0].tagName == 'IMG') { // Click on IMG - do rename - e.stopPropagation(); - var $a = $clicky.parent().find('a'); - - if ($clicky.hasClass('rename')) { - var newname = window.prompt(LANG.plugins.move.renameitem, $li.data('name')); - newname = cleanID(newname); - if (newname) { - if (checkNameAllowed($li, $li.parent(), newname)) { - $li.data('name', newname); - $a.text(newname); - checkForMovement($li); - } else { - alert(LANG.plugins.move.duplicate.replace('%s', newname)); - } - } - } else { - var newname = window.prompt(LANG.plugins.move.add); - newname = cleanID(newname); - if (newname) { - if (checkNameAllowed($li, $li.children('ul'), newname)) { - var $new_li = jQuery(add_template.replace(/%s/g, newname)); - $li.children('ul').prepend($new_li); - - $new_li.each(initTree); - } else { - alert(LANG.plugins.move.duplicate.replace('%s', newname)); - } - } - } - e.preventDefault(); - } - }).find('li').each(initTree); - -/** - * Gather all moves from the trees and put them as JSON into the form before submit - * - * @fixme has some duplicate code - */ -jQuery('#plugin_move__tree_execute').submit(function (e) { - var data = []; - - $GUI.find('.tree_pages .moved').each(function (idx, el) { - var $el = jQuery(el); - var newid = determineNewID($el); - - data[data.length] = { - 'class': $el.hasClass('type-d') ? 'ns' : 'doc', - type: 'page', - src: $el.data('id'), - dst: newid - }; - }); - $GUI.find('.tree_media .moved').each(function (idx, el) { - var $el = jQuery(el); - var newid = determineNewID($el); - - data[data.length] = { - 'class': $el.hasClass('type-d') ? 'ns' : 'doc', - type: 'media', - src: $el.data('id'), - dst: newid - }; - }); - - jQuery(this).find('input[name=json]').val(JSON.stringify(data)); -}); From 574efcb303b70ddf322db9835ca7789850be3dc9 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 18 Jul 2024 12:14:36 +0200 Subject: [PATCH 06/12] drop obsolete code in tree component --- admin/tree.php | 135 ++----------------------------------------------- 1 file changed, 3 insertions(+), 132 deletions(-) diff --git a/admin/tree.php b/admin/tree.php index acf034a..035bc5d 100644 --- a/admin/tree.php +++ b/admin/tree.php @@ -2,9 +2,8 @@ class admin_plugin_move_tree extends DokuWiki_Admin_Plugin { - - const TYPE_PAGES = 1; - const TYPE_MEDIA = 2; + public const TYPE_PAGES = 1; + public const TYPE_MEDIA = 2; /** * @param $language @@ -15,13 +14,12 @@ public function getMenuText($language) return false; // do not show in Admin menu } - /** * If this admin plugin is for admins only * * @return bool false */ - function forAdminOnly() + public function forAdminOnly() { return false; } @@ -91,71 +89,6 @@ public function html() } } - public function htmlOld() - { - global $ID; - - echo $this->locale_xhtml('tree'); - echo ''; - - echo '
              '; - - echo '
              '; - echo '

              ' . $this->getLang('move_pages') . '

              '; - $this->htmlTree(self::TYPE_PAGES); - echo '
              '; - - echo '
              '; - echo '

              ' . $this->getLang('move_media') . '

              '; - $this->htmlTree(self::TYPE_MEDIA); - echo '
              '; - - /** @var helper_plugin_move_plan $plan */ - $plan = plugin_load('helper', 'move_plan'); - echo '
              '; - if ($plan->isCommited()) { - echo '
              ' . $this->getLang('moveinprogress') . '
              '; - } else { - $form = new Doku_Form(array('action' => wl($ID), 'id' => 'plugin_move__tree_execute')); - $form->addHidden('id', $ID); - $form->addHidden('page', 'move_main'); - $form->addHidden('json', ''); - $form->addElement(form_makeCheckboxField('autoskip', '1', $this->getLang('autoskip'), '', '', ($this->getConf('autoskip') ? array('checked' => 'checked') : array()))); - $form->addElement('
              '); - $form->addElement(form_makeCheckboxField('autorewrite', '1', $this->getLang('autorewrite'), '', '', ($this->getConf('autorewrite') ? array('checked' => 'checked') : array()))); - $form->addElement('
              '); - $form->addElement('
              '); - $form->addElement(form_makeButton('submit', 'admin', $this->getLang('btn_start'))); - $form->printForm(); - } - echo '
              '; - - echo '
              '; - } - - /** - * print the HTML tree structure - * - * @param int $type - */ - protected function htmlTree($type = self::TYPE_PAGES) - { - $data = $this->tree($type); - - // wrap a list with the root level around the other namespaces - array_unshift( - $data, array( - 'level' => 0, 'id' => '*', 'type' => 'd', - 'open' => 'true', 'label' => $this->getLang('root') - ) - ); - echo html_buildlist( - $data, 'tree_list idx', - array($this, 'html_list'), - array($this, 'html_li') - ); - } - /** * Build a tree info structure from media or page directories * @@ -190,66 +123,4 @@ public function tree($type = self::TYPE_PAGES, $open = '', $base = '') return $data; } - - /** - * Item formatter for the tree view - * - * User function for html_buildlist() - * - * @author Andreas Gohr - */ - function html_list($item) - { - $ret = ''; - // what to display - if (!empty($item['label'])) { - $base = $item['label']; - } else { - $base = ':' . $item['id']; - $base = substr($base, strrpos($base, ':') + 1); - } - - if ($item['id'] == '*') $item['id'] = ''; - - if ($item['id']) { - $ret .= ' '; - } - - // namespace or page? - if ($item['type'] == 'd') { - $ret .= ''; - $ret .= $base; - $ret .= ''; - } else { - $ret .= ''; - $ret .= noNS($item['id']); - $ret .= ''; - } - - if ($item['id']) $ret .= ''; - else $ret .= ''; - - return $ret; - } - - /** - * print the opening LI for a list item - * - * @param array $item - * @return string - */ - function html_li($item) - { - if ($item['id'] == '*') $item['id'] = ''; - - $params = array(); - $params['class'] = ' type-' . $item['type']; - if ($item['type'] == 'd') $params['class'] .= ' ' . ($item['open'] ? 'open' : 'closed'); - $params['data-name'] = noNS($item['id']); - $params['data-id'] = $item['id']; - $attr = buildAttributes($params); - - return "
            • "; - } - } From 9441c66441547f79d4efbd7b78eba05408135902 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 18 Jul 2024 13:38:02 +0200 Subject: [PATCH 07/12] open the tree at the current page's position --- script/tree.js | 64 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/script/tree.js b/script/tree.js index 039b876..2c8d6d7 100644 --- a/script/tree.js +++ b/script/tree.js @@ -36,8 +36,6 @@ class PluginMoveTree { this.#mediaTree = this.#mainElement.querySelector('.move-media'); this.#pageTree = this.#mainElement.querySelector('.move-pages'); - this.loadSubTree('', 'pages'); - this.loadSubTree('', 'media'); this.#dragIcon = this.icon('drag'); this.#dragIcon.classList.add('drag-icon'); @@ -50,10 +48,27 @@ class PluginMoveTree { this.#mainElement.addEventListener('dragend', this.dragEndHandler.bind(this)); this.#mainElement.querySelector('form').addEventListener('submit', this.submitHandler.bind(this)); + // load and open the initial tree + this.#init(); + // make tree visible this.#mainElement.style.display = 'block'; } + /** + * Initialize the tree + * + * @returns {Promise} + */ + async #init() { + await Promise.all([ + this.loadSubTree('', 'pages'), + this.loadSubTree('', 'media'), + ]); + + await this.openNamespace(JSINFO.namespace); + } + /** * Handle all item clicks * @@ -106,7 +121,7 @@ class PluginMoveTree { data.push(entry); // if this is a namspace that is shared between media and pages, add a second entry - if(entry.class === 'ns' && entry.type === 'media' && this.isItemPage(li)) { + if (entry.class === 'ns' && entry.type === 'media' && this.isItemPage(li)) { entry = {...entry}; // clone entry.type = 'page'; data.push(entry); @@ -177,13 +192,13 @@ class PluginMoveTree { } // same ID? we consider this an abort - if(newID === src.dataset.id) { + if (newID === src.dataset.id) { src.classList.remove('selected'); return; } // moving into self? ignore - if(dst.contains(src)) { + if (dst.contains(src)) { return; } @@ -210,6 +225,33 @@ class PluginMoveTree { } } + /** + * Open the given namespace and all its parents + * + * @param {string} namespace + * @returns {Promise} + */ + async openNamespace(namespace) { + const namespaces = namespace.split(':'); + + for (let i = 0; i < namespaces.length; i++) { + const ns = namespaces.slice(0, i + 1).join(':'); + const li = this.#mainElement.querySelectorAll(`li[data-orig="${ns}"].move-ns`); + if (!li.length) return; + + // we might have multiple namespaces with the same ID (media and pages) + // we open both in parallel and wait for them + const promises = []; + for (const el of li) { + const ul = el.querySelector('ul'); + if (!ul) { + promises.push(this.toggleNamespace(el)); + } + } + await Promise.all(promises); + } + } + /** * Rename an item via a prompt dialog * @@ -253,8 +295,9 @@ class PluginMoveTree { * Open or close a namespace * * @param li + * @returns {Promise} */ - toggleNamespace(li) { + async toggleNamespace(li) { const isOpen = li.classList.toggle('open'); // swap icon @@ -277,12 +320,15 @@ class PluginMoveTree { ul.dataset.orig = li.dataset.orig; li.appendChild(ul); + const promises = []; + if (li.classList.contains('move-pages')) { - this.loadSubTree(li.dataset.orig, 'pages'); + promises.push(this.loadSubTree(li.dataset.orig, 'pages')); } if (li.classList.contains('move-media')) { - this.loadSubTree(li.dataset.orig, 'media'); + promises.push(this.loadSubTree(li.dataset.orig, 'media')); } + await Promise.all(promises); } else { const ul = li.querySelector('ul'); if (ul) { @@ -378,7 +424,7 @@ class PluginMoveTree { const ns = parent.dataset.id; // parent is the namespace for (const li of parent.children) { - if(!this.isItemNamespace(li)) continue; + if (!this.isItemNamespace(li)) continue; const newID = this.getNewId(li.dataset.id, ns); li.dataset.id = newID; From 86b5ad52ddf77d228c4f62209b9bbe972545620a Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 19 Aug 2024 11:44:42 +0200 Subject: [PATCH 08/12] fix move into self check We let the browser throw an error instead of trying to figure it out ourself. --- script/tree.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/script/tree.js b/script/tree.js index 2c8d6d7..f375ea3 100644 --- a/script/tree.js +++ b/script/tree.js @@ -81,6 +81,7 @@ class PluginMoveTree { // we want to handle clicks on these elements only const clicked = target.closest('i,button,span'); + if (!clicked) return; // icon click selects the item if (clicked.tagName.toLowerCase() === 'i') { @@ -186,6 +187,8 @@ class PluginMoveTree { const elements = this.#mainElement.querySelectorAll('.selected'); elements.forEach(src => { const newID = this.getNewId(src.dataset.id, dst.dataset.id); + console.log('move started', src.dataset.id + ' → ' + newID); + // ensure that item stays in its own tree, ignore cross-tree moves if (this.itemTree(src).contains(dst) === false) { return; @@ -197,17 +200,19 @@ class PluginMoveTree { return; } - // moving into self? ignore - if (dst.contains(src)) { + // check if item with same ID already exists FIXME this also needs to check the type! + if (this.itemTree(src).querySelector(`li[data-id="${newID}"]`)) { + alert(LANG.plugins.move.duplicate.replace('%s', newID)); return; } - // check if item with same ID already exists - if (this.itemTree(src).querySelector(`li[data-id="${newID}"]`)) { - alert(LANG.plugins.move.duplicate.replace('%s', newID)); + try { + dst.append(src); + } catch (e) { + console.log('move aborted', e.message); // moved into itself + src.classList.remove('selected'); return; } - dst.append(src); this.updateMovedItem(src, newID); }); this.updatePassiveSubNamespaces(dst); From df53a0beb324fccde74fb36b0117f73cd0bd69ee Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 19 Aug 2024 13:09:14 +0200 Subject: [PATCH 09/12] fix dragging to root, we need an element for that --- admin/tree.php | 26 +++++++++++++++++++++++--- images/folder-home.svg | 1 + script/tree.js | 29 +++++++++++++++++++++++------ style.less | 13 +++++++++---- 4 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 images/folder-home.svg diff --git a/admin/tree.php b/admin/tree.php index 035bc5d..bb79713 100644 --- a/admin/tree.php +++ b/admin/tree.php @@ -66,10 +66,10 @@ public function html() echo '
              '; echo '
              '; if ($dual) { - echo '
                '; - echo '
                  '; + $this->printTreeRoot('move-pages'); + $this->printTreeRoot('move-media'); } else { - echo '
                    '; + $this->printTreeRoot('move-pages move-media'); } echo '
                    '; @@ -89,6 +89,26 @@ public function html() } } + /** + * Print the root of the tree + * + * @param string $classes The classes to apply to the root + * @return void + */ + protected function printTreeRoot($classes) { + echo '
                      '; + echo '
                    • '; + echo '
                      '; + echo ''; + echo inlineSVG(DOKU_PLUGIN . 'move/images/folder-home.svg'); + echo ''; + echo ':'; + echo '
                      '; + echo '
                        '; + echo '
                      • '; + echo '
                      '; + } + /** * Build a tree info structure from media or page directories * diff --git a/images/folder-home.svg b/images/folder-home.svg new file mode 100644 index 0000000..e88d3e5 --- /dev/null +++ b/images/folder-home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/script/tree.js b/script/tree.js index f375ea3..2d2705a 100644 --- a/script/tree.js +++ b/script/tree.js @@ -83,6 +83,9 @@ class PluginMoveTree { const clicked = target.closest('i,button,span'); if (!clicked) return; + // ignore clicks on the root element + if(li.classList.contains('tree-root')) return; + // icon click selects the item if (clicked.tagName.toLowerCase() === 'i') { ev.stopPropagation(); @@ -161,14 +164,28 @@ class PluginMoveTree { * @param {DragEvent} ev */ dragOverHandler(ev) { + // remove any previous drop zone + if (this.#dragTarget) { + this.#dragTarget.classList.remove('drop-zone'); + } + if (!ev.target) return; // the element the mouse is over - const ul = ev.target.closest('ul'); + + const li = ev.target.closest('li'); + if (!li) return; + + let ul; // the UL we drop into + if (li.classList.contains('move-ns')) { + // drop on a namespace, use its UL + ul = li.querySelector('ul'); + } else { + // drop on a file or page, use parent UL + ul = ev.target.closest('ul'); + } if (!ul) return; + if(ul.classList.contains('open') === false) return; // only drop into open namespaces ev.preventDefault(); // allow drop - if (this.#dragTarget && this.#dragTarget !== ul) { - this.#dragTarget.classList.remove('drop-zone'); - } this.#dragTarget = ul; this.#dragTarget.classList.add('drop-zone'); } @@ -180,8 +197,8 @@ class PluginMoveTree { */ dragDropHandler(ev) { if (!ev.target) return; - const dst = ev.target.closest('ul'); - if (!dst) return; + + const dst = this.#dragTarget; // the UL we drop into // move all selected items to the drop target const elements = this.#mainElement.querySelectorAll('.selected'); diff --git a/style.less b/style.less index b495d70..a82a7ec 100644 --- a/style.less +++ b/style.less @@ -14,9 +14,8 @@ // basic list layout ul { list-style-type: none; - margin: 0.25em 0 0.25em 2em; - padding: 0; - border: 1px solid transparent; + margin: 0.25em/2 0 0.25em 2em; + padding: 0.25em/2; li { margin: 0; @@ -69,7 +68,7 @@ } .drop-zone { - border: 1px dashed @ini_border; + border-top: 3px dashed @ini_link; } .drag-icon { @@ -89,6 +88,12 @@ display: block; margin: 0.5em 0; } + + // root is not interactable + li.tree-root > div.li i, + li.tree-root > div.li span { + cursor: auto; + } } From 6deb5ecca02e4c4753fb451391845009b5694eef Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 19 Aug 2024 13:10:03 +0200 Subject: [PATCH 10/12] fix duplicate checking It's fine to have a page and a namespace named the same. Or a media file and a page named the same. --- script/tree.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/script/tree.js b/script/tree.js index 2d2705a..fa11027 100644 --- a/script/tree.js +++ b/script/tree.js @@ -217,9 +217,21 @@ class PluginMoveTree { return; } - // check if item with same ID already exists FIXME this also needs to check the type! - if (this.itemTree(src).querySelector(`li[data-id="${newID}"]`)) { + // check if item with same ID and type already exists + let dupSelector = `li[data-id="${newID}"]`; + if (this.isItemMedia(src)) { + dupSelector += '.move-media'; + } else { + dupSelector += '.move-pages'; + } + if (this.isItemNamespace(src)) { + dupSelector += '.move-ns'; + } else { + dupSelector += ':not(.move-ns)'; + } + if (this.itemTree(src).querySelector(dupSelector)) { alert(LANG.plugins.move.duplicate.replace('%s', newID)); + src.classList.remove('selected'); return; } From 56cdc558af267a129d7d09c1cfc7456f6ad9129e Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 12 Jun 2025 13:01:07 +0200 Subject: [PATCH 11/12] guard against renaming to empty or same ID --- action/rename.php | 5 +++++ script/rename.js | 1 + 2 files changed, 6 insertions(+) diff --git a/action/rename.php b/action/rename.php index 814767b..a2c16a0 100644 --- a/action/rename.php +++ b/action/rename.php @@ -119,6 +119,11 @@ public function handle_ajax(Doku_Event $event) { return; } + if(!$dst || $dst == $src) { + echo json_encode(['error' => $this->getLang('nodst')]); + return; + } + /** @var helper_plugin_move_plan $plan */ $plan = plugin_load('helper', 'move_plan'); if($plan->isCommited()) { diff --git a/script/rename.js b/script/rename.js index cf4d916..f5f4974 100644 --- a/script/rename.js +++ b/script/rename.js @@ -28,6 +28,7 @@ const renameFN = function () { const newid = $dialog.find('input[name=id]').val(); if (!newid) return false; + if (newid === JSINFO.id) return false; const doMedia = $dialog.find('input[name=media]').is(':checked'); From 14eb21e0326672dbd52594b8310dc3920bf78e7c Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 12 Jun 2025 13:32:45 +0200 Subject: [PATCH 12/12] fix issues with self-moves * avoid moves to the same destination in admin interface * avoid renames to self in tree manager * avoid moves where items have been moved back to the original destination in tree manager Note: all of this is mostly cosmetics. Self-moves are also detected during plan execution, trigger an error and can be skipped. --- admin/main.php | 13 +++++++++++-- script/tree.js | 11 ++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/admin/main.php b/admin/main.php index fbd0c63..eb7ae9f 100644 --- a/admin/main.php +++ b/admin/main.php @@ -105,16 +105,25 @@ protected function createPlanFromInput() { $this->plan->setOption('autorewrite', $INPUT->bool('autorewrite')); if($ID && $INPUT->has('dst')) { + // input came from form $dst = trim($INPUT->str('dst')); if($dst == '') { msg($this->getLang('nodst'), -1); return false; } - // input came from form if($INPUT->str('class') == 'namespace') { $src = getNS($ID); + } else { + $src = $ID; + } + if($dst == $src) { + msg(sprintf($this->getLang('notchanged'), $src), -1); + return false; + } + + if($INPUT->str('class') == 'namespace') { if($INPUT->str('type') == 'both') { $this->plan->addPageNamespaceMove($src, $dst); $this->plan->addMediaNamespaceMove($src, $dst); @@ -124,7 +133,7 @@ protected function createPlanFromInput() { $this->plan->addMediaNamespaceMove($src, $dst); } } else { - $this->plan->addPageMove($ID, $INPUT->str('dst')); + $this->plan->addPageMove($src, $INPUT->str('dst')); } $this->plan->commit(); return true; diff --git a/script/tree.js b/script/tree.js index fa11027..bd309e8 100644 --- a/script/tree.js +++ b/script/tree.js @@ -292,10 +292,11 @@ class PluginMoveTree { * @param li */ renameGui(li) { - const newname = window.prompt(LANG.plugins.move.renameitem, this.getBase(li.dataset.id)); + const basename = this.getBase(li.dataset.id); + const newname = window.prompt(LANG.plugins.move.renameitem, basename); const clean = this.cleanID(newname); - if (!clean) { + if (!clean || clean === basename || newname === basename ) { return; } @@ -492,7 +493,11 @@ class PluginMoveTree { updateMovedItem(li, newID) { const name = li.querySelector('span'); - if (li.dataset.id !== newID) { + if (li.dataset.orig === newID) { + // item was moved back to its original ID + li.classList.remove('changed'); + name.title = ''; + } else if (li.dataset.id !== newID) { li.dataset.id = newID; li.classList.add('changed'); name.textContent = this.getBase(newID);