diff --git a/action/rename.php b/action/rename.php index 861af955..a2c16a0d 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,64 @@ 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; + } + + 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()) { + 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/action/tree.php b/action/tree.php index 644c9789..ac5b0cf8 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/main.php b/admin/main.php index fbd0c633..eb7ae9f7 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/admin/tree.php b/admin/tree.php index a3482dc7..bb797133 100644 --- a/admin/tree.php +++ b/admin/tree.php @@ -1,189 +1,146 @@ 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 '
'; + $dual = $INPUT->bool('dual', $this->getConf('dual')); /** @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')); - $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 ''; + + echo '
'; + echo '
'; + if ($dual) { + $this->printTreeRoot('move-pages'); + $this->printTreeRoot('move-media'); + } else { + $this->printTreeRoot('move-pages move-media'); + } + 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 '
'; } - echo '
'; - - echo '
'; } /** - * print the HTML tree structure + * Print the root of the tree * - * @param int $type + * @param string $classes The classes to apply to the root + * @return void */ - 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') - ); + protected function printTreeRoot($classes) { + echo ''; } /** * 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); } 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 "
  • "; - } - } diff --git a/conf/default.php b/conf/default.php index cc1cbe7a..efeafc59 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 35e14763..3323029b 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/images/folder-home.svg b/images/folder-home.svg new file mode 100644 index 00000000..e88d3e52 --- /dev/null +++ b/images/folder-home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lang/cs/lang.php b/lang/cs/lang.php index 51df5b6e..70616bb1 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 73cf2939..f9dd1f46 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 2fb0ef3f..f04aa0ce 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 e6f64799..ccf42f01 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 739afc18..aae3c209 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/en/lang.php b/lang/en/lang.php index 48a81d62..c020a0ee 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.'; @@ -79,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 659f2e1a..2aea7ccb 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 621abf87..e704c4b1 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/lang/es/lang.php b/lang/es/lang.php index 0c09498f..67474067 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 8e9242a2..bfe3c1a9 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 ad9f222c..5be84cb3 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 55a9df30..40f1a533 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 8b3adf86..791dbd88 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 3396dcdf..00e2f800 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 8a11125e..4fef32e0 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 4fd72358..d645494c 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 f68c2dd4..5338887f 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 508ca584..8830d24a 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 281223ff..d599a279 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 ad97f750..9984b404 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 ce4a6603..58a02ec4 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 2752eb16..2502c28f 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'] = '[跟目录]'; diff --git a/script.js b/script.js index 9fba837b..9c4604a3 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/rename.js b/script/rename.js index 03a8e5f5..f5f49744 100644 --- a/script/rename.js +++ b/script/rename.js @@ -14,6 +14,9 @@ '' + + '' + '' + '' ); @@ -25,6 +28,9 @@ 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'); // remove buttons and show throbber $dialog.html( @@ -39,12 +45,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; } diff --git a/script/tree.js b/script/tree.js index 6dcd650b..bd309e88 100644 --- a/script/tree.js +++ b/script/tree.js @@ -1,260 +1,672 @@ /** - * 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': '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', + }; -$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.#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)); + + // 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 + * + * @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'); + 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(); + 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) { + // 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 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'); } - }).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 (!ul) return; + if(ul.classList.contains('open') === false) return; // only drop into open namespaces + ev.preventDefault(); // allow drop + + this.#dragTarget = ul; + this.#dragTarget.classList.add('drop-zone'); + } + + /** + * Handle the Drop operation + * + * @param {DragEvent} ev + */ + dragDropHandler(ev) { + if (!ev.target) return; + + const dst = this.#dragTarget; // the UL we drop into + + // 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); + 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; + } + + // same ID? we consider this an abort + if (newID === src.dataset.id) { + src.classList.remove('selected'); + return; + } + + // 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; } - if ($me.parents().addBack().is($dropped)) { + try { + dst.append(src); + } catch (e) { + console.log('move aborted', e.message); // moved into itself + src.classList.remove('selected'); return; } + this.updateMovedItem(src, newID); + }); + this.updatePassiveSubNamespaces(dst); + this.sortList(dst); + } + + /** + * Clean up after drag'n'drop operation + * + * @param {DragEvent} ev + */ + dragEndHandler(ev) { + if (this.#dragTarget) { + this.#dragTarget.classList.remove('drop-zone'); + } + } - var insert_child = !($me.hasClass("type-f") || $me.hasClass("closed")); - var $new_parent = insert_child ? $me.children('ul') : $me.parent(); - var allowed = true; + /** + * Open the given namespace and all its parents + * + * @param {string} namespace + * @returns {Promise} + */ + async openNamespace(namespace) { + const namespaces = namespace.split(':'); - $dropped.each(function () { - var $this = jQuery(this); - allowed &= checkNameAllowed($this, $new_parent, $this.data('name')); - }); + 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; - if (allowed) { - if (insert_child) { - $dropped.prependTo($new_parent); - } else { - $dropped.insertAfter($me); + // 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 + * + * @param li + */ + renameGui(li) { + const basename = this.getBase(li.dataset.id); + const newname = window.prompt(LANG.plugins.move.renameitem, basename); + const clean = this.cleanID(newname); - checkForMovement($dropped); + if (!clean || clean === basename || newname === basename ) { + 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 = '
  • '; + // 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; + } + } -/** - * 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'); + // 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; + } - } 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); - } - ); - } + // update the item + this.updateMovedItem(li, newID); + + // if this was a namespace, update sub namespaces + if (this.isItemNamespace(li)) { + this.updatePassiveSubNamespaces(li.querySelector('ul')); + } + } + + + /** + * Open or close a namespace + * + * @param li + * @returns {Promise} + */ + async 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); + + const promises = []; + + if (li.classList.contains('move-pages')) { + promises.push(this.loadSubTree(li.dataset.orig, 'pages')); + } + if (li.classList.contains('move-media')) { + promises.push(this.loadSubTree(li.dataset.orig, 'media')); + } + await Promise.all(promises); + } 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`); } - 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)); - } + // 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.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); + 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/style.less b/style.less index 5a7dfbb8..a82a7eca 100644 --- a/style.less +++ b/style.less @@ -2,60 +2,101 @@ * 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; + > div.trees { + display: flex; + gap: 1em; + margin: 1em 0; } - .controls { - clear: left; - display: block; - } - - 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/2 0 0.25em 2em; + padding: 0.25em/2; 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-top: 3px dashed @ini_link; + } + + .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; + } + + // root is not interactable + li.tree-root > div.li i, + li.tree-root > div.li span { + cursor: auto; + } } + /** * The progress page */ @@ -76,9 +117,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 +154,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 {