From d45c3ece70f24603ed89b40e6ee79d65f6da4527 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Sun, 5 Jul 2020 17:07:08 +0200 Subject: [PATCH 01/11] First solution: static tree --- config/auth_actions.php | 1 + docs/database/tables/tags_links.sql | 14 +++ src/Controller/TagsLinksController.php | 137 ++++++++++++++++++++++ src/Model/Table/TagsLinksTable.php | 93 +++++++++++++++ src/Template/Element/top_menu.ctp | 4 + src/Template/TagsLinks/manage.ctp | 133 ++++++++++++++++++++++ webroot/css/tags_links/manage.css | 28 +++++ webroot/js/tagslinks.js | 6 + webroot/js/tagslinksautocompletion.js | 150 +++++++++++++++++++++++++ 9 files changed, 566 insertions(+) create mode 100644 docs/database/tables/tags_links.sql create mode 100644 src/Controller/TagsLinksController.php create mode 100644 src/Model/Table/TagsLinksTable.php create mode 100644 src/Template/TagsLinks/manage.ctp create mode 100644 webroot/css/tags_links/manage.css create mode 100644 webroot/js/tagslinks.js create mode 100644 webroot/js/tagslinksautocompletion.js diff --git a/config/auth_actions.php b/config/auth_actions.php index 285039e0dc..6188e7a10d 100644 --- a/config/auth_actions.php +++ b/config/auth_actions.php @@ -105,6 +105,7 @@ ], 'sentences_lists' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'tags' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], + 'tags_links' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'transcriptions' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'user' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'users' => [ '*' => [ User::ROLE_ADMIN ] ], diff --git a/docs/database/tables/tags_links.sql b/docs/database/tables/tags_links.sql new file mode 100644 index 0000000000..51a3be5809 --- /dev/null +++ b/docs/database/tables/tags_links.sql @@ -0,0 +1,14 @@ +-- +-- Table structure for table `tags_links` +-- +-- This table stores the tags relations. +-- + +DROP TABLE IF EXISTS `tags_links`; +CREATE TABLE `tags_links` ( + `parent` int(11) NOT NULL, + `child` int(11) NOT NULL, + `user_id` int(11) DEFAULT NULL, + `added_time` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`parent`, `child`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/Controller/TagsLinksController.php b/src/Controller/TagsLinksController.php new file mode 100644 index 0000000000..63b05871c1 --- /dev/null +++ b/src/Controller/TagsLinksController.php @@ -0,0 +1,137 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace App\Controller; + +use App\Controller\AppController; +use App\Event\SuggestdListener; +use Cake\Event\Event; +use App\Model\CurrentUser; + + +class TagsLinksController extends AppController +{ + /** + * Controller name + * + * @var string + * @access public + */ + public $name = 'TagsLinks'; + public $components = ['CommonSentence', 'Flash']; + public $helpers = ['Pagination']; + + public function manage(){ + $all_tags = array(); + $links = array(); + $temp = array(); + $s = 0; + $this->loadModel('Tags'); + foreach ($this->Tags->find('all')->select(['id', 'name', 'nbrOfSentences']) as $key => $value){ + $all_tags[$value['id']] = array($value['name'], $value['nbrOfSentences']); + $links[$value['id']] = array(); + $temp[$value['id']] = array(); + } + foreach ($this->TagsLinks->find('all')->select(['parent', 'child']) as $key => $value){ + $s += 1; + array_push($links[$value['child']], $value['parent']); + array_push($temp[$value['parent']], $value['child']); + } + + $tree = array(); + foreach ($temp as $parent => $children){ + if (!count($children)) + $tree[$parent] = array(); + } + while ($s) { + foreach ($links as $child => $parents) { + if (count($parents) && array_key_exists($child, $tree)){ + // 1. copy + $subtree = $tree[$child]; + // 2. remove + unset($tree[$child]); + // 3. replace + foreach ($parents as $parent) { + if (!array_key_exists($parent, $tree)) + $tree[$parent] = []; + $tree[$parent][$child] = $subtree; + } + // 4. remove used links + $s -= count($parents); + $links[$child] = []; + } + } + } + + $this->set('all_tags', $all_tags); + $this->set('tree', $tree); + } + + /** + * Add a link + * + * @return void + */ + + public function add() + { + $parent = $this->request->data('parentTag'); + $child = $this->request->data('childTag'); + $userId = CurrentUser::get("id"); + $username = CurrentUser::get("username"); + $link = $this->TagsLinks->addLink($parent, $child, $userId); + return $this->redirect([ + 'controller' => 'tags_links', + 'action' => 'manage' + ]); + } + + /** + * Remove a link + * + * @return void + */ + + public function remove($parent, $child) + { + $link = $this->TagsLinks->removeLink($parent, $child); + return $this->redirect([ + 'controller' => 'tags_links', + 'action' => 'manage' + ]); + } + + public function autocomplete($search) + { + $this->helpers[] = 'Tags'; + + $this->loadModel('Tags'); + $query = $this->Tags->find(); + $query->select(['name', 'id', 'nbrOfSentences']); + if (!empty($search)) { + $pattern = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $search).'%'; + $query->where(['name LIKE :search'])->bind(':search', $pattern, 'string'); + } + $allTags = $query->order(['nbrOfSentences' => 'DESC'])->limit(10)->all(); + + $this->loadComponent('RequestHandler'); + $this->set('allTags', $allTags); + $this->set('_serialize', ['allTags']); + $this->RequestHandler->renderAs($this, 'json'); + } +} diff --git a/src/Model/Table/TagsLinksTable.php b/src/Model/Table/TagsLinksTable.php new file mode 100644 index 0000000000..317664cae0 --- /dev/null +++ b/src/Model/Table/TagsLinksTable.php @@ -0,0 +1,93 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace App\Model\Table; + +use App\Model\CurrentUser; +use Cake\Database\Schema\TableSchema; +use Cake\ORM\Table; +use Cake\Event\Event; + +class TagsLinksTable extends Table +{ + public function initialize(array $config) + { + // $this->hasMany('Tags'); + $this->belongsToMany('Tags'); + $this->belongsTo('Users'); + $this->addBehavior('Timestamp'); + } + + /** + * Add a link + * + * @param String $parent + * @param String $child + * @param int $userId + * + * @return bool + */ + public function addLink($parent, $child, $userId) + { + // parent and child must reference the Tag database + $id_parent = $this->Tags->getIdFromName($parent); + $id_child = $this->Tags->getIdFromName($child); + if ($id_parent == null || $id_child == null){ + return false; + } + // parent and child must be different + if ($id_parent == $id_child) + return false; + + // prevent cycles: there must not exist a path from $child to $parent + $candidates = array($id_child); + $cycle = false; + while (!$cycle && count($candidates)) { + $new_candidates = array(); + foreach ($this->find('all')->where(['parent IN' => $candidates])->select(['child']) as $key => $value){ + array_push($new_candidates, $value['child']); + } + $candidates = $new_candidates; + $cycle = in_array($id_parent, $candidates); + } + if ($cycle) + return false; + + $data = $this->newEntity([ + 'parent' => $id_parent, + 'child' => $id_child, + 'user_id' => $userId + ]); + $added = $this->save($data); + return $added; + } + + /** + * Remove a link + * + * @param int $parent + * @param int $child + */ + public function removeLink($parent, $child) + { + $this->deleteAll([ + 'parent' => $parent, + 'child' => $child + ]); + } +} diff --git a/src/Template/Element/top_menu.ctp b/src/Template/Element/top_menu.ctp index 207dc22aad..2d4d1c0c06 100644 --- a/src/Template/Element/top_menu.ctp +++ b/src/Template/Element/top_menu.ctp @@ -68,6 +68,10 @@ $menuElements = array( "controller" => "tags", "action" => "view_all" ), + __('Manage tags') => array( + "controller" => "tagsLinks", + "action" => "manage" + ), __('Browse audio') => array( "controller" => "audio", "action" => "index" diff --git a/src/Template/TagsLinks/manage.ctp b/src/Template/TagsLinks/manage.ctp new file mode 100644 index 0000000000..fdec3a3e02 --- /dev/null +++ b/src/Template/TagsLinks/manage.ctp @@ -0,0 +1,133 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * PHP version 5 + * + * @category PHP + * @package Tatoeba + * @author Allan SIMON + * @license Affero General Public License + * @link https://tatoeba.org + */ + +$this->set('title_for_layout', $this->Pages->formatTitle(__('Structure tags'))); +$this->Html->script('tagslinks.js', ['block' => 'scriptBottom']); +$this->Html->script('tagslinksautocompletion.js', ['block' => 'scriptBottom']); +echo $this->Html->css('tagslinks/manage.css'); + +function displayTree($tree, $tags, $before, $depth, $t) { + echo "
    "; + foreach ($tree as $parent => $children) { + echo "
  1. "; + echo "
    "; + echo "".$tags[$parent][0].""; + if (!count($children) && $depth > 0) + echo $t->Html->link( + 'X', + array( + "controller" => "tagsLinks", + "action" => "remove", + $before, + $parent + ), + array( + "class" => "removeTagFromSentenceButton", + "id" => 'deleteButton'.$before.$parent, + "title" => "remove", + "escape" => false + ), + null + ); + echo "
    "; + foreach ($children as $head => $sub){ + $subtree = []; + $subtree[$head] = $sub; + displayTree($subtree, $tags, $parent, $depth+1, $t); + } + echo "
  2. "; + } + echo "
"; +} + +?> + +
+
+ +
+

+
+
+ + Form->create('TagsLinks', [ + 'url' => array('action' => 'add'), + ]); + ?> + + + Form->input('parentTag'); + ?> +
+
+ + Form->input('childTag'); + ?> +
+
+ + + + + + Form->end(); + ?> +
+ +
+ +
+

+
+
+ +
+
+ +
+
+
+ + +
\ No newline at end of file diff --git a/webroot/css/tags_links/manage.css b/webroot/css/tags_links/manage.css new file mode 100644 index 0000000000..87698da1c1 --- /dev/null +++ b/webroot/css/tags_links/manage.css @@ -0,0 +1,28 @@ +.angular-ui-tree-dropzone,.angular-ui-tree-empty{border:1px dashed #bbb;min-height:100px;background-color:#e5e5e5;background-image:-webkit-linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff),-webkit-linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff);background-image:linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff),linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff);background-size:60px 60px;background-position:0 0,30px 30px}.angular-ui-tree-empty{pointer-events:none}.angular-ui-tree-nodes{position:relative;margin:0;padding:0;list-style:none}.angular-ui-tree-nodes .angular-ui-tree-nodes{padding-left:20px}.angular-ui-tree-node,.angular-ui-tree-placeholder{position:relative;margin:0;padding:0;min-height:20px;line-height:20px}.angular-ui-tree-hidden{display:none}.angular-ui-tree-placeholder{margin:10px;padding:0;min-height:30px}.angular-ui-tree-handle{cursor:move;text-decoration:none;font-weight:700;box-sizing:border-box;min-height:20px;line-height:20px}.angular-ui-tree-drag{position:absolute;pointer-events:none;z-index:999;opacity:.8}.angular-ui-tree-drag .tree-node-content{margin-top:0} + +.autocompletionDiv { + position: absolute; + width: 100%; +} + +.autocompletionDiv ul { + list-style: none; + border: 1px solid #CCC; + margin: 0; + padding: 0; + position: absolute; + top: 30px; + z-index: 999; + background: #FFF; +} + +.autocompletionDiv .selected { + background: #FFE684; + color: #FFF; +} + +.autocompletionDiv a { + display: block; + padding: 2px 2px 2px 10px; + text-indent: -10px; +} \ No newline at end of file diff --git a/webroot/js/tagslinks.js b/webroot/js/tagslinks.js new file mode 100644 index 0000000000..71aa61e5e0 --- /dev/null +++ b/webroot/js/tagslinks.js @@ -0,0 +1,6 @@ +/** + * @license Angular UI Tree v2.22.5 + * (c) 2010-2017. https://github.com/angular-ui-tree/angular-ui-tree + * License: MIT + */ +!function(){"use strict";angular.module("ui.tree",[]).constant("treeConfig",{treeClass:"angular-ui-tree",emptyTreeClass:"angular-ui-tree-empty",dropzoneClass:"angular-ui-tree-dropzone",hiddenClass:"angular-ui-tree-hidden",nodesClass:"angular-ui-tree-nodes",nodeClass:"angular-ui-tree-node",handleClass:"angular-ui-tree-handle",placeholderClass:"angular-ui-tree-placeholder",dragClass:"angular-ui-tree-drag",dragThreshold:3,defaultCollapsed:!1,appendChildOnHover:!0})}(),function(){"use strict";angular.module("ui.tree").controller("TreeHandleController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$nodeScope=null,e.$type="uiTreeHandle"}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeNodeController",["$scope","$element",function(e,n){function t(e){if(!e)return 0;var n,o,l,r=0,a=e.childNodes();if(!a||0===a.length)return 0;for(l=a.length-1;l>=0;l--)n=a[l],o=1+t(n),r=Math.max(r,o);return r}this.scope=e,e.$element=n,e.$modelValue=null,e.$parentNodeScope=null,e.$childNodesScope=null,e.$parentNodesScope=null,e.$treeScope=null,e.$handleScope=null,e.$type="uiTreeNode",e.$$allowNodeDrop=!1,e.collapsed=!1,e.expandOnHover=!1,e.init=function(t){var o=t[0];e.$treeScope=t[1]?t[1].scope:null,e.$parentNodeScope=o.scope.$nodeScope,e.$modelValue=o.scope.$modelValue[e.$index],e.$parentNodesScope=o.scope,o.scope.initSubNode(e),n.on("$destroy",function(){o.scope.destroySubNode(e)})},e.index=function(){return e.$parentNodesScope.$modelValue.indexOf(e.$modelValue)},e.dragEnabled=function(){return!(e.$treeScope&&!e.$treeScope.dragEnabled)},e.isSibling=function(n){return e.$parentNodesScope==n.$parentNodesScope},e.isChild=function(n){var t=e.childNodes();return t&&t.indexOf(n)>-1},e.prev=function(){var n=e.index();return n>0?e.siblings()[n-1]:null},e.siblings=function(){return e.$parentNodesScope.childNodes()},e.childNodesCount=function(){return e.childNodes()?e.childNodes().length:0},e.hasChild=function(){return e.childNodesCount()>0},e.childNodes=function(){return e.$childNodesScope&&e.$childNodesScope.$modelValue?e.$childNodesScope.childNodes():null},e.accept=function(n,t){return e.$childNodesScope&&e.$childNodesScope.$modelValue&&e.$childNodesScope.accept(n,t)},e.remove=function(){return e.$parentNodesScope.removeNode(e)},e.toggle=function(){e.collapsed=!e.collapsed,e.$treeScope.$callbacks.toggle(e.collapsed,e)},e.collapse=function(){e.collapsed=!0},e.expand=function(){e.collapsed=!1},e.depth=function(){var n=e.$parentNodeScope;return n?n.depth()+1:1},e.maxSubDepth=function(){return e.$childNodesScope?t(e.$childNodesScope):0}}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeNodesController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$modelValue=null,e.$nodeScope=null,e.$treeScope=null,e.$type="uiTreeNodes",e.$nodesMap={},e.nodropEnabled=!1,e.maxDepth=0,e.cloneEnabled=!1,e.initSubNode=function(n){return n.$modelValue?void(e.$nodesMap[n.$modelValue.$$hashKey]=n):null},e.destroySubNode=function(n){return n.$modelValue?void(e.$nodesMap[n.$modelValue.$$hashKey]=null):null},e.accept=function(n,t){return e.$treeScope.$callbacks.accept(n,e,t)},e.beforeDrag=function(n){return e.$treeScope.$callbacks.beforeDrag(n)},e.isParent=function(n){return n.$parentNodesScope==e},e.hasChild=function(){return e.$modelValue.length>0},e.safeApply=function(e){var n=this.$root.$$phase;"$apply"==n||"$digest"==n?e&&"function"==typeof e&&e():this.$apply(e)},e.removeNode=function(n){var t=e.$modelValue.indexOf(n.$modelValue);return t>-1?(e.safeApply(function(){e.$modelValue.splice(t,1)[0]}),e.$treeScope.$callbacks.removed(n)):null},e.insertNode=function(n,t){e.safeApply(function(){e.$modelValue.splice(n,0,t)})},e.childNodes=function(){var n,t=[];if(e.$modelValue)for(n=0;n0&&e.depth()+n.maxSubDepth()+1>t}}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$nodesScope=null,e.$type="uiTree",e.$emptyElm=null,e.$dropzoneElm=null,e.$callbacks=null,e.dragEnabled=!0,e.emptyPlaceholderEnabled=!0,e.maxDepth=0,e.dragDelay=0,e.cloneEnabled=!1,e.nodropEnabled=!1,e.dropzoneEnabled=!1,e.isEmpty=function(){return e.$nodesScope&&e.$nodesScope.$modelValue&&0===e.$nodesScope.$modelValue.length},e.place=function(n){e.$nodesScope.$element.append(n),e.$emptyElm.remove()},this.resetEmptyElement=function(){e.$nodesScope.$modelValue&&0!==e.$nodesScope.$modelValue.length||!e.emptyPlaceholderEnabled?e.$emptyElm.remove():n.append(e.$emptyElm)},this.resetDropzoneElement=function(){e.$nodesScope.$modelValue&&0===e.$nodesScope.$modelValue.length||!e.dropzoneEnabled?e.$dropzoneElm.remove():n.append(e.$dropzoneElm)},e.resetEmptyElement=this.resetEmptyElement,e.resetDropzoneElement=this.resetDropzoneElement}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTree",["treeConfig","$window",function(e,n){return{restrict:"A",scope:!0,controller:"TreeController",link:function(t,o,l,r){var a,d,i,c={accept:null,beforeDrag:null},s={};angular.extend(s,e),s.treeClass&&o.addClass(s.treeClass),"table"===o.prop("tagName").toLowerCase()?(t.$emptyElm=angular.element(n.document.createElement("tr")),d=o.find("tr"),i=d.length>0?angular.element(d).children().length:1e6,a=angular.element(n.document.createElement("td")).attr("colspan",i),t.$emptyElm.append(a)):(t.$emptyElm=angular.element(n.document.createElement("div")),t.$dropzoneElm=angular.element(n.document.createElement("div"))),s.emptyTreeClass&&t.$emptyElm.addClass(s.emptyTreeClass),s.dropzoneClass&&t.$dropzoneElm.addClass(s.dropzoneClass),t.$watch("$nodesScope.$modelValue.length",function(e){angular.isNumber(e)&&(r.resetEmptyElement(),r.resetDropzoneElement())},!0),t.$watch(l.dragEnabled,function(e){"boolean"==typeof e&&(t.dragEnabled=e)}),t.$watch(l.emptyPlaceholderEnabled,function(e){"boolean"==typeof e&&(t.emptyPlaceholderEnabled=e,r.resetEmptyElement())}),t.$watch(l.nodropEnabled,function(e){"boolean"==typeof e&&(t.nodropEnabled=e)}),t.$watch(l.dropzoneEnabled,function(e){"boolean"==typeof e&&(t.dropzoneEnabled=e,r.resetDropzoneElement())}),t.$watch(l.cloneEnabled,function(e){"boolean"==typeof e&&(t.cloneEnabled=e)}),t.$watch(l.maxDepth,function(e){"number"==typeof e&&(t.maxDepth=e)}),t.$watch(l.dragDelay,function(e){"number"==typeof e&&(t.dragDelay=e)}),c.accept=function(e,n,t){return!(n.nodropEnabled||n.$treeScope.nodropEnabled||n.outOfDepth(e))},c.beforeDrag=function(e){return!0},c.expandTimeoutStart=function(){},c.expandTimeoutCancel=function(){},c.expandTimeoutEnd=function(){},c.removed=function(e){},c.dropped=function(e){},c.dragStart=function(e){},c.dragMove=function(e){},c.dragStop=function(e){},c.beforeDrop=function(e){},c.toggle=function(e,n){},t.$watch(l.uiTree,function(e,n){angular.forEach(e,function(e,n){c[n]&&"function"==typeof e&&(c[n]=e)}),t.$callbacks=c},!0)}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeHandle",["treeConfig",function(e){return{require:"^uiTreeNode",restrict:"A",scope:!0,controller:"TreeHandleController",link:function(n,t,o,l){var r={};angular.extend(r,e),r.handleClass&&t.addClass(r.handleClass),n!=l.scope&&(n.$nodeScope=l.scope,l.scope.$handleScope=n)}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeNode",["treeConfig","UiTreeHelper","$window","$document","$timeout","$q",function(e,n,t,o,l,r){return{require:["^uiTreeNodes","^uiTree"],restrict:"A",controller:"TreeNodeController",link:function(a,d,i,c){var s,u,p,m,f,h,$,g,b,v,N,S,y,E,x,C,T,w,D,H,O,Y,A,X,V,k,z,M={},I="ontouchstart"in window,P=null,L=document.body,W=document.documentElement;angular.extend(M,e),M.nodeClass&&d.addClass(M.nodeClass),a.init(c),a.collapsed=!!n.getNodeAttribute(a,"collapsed")||e.defaultCollapsed,a.expandOnHover=!!n.getNodeAttribute(a,"expandOnHover"),a.scrollContainer=n.getNodeAttribute(a,"scrollContainer")||i.scrollContainer||null,a.sourceOnly=a.nodropEnabled||a.$treeScope.nodropEnabled,a.$watch(i.collapsed,function(e){"boolean"==typeof e&&(a.collapsed=e)}),a.$watch("collapsed",function(e){n.setNodeAttribute(a,"collapsed",e),i.$set("collapsed",e)}),a.$watch(i.expandOnHover,function(e){"boolean"!=typeof e&&"number"!=typeof e||(a.expandOnHover=e)}),a.$watch("expandOnHover",function(e){n.setNodeAttribute(a,"expandOnHover",e),i.$set("expandOnHover",e)}),i.$observe("scrollContainer",function(e){"string"==typeof e&&(a.scrollContainer=e)}),a.$watch("scrollContainer",function(e){n.setNodeAttribute(a,"scrollContainer",e),i.$set("scrollContainer",e),$=document.querySelector(e)}),a.$on("angular-ui-tree:collapse-all",function(){a.collapsed=!0}),a.$on("angular-ui-tree:expand-all",function(){a.collapsed=!1}),S=function(e){if((I||2!==e.button&&3!==e.which)&&!(e.uiTreeDragging||e.originalEvent&&e.originalEvent.uiTreeDragging)){var l,r,i,c,$,g,S,y,E,x=angular.element(e.target);if(l=n.treeNodeHandlerContainerOfElement(x),l&&(x=angular.element(l)),r=d.clone(),y=n.elementIsTreeNode(x),E=n.elementIsTreeNodeHandle(x),(y||E)&&!(y&&n.elementContainsTreeNodeHandler(x)||(i=x.prop("tagName").toLowerCase(),"input"==i||"textarea"==i||"button"==i||"select"==i))){for(V=angular.element(e.target),k=V[0].attributes["ui-tree"];V&&V[0]&&V[0]!==d&&!k;){if(V[0].attributes&&(k=V[0].attributes["ui-tree"]),n.nodrag(V))return;V=V.parent()}a.beforeDrag(a)&&(e.uiTreeDragging=!0,e.originalEvent&&(e.originalEvent.uiTreeDragging=!0),e.preventDefault(),$=n.eventObj(e),s=!0,u=n.dragInfo(a),z=u.source.$treeScope.$id,c=d.prop("tagName"),"tr"===c.toLowerCase()?(m=angular.element(t.document.createElement(c)),g=angular.element(t.document.createElement("td")).addClass(M.placeholderClass).attr("colspan",d[0].children.length),m.append(g)):m=angular.element(t.document.createElement(c)).addClass(M.placeholderClass),f=angular.element(t.document.createElement(c)),M.hiddenClass&&f.addClass(M.hiddenClass),p=n.positionStarted($,d),m.css("height",d.prop("offsetHeight")+"px"),h=angular.element(t.document.createElement(a.$parentNodesScope.$element.prop("tagName"))).addClass(a.$parentNodesScope.$element.attr("class")).addClass(M.dragClass),h.css("width",n.width(d)+"px"),h.css("z-index",9999),S=(d[0].querySelector(".angular-ui-tree-handle")||d[0]).currentStyle,S&&(document.body.setAttribute("ui-tree-cursor",o.find("body").css("cursor")||""),o.find("body").css({cursor:S.cursor+"!important"})),a.sourceOnly&&m.css("display","none"),d.after(m),d.after(f),u.isClone()&&a.sourceOnly?h.append(r):h.append(d),o.find("body").append(h),h.css({left:$.pageX-p.offsetX+"px",top:$.pageY-p.offsetY+"px"}),b={placeholder:m,dragging:h},O(),a.$apply(function(){a.$treeScope.$callbacks.dragStart(u.eventArgs(b,p))}),v=Math.max(L.scrollHeight,L.offsetHeight,W.clientHeight,W.scrollHeight,W.offsetHeight),N=Math.max(L.scrollWidth,L.offsetWidth,W.clientWidth,W.scrollWidth,W.offsetWidth))}}},y=function(e){var o,r,d,i,c,f,S,y,E,x,C,T,w,D,H,O,Y,A,V,k,I,L,W,q,F=n.eventObj(e);if(h){if(e.preventDefault(),t.getSelection?t.getSelection().removeAllRanges():t.document.selection&&t.document.selection.empty(),d=F.pageX-p.offsetX,i=F.pageY-p.offsetY,d<0&&(d=0),i<0&&(i=0),i+10>v&&(i=v-10),d+10>N&&(d=N-10),h.css({left:d+"px",top:i+"px"}),$?(S=$.getBoundingClientRect(),c=$.scrollTop,f=c+$.clientHeight,S.bottomF.clientY&&c>0&&(O=Math.min(c,10),$.scrollTop-=O)):(c=window.pageYOffset||t.document.documentElement.scrollTop,f=c+(window.innerHeight||t.document.clientHeight||t.document.clientHeight),fF.pageY&&(O=Math.min(c,10),window.scrollBy(0,-O))),n.positionMoved(e,p,s),s)return void(s=!1);if(E=F.pageX-(t.pageXOffset||t.document.body.scrollLeft||t.document.documentElement.scrollLeft)-(t.document.documentElement.clientLeft||0),x=F.pageY-(t.pageYOffset||t.document.body.scrollTop||t.document.documentElement.scrollTop)-(t.document.documentElement.clientTop||0),angular.isFunction(h.hide)?h.hide():(C=h[0].style.display,h[0].style.display="none"),t.document.elementFromPoint(E,x),w=angular.element(t.document.elementFromPoint(E,x)),X=n.treeNodeHandlerContainerOfElement(w),X&&(w=angular.element(X)),angular.isFunction(h.show)?h.show():h[0].style.display=C,n.elementIsTree(w)?T=w.controller("uiTree").scope:n.elementIsTreeNodeHandle(w)?T=w.controller("uiTreeHandle").scope:n.elementIsTreeNode(w)?T=w.controller("uiTreeNode").scope:n.elementIsTreeNodes(w)?T=w.controller("uiTreeNodes").scope:n.elementIsPlaceholder(w)?T=w.controller("uiTreeNodes").scope:n.elementIsDropzone(w)?(T=w.controller("uiTree").scope,q=!0):w.controller("uiTreeNode")&&(T=w.controller("uiTreeNode").scope),V=T&&T.$treeScope&&T.$treeScope.$id&&T.$treeScope.$id===z,V&&p.dirAx)p.distX>0&&(o=u.prev(),o&&!o.collapsed&&o.accept(a,o.childNodesCount())&&(o.$childNodesScope.$element.append(m),u.moveTo(o.$childNodesScope,o.childNodes(),o.childNodesCount()))),p.distX<0&&(r=u.next(),r||(y=u.parentNode(),y&&y.$parentNodesScope.accept(a,y.index()+1)&&(y.$element.after(m),u.moveTo(y.$parentNodesScope,y.siblings(),y.index()+1))));else{if(D=!1,!T)return;if(!T.$treeScope||T.$parent.nodropEnabled||T.$treeScope.nodropEnabled||m.css("display",""),"uiTree"===T.$type&&T.dragEnabled&&(D=T.isEmpty()),"uiTreeHandle"===T.$type&&(T=T.$nodeScope),"uiTreeNode"!==T.$type&&!D&&!q)return void(M.appendChildOnHover&&(r=u.next(),!r&&g&&(y=u.parentNode(),y.$element.after(m),u.moveTo(y.$parentNodesScope,y.siblings(),y.index()+1),g=!1)));P&&m.parent()[0]!=P.$element[0]&&(P.resetEmptyElement(),P.resetDropzoneElement(),P=null),D?(P=T,T.$nodesScope.accept(a,0)&&u.moveTo(T.$nodesScope,T.$nodesScope.childNodes(),0)):q?(P=T,T.$nodesScope.accept(a,T.$nodesScope.childNodes().length)&&u.moveTo(T.$nodesScope,T.$nodesScope.childNodes(),T.$nodesScope.childNodes().length)):T.dragEnabled()&&(angular.isDefined(a.expandTimeoutOn)&&a.expandTimeoutOn!==T.id&&(l.cancel(a.expandTimeout),delete a.expandTimeout,delete a.expandTimeoutOn,a.$callbacks.expandTimeoutCancel()),T.collapsed&&(a.expandOnHover===!0||angular.isNumber(a.expandOnHover)&&0===a.expandOnHover?(T.collapsed=!1,T.$treeScope.$callbacks.toggle(!1,T)):a.expandOnHover!==!1&&angular.isNumber(a.expandOnHover)&&a.expandOnHover>0&&angular.isUndefined(a.expandTimeoutOn)&&(a.expandTimeoutOn=T.$id,a.$callbacks.expandTimeoutStart(),a.expandTimeout=l(function(){a.$callbacks.expandTimeoutEnd(),T.collapsed=!1,T.$treeScope.$callbacks.toggle(!1,T)},a.expandOnHover))),w=T.$element,Y=n.offset(w),I=n.height(w),L=T.$childNodesScope?T.$childNodesScope.$element:null,W=L?n.height(L):0,I-=W,k=M.appendChildOnHover?.25*I:n.height(w)/2,A=F.pageY0?D.exec(function(){x(e)},a.dragDelay):x(e)}),d.bind("touchend touchcancel mouseup",function(){a.dragDelay>0&&D.cancel()})},H(),O=function(){angular.element(o).bind("touchend",T),angular.element(o).bind("touchcancel",T),angular.element(o).bind("touchmove",C),angular.element(o).bind("mouseup",T),angular.element(o).bind("mousemove",C),angular.element(o).bind("mouseleave",w),angular.element(o).bind("keydown",A)},Y=function(){angular.element(o).unbind("touchend",T),angular.element(o).unbind("touchcancel",T),angular.element(o).unbind("touchmove",C),angular.element(o).unbind("mouseup",T),angular.element(o).unbind("mousemove",C),angular.element(o).unbind("mouseleave",w),angular.element(o).unbind("keydown",A)}}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeNodes",["treeConfig","$window",function(e){return{require:["ngModel","?^uiTreeNode","^uiTree"],restrict:"A",scope:!0,controller:"TreeNodesController",link:function(n,t,o,l){var r={},a=l[0],d=l[1],i=l[2];angular.extend(r,e),r.nodesClass&&t.addClass(r.nodesClass),d?(d.scope.$childNodesScope=n,n.$nodeScope=d.scope):i.scope.$nodesScope=n,n.$treeScope=i.scope,a&&(a.$render=function(){n.$modelValue=a.$modelValue}),n.$watch(function(){return o.maxDepth},function(e){"number"==typeof e&&(n.maxDepth=e)}),n.$watch(function(){return o.nodropEnabled},function(e){"undefined"!=typeof e&&(n.nodropEnabled=!0)},!0)}}}])}(),function(){"use strict";function e(e,n){if(void 0===n)return null;for(var t=n.parentNode,o=1,l="function"==typeof t.setAttribute&&t.hasAttribute(e)?t:null;t&&"function"==typeof t.setAttribute&&!t.hasAttribute(e);){if(t=t.parentNode,l=t,t===document.documentElement){l=null;break}o++}return l}angular.module("ui.tree").factory("UiTreeHelper",["$document","$window","treeConfig",function(n,t,o){return{nodesData:{},setNodeAttribute:function(e,n,t){if(!e.$modelValue)return null;var o=this.nodesData[e.$modelValue.$$hashKey];o||(o={},this.nodesData[e.$modelValue.$$hashKey]=o),o[n]=t},getNodeAttribute:function(e,n){if(!e.$modelValue)return null;var t=this.nodesData[e.$modelValue.$$hashKey];return t?t[n]:null},nodrag:function(e){return"undefined"!=typeof e.attr("data-nodrag")&&"false"!==e.attr("data-nodrag")},eventObj:function(e){var n=e;return void 0!==e.targetTouches?n=e.targetTouches.item(0):void 0!==e.originalEvent&&void 0!==e.originalEvent.targetTouches&&(n=e.originalEvent.targetTouches.item(0)),n},dragInfo:function(e){return{source:e,sourceInfo:{cloneModel:e.$treeScope.cloneEnabled===!0?angular.copy(e.$modelValue):void 0,nodeScope:e,index:e.index(),nodesScope:e.$parentNodesScope},index:e.index(),siblings:e.siblings().slice(0),parent:e.$parentNodesScope,resetParent:function(){this.parent=e.$parentNodesScope},moveTo:function(e,n,t){this.parent=e,this.siblings=n.slice(0);var o=this.siblings.indexOf(this.source);o>-1&&(this.siblings.splice(o,1),this.source.index()0?this.siblings[this.index-1]:null},next:function(){return this.index0&&(o=e.originalEvent.touches[0].pageX,l=e.originalEvent.touches[0].pageY),t.offsetX=o-this.offset(n).left,t.offsetY=l-this.offset(n).top,t.startX=t.lastX=o,t.startY=t.lastY=l,t.nowX=t.nowY=t.distX=t.distY=t.dirAx=0,t.dirX=t.dirY=t.lastDirX=t.lastDirY=t.distAxX=t.distAxY=0,t},positionMoved:function(e,n,t){var o,l=e.pageX,r=e.pageY;return e.originalEvent&&e.originalEvent.touches&&e.originalEvent.touches.length>0&&(l=e.originalEvent.touches[0].pageX,r=e.originalEvent.touches[0].pageY),n.lastX=n.nowX,n.lastY=n.nowY,n.nowX=l,n.nowY=r,n.distX=n.nowX-n.lastX,n.distY=n.nowY-n.lastY,n.lastDirX=n.dirX,n.lastDirY=n.dirY,n.dirX=0===n.distX?0:n.distX>0?1:-1,n.dirY=0===n.distY?0:n.distY>0?1:-1,o=Math.abs(n.distX)>Math.abs(n.distY)?1:0,t?(n.dirAx=o,void(n.moving=!0)):(n.dirAx!==o?(n.distAxX=0,n.distAxY=0):(n.distAxX+=Math.abs(n.distX),0!==n.dirX&&n.dirX!==n.lastDirX&&(n.distAxX=0),n.distAxY+=Math.abs(n.distY),0!==n.dirY&&n.dirY!==n.lastDirY&&(n.distAxY=0)),void(n.dirAx=o))},elementIsTreeNode:function(e){return"undefined"!=typeof e.attr("ui-tree-node")},elementIsTreeNodeHandle:function(e){return"undefined"!=typeof e.attr("ui-tree-handle")},elementIsTree:function(e){return"undefined"!=typeof e.attr("ui-tree")},elementIsTreeNodes:function(e){return"undefined"!=typeof e.attr("ui-tree-nodes")},elementIsPlaceholder:function(e){return e.hasClass(o.placeholderClass)},elementIsDropzone:function(e){return e.hasClass(o.dropzoneClass)},elementContainsTreeNodeHandler:function(e){return e[0].querySelectorAll("[ui-tree-handle]").length>=1},treeNodeHandlerContainerOfElement:function(n){return e("ui-tree-handle",n[0])}}}])}(); \ No newline at end of file diff --git a/webroot/js/tagslinksautocompletion.js b/webroot/js/tagslinksautocompletion.js new file mode 100644 index 0000000000..9d753723e5 --- /dev/null +++ b/webroot/js/tagslinksautocompletion.js @@ -0,0 +1,150 @@ +/** + * Tatoeba Project, free collaborative creation of multilingual corpuses project + * Copyright (C) 2010 Allan SIMON + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// we need to declare this variable and function out of the "documentready" +// block to make them somewhat global + +// The mechanism is more or less the following +// Each time you input new character it increase a counter and start a timer +// at the end of the timer, it decrease the counter by one and check if the +// counter is zero and will call the ajax action only if it's zero +// This way we're sure the ajax action will occur at the expiration of the +// last counter +// This both avoid two hackish solution +// 1 send after each input (send too many request) +// 2 send each X ms (send also too many request) + +var currentSuggestPosition = -1; +var countBeforeRequest = 0; +var suggestLength = 0; +var previousText = ''; +var isSuggestListActive = false; + +function removeSuggestList(divTag) { + isSuggestListActive = false; + currentSuggestPosition = -1; + $(divTag).empty(); +} + +function changeActiveSuggestion(inputTag, offset) { + $("#suggestItem" + currentSuggestPosition % suggestLength).removeClass("selected"); + currentSuggestPosition += offset; + if (currentSuggestPosition < 0) { + currentSuggestPosition = suggestLength - 1; + } + var selectedItem = $("#suggestItem" + currentSuggestPosition % suggestLength); + selectedItem.addClass("selected"); + suggestSelect(inputTag, selectedItem[0].innerHTML); +} + +function suggestSelect(inputTag, suggestionStr) { + $(inputTag).val(suggestionStr); + $(inputTag).focus(); + return false; +} + +function sendToAutocomplete(inputTag, divTag) { + countBeforeRequest--; + if (countBeforeRequest > 0) { + return; + } + countBeforeRequest = 0; + + var tag = $(inputTag).val(); + + if (tag == '') { + $(divTag).empty(); + previousText = tag; + return; + } + var rootUrl = get_tatoeba_root_url(); + if (tag != previousText) { + $.get( + rootUrl + "/tags_links/autocomplete/" + tag, + function(data) { + suggestShowResults(inputTag, divTag, data); + } + ); + previousText = tag; + } +}; + +function suggestShowResults(inputTag, divTag, suggestions) { + removeSuggestList(divTag); + if (suggestions.allTags.length == 0) { + return; + } + + isSuggestListActive = true; + + var ul = document.createElement("ul"); + $(divTag).append(ul); + suggestions.allTags.forEach(function(suggestion, index) { + var text = document.createTextNode(suggestion.name + " (" + suggestion.nbrOfSentences + ")"); + + var link = document.createElement("a"); + link.id = "suggestedItem" + index; + link.dataset.tagName = suggestion.name; + link.onclick = "suggestSelect(this.dataset.tagName)"; + link.setAttribute("onclick", "suggestSelect(this.dataset.tagName)"); + link.style = "color:black"; + link.appendChild(text); + + var li = document.createElement("li"); + li.appendChild(link); + + ul.appendChild(li); + }); +} + +function bindAutocomplete(inputId, divId) { + var inputTag = "#" + inputId; + var divTag = "#" + divId; + $(inputTag).attr("autocomplete", "off"); + $(inputTag).blur(function() { + setTimeout(function() { + removeSuggestList(divTag) + }, 300); + }); + + $(inputTag).keyup(function(e){ + switch(e.keyCode) { + case 38: //up + changeActiveSuggestion(inputTag, -1); + break; + case 40://down + changeActiveSuggestion(inputTag, 1); + break; + case 27: //escape + removeSuggestList(divTag); + break; + default: + var tag = $(this).val(); + countBeforeRequest++; + setTimeout(function(){ + sendToAutocomplete(inputTag, divTag) + }, 200); + break; + } + }); +} + +$(document).ready(function() { + bindAutocomplete("parenttag", "autocompletionParent"); + bindAutocomplete("childtag", "autocompletionChild"); +}); \ No newline at end of file From 64b95b9d274e0f02b367efcd31c6fac31c0b5a50 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Mon, 6 Jul 2020 15:49:58 +0200 Subject: [PATCH 02/11] Made autocompletion an independent module ; improve the tags tree layout --- src/Controller/TagsController.php | 10 + src/Controller/TagsLinksController.php | 19 -- .../Behavior/AutocompletableBehavior.php | 70 +++++ src/Model/Table/TagsTable.php | 1 + src/Template/Element/tagslink.ctp | 61 +++++ src/Template/TagsLinks/manage.ctp | 83 ++---- src/View/Helper/TagsHelper.php | 2 +- webroot/css/autocompletion.css | 26 ++ webroot/css/tags_links/manage.css | 28 -- webroot/js/autocompletion.js | 251 ++++++++---------- webroot/js/tags.add.js | 2 + webroot/js/tagslinks.js | 29 +- webroot/js/tagslinksautocompletion.js | 150 ----------- 13 files changed, 321 insertions(+), 411 deletions(-) create mode 100644 src/Model/Behavior/AutocompletableBehavior.php create mode 100644 src/Template/Element/tagslink.ctp create mode 100644 webroot/css/autocompletion.css delete mode 100644 webroot/css/tags_links/manage.css delete mode 100644 webroot/js/tagslinksautocompletion.js diff --git a/src/Controller/TagsController.php b/src/Controller/TagsController.php index d0c7c1bff9..70f8f7d9be 100644 --- a/src/Controller/TagsController.php +++ b/src/Controller/TagsController.php @@ -246,4 +246,14 @@ public function search() $search ]); } + + public function autocomplete($search) + { + $allTags = $this->Tags->Autocomplete($search); + + $this->loadComponent('RequestHandler'); + $this->set('allTags', $allTags); + $this->set('_serialize', ['allTags']); + $this->RequestHandler->renderAs($this, 'json'); + } } diff --git a/src/Controller/TagsLinksController.php b/src/Controller/TagsLinksController.php index 63b05871c1..89aae3fb8f 100644 --- a/src/Controller/TagsLinksController.php +++ b/src/Controller/TagsLinksController.php @@ -115,23 +115,4 @@ public function remove($parent, $child) 'action' => 'manage' ]); } - - public function autocomplete($search) - { - $this->helpers[] = 'Tags'; - - $this->loadModel('Tags'); - $query = $this->Tags->find(); - $query->select(['name', 'id', 'nbrOfSentences']); - if (!empty($search)) { - $pattern = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $search).'%'; - $query->where(['name LIKE :search'])->bind(':search', $pattern, 'string'); - } - $allTags = $query->order(['nbrOfSentences' => 'DESC'])->limit(10)->all(); - - $this->loadComponent('RequestHandler'); - $this->set('allTags', $allTags); - $this->set('_serialize', ['allTags']); - $this->RequestHandler->renderAs($this, 'json'); - } } diff --git a/src/Model/Behavior/AutocompletableBehavior.php b/src/Model/Behavior/AutocompletableBehavior.php new file mode 100644 index 0000000000..70661b478f --- /dev/null +++ b/src/Model/Behavior/AutocompletableBehavior.php @@ -0,0 +1,70 @@ +. + */ +namespace App\Model\Behavior; + +use Cake\ORM\Behavior; +use Cake\ORM\Query; + +/** + * Behavior for handling autocompletion + * + * This behavior adds the method 'Autocomplete' to a table which returns + * the top n results that contain a given query + */ +class AutocompletableBehavior extends Behavior +{ + /** + * Default config + * + * index column to search from + * fields columns to return + * + * @var array + */ + protected $_defaultConfig = [ + 'implementedMethods' => + ['Autocomplete' => 'Autocomplete'], + 'index' => ['name' => 'name', 'type' => 'string'], + 'fields' => ['name', 'id', 'nbrOfSentences'], + 'order' => ['nbrOfSentences' => 'DESC'], + 'limit' => 10 + ]; + + public function Autocomplete($search) + { + $query = $this->getTable()->find(); + $query->select($this->config('fields')); + + if (!empty($search)) { + $pattern = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $search).'%'; + $query->where([$this->config('index')['name'].' LIKE :search']) + ->bind(':search', $pattern, $this->config('index')['type']); + } + + if (count($this->config('order'))) { + $query->order($this->config('order')); + } + + if ($this->config('limit') > 0) { + $query->limit($this->config('limit')); + } + + return $query->all(); + } +} diff --git a/src/Model/Table/TagsTable.php b/src/Model/Table/TagsTable.php index 0799b528d6..549b60235a 100644 --- a/src/Model/Table/TagsTable.php +++ b/src/Model/Table/TagsTable.php @@ -53,6 +53,7 @@ public function initialize(array $config) $this->belongsTo('Users'); $this->addBehavior('Timestamp'); + $this->addBehavior('Autocompletable'); } /** * Cakephp callback before each saving operation diff --git a/src/Template/Element/tagslink.ctp b/src/Template/Element/tagslink.ctp new file mode 100644 index 0000000000..2210858403 --- /dev/null +++ b/src/Template/Element/tagslink.ctp @@ -0,0 +1,61 @@ +Url->build([ + 'controller' => 'tagsLinks', + 'action' => 'remove', + $root, + $parent +]); + +$cssClass = $depth == 0 ? 'wall-thread' : 'reply' +?> + + + + + + + + 0 && count($children) == 0) { ?> + + delete + + + 0) { ?> + + {{vm.hiddenReplies[] ? 'expand_more' : 'expand_less'}} + + + + + + + + + + 0) { ?> +
+ $new_children) { + echo $this->element('tagslink', [ + 'root' => $parent, + 'parent' => $new_parent, + 'children' => $new_children, + 'labels' => $labels, + 'depth' => $depth + 1 + ]); + } + ?> +
+ +
+
\ No newline at end of file diff --git a/src/Template/TagsLinks/manage.ctp b/src/Template/TagsLinks/manage.ctp index fdec3a3e02..5648836205 100644 --- a/src/Template/TagsLinks/manage.ctp +++ b/src/Template/TagsLinks/manage.ctp @@ -26,43 +26,9 @@ */ $this->set('title_for_layout', $this->Pages->formatTitle(__('Structure tags'))); +echo $this->Html->css('autocompletion.css'); +$this->Html->script('autocompletion.js', ['block' => 'scriptBottom']); $this->Html->script('tagslinks.js', ['block' => 'scriptBottom']); -$this->Html->script('tagslinksautocompletion.js', ['block' => 'scriptBottom']); -echo $this->Html->css('tagslinks/manage.css'); - -function displayTree($tree, $tags, $before, $depth, $t) { - echo "
    "; - foreach ($tree as $parent => $children) { - echo "
  1. "; - echo "
    "; - echo "".$tags[$parent][0].""; - if (!count($children) && $depth > 0) - echo $t->Html->link( - 'X', - array( - "controller" => "tagsLinks", - "action" => "remove", - $before, - $parent - ), - array( - "class" => "removeTagFromSentenceButton", - "id" => 'deleteButton'.$before.$parent, - "title" => "remove", - "escape" => false - ), - null - ); - echo "
    "; - foreach ($children as $head => $sub){ - $subtree = []; - $subtree[$head] = $sub; - displayTree($subtree, $tags, $parent, $depth+1, $t); - } - echo "
  2. "; - } - echo "
"; -} ?> @@ -84,13 +50,13 @@ function displayTree($tree, $tags, $before, $depth, $t) { Form->input('parentTag'); ?> -
+
Form->input('childTag'); ?> -
+
@@ -102,32 +68,17 @@ function displayTree($tree, $tags, $before, $depth, $t) { ?> -
- -
-

-
-
- -
-
- -
-
-
- - +
+ $children) { + echo $this->element('tagslink', [ + 'root' => 0, + 'parent' => $parent, + 'children' => $children, + 'labels' => $all_tags, + 'depth' => 0 + ]); + } + ?> +
\ No newline at end of file diff --git a/src/View/Helper/TagsHelper.php b/src/View/Helper/TagsHelper.php index 35d55dad9e..a4b093e953 100644 --- a/src/View/Helper/TagsHelper.php +++ b/src/View/Helper/TagsHelper.php @@ -185,8 +185,8 @@ public function displayTagInCloud($tagName, $tagId, $count) { public function displayAddTagForm($sentenceId = null) { - $this->Html->script('tags.add.js', ['block' => 'scriptBottom']); $this->Html->script('autocompletion.js', ['block' => 'scriptBottom']); + $this->Html->script('tags.add.js', ['block' => 'scriptBottom']); echo $this->Form->create('Tag', [ 'id' => 'tag-form', diff --git a/webroot/css/autocompletion.css b/webroot/css/autocompletion.css new file mode 100644 index 0000000000..756a983dfd --- /dev/null +++ b/webroot/css/autocompletion.css @@ -0,0 +1,26 @@ +.autocompletionDiv { + position: absolute; + width: 100%; +} + +.autocompletionDiv ul { + list-style: none; + border: 1px solid #CCC; + margin: 0; + padding: 0; + position: absolute; + top: 30px; + z-index: 999; + background: #FFF; +} + +.autocompletionDiv .selected { + background: #FFE684; + color: #FFF; +} + +.autocompletionDiv a { + display: block; + padding: 2px 2px 2px 10px; + text-indent: -10px; +} diff --git a/webroot/css/tags_links/manage.css b/webroot/css/tags_links/manage.css deleted file mode 100644 index 87698da1c1..0000000000 --- a/webroot/css/tags_links/manage.css +++ /dev/null @@ -1,28 +0,0 @@ -.angular-ui-tree-dropzone,.angular-ui-tree-empty{border:1px dashed #bbb;min-height:100px;background-color:#e5e5e5;background-image:-webkit-linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff),-webkit-linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff);background-image:linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff),linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff);background-size:60px 60px;background-position:0 0,30px 30px}.angular-ui-tree-empty{pointer-events:none}.angular-ui-tree-nodes{position:relative;margin:0;padding:0;list-style:none}.angular-ui-tree-nodes .angular-ui-tree-nodes{padding-left:20px}.angular-ui-tree-node,.angular-ui-tree-placeholder{position:relative;margin:0;padding:0;min-height:20px;line-height:20px}.angular-ui-tree-hidden{display:none}.angular-ui-tree-placeholder{margin:10px;padding:0;min-height:30px}.angular-ui-tree-handle{cursor:move;text-decoration:none;font-weight:700;box-sizing:border-box;min-height:20px;line-height:20px}.angular-ui-tree-drag{position:absolute;pointer-events:none;z-index:999;opacity:.8}.angular-ui-tree-drag .tree-node-content{margin-top:0} - -.autocompletionDiv { - position: absolute; - width: 100%; -} - -.autocompletionDiv ul { - list-style: none; - border: 1px solid #CCC; - margin: 0; - padding: 0; - position: absolute; - top: 30px; - z-index: 999; - background: #FFF; -} - -.autocompletionDiv .selected { - background: #FFE684; - color: #FFF; -} - -.autocompletionDiv a { - display: block; - padding: 2px 2px 2px 10px; - text-indent: -10px; -} \ No newline at end of file diff --git a/webroot/js/autocompletion.js b/webroot/js/autocompletion.js index 39ec63c008..3ccb26d155 100644 --- a/webroot/js/autocompletion.js +++ b/webroot/js/autocompletion.js @@ -16,157 +16,126 @@ * along with this program. If not, see . */ -// we need to declare this variable and function out of the "documentready" -// block to make them somewhat global - -// The mechanism is more or less the following -// Each time you input new character it increase a counter and start a timer -// at the end of the timer, it decrease the counter by one and check if the -// counter is zero and will call the ajax action only if it's zero -// This way we're sure the ajax action will occur at the expiration of the -// last counter -// This both avoid two hackish solution -// 1 send after each input (send too many request) -// 2 send each X ms (send also too many request) - -var currentSuggestPosition = -1; -var countBeforeRequest = 0; -var suggestLength = 0; -var previousText = ''; -var isSuggestListActive = false; - -function sendToAutocomplete() { - countBeforeRequest--; - if (countBeforeRequest > 0) { - return; - } - - var tag = $("#TagTagName").val(); - - if (tag == '') { - $("#autocompletionDiv").empty(); - previousText = tag; - return; - } - var rootUrl = get_tatoeba_root_url(); - if ( tag != previousText) { - $.get( - rootUrl + "/autocompletions/request/" + tag, - function(data) { - suggestShowResults(data); +class AutocompleteBox { + constructor(inputId, divId) { + this.currentSuggestPosition = -1; + this.countBeforeRequest = 0; + this. suggestLength = 0; + this.previousText = ''; + this.isSuggestListActive = false; + + this.inputTag = "#" + inputId; + this.divTag = "#" + divId; + + $(this.inputTag).attr("autocomplete", "off"); + $(this.divTag).addClass("autocompletionDiv"); + + var that = this; + $(this.inputTag).blur(function() { + setTimeout(function() { + that.removeSuggestList() + }, 300); + }); + + $(this.inputTag).keyup(function(e){ + switch(e.keyCode) { + case 38: //up + this.changeActiveSuggestion(-1); + break; + case 40://down + this.changeActiveSuggestion(1); + break; + case 27: //escape + this.removeSuggestList(); + break; + default: + this.countBeforeRequest++; + setTimeout(function(){ + that.sendToAutocomplete() + }, 200); + break; } - ); - previousText = tag; + }); } -}; -/** - * replace the input text by the clicked one - */ -function suggestSelect(suggestionStr) { - $("#TagTagName").val(suggestionStr); - $("#TagTagName").focus(); - return false; -} -/** - * transform the xml result into html content - */ -function suggestShowResults(xmlDocResults) { - // we remove the old one - removeSuggestList(); - suggestions = xmlDocResults.getElementsByTagName('item'); - - if (suggestions.length == 0) { - return; + removeSuggestList() { + this.isSuggestListActive = false; + this.currentSuggestPosition = -1; + $(this.divTag).empty(); } - suggestLength = suggestions.length; - var ul = document.createElement("ul"); - $("#autocompletionDiv").append(ul); - isSuggestListActive = true; - for (var i in suggestions) { - // for some weird reason the last element in suggestion is the number - // of element in the array Oo - // and even a function oO wow we're leaving in a strange world .... - if (!isNaN(parseInt(suggestions[i])) || $.isFunction(suggestions[i])) { - continue; + changeActiveSuggestion(offset) { + $("#suggestItem" + this.currentSuggestPosition % this.suggestLength).removeClass("selected"); + this.currentSuggestPosition += offset; + if (this.currentSuggestPosition < 0) { + this.currentSuggestPosition = this.suggestLength - 1; } - suggestion = suggestions[i].firstChild.data; - var li = document.createElement("li"); - li.innerHTML = ""+ - suggestion + - ""; - ul.appendChild(li); + var selectedItem = $("#suggestItem" + this.currentSuggestPosition % this.suggestLength); + selectedItem.addClass("selected"); + this.suggestSelect(this.selectedItem[0].innerHTML); } -} -/** - * - */ -function changeActiveSuggestion(offset) { - $("#suggestItem"+currentSuggestPosition % suggestLength).removeClass("selected"); - currentSuggestPosition += offset; - if (currentSuggestPosition < 0) { - currentSuggestPosition = suggestLength - 1; + suggestSelect(suggestionStr) { + $(this.inputTag).val(suggestionStr); + $(this.inputTag).focus(); + return false; } - var selectedItem = $("#suggestItem"+currentSuggestPosition % suggestLength); - selectedItem.addClass("selected"); - suggestSelect(selectedItem[0].innerHTML); -} - -/** - * - */ -function removeSuggestList() { - isSuggestListActive = false; - currentSuggestPosition = -1; - $("#autocompletionDiv").empty(); -} - - -$(document).ready(function() -{ - - // it desactivates browsers autocompletion - // TODO: it's not something in the standard, so if you - // know a standard way to do this ... - $("#TagTagName").attr("autocomplete","off"); - - $("#TagTagName").blur(function() { - setTimeout(function() { - removeSuggestList()}, - 300 - ); - }); - - $("#TagTagName").keyup(function(e){ - switch(e.keyCode) { - case 38: //up - changeActiveSuggestion(-1); - break; - case 40://down - changeActiveSuggestion(1); - break; - case 27: //escape - removeSuggestList(); - break; - default: - var tag = $(this).val(); - countBeforeRequest++; - setTimeout("sendToAutocomplete()",200); - break; + sendToAutocomplete() { + this.countBeforeRequest--; + if (this.countBeforeRequest > 0) { + return; + } + this.countBeforeRequest = 0; + + var tag = $(this.inputTag).val(); + + if (tag == '') { + $(this.divTag).empty(); + this.previousText = tag; + return; } - - }); - $("#TagAddTagPostForm").submit(function(){ - if (isSuggestListActive) { - var text = $("#suggestItem"+currentSuggestPosition).html() - $("#TagTagName").val(text); - removeSuggestList(); - return false; + var rootUrl = get_tatoeba_root_url(); + var that = this; + if (tag != this.previousText) { + $.get( + rootUrl + "/tags/autocomplete/" + tag, + function(data) { + that.suggestShowResults(data); + } + ); + this.previousText = tag; } - }); - -}); + } + + suggestShowResults(suggestions) { + this.removeSuggestList(); + if (suggestions.allTags.length == 0) { + return; + } + + this.isSuggestListActive = true; + var that = this; + + var ul = document.createElement("ul"); + $(this.divTag).append(ul); + suggestions.allTags.forEach(function(suggestion, index) { + var text = document.createTextNode(suggestion.name + " (" + suggestion.nbrOfSentences + ")"); + + var link = document.createElement("a"); + link.id = "suggestedItem" + index; + link.dataset.tagName = suggestion.name; + link.addEventListener("click", function(){ + that.suggestSelect(suggestion.name) + }); + link.style = "color:black"; + link.appendChild(text); + + var li = document.createElement("li"); + li.appendChild(link); + + ul.appendChild(li); + }); + } +} \ No newline at end of file diff --git a/webroot/js/tags.add.js b/webroot/js/tags.add.js index 343fc8e736..f07bc866e9 100644 --- a/webroot/js/tags.add.js +++ b/webroot/js/tags.add.js @@ -18,6 +18,8 @@ $(document).ready(function() { + new AutocompleteBox("TagTagName", "autocompletionDiv"); + $("#TagTagName").keydown(function(e){ if (e.keyCode == 13) { addTag(); diff --git a/webroot/js/tagslinks.js b/webroot/js/tagslinks.js index 71aa61e5e0..bc2eee0d92 100644 --- a/webroot/js/tagslinks.js +++ b/webroot/js/tagslinks.js @@ -1,6 +1,23 @@ -/** - * @license Angular UI Tree v2.22.5 - * (c) 2010-2017. https://github.com/angular-ui-tree/angular-ui-tree - * License: MIT - */ -!function(){"use strict";angular.module("ui.tree",[]).constant("treeConfig",{treeClass:"angular-ui-tree",emptyTreeClass:"angular-ui-tree-empty",dropzoneClass:"angular-ui-tree-dropzone",hiddenClass:"angular-ui-tree-hidden",nodesClass:"angular-ui-tree-nodes",nodeClass:"angular-ui-tree-node",handleClass:"angular-ui-tree-handle",placeholderClass:"angular-ui-tree-placeholder",dragClass:"angular-ui-tree-drag",dragThreshold:3,defaultCollapsed:!1,appendChildOnHover:!0})}(),function(){"use strict";angular.module("ui.tree").controller("TreeHandleController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$nodeScope=null,e.$type="uiTreeHandle"}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeNodeController",["$scope","$element",function(e,n){function t(e){if(!e)return 0;var n,o,l,r=0,a=e.childNodes();if(!a||0===a.length)return 0;for(l=a.length-1;l>=0;l--)n=a[l],o=1+t(n),r=Math.max(r,o);return r}this.scope=e,e.$element=n,e.$modelValue=null,e.$parentNodeScope=null,e.$childNodesScope=null,e.$parentNodesScope=null,e.$treeScope=null,e.$handleScope=null,e.$type="uiTreeNode",e.$$allowNodeDrop=!1,e.collapsed=!1,e.expandOnHover=!1,e.init=function(t){var o=t[0];e.$treeScope=t[1]?t[1].scope:null,e.$parentNodeScope=o.scope.$nodeScope,e.$modelValue=o.scope.$modelValue[e.$index],e.$parentNodesScope=o.scope,o.scope.initSubNode(e),n.on("$destroy",function(){o.scope.destroySubNode(e)})},e.index=function(){return e.$parentNodesScope.$modelValue.indexOf(e.$modelValue)},e.dragEnabled=function(){return!(e.$treeScope&&!e.$treeScope.dragEnabled)},e.isSibling=function(n){return e.$parentNodesScope==n.$parentNodesScope},e.isChild=function(n){var t=e.childNodes();return t&&t.indexOf(n)>-1},e.prev=function(){var n=e.index();return n>0?e.siblings()[n-1]:null},e.siblings=function(){return e.$parentNodesScope.childNodes()},e.childNodesCount=function(){return e.childNodes()?e.childNodes().length:0},e.hasChild=function(){return e.childNodesCount()>0},e.childNodes=function(){return e.$childNodesScope&&e.$childNodesScope.$modelValue?e.$childNodesScope.childNodes():null},e.accept=function(n,t){return e.$childNodesScope&&e.$childNodesScope.$modelValue&&e.$childNodesScope.accept(n,t)},e.remove=function(){return e.$parentNodesScope.removeNode(e)},e.toggle=function(){e.collapsed=!e.collapsed,e.$treeScope.$callbacks.toggle(e.collapsed,e)},e.collapse=function(){e.collapsed=!0},e.expand=function(){e.collapsed=!1},e.depth=function(){var n=e.$parentNodeScope;return n?n.depth()+1:1},e.maxSubDepth=function(){return e.$childNodesScope?t(e.$childNodesScope):0}}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeNodesController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$modelValue=null,e.$nodeScope=null,e.$treeScope=null,e.$type="uiTreeNodes",e.$nodesMap={},e.nodropEnabled=!1,e.maxDepth=0,e.cloneEnabled=!1,e.initSubNode=function(n){return n.$modelValue?void(e.$nodesMap[n.$modelValue.$$hashKey]=n):null},e.destroySubNode=function(n){return n.$modelValue?void(e.$nodesMap[n.$modelValue.$$hashKey]=null):null},e.accept=function(n,t){return e.$treeScope.$callbacks.accept(n,e,t)},e.beforeDrag=function(n){return e.$treeScope.$callbacks.beforeDrag(n)},e.isParent=function(n){return n.$parentNodesScope==e},e.hasChild=function(){return e.$modelValue.length>0},e.safeApply=function(e){var n=this.$root.$$phase;"$apply"==n||"$digest"==n?e&&"function"==typeof e&&e():this.$apply(e)},e.removeNode=function(n){var t=e.$modelValue.indexOf(n.$modelValue);return t>-1?(e.safeApply(function(){e.$modelValue.splice(t,1)[0]}),e.$treeScope.$callbacks.removed(n)):null},e.insertNode=function(n,t){e.safeApply(function(){e.$modelValue.splice(n,0,t)})},e.childNodes=function(){var n,t=[];if(e.$modelValue)for(n=0;n0&&e.depth()+n.maxSubDepth()+1>t}}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$nodesScope=null,e.$type="uiTree",e.$emptyElm=null,e.$dropzoneElm=null,e.$callbacks=null,e.dragEnabled=!0,e.emptyPlaceholderEnabled=!0,e.maxDepth=0,e.dragDelay=0,e.cloneEnabled=!1,e.nodropEnabled=!1,e.dropzoneEnabled=!1,e.isEmpty=function(){return e.$nodesScope&&e.$nodesScope.$modelValue&&0===e.$nodesScope.$modelValue.length},e.place=function(n){e.$nodesScope.$element.append(n),e.$emptyElm.remove()},this.resetEmptyElement=function(){e.$nodesScope.$modelValue&&0!==e.$nodesScope.$modelValue.length||!e.emptyPlaceholderEnabled?e.$emptyElm.remove():n.append(e.$emptyElm)},this.resetDropzoneElement=function(){e.$nodesScope.$modelValue&&0===e.$nodesScope.$modelValue.length||!e.dropzoneEnabled?e.$dropzoneElm.remove():n.append(e.$dropzoneElm)},e.resetEmptyElement=this.resetEmptyElement,e.resetDropzoneElement=this.resetDropzoneElement}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTree",["treeConfig","$window",function(e,n){return{restrict:"A",scope:!0,controller:"TreeController",link:function(t,o,l,r){var a,d,i,c={accept:null,beforeDrag:null},s={};angular.extend(s,e),s.treeClass&&o.addClass(s.treeClass),"table"===o.prop("tagName").toLowerCase()?(t.$emptyElm=angular.element(n.document.createElement("tr")),d=o.find("tr"),i=d.length>0?angular.element(d).children().length:1e6,a=angular.element(n.document.createElement("td")).attr("colspan",i),t.$emptyElm.append(a)):(t.$emptyElm=angular.element(n.document.createElement("div")),t.$dropzoneElm=angular.element(n.document.createElement("div"))),s.emptyTreeClass&&t.$emptyElm.addClass(s.emptyTreeClass),s.dropzoneClass&&t.$dropzoneElm.addClass(s.dropzoneClass),t.$watch("$nodesScope.$modelValue.length",function(e){angular.isNumber(e)&&(r.resetEmptyElement(),r.resetDropzoneElement())},!0),t.$watch(l.dragEnabled,function(e){"boolean"==typeof e&&(t.dragEnabled=e)}),t.$watch(l.emptyPlaceholderEnabled,function(e){"boolean"==typeof e&&(t.emptyPlaceholderEnabled=e,r.resetEmptyElement())}),t.$watch(l.nodropEnabled,function(e){"boolean"==typeof e&&(t.nodropEnabled=e)}),t.$watch(l.dropzoneEnabled,function(e){"boolean"==typeof e&&(t.dropzoneEnabled=e,r.resetDropzoneElement())}),t.$watch(l.cloneEnabled,function(e){"boolean"==typeof e&&(t.cloneEnabled=e)}),t.$watch(l.maxDepth,function(e){"number"==typeof e&&(t.maxDepth=e)}),t.$watch(l.dragDelay,function(e){"number"==typeof e&&(t.dragDelay=e)}),c.accept=function(e,n,t){return!(n.nodropEnabled||n.$treeScope.nodropEnabled||n.outOfDepth(e))},c.beforeDrag=function(e){return!0},c.expandTimeoutStart=function(){},c.expandTimeoutCancel=function(){},c.expandTimeoutEnd=function(){},c.removed=function(e){},c.dropped=function(e){},c.dragStart=function(e){},c.dragMove=function(e){},c.dragStop=function(e){},c.beforeDrop=function(e){},c.toggle=function(e,n){},t.$watch(l.uiTree,function(e,n){angular.forEach(e,function(e,n){c[n]&&"function"==typeof e&&(c[n]=e)}),t.$callbacks=c},!0)}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeHandle",["treeConfig",function(e){return{require:"^uiTreeNode",restrict:"A",scope:!0,controller:"TreeHandleController",link:function(n,t,o,l){var r={};angular.extend(r,e),r.handleClass&&t.addClass(r.handleClass),n!=l.scope&&(n.$nodeScope=l.scope,l.scope.$handleScope=n)}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeNode",["treeConfig","UiTreeHelper","$window","$document","$timeout","$q",function(e,n,t,o,l,r){return{require:["^uiTreeNodes","^uiTree"],restrict:"A",controller:"TreeNodeController",link:function(a,d,i,c){var s,u,p,m,f,h,$,g,b,v,N,S,y,E,x,C,T,w,D,H,O,Y,A,X,V,k,z,M={},I="ontouchstart"in window,P=null,L=document.body,W=document.documentElement;angular.extend(M,e),M.nodeClass&&d.addClass(M.nodeClass),a.init(c),a.collapsed=!!n.getNodeAttribute(a,"collapsed")||e.defaultCollapsed,a.expandOnHover=!!n.getNodeAttribute(a,"expandOnHover"),a.scrollContainer=n.getNodeAttribute(a,"scrollContainer")||i.scrollContainer||null,a.sourceOnly=a.nodropEnabled||a.$treeScope.nodropEnabled,a.$watch(i.collapsed,function(e){"boolean"==typeof e&&(a.collapsed=e)}),a.$watch("collapsed",function(e){n.setNodeAttribute(a,"collapsed",e),i.$set("collapsed",e)}),a.$watch(i.expandOnHover,function(e){"boolean"!=typeof e&&"number"!=typeof e||(a.expandOnHover=e)}),a.$watch("expandOnHover",function(e){n.setNodeAttribute(a,"expandOnHover",e),i.$set("expandOnHover",e)}),i.$observe("scrollContainer",function(e){"string"==typeof e&&(a.scrollContainer=e)}),a.$watch("scrollContainer",function(e){n.setNodeAttribute(a,"scrollContainer",e),i.$set("scrollContainer",e),$=document.querySelector(e)}),a.$on("angular-ui-tree:collapse-all",function(){a.collapsed=!0}),a.$on("angular-ui-tree:expand-all",function(){a.collapsed=!1}),S=function(e){if((I||2!==e.button&&3!==e.which)&&!(e.uiTreeDragging||e.originalEvent&&e.originalEvent.uiTreeDragging)){var l,r,i,c,$,g,S,y,E,x=angular.element(e.target);if(l=n.treeNodeHandlerContainerOfElement(x),l&&(x=angular.element(l)),r=d.clone(),y=n.elementIsTreeNode(x),E=n.elementIsTreeNodeHandle(x),(y||E)&&!(y&&n.elementContainsTreeNodeHandler(x)||(i=x.prop("tagName").toLowerCase(),"input"==i||"textarea"==i||"button"==i||"select"==i))){for(V=angular.element(e.target),k=V[0].attributes["ui-tree"];V&&V[0]&&V[0]!==d&&!k;){if(V[0].attributes&&(k=V[0].attributes["ui-tree"]),n.nodrag(V))return;V=V.parent()}a.beforeDrag(a)&&(e.uiTreeDragging=!0,e.originalEvent&&(e.originalEvent.uiTreeDragging=!0),e.preventDefault(),$=n.eventObj(e),s=!0,u=n.dragInfo(a),z=u.source.$treeScope.$id,c=d.prop("tagName"),"tr"===c.toLowerCase()?(m=angular.element(t.document.createElement(c)),g=angular.element(t.document.createElement("td")).addClass(M.placeholderClass).attr("colspan",d[0].children.length),m.append(g)):m=angular.element(t.document.createElement(c)).addClass(M.placeholderClass),f=angular.element(t.document.createElement(c)),M.hiddenClass&&f.addClass(M.hiddenClass),p=n.positionStarted($,d),m.css("height",d.prop("offsetHeight")+"px"),h=angular.element(t.document.createElement(a.$parentNodesScope.$element.prop("tagName"))).addClass(a.$parentNodesScope.$element.attr("class")).addClass(M.dragClass),h.css("width",n.width(d)+"px"),h.css("z-index",9999),S=(d[0].querySelector(".angular-ui-tree-handle")||d[0]).currentStyle,S&&(document.body.setAttribute("ui-tree-cursor",o.find("body").css("cursor")||""),o.find("body").css({cursor:S.cursor+"!important"})),a.sourceOnly&&m.css("display","none"),d.after(m),d.after(f),u.isClone()&&a.sourceOnly?h.append(r):h.append(d),o.find("body").append(h),h.css({left:$.pageX-p.offsetX+"px",top:$.pageY-p.offsetY+"px"}),b={placeholder:m,dragging:h},O(),a.$apply(function(){a.$treeScope.$callbacks.dragStart(u.eventArgs(b,p))}),v=Math.max(L.scrollHeight,L.offsetHeight,W.clientHeight,W.scrollHeight,W.offsetHeight),N=Math.max(L.scrollWidth,L.offsetWidth,W.clientWidth,W.scrollWidth,W.offsetWidth))}}},y=function(e){var o,r,d,i,c,f,S,y,E,x,C,T,w,D,H,O,Y,A,V,k,I,L,W,q,F=n.eventObj(e);if(h){if(e.preventDefault(),t.getSelection?t.getSelection().removeAllRanges():t.document.selection&&t.document.selection.empty(),d=F.pageX-p.offsetX,i=F.pageY-p.offsetY,d<0&&(d=0),i<0&&(i=0),i+10>v&&(i=v-10),d+10>N&&(d=N-10),h.css({left:d+"px",top:i+"px"}),$?(S=$.getBoundingClientRect(),c=$.scrollTop,f=c+$.clientHeight,S.bottomF.clientY&&c>0&&(O=Math.min(c,10),$.scrollTop-=O)):(c=window.pageYOffset||t.document.documentElement.scrollTop,f=c+(window.innerHeight||t.document.clientHeight||t.document.clientHeight),fF.pageY&&(O=Math.min(c,10),window.scrollBy(0,-O))),n.positionMoved(e,p,s),s)return void(s=!1);if(E=F.pageX-(t.pageXOffset||t.document.body.scrollLeft||t.document.documentElement.scrollLeft)-(t.document.documentElement.clientLeft||0),x=F.pageY-(t.pageYOffset||t.document.body.scrollTop||t.document.documentElement.scrollTop)-(t.document.documentElement.clientTop||0),angular.isFunction(h.hide)?h.hide():(C=h[0].style.display,h[0].style.display="none"),t.document.elementFromPoint(E,x),w=angular.element(t.document.elementFromPoint(E,x)),X=n.treeNodeHandlerContainerOfElement(w),X&&(w=angular.element(X)),angular.isFunction(h.show)?h.show():h[0].style.display=C,n.elementIsTree(w)?T=w.controller("uiTree").scope:n.elementIsTreeNodeHandle(w)?T=w.controller("uiTreeHandle").scope:n.elementIsTreeNode(w)?T=w.controller("uiTreeNode").scope:n.elementIsTreeNodes(w)?T=w.controller("uiTreeNodes").scope:n.elementIsPlaceholder(w)?T=w.controller("uiTreeNodes").scope:n.elementIsDropzone(w)?(T=w.controller("uiTree").scope,q=!0):w.controller("uiTreeNode")&&(T=w.controller("uiTreeNode").scope),V=T&&T.$treeScope&&T.$treeScope.$id&&T.$treeScope.$id===z,V&&p.dirAx)p.distX>0&&(o=u.prev(),o&&!o.collapsed&&o.accept(a,o.childNodesCount())&&(o.$childNodesScope.$element.append(m),u.moveTo(o.$childNodesScope,o.childNodes(),o.childNodesCount()))),p.distX<0&&(r=u.next(),r||(y=u.parentNode(),y&&y.$parentNodesScope.accept(a,y.index()+1)&&(y.$element.after(m),u.moveTo(y.$parentNodesScope,y.siblings(),y.index()+1))));else{if(D=!1,!T)return;if(!T.$treeScope||T.$parent.nodropEnabled||T.$treeScope.nodropEnabled||m.css("display",""),"uiTree"===T.$type&&T.dragEnabled&&(D=T.isEmpty()),"uiTreeHandle"===T.$type&&(T=T.$nodeScope),"uiTreeNode"!==T.$type&&!D&&!q)return void(M.appendChildOnHover&&(r=u.next(),!r&&g&&(y=u.parentNode(),y.$element.after(m),u.moveTo(y.$parentNodesScope,y.siblings(),y.index()+1),g=!1)));P&&m.parent()[0]!=P.$element[0]&&(P.resetEmptyElement(),P.resetDropzoneElement(),P=null),D?(P=T,T.$nodesScope.accept(a,0)&&u.moveTo(T.$nodesScope,T.$nodesScope.childNodes(),0)):q?(P=T,T.$nodesScope.accept(a,T.$nodesScope.childNodes().length)&&u.moveTo(T.$nodesScope,T.$nodesScope.childNodes(),T.$nodesScope.childNodes().length)):T.dragEnabled()&&(angular.isDefined(a.expandTimeoutOn)&&a.expandTimeoutOn!==T.id&&(l.cancel(a.expandTimeout),delete a.expandTimeout,delete a.expandTimeoutOn,a.$callbacks.expandTimeoutCancel()),T.collapsed&&(a.expandOnHover===!0||angular.isNumber(a.expandOnHover)&&0===a.expandOnHover?(T.collapsed=!1,T.$treeScope.$callbacks.toggle(!1,T)):a.expandOnHover!==!1&&angular.isNumber(a.expandOnHover)&&a.expandOnHover>0&&angular.isUndefined(a.expandTimeoutOn)&&(a.expandTimeoutOn=T.$id,a.$callbacks.expandTimeoutStart(),a.expandTimeout=l(function(){a.$callbacks.expandTimeoutEnd(),T.collapsed=!1,T.$treeScope.$callbacks.toggle(!1,T)},a.expandOnHover))),w=T.$element,Y=n.offset(w),I=n.height(w),L=T.$childNodesScope?T.$childNodesScope.$element:null,W=L?n.height(L):0,I-=W,k=M.appendChildOnHover?.25*I:n.height(w)/2,A=F.pageY0?D.exec(function(){x(e)},a.dragDelay):x(e)}),d.bind("touchend touchcancel mouseup",function(){a.dragDelay>0&&D.cancel()})},H(),O=function(){angular.element(o).bind("touchend",T),angular.element(o).bind("touchcancel",T),angular.element(o).bind("touchmove",C),angular.element(o).bind("mouseup",T),angular.element(o).bind("mousemove",C),angular.element(o).bind("mouseleave",w),angular.element(o).bind("keydown",A)},Y=function(){angular.element(o).unbind("touchend",T),angular.element(o).unbind("touchcancel",T),angular.element(o).unbind("touchmove",C),angular.element(o).unbind("mouseup",T),angular.element(o).unbind("mousemove",C),angular.element(o).unbind("mouseleave",w),angular.element(o).unbind("keydown",A)}}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeNodes",["treeConfig","$window",function(e){return{require:["ngModel","?^uiTreeNode","^uiTree"],restrict:"A",scope:!0,controller:"TreeNodesController",link:function(n,t,o,l){var r={},a=l[0],d=l[1],i=l[2];angular.extend(r,e),r.nodesClass&&t.addClass(r.nodesClass),d?(d.scope.$childNodesScope=n,n.$nodeScope=d.scope):i.scope.$nodesScope=n,n.$treeScope=i.scope,a&&(a.$render=function(){n.$modelValue=a.$modelValue}),n.$watch(function(){return o.maxDepth},function(e){"number"==typeof e&&(n.maxDepth=e)}),n.$watch(function(){return o.nodropEnabled},function(e){"undefined"!=typeof e&&(n.nodropEnabled=!0)},!0)}}}])}(),function(){"use strict";function e(e,n){if(void 0===n)return null;for(var t=n.parentNode,o=1,l="function"==typeof t.setAttribute&&t.hasAttribute(e)?t:null;t&&"function"==typeof t.setAttribute&&!t.hasAttribute(e);){if(t=t.parentNode,l=t,t===document.documentElement){l=null;break}o++}return l}angular.module("ui.tree").factory("UiTreeHelper",["$document","$window","treeConfig",function(n,t,o){return{nodesData:{},setNodeAttribute:function(e,n,t){if(!e.$modelValue)return null;var o=this.nodesData[e.$modelValue.$$hashKey];o||(o={},this.nodesData[e.$modelValue.$$hashKey]=o),o[n]=t},getNodeAttribute:function(e,n){if(!e.$modelValue)return null;var t=this.nodesData[e.$modelValue.$$hashKey];return t?t[n]:null},nodrag:function(e){return"undefined"!=typeof e.attr("data-nodrag")&&"false"!==e.attr("data-nodrag")},eventObj:function(e){var n=e;return void 0!==e.targetTouches?n=e.targetTouches.item(0):void 0!==e.originalEvent&&void 0!==e.originalEvent.targetTouches&&(n=e.originalEvent.targetTouches.item(0)),n},dragInfo:function(e){return{source:e,sourceInfo:{cloneModel:e.$treeScope.cloneEnabled===!0?angular.copy(e.$modelValue):void 0,nodeScope:e,index:e.index(),nodesScope:e.$parentNodesScope},index:e.index(),siblings:e.siblings().slice(0),parent:e.$parentNodesScope,resetParent:function(){this.parent=e.$parentNodesScope},moveTo:function(e,n,t){this.parent=e,this.siblings=n.slice(0);var o=this.siblings.indexOf(this.source);o>-1&&(this.siblings.splice(o,1),this.source.index()0?this.siblings[this.index-1]:null},next:function(){return this.index0&&(o=e.originalEvent.touches[0].pageX,l=e.originalEvent.touches[0].pageY),t.offsetX=o-this.offset(n).left,t.offsetY=l-this.offset(n).top,t.startX=t.lastX=o,t.startY=t.lastY=l,t.nowX=t.nowY=t.distX=t.distY=t.dirAx=0,t.dirX=t.dirY=t.lastDirX=t.lastDirY=t.distAxX=t.distAxY=0,t},positionMoved:function(e,n,t){var o,l=e.pageX,r=e.pageY;return e.originalEvent&&e.originalEvent.touches&&e.originalEvent.touches.length>0&&(l=e.originalEvent.touches[0].pageX,r=e.originalEvent.touches[0].pageY),n.lastX=n.nowX,n.lastY=n.nowY,n.nowX=l,n.nowY=r,n.distX=n.nowX-n.lastX,n.distY=n.nowY-n.lastY,n.lastDirX=n.dirX,n.lastDirY=n.dirY,n.dirX=0===n.distX?0:n.distX>0?1:-1,n.dirY=0===n.distY?0:n.distY>0?1:-1,o=Math.abs(n.distX)>Math.abs(n.distY)?1:0,t?(n.dirAx=o,void(n.moving=!0)):(n.dirAx!==o?(n.distAxX=0,n.distAxY=0):(n.distAxX+=Math.abs(n.distX),0!==n.dirX&&n.dirX!==n.lastDirX&&(n.distAxX=0),n.distAxY+=Math.abs(n.distY),0!==n.dirY&&n.dirY!==n.lastDirY&&(n.distAxY=0)),void(n.dirAx=o))},elementIsTreeNode:function(e){return"undefined"!=typeof e.attr("ui-tree-node")},elementIsTreeNodeHandle:function(e){return"undefined"!=typeof e.attr("ui-tree-handle")},elementIsTree:function(e){return"undefined"!=typeof e.attr("ui-tree")},elementIsTreeNodes:function(e){return"undefined"!=typeof e.attr("ui-tree-nodes")},elementIsPlaceholder:function(e){return e.hasClass(o.placeholderClass)},elementIsDropzone:function(e){return e.hasClass(o.dropzoneClass)},elementContainsTreeNodeHandler:function(e){return e[0].querySelectorAll("[ui-tree-handle]").length>=1},treeNodeHandlerContainerOfElement:function(n){return e("ui-tree-handle",n[0])}}}])}(); \ No newline at end of file +(function(){ + 'use strict'; + + new AutocompleteBox("parenttag", "autocompletionParent"); + new AutocompleteBox("childtag", "autocompletionChild"); + + angular + .module('app') + .controller('TagsLinksController', [ + '$http', '$location', '$anchorScroll', TagsLinksController + ]); + + function TagsLinksController($http, $location, $anchorScroll) { + var vm = this; + vm.expandOrCollapse = expandOrCollapse; + vm.hiddenReplies = {}; + + function expandOrCollapse(id) { + console.log("e"); + vm.hiddenReplies[id] = !vm.hiddenReplies[id]; + } + } +})(); \ No newline at end of file diff --git a/webroot/js/tagslinksautocompletion.js b/webroot/js/tagslinksautocompletion.js deleted file mode 100644 index 9d753723e5..0000000000 --- a/webroot/js/tagslinksautocompletion.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Tatoeba Project, free collaborative creation of multilingual corpuses project - * Copyright (C) 2010 Allan SIMON - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -// we need to declare this variable and function out of the "documentready" -// block to make them somewhat global - -// The mechanism is more or less the following -// Each time you input new character it increase a counter and start a timer -// at the end of the timer, it decrease the counter by one and check if the -// counter is zero and will call the ajax action only if it's zero -// This way we're sure the ajax action will occur at the expiration of the -// last counter -// This both avoid two hackish solution -// 1 send after each input (send too many request) -// 2 send each X ms (send also too many request) - -var currentSuggestPosition = -1; -var countBeforeRequest = 0; -var suggestLength = 0; -var previousText = ''; -var isSuggestListActive = false; - -function removeSuggestList(divTag) { - isSuggestListActive = false; - currentSuggestPosition = -1; - $(divTag).empty(); -} - -function changeActiveSuggestion(inputTag, offset) { - $("#suggestItem" + currentSuggestPosition % suggestLength).removeClass("selected"); - currentSuggestPosition += offset; - if (currentSuggestPosition < 0) { - currentSuggestPosition = suggestLength - 1; - } - var selectedItem = $("#suggestItem" + currentSuggestPosition % suggestLength); - selectedItem.addClass("selected"); - suggestSelect(inputTag, selectedItem[0].innerHTML); -} - -function suggestSelect(inputTag, suggestionStr) { - $(inputTag).val(suggestionStr); - $(inputTag).focus(); - return false; -} - -function sendToAutocomplete(inputTag, divTag) { - countBeforeRequest--; - if (countBeforeRequest > 0) { - return; - } - countBeforeRequest = 0; - - var tag = $(inputTag).val(); - - if (tag == '') { - $(divTag).empty(); - previousText = tag; - return; - } - var rootUrl = get_tatoeba_root_url(); - if (tag != previousText) { - $.get( - rootUrl + "/tags_links/autocomplete/" + tag, - function(data) { - suggestShowResults(inputTag, divTag, data); - } - ); - previousText = tag; - } -}; - -function suggestShowResults(inputTag, divTag, suggestions) { - removeSuggestList(divTag); - if (suggestions.allTags.length == 0) { - return; - } - - isSuggestListActive = true; - - var ul = document.createElement("ul"); - $(divTag).append(ul); - suggestions.allTags.forEach(function(suggestion, index) { - var text = document.createTextNode(suggestion.name + " (" + suggestion.nbrOfSentences + ")"); - - var link = document.createElement("a"); - link.id = "suggestedItem" + index; - link.dataset.tagName = suggestion.name; - link.onclick = "suggestSelect(this.dataset.tagName)"; - link.setAttribute("onclick", "suggestSelect(this.dataset.tagName)"); - link.style = "color:black"; - link.appendChild(text); - - var li = document.createElement("li"); - li.appendChild(link); - - ul.appendChild(li); - }); -} - -function bindAutocomplete(inputId, divId) { - var inputTag = "#" + inputId; - var divTag = "#" + divId; - $(inputTag).attr("autocomplete", "off"); - $(inputTag).blur(function() { - setTimeout(function() { - removeSuggestList(divTag) - }, 300); - }); - - $(inputTag).keyup(function(e){ - switch(e.keyCode) { - case 38: //up - changeActiveSuggestion(inputTag, -1); - break; - case 40://down - changeActiveSuggestion(inputTag, 1); - break; - case 27: //escape - removeSuggestList(divTag); - break; - default: - var tag = $(this).val(); - countBeforeRequest++; - setTimeout(function(){ - sendToAutocomplete(inputTag, divTag) - }, 200); - break; - } - }); -} - -$(document).ready(function() { - bindAutocomplete("parenttag", "autocompletionParent"); - bindAutocomplete("childtag", "autocompletionChild"); -}); \ No newline at end of file From 178689653950857d0b057562978db22c94dc30da Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Fri, 31 Jul 2020 01:16:27 +0200 Subject: [PATCH 03/11] Transitioned to external structure to store hierarchy on tags; made the autocompletion module mode general --- .../20200729155726_CreateSuperTags.php | 37 ++++ ...=> 20200729155842_CreateTagsSuperTags.php} | 9 +- config/auth_actions.php | 3 +- docs/database/tables/tags_links.sql | 14 -- src/Controller/SuperTagsController.php | 45 +++++ src/Controller/TagsController.php | 4 +- src/Controller/TagsLinksController.php | 118 ----------- src/Controller/TagsSuperTagsController.php | 154 ++++++++++++++ .../Behavior/AutocompletableBehavior.php | 9 + src/Model/Entity/Contribution.php | 43 ++++ src/Model/Entity/UsersSentence.php | 43 ++++ src/Model/Entity/Wall.php | 40 ++++ src/Model/Table/SuperTagsTable.php | 101 ++++++++++ src/Model/Table/TagsLinksTable.php | 93 --------- src/Model/Table/TagsSuperTagsTable.php | 123 ++++++++++++ src/Template/Element/superTag.ctp | 110 ++++++++++ src/Template/Element/tagslink.ctp | 61 ------ src/Template/Element/top_menu.ctp | 4 +- src/Template/TagsLinks/manage.ctp | 85 -------- src/Template/TagsSuperTags/manage.ctp | 189 ++++++++++++++++++ webroot/js/autocompletion.js | 16 +- webroot/js/tags.add.js | 4 +- webroot/js/tagsSuperTags.js | 38 ++++ webroot/js/tagslinks.js | 23 --- 24 files changed, 957 insertions(+), 409 deletions(-) create mode 100644 config/Migrations/20200729155726_CreateSuperTags.php rename config/Migrations/{20200828235617_CreateTagsLinks.php => 20200729155842_CreateTagsSuperTags.php} (76%) delete mode 100644 docs/database/tables/tags_links.sql create mode 100644 src/Controller/SuperTagsController.php delete mode 100644 src/Controller/TagsLinksController.php create mode 100644 src/Controller/TagsSuperTagsController.php create mode 100644 src/Model/Entity/Contribution.php create mode 100644 src/Model/Entity/UsersSentence.php create mode 100644 src/Model/Entity/Wall.php create mode 100644 src/Model/Table/SuperTagsTable.php delete mode 100644 src/Model/Table/TagsLinksTable.php create mode 100644 src/Model/Table/TagsSuperTagsTable.php create mode 100644 src/Template/Element/superTag.ctp delete mode 100644 src/Template/Element/tagslink.ctp delete mode 100644 src/Template/TagsLinks/manage.ctp create mode 100644 src/Template/TagsSuperTags/manage.ctp create mode 100644 webroot/js/tagsSuperTags.js delete mode 100644 webroot/js/tagslinks.js diff --git a/config/Migrations/20200729155726_CreateSuperTags.php b/config/Migrations/20200729155726_CreateSuperTags.php new file mode 100644 index 0000000000..0b5a7eeb30 --- /dev/null +++ b/config/Migrations/20200729155726_CreateSuperTags.php @@ -0,0 +1,37 @@ +table('super_tags'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 50, + 'null' => false, + ]); + $table->addColumn('description', 'string', [ + 'default' => null, + 'limit' => 500, + 'null' => true, + ]); + $table->addColumn('user_id', 'integer', [ + 'default' => null, + 'limit' => 11, + 'null' => true, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => 'CURRENT_TIMESTAMP', + 'null' => true, + ]); + $table->create(); + } +} diff --git a/config/Migrations/20200828235617_CreateTagsLinks.php b/config/Migrations/20200729155842_CreateTagsSuperTags.php similarity index 76% rename from config/Migrations/20200828235617_CreateTagsLinks.php rename to config/Migrations/20200729155842_CreateTagsSuperTags.php index 1698e0038b..3b3be713dd 100644 --- a/config/Migrations/20200828235617_CreateTagsLinks.php +++ b/config/Migrations/20200729155842_CreateTagsSuperTags.php @@ -1,7 +1,7 @@ table('tagsLinks'); + $table = $this->table('tags_super_tags'); $table->addColumn('parent', 'integer', [ 'default' => null, 'limit' => 11, @@ -23,6 +23,11 @@ public function change() 'limit' => 11, 'null' => false, ]); + $table->addColumn('child_type', 'enum', [ + 'default' => null, + 'null' => false, + 'values' => ['tag', 'superTag'], + ]); $table->addColumn('user_id', 'integer', [ 'default' => null, 'limit' => 11, diff --git a/config/auth_actions.php b/config/auth_actions.php index 65974c0e23..d979359679 100644 --- a/config/auth_actions.php +++ b/config/auth_actions.php @@ -110,8 +110,9 @@ 'edit_correctness' => [ User::ROLE_ADMIN ], ], 'sentences_lists' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], + 'super_tags' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], 'tags' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], - 'tags_links' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], + 'tags_super_tags' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], 'transcriptions' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'user' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'users' => [ '*' => [ User::ROLE_ADMIN ] ], diff --git a/docs/database/tables/tags_links.sql b/docs/database/tables/tags_links.sql deleted file mode 100644 index 51a3be5809..0000000000 --- a/docs/database/tables/tags_links.sql +++ /dev/null @@ -1,14 +0,0 @@ --- --- Table structure for table `tags_links` --- --- This table stores the tags relations. --- - -DROP TABLE IF EXISTS `tags_links`; -CREATE TABLE `tags_links` ( - `parent` int(11) NOT NULL, - `child` int(11) NOT NULL, - `user_id` int(11) DEFAULT NULL, - `added_time` datetime DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`parent`, `child`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/Controller/SuperTagsController.php b/src/Controller/SuperTagsController.php new file mode 100644 index 0000000000..4d9e64ca32 --- /dev/null +++ b/src/Controller/SuperTagsController.php @@ -0,0 +1,45 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace App\Controller; + +use App\Controller\AppController; +use Cake\Event\Event; +use App\Model\CurrentUser; + + +class SuperTagsController extends AppController +{ + /** + * Controller name + * + * @var string + * @access public + */ + public $name = 'SuperTags'; + public $components = ['CommonSentence', 'Flash']; + + public function autocomplete($search) { + $results = $this->SuperTags->Autocomplete($search); + + $this->loadComponent('RequestHandler'); + $this->set('results', $results); + $this->set('_serialize', ['results']); + $this->RequestHandler->renderAs($this, 'json'); + } +} diff --git a/src/Controller/TagsController.php b/src/Controller/TagsController.php index f9797e28a3..dc56b7655d 100644 --- a/src/Controller/TagsController.php +++ b/src/Controller/TagsController.php @@ -247,8 +247,8 @@ public function autocomplete($search) $allTags = $this->Tags->Autocomplete($search); $this->loadComponent('RequestHandler'); - $this->set('allTags', $allTags); - $this->set('_serialize', ['allTags']); + $this->set('results', $allTags); + $this->set('_serialize', ['results']); $this->RequestHandler->renderAs($this, 'json'); } } diff --git a/src/Controller/TagsLinksController.php b/src/Controller/TagsLinksController.php deleted file mode 100644 index da61e964b8..0000000000 --- a/src/Controller/TagsLinksController.php +++ /dev/null @@ -1,118 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -namespace App\Controller; - -use App\Controller\AppController; -use App\Event\SuggestdListener; -use Cake\Event\Event; -use App\Model\CurrentUser; - - -class TagsLinksController extends AppController -{ - /** - * Controller name - * - * @var string - * @access public - */ - public $name = 'TagsLinks'; - public $components = ['CommonSentence', 'Flash']; - public $helpers = ['Pagination']; - - public function manage(){ - $all_tags = array(); - $links = array(); - $temp = array(); - $s = 0; - $this->loadModel('Tags'); - foreach ($this->Tags->find('all', array('order' => 'id DESC'))->select(['id', 'name', 'nbrOfSentences']) as $key => $value){ - $all_tags[$value['id']] = array($value['name'], $value['nbrOfSentences']); - $links[$value['id']] = array(); - $temp[$value['id']] = array(); - } - foreach ($this->TagsLinks->find('all')->select(['parent', 'child']) as $key => $value){ - $s += 1; - array_push($links[$value['child']], $value['parent']); - array_push($temp[$value['parent']], $value['child']); - } - - $tree = array(); - foreach ($temp as $parent => $children){ - if (!count($children)) - $tree[$parent] = array(); - } - while ($s) { - foreach ($links as $child => $parents) { - if (count($parents) && array_key_exists($child, $tree)){ - // 1. copy - $subtree = $tree[$child]; - // 2. remove - unset($tree[$child]); - // 3. replace - foreach ($parents as $parent) { - if (!array_key_exists($parent, $tree)) - $tree[$parent] = []; - $tree[$parent][$child] = $subtree; - } - // 4. remove used links - $s -= count($parents); - $links[$child] = []; - } - } - } - - $this->set('all_tags', $all_tags); - $this->set('tree', $tree); - } - - /** - * Add a link - * - * @return void - */ - - public function add() - { - $parent = $this->request->data('parentTag'); - $child = $this->request->data('childTag'); - $userId = CurrentUser::get("id"); - $username = CurrentUser::get("username"); - $link = $this->TagsLinks->addLink($parent, $child, $userId); - return $this->redirect([ - 'controller' => 'tags_links', - 'action' => 'manage' - ]); - } - - /** - * Remove a link - * - * @return void - */ - - public function remove($parent, $child) - { - $link = $this->TagsLinks->removeLink($parent, $child); - return $this->redirect([ - 'controller' => 'tags_links', - 'action' => 'manage' - ]); - } -} diff --git a/src/Controller/TagsSuperTagsController.php b/src/Controller/TagsSuperTagsController.php new file mode 100644 index 0000000000..d2030050eb --- /dev/null +++ b/src/Controller/TagsSuperTagsController.php @@ -0,0 +1,154 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace App\Controller; + +use App\Controller\AppController; +use Cake\Event\Event; +use App\Model\CurrentUser; + + +class TagsSuperTagsController extends AppController +{ + /** + * Controller name + * + * @var string + * @access public + */ + public $name = 'TagsSuperTags'; + public $components = ['CommonSentence', 'Flash']; + public $helpers = ['Pagination']; + + public function manage(){ + $all_tags = []; + $this->loadModel('Tags'); + foreach ($this->Tags->find('all')->select(['id', 'name', 'nbrOfSentences']) as $key => $value) + $all_tags[$value['id']] = [ + 'name' => $value['name'], + 'nbrOfSentences' => $value['nbrOfSentences'] + ]; + + $all_superTags = []; + $all_tagsLinks = []; + $all_superTagsLinks = []; + $this->loadModel('SuperTags'); + foreach ($this->SuperTags->find('all')->select(['id', 'name']) as $value) { + $superTagId = $value['id']; + $all_superTags[$superTagId] = $value['name']; + $all_tagsLinks[$superTagId] = []; + $all_superTagsLinks[$superTagId] = []; + } + + $remaining_links = 0; + $non_leaves = []; + foreach ($this->TagsSuperTags->find('all')->select(['parent', 'child', 'child_type']) as $value) { + if ($value['child_type'] == 'tag') + array_push($all_tagsLinks[$value['parent']], $value['child']); + else { + array_push($all_superTagsLinks[$value['child']], $value['parent']); + $non_leaves[$value['parent']] = 1; + $remaining_links += 1; + } + } + + $tree = []; + $leaves = array_diff(array_keys($all_superTags), array_keys($non_leaves)); + foreach ($leaves as $leave) + $tree[$leave] = []; + + while ($remaining_links) { + foreach ($all_superTagsLinks as $child => $parents) { + if (count($parents) && array_key_exists($child, $tree)){ + // 1. copy + $subtree = $tree[$child]; + // 2. remove + unset($tree[$child]); + // 3. replace + foreach ($parents as $parent) { + if (!array_key_exists($parent, $tree)) + $tree[$parent] = []; + $tree[$parent][$child] = $subtree; + } + // 4. remove used links + $remaining_links -= count($parents); + $all_superTagsLinks[$child] = []; + } + } + } + + $this->set('all_tags', $all_tags); + $this->set('all_super_tags', $all_superTags); + $this->set('all_tags_links', $all_tagsLinks); + $this->set('all_super_tags_links', $tree); + } + + /** + * Add a super tag + * + * @return void + */ + public function createSuperTag(){ + $name = $this->request->data('name'); + $description = $this->request->data('description'); + $userId = CurrentUser::get('id'); + + $this->loadModel('SuperTags'); + $added = $this->SuperTags->create($name, $description, $userId); + + return $this->redirect([ + 'controller' => 'tags_super_tags', + 'action' => 'manage', + '?' => ['superTagAdded' => $added], + ]); + } + + public function createTagSuperTag(){ + $parent = $this->request->data('parent'); + $child = $this->request->data('child'); + $childType = ($this->request->data('childType') == 0) ? 'tag' : 'superTag'; + $userId = CurrentUser::get('id'); + + $added = $this->TagsSuperTags->create($parent, $child, $childType, $userId); + + return $this->redirect([ + 'controller' => 'tags_super_tags', + 'action' => 'manage', + '?' => ['tagSuperTagAdded' => $added], + ]); + } + + public function removeSuperTag($superTagId){ + $this->loadModel('SuperTags'); + $this->SuperTags->remove($superTagId); + + return $this->redirect([ + 'controller' => 'tags_super_tags', + 'action' => 'manage', + ]); + } + + public function removeTagSuperTag($parent, $child, $childType){ + $this->TagsSuperTags->remove($parent, $child, $childType); + + return $this->redirect([ + 'controller' => 'tags_super_tags', + 'action' => 'manage', + ]); + } +} \ No newline at end of file diff --git a/src/Model/Behavior/AutocompletableBehavior.php b/src/Model/Behavior/AutocompletableBehavior.php index 70661b478f..2be3033323 100644 --- a/src/Model/Behavior/AutocompletableBehavior.php +++ b/src/Model/Behavior/AutocompletableBehavior.php @@ -46,6 +46,15 @@ class AutocompletableBehavior extends Behavior 'limit' => 10 ]; + public function initialize(array $config) + { + foreach (['index', 'fields', 'order', 'limit'] as $conf) { + if (isset($config[$conf])) { + $this->_config[$conf] = $config[$conf]; + } + } + } + public function Autocomplete($search) { $query = $this->getTable()->find(); diff --git a/src/Model/Entity/Contribution.php b/src/Model/Entity/Contribution.php new file mode 100644 index 0000000000..4a08cc51cb --- /dev/null +++ b/src/Model/Entity/Contribution.php @@ -0,0 +1,43 @@ +. + */ +namespace App\Model\Entity; + +use Cake\ORM\Entity; + +class Contribution extends Entity +{ + protected function _getOldFormat() + { + return [ + 'Contribution' => [ + 'id' => $this->id, + 'sentence_id' => $this->sentence_id, + 'sentence_lang' => $this->sentence_lang, + 'translation_id' => $this->translation_id, + 'translation_lang' => $this->translation_lang, + 'script' => $this->script, + 'text' => $this->text, + 'action' => $this->action, + 'user_id' => $this->user_id, + 'type' => $this->type, + 'datetime' => $this->datetime, + ] + ]; + } +} \ No newline at end of file diff --git a/src/Model/Entity/UsersSentence.php b/src/Model/Entity/UsersSentence.php new file mode 100644 index 0000000000..d33d5be5f7 --- /dev/null +++ b/src/Model/Entity/UsersSentence.php @@ -0,0 +1,43 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * @category PHP + * @package Tatoeba + * @author HO Ngoc Phuong Trang + * @license Affero General Public License + * @link https://tatoeba.org + */ +namespace App\Model\Entity; + +use Cake\ORM\Entity; + +class UsersSentence extends Entity +{ + protected function _getOldFormat() + { + return [ + 'UsersSentences' => [ + 'id' => $this->id, + 'user_id' => $this->user_id, + 'sentence_id' => $this->sentence_id, + 'correctness' => $this->correctness, + 'dirty' => $this->dirty + ] + ]; + } +} \ No newline at end of file diff --git a/src/Model/Entity/Wall.php b/src/Model/Entity/Wall.php new file mode 100644 index 0000000000..272e7cd510 --- /dev/null +++ b/src/Model/Entity/Wall.php @@ -0,0 +1,40 @@ +. + */ +namespace App\Model\Entity; + +use Cake\ORM\Entity; + +class Wall extends Entity +{ + protected function _getOldFormat() + { + return [ + 'Wall' => [ + 'id' => $this->id, + 'owner' => $this->owner, + 'date' => $this->date, + 'parent_id' => $this->parent_id, + 'content' => $this->content, + 'modified' => $this->modified, + 'lft' => $this->lft, + 'rght' => $this->rght, + ] + ]; + } +} \ No newline at end of file diff --git a/src/Model/Table/SuperTagsTable.php b/src/Model/Table/SuperTagsTable.php new file mode 100644 index 0000000000..2fd7bbd434 --- /dev/null +++ b/src/Model/Table/SuperTagsTable.php @@ -0,0 +1,101 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace App\Model\Table; + +use App\Model\CurrentUser; +use Cake\Database\Schema\TableSchema; +use Cake\ORM\Table; +use Cake\Event\Event; + +class SuperTagsTable extends Table +{ + public function initialize(array $config) + { + $this->belongsTo('Users'); + $this->hasMany('TagsSuperTags'); + $this->addBehavior('Autocompletable', [ + 'fields' => ['name', 'id'], + 'order' => [], + ]); + } + + /** + * Select a supertag + * + * @param String $name + * + * @return Integer + */ + public function getIdFromName($name) { + $result = $this->find('all') + ->where(['name' => $name]) + ->select(['id']) + ->first(); + + return $result ? $result->id : null; + } + + /** + * Add a supertag + * + * @param String $name + * @param String $description + * @param int $userId + * + * @return bool + */ + public function create($name, $description, $userId) + { + if (!empty($name)){ + $exists = $this->find('all') + ->where(['name' => $name]) + ->count(); + if ($exists > 0) + return false; + + $data = $this->newEntity([ + 'name' => $name, + 'description' => $description, + 'user_id' => $userId + ]); + $added = $this->save($data); + return $added; + } + else + return false; + + } + + /** + * Remove a supertag + * + * @param int $id + */ + public function remove($superTagId) + { + // check whether the supertag has no children + $children = $this->TagsSuperTags->find('all') + ->where(['parent' => $superTagId]) + ->count(); + if ($children == 0) + $this->deleteAll(['id' => $superTagId]); + + return ($children == 0); + } +} diff --git a/src/Model/Table/TagsLinksTable.php b/src/Model/Table/TagsLinksTable.php deleted file mode 100644 index 317664cae0..0000000000 --- a/src/Model/Table/TagsLinksTable.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -namespace App\Model\Table; - -use App\Model\CurrentUser; -use Cake\Database\Schema\TableSchema; -use Cake\ORM\Table; -use Cake\Event\Event; - -class TagsLinksTable extends Table -{ - public function initialize(array $config) - { - // $this->hasMany('Tags'); - $this->belongsToMany('Tags'); - $this->belongsTo('Users'); - $this->addBehavior('Timestamp'); - } - - /** - * Add a link - * - * @param String $parent - * @param String $child - * @param int $userId - * - * @return bool - */ - public function addLink($parent, $child, $userId) - { - // parent and child must reference the Tag database - $id_parent = $this->Tags->getIdFromName($parent); - $id_child = $this->Tags->getIdFromName($child); - if ($id_parent == null || $id_child == null){ - return false; - } - // parent and child must be different - if ($id_parent == $id_child) - return false; - - // prevent cycles: there must not exist a path from $child to $parent - $candidates = array($id_child); - $cycle = false; - while (!$cycle && count($candidates)) { - $new_candidates = array(); - foreach ($this->find('all')->where(['parent IN' => $candidates])->select(['child']) as $key => $value){ - array_push($new_candidates, $value['child']); - } - $candidates = $new_candidates; - $cycle = in_array($id_parent, $candidates); - } - if ($cycle) - return false; - - $data = $this->newEntity([ - 'parent' => $id_parent, - 'child' => $id_child, - 'user_id' => $userId - ]); - $added = $this->save($data); - return $added; - } - - /** - * Remove a link - * - * @param int $parent - * @param int $child - */ - public function removeLink($parent, $child) - { - $this->deleteAll([ - 'parent' => $parent, - 'child' => $child - ]); - } -} diff --git a/src/Model/Table/TagsSuperTagsTable.php b/src/Model/Table/TagsSuperTagsTable.php new file mode 100644 index 0000000000..7012217956 --- /dev/null +++ b/src/Model/Table/TagsSuperTagsTable.php @@ -0,0 +1,123 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace App\Model\Table; + +use App\Model\CurrentUser; +use Cake\Database\Schema\TableSchema; +use Cake\ORM\Table; +use Cake\Event\Event; +use Cake\Utility\ClassRegistry; + +class TagsSuperTagsTable extends Table +{ + public function initialize(array $config) + { + $this->belongsTo('Users'); + $this->belongsTo('Tags'); + $this->belongsTo('SuperTags'); + } + + /** + * Add a link + * + * @param String $parent + * @param String $child + * @param String $childType + * @param int $userId + * + * @return bool + */ + public function create($parent, $child, $childType, $userId) + { + // parent must reference SuperTag and child must reference childType (tag || superTag) + $idParent = $this->SuperTags->getIdFromName($parent); + if ($childType == 'tag') + $idChild = $this->Tags->getIdFromName($child); + else + $idChild = $this->SuperTags->getIdFromName($child); + + // both child and parent must be true references + if ($idParent == null || $idChild == null) + return false; + + // duplicates are not allowed + $exists = $this->find('all') + ->where([ + 'parent' => $idParent, + 'child' => $idChild, + 'child_type' => $childType + ])->count(); + if ($exists > 0) + return false; + + if ($childType == 'superTag') { + // child and parent must be different + if ($idParent == $idChild) + return false; + + // there must not already exist a path from child to parent + $candidates = array($idChild); + $cycle = false; + while (!$cycle && count($candidates)) { + $new_candidates = array(); + $result = $this->find('all') + ->where([ + 'parent IN' => $candidates, + 'child_type = ' => 'superTag' + ]) + ->select(['child']); + foreach ($result as $key => $value) + array_push($new_candidates, $value['child']); + + $candidates = $new_candidates; + $cycle = in_array($idParent, $candidates); + } + if ($cycle) + return false; + } + + $data = $this->newEntity([ + 'parent' => $idParent, + 'child' => $idChild, + 'child_type' => $childType, + 'user_id' => $userId + ]); + $added = $this->save($data); + return $added; + } + + /** + * Remove a link + * + * @param int $parent + * @param int $child + * @param String $childType + */ + public function remove($parent, $child, $childType) + { + // if $childType == tag, detach child (tag) from parent (superTag) + // else if $childType == superTag, detag child (superTag) from parent (superTag), along with all its children + $this->deleteAll([ + 'parent' => $parent, + 'child' => $child, + 'child_type' => $childType + ]); + return true; + } +} diff --git a/src/Template/Element/superTag.ctp b/src/Template/Element/superTag.ctp new file mode 100644 index 0000000000..82b36f485b --- /dev/null +++ b/src/Template/Element/superTag.ctp @@ -0,0 +1,110 @@ +Url->build([ + 'controller' => 'tagsSuperTags', + 'action' => 'removeTagSuperTag', + $root, + $parent, + 'superTag' +]); + +$deleteUrl = $this->Url->build([ + 'controller' => 'tagsSuperTags', + 'action' => 'removeSuperTag', + $parent +]); +?> + + + + + + + + + + + reply + + + + + + + + delete + + + + + + 0) { + ?> + + {{vm.hiddenReplies[] ? 'expand_more' : 'expand_less'}} + + + + + + + + + + + + Url->build([ + 'controller' => 'tagsSuperTags', + 'action' => 'removeTagSuperTag', + $parent, + $tag, + 'tag' + ]); + ?> + + + + reply + + + + + + + 0) { + ?> +
+ $new_children) { + echo $this->element('superTag', [ + 'root' => $parent, + 'depth' => $depth + 1, + 'parent' => $new_parent, + 'children' => $new_children, + ]); + } + ?> +
+ +
+
\ No newline at end of file diff --git a/src/Template/Element/tagslink.ctp b/src/Template/Element/tagslink.ctp deleted file mode 100644 index 3dc21d8e53..0000000000 --- a/src/Template/Element/tagslink.ctp +++ /dev/null @@ -1,61 +0,0 @@ -Url->build([ - 'controller' => 'tagsLinks', - 'action' => 'remove', - $root, - $parent -]); - -$cssClass = $depth == 0 ? 'wall-thread' : 'reply'; -?> - - - - - - - - 0 && count($children) == 0) { ?> - - delete - - - 0) { ?> - - {{vm.hiddenReplies[] ? 'expand_more' : 'expand_less'}} - - - - - - - - - - 0) { ?> -
- $new_children) { - echo $this->element('tagslink', [ - 'root' => $parent, - 'parent' => $new_parent, - 'children' => $new_children, - 'labels' => $labels, - 'depth' => $depth + 1 - ]); - } - ?> -
- -
-
\ No newline at end of file diff --git a/src/Template/Element/top_menu.ctp b/src/Template/Element/top_menu.ctp index 656808578c..d306ca1024 100644 --- a/src/Template/Element/top_menu.ctp +++ b/src/Template/Element/top_menu.ctp @@ -71,8 +71,8 @@ $menuElements = array( "controller" => "tags", "action" => "view_all" ), - __('Manage tags') => array( - "controller" => "tagsLinks", + __('Manage supertags') => array( + "controller" => "tagsSuperTags", "action" => "manage" ), /* @translators: menu item on the top (verb) */ diff --git a/src/Template/TagsLinks/manage.ctp b/src/Template/TagsLinks/manage.ctp deleted file mode 100644 index 2937de8ef1..0000000000 --- a/src/Template/TagsLinks/manage.ctp +++ /dev/null @@ -1,85 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * PHP version 5 - * - * @category PHP - * @package Tatoeba - * @author Allan SIMON - * @license Affero General Public License - * @link https://tatoeba.org - */ - -$this->set('title_for_layout', $this->Pages->formatTitle(__('Structure tags'))); -echo $this->Html->css('autocompletion.css'); -$this->AssetCompress->script('sentences-block-for-members.js', ['block' => 'scriptBottom']); -$this->Html->script('autocompletion.js', ['block' => 'scriptBottom']); -$this->Html->script('tagslinks.js', ['block' => 'scriptBottom']); - -?> - -
-
- -
-

-
-
- - Form->create('TagsLinks', [ - 'url' => array('action' => 'add'), - ]); - ?> - - - Form->input('parentTag'); - ?> -
-
- - Form->input('childTag'); - ?> -
-
- - - - - - Form->end(); - ?> -
- -
- $children) { - echo $this->element('tagslink', [ - 'root' => 0, - 'parent' => $parent, - 'children' => $children, - 'labels' => $all_tags, - 'depth' => 0 - ]); - } - ?> -
-
\ No newline at end of file diff --git a/src/Template/TagsSuperTags/manage.ctp b/src/Template/TagsSuperTags/manage.ctp new file mode 100644 index 0000000000..943df25888 --- /dev/null +++ b/src/Template/TagsSuperTags/manage.ctp @@ -0,0 +1,189 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * PHP version 5 + * + * @category PHP + * @package Tatoeba + * @author Alexandre Magueresse + * @license Affero General Public License + * @link https://tatoeba.org + */ + +$this->set('title_for_layout', $this->Pages->formatTitle(__('Super tags management'))); +echo $this->Html->css('autocompletion.css'); +$this->AssetCompress->script('sentences-block-for-members.js', ['block' => 'scriptBottom']); +$this->Html->script('autocompletion.js', ['block' => 'scriptBottom']); +$this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']); + +?> + +
+
+ +
+

+
+
+ +
+ request['?']) && isset($this->request['?']['superTagAdded']) && $this->request['?']['superTagAdded'] == '0'){ + ?> +
+ +
+ + + Form->create( + 'CreateSuperTag', + [ + 'url' => [ + 'action' => 'createSuperTag' + ], + ] + ); + ?> + + + Form->input( + 'name', + [ + 'label' => __('Name (required)') + ] + ); + ?> + + + + Form->input( + 'description', + [ + 'label' => __('Description (optional)') + ] + ); + ?> + + + + + + + + Form->end(); + ?> +
+
+ +
+ +
+

+
+
+ +
+ request['?']) && isset($this->request['?']['tagSuperTagAdded']) && $this->request['?']['tagSuperTagAdded'] == '0'){ + ?> +
+ +
+ + + Form->create( + 'CreateTagSuperTag', + [ + 'url' => [ + 'action' => 'createTagSuperTag' + ], + ] + ); + ?> + + + Form->input( + 'parent', + [ + 'label' => __('Parent (super tag)') + ] + ); + ?> +
+
+ + + Form->select( + 'childType', + [__('Tag'), __('Super tag')], + [ + 'label' => __('Child type'), + 'id' => 'child_type', + 'empty' => __('Child type (choose one)'), + ] + ); + ?> + + + + Form->input( + 'child', + [ + 'label' => __('Child (tag or super tag)') + ] + ); + ?> +
+
+ + + + + + + Form->end(); + ?> +
+
+ +
+ $children) { + echo $this->element('superTag', [ + 'root' => -1, + 'depth' => 0, + 'parent' => $parent, + 'children' => $children, + ]); + } + ?> +
+
diff --git a/webroot/js/autocompletion.js b/webroot/js/autocompletion.js index 497a9dce80..849e4edd8e 100644 --- a/webroot/js/autocompletion.js +++ b/webroot/js/autocompletion.js @@ -17,7 +17,10 @@ */ class AutocompleteBox { - constructor(inputId, divId) { + constructor(url, format, inputId, divId) { + this.url = get_tatoeba_root_url() + "/" + url + "/"; + this.format = format; + this.currentSuggestPosition = -1; this.countBeforeRequest = 0; this. suggestLength = 0; @@ -104,11 +107,10 @@ class AutocompleteBox { return; } - var rootUrl = get_tatoeba_root_url(); var that = this; if (tag != this.previousText) { $.get( - rootUrl + "/tags/autocomplete/" + tag, + this.url + tag, function(data) { that.suggestShowResults(data); } @@ -119,18 +121,18 @@ class AutocompleteBox { suggestShowResults(suggestions) { this.removeSuggestList(); - if (suggestions.allTags.length == 0) { + if (suggestions.results.length == 0) { return; } - this.suggestLength = suggestions.allTags.length; + this.suggestLength = suggestions.results.length; this.isSuggestListActive = true; var that = this; var ul = document.createElement("ul"); $(this.divTag).append(ul); - suggestions.allTags.forEach(function(suggestion, index) { - var text = document.createTextNode(suggestion.name + " (" + suggestion.nbrOfSentences + ")"); + suggestions.results.forEach(function(suggestion, index) { + var text = document.createTextNode(that.format(suggestion)); var link = document.createElement("a"); link.id = "suggestedItem" + index; diff --git a/webroot/js/tags.add.js b/webroot/js/tags.add.js index f07bc866e9..29d5e899c3 100644 --- a/webroot/js/tags.add.js +++ b/webroot/js/tags.add.js @@ -18,7 +18,9 @@ $(document).ready(function() { - new AutocompleteBox("TagTagName", "autocompletionDiv"); + new AutocompleteBox("tags/autocomplete", function(suggestion) { + return suggestion.name + " (" + suggestion.nbrOfSentences + ")"; + }, "TagTagName", "autocompletionDiv"); $("#TagTagName").keydown(function(e){ if (e.keyCode == 13) { diff --git a/webroot/js/tagsSuperTags.js b/webroot/js/tagsSuperTags.js new file mode 100644 index 0000000000..ab80a910c3 --- /dev/null +++ b/webroot/js/tagsSuperTags.js @@ -0,0 +1,38 @@ +(function(){ + 'use strict'; + + new AutocompleteBox("super_tags/autocomplete", function(suggestion) { + return suggestion.name; + }, "parent", "autocompletionParent"); + + $("#child_type").on('change', function(){ + if (parseInt(this.value) == 0) { + new AutocompleteBox("tags/autocomplete", function(suggestion) { + return suggestion.name + " (" + suggestion.nbrOfSentences + ")"; + }, "child", "autocompletionChild"); + } else { + new AutocompleteBox("super_tags/autocomplete", function(suggestion) { + return suggestion.name; + }, "child", "autocompletionChild"); + } + }); + + + + angular + .module('app') + .controller('TagsSuperTagsController', [ + '$http', '$location', '$anchorScroll', TagsSuperTagsController + ]); + + function TagsSuperTagsController($http, $location, $anchorScroll) { + var vm = this; + vm.expandOrCollapse = expandOrCollapse; + vm.hiddenReplies = {}; + + function expandOrCollapse(id) { + console.log("e"); + vm.hiddenReplies[id] = !vm.hiddenReplies[id]; + } + } +})(); \ No newline at end of file diff --git a/webroot/js/tagslinks.js b/webroot/js/tagslinks.js deleted file mode 100644 index bc2eee0d92..0000000000 --- a/webroot/js/tagslinks.js +++ /dev/null @@ -1,23 +0,0 @@ -(function(){ - 'use strict'; - - new AutocompleteBox("parenttag", "autocompletionParent"); - new AutocompleteBox("childtag", "autocompletionChild"); - - angular - .module('app') - .controller('TagsLinksController', [ - '$http', '$location', '$anchorScroll', TagsLinksController - ]); - - function TagsLinksController($http, $location, $anchorScroll) { - var vm = this; - vm.expandOrCollapse = expandOrCollapse; - vm.hiddenReplies = {}; - - function expandOrCollapse(id) { - console.log("e"); - vm.hiddenReplies[id] = !vm.hiddenReplies[id]; - } - } -})(); \ No newline at end of file From 5e06566e295fa8cbc3eaaf738906b63b3eb57915 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Fri, 31 Jul 2020 12:22:50 +0200 Subject: [PATCH 04/11] Harmonized code and handle tag/superTag deletion --- src/Controller/SuperTagsController.php | 28 ++++++++++++++++++++ src/Controller/TagsSuperTagsController.php | 30 ---------------------- src/Model/Table/SuperTagsTable.php | 8 ++++-- src/Model/Table/TagsTable.php | 10 +++++++- src/Template/Element/superTag.ctp | 2 +- src/Template/TagsSuperTags/manage.ctp | 5 ++-- 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/Controller/SuperTagsController.php b/src/Controller/SuperTagsController.php index 4d9e64ca32..99bf1a79cd 100644 --- a/src/Controller/SuperTagsController.php +++ b/src/Controller/SuperTagsController.php @@ -34,6 +34,34 @@ class SuperTagsController extends AppController public $name = 'SuperTags'; public $components = ['CommonSentence', 'Flash']; + /** + * Add a super tag + * + * @return void + */ + public function createSuperTag(){ + $name = $this->request->data('name'); + $description = $this->request->data('description'); + $userId = CurrentUser::get('id'); + + $added = $this->SuperTags->create($name, $description, $userId); + + return $this->redirect([ + 'controller' => 'tags_super_tags', + 'action' => 'manage', + '?' => ['superTagAdded' => $added], + ]); + } + + public function removeSuperTag($superTagId){ + $this->SuperTags->remove($superTagId); + + return $this->redirect([ + 'controller' => 'tags_super_tags', + 'action' => 'manage', + ]); + } + public function autocomplete($search) { $results = $this->SuperTags->Autocomplete($search); diff --git a/src/Controller/TagsSuperTagsController.php b/src/Controller/TagsSuperTagsController.php index d2030050eb..fab66667b5 100644 --- a/src/Controller/TagsSuperTagsController.php +++ b/src/Controller/TagsSuperTagsController.php @@ -98,26 +98,6 @@ public function manage(){ $this->set('all_super_tags_links', $tree); } - /** - * Add a super tag - * - * @return void - */ - public function createSuperTag(){ - $name = $this->request->data('name'); - $description = $this->request->data('description'); - $userId = CurrentUser::get('id'); - - $this->loadModel('SuperTags'); - $added = $this->SuperTags->create($name, $description, $userId); - - return $this->redirect([ - 'controller' => 'tags_super_tags', - 'action' => 'manage', - '?' => ['superTagAdded' => $added], - ]); - } - public function createTagSuperTag(){ $parent = $this->request->data('parent'); $child = $this->request->data('child'); @@ -133,16 +113,6 @@ public function createTagSuperTag(){ ]); } - public function removeSuperTag($superTagId){ - $this->loadModel('SuperTags'); - $this->SuperTags->remove($superTagId); - - return $this->redirect([ - 'controller' => 'tags_super_tags', - 'action' => 'manage', - ]); - } - public function removeTagSuperTag($parent, $child, $childType){ $this->TagsSuperTags->remove($parent, $child, $childType); diff --git a/src/Model/Table/SuperTagsTable.php b/src/Model/Table/SuperTagsTable.php index 2fd7bbd434..41c2fbcdb3 100644 --- a/src/Model/Table/SuperTagsTable.php +++ b/src/Model/Table/SuperTagsTable.php @@ -27,8 +27,10 @@ class SuperTagsTable extends Table { public function initialize(array $config) { - $this->belongsTo('Users'); $this->hasMany('TagsSuperTags'); + $this->belongsToMany('SuperTags'); + + $this->belongsTo('Users'); $this->addBehavior('Autocompletable', [ 'fields' => ['name', 'id'], 'order' => [], @@ -93,8 +95,10 @@ public function remove($superTagId) $children = $this->TagsSuperTags->find('all') ->where(['parent' => $superTagId]) ->count(); - if ($children == 0) + if ($children == 0){ + $this->TagsSuperTags->deleteAll(['child' => $superTagId, 'child_type' => 'superTag']); $this->deleteAll(['id' => $superTagId]); + } return ($children == 0); } diff --git a/src/Model/Table/TagsTable.php b/src/Model/Table/TagsTable.php index 463e5b8b43..b701615ef3 100644 --- a/src/Model/Table/TagsTable.php +++ b/src/Model/Table/TagsTable.php @@ -49,6 +49,9 @@ public function getOKTagName() public function initialize(array $config) { + $this->hasMany('TagsSuperTags'); + $this->belongsToMany('SuperTags'); + $this->hasMany('TagsSentences'); $this->belongsToMany('Sentences'); $this->belongsTo('Users'); @@ -129,10 +132,15 @@ public function removeTagFromSentence($tagId, $sentenceId) { if (!$this->canRemoveTagFromSentence($tagId, $sentenceId)) { return false; } - return $this->TagsSentences->removeTagFromSentence( + $count = $this->TagsSentences->removeTagFromSentence( $tagId, $sentenceId ); + + if ($this->getNameFromId($tagId) == null) + $this->TagsSuperTags->deleteAll(['child' => $tagId, 'child_type' => 'tag']); + + return $count; } private function canRemoveTagFromSentence($tagId, $sentenceId) { diff --git a/src/Template/Element/superTag.ctp b/src/Template/Element/superTag.ctp index 82b36f485b..f2d5927efd 100644 --- a/src/Template/Element/superTag.ctp +++ b/src/Template/Element/superTag.ctp @@ -11,7 +11,7 @@ $detachUrl = $this->Url->build([ ]); $deleteUrl = $this->Url->build([ - 'controller' => 'tagsSuperTags', + 'controller' => 'superTags', 'action' => 'removeSuperTag', $parent ]); diff --git a/src/Template/TagsSuperTags/manage.ctp b/src/Template/TagsSuperTags/manage.ctp index 943df25888..fc8de62f47 100644 --- a/src/Template/TagsSuperTags/manage.ctp +++ b/src/Template/TagsSuperTags/manage.ctp @@ -46,7 +46,7 @@ $this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']); if (isset($this->request['?']) && isset($this->request['?']['superTagAdded']) && $this->request['?']['superTagAdded'] == '0'){ ?>
- +
Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']); 'CreateSuperTag', [ 'url' => [ + 'controller' => 'superTags', 'action' => 'createSuperTag' ], ] @@ -108,7 +109,7 @@ $this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']); if (isset($this->request['?']) && isset($this->request['?']['tagSuperTagAdded']) && $this->request['?']['tagSuperTagAdded'] == '0'){ ?>
- +
Date: Thu, 20 Aug 2020 22:40:13 +0200 Subject: [PATCH 05/11] Changed data structure to simplify the implementation and use the Tree behaviour --- .../20200729155842_CreateTagsSuperTags.php | 42 ----- ...> 20200820134346_CreateCategoriesTree.php} | 21 ++- .../20200820134407_AddCategoryIdToTags.php | 23 +++ config/auth_actions.php | 3 +- src/Controller/CategoriesTreeController.php | 147 ++++++++++++++++++ src/Controller/SuperTagsController.php | 73 --------- src/Controller/TagsSuperTagsController.php | 124 --------------- ...rTagsTable.php => CategoriesTreeTable.php} | 89 +++++------ src/Model/Table/TagsSuperTagsTable.php | 123 --------------- src/Model/Table/TagsTable.php | 23 ++- .../manage.ctp | 111 ++++++------- src/Template/Element/categories_tree.ctp | 93 +++++++++++ src/Template/Element/superTag.ctp | 110 ------------- src/Template/Element/top_menu.ctp | 4 +- webroot/js/categoriesTree.js | 32 ++++ webroot/js/tagsSuperTags.js | 38 ----- 16 files changed, 427 insertions(+), 629 deletions(-) delete mode 100644 config/Migrations/20200729155842_CreateTagsSuperTags.php rename config/Migrations/{20200729155726_CreateSuperTags.php => 20200820134346_CreateCategoriesTree.php} (56%) create mode 100644 config/Migrations/20200820134407_AddCategoryIdToTags.php create mode 100644 src/Controller/CategoriesTreeController.php delete mode 100644 src/Controller/SuperTagsController.php delete mode 100644 src/Controller/TagsSuperTagsController.php rename src/Model/Table/{SuperTagsTable.php => CategoriesTreeTable.php} (60%) delete mode 100644 src/Model/Table/TagsSuperTagsTable.php rename src/Template/{TagsSuperTags => CategoriesTree}/manage.ctp (65%) create mode 100644 src/Template/Element/categories_tree.ctp delete mode 100644 src/Template/Element/superTag.ctp create mode 100644 webroot/js/categoriesTree.js delete mode 100644 webroot/js/tagsSuperTags.js diff --git a/config/Migrations/20200729155842_CreateTagsSuperTags.php b/config/Migrations/20200729155842_CreateTagsSuperTags.php deleted file mode 100644 index 3b3be713dd..0000000000 --- a/config/Migrations/20200729155842_CreateTagsSuperTags.php +++ /dev/null @@ -1,42 +0,0 @@ -table('tags_super_tags'); - $table->addColumn('parent', 'integer', [ - 'default' => null, - 'limit' => 11, - 'null' => false, - ]); - $table->addColumn('child', 'integer', [ - 'default' => null, - 'limit' => 11, - 'null' => false, - ]); - $table->addColumn('child_type', 'enum', [ - 'default' => null, - 'null' => false, - 'values' => ['tag', 'superTag'], - ]); - $table->addColumn('user_id', 'integer', [ - 'default' => null, - 'limit' => 11, - 'null' => true, - ]); - $table->addColumn('added_time', 'datetime', [ - 'default' => 'CURRENT_TIMESTAMP', - 'null' => true, - ]); - $table->create(); - } -} diff --git a/config/Migrations/20200729155726_CreateSuperTags.php b/config/Migrations/20200820134346_CreateCategoriesTree.php similarity index 56% rename from config/Migrations/20200729155726_CreateSuperTags.php rename to config/Migrations/20200820134346_CreateCategoriesTree.php index 0b5a7eeb30..f8755be13e 100644 --- a/config/Migrations/20200729155726_CreateSuperTags.php +++ b/config/Migrations/20200820134346_CreateCategoriesTree.php @@ -1,7 +1,7 @@ table('super_tags'); + $table = $this->table('categories_tree'); $table->addColumn('name', 'string', [ 'default' => null, - 'limit' => 50, + 'limit' => 255, 'null' => false, ]); - $table->addColumn('description', 'string', [ + $table->addColumn('description', 'text', [ 'default' => null, - 'limit' => 500, 'null' => true, ]); - $table->addColumn('user_id', 'integer', [ + $table->addColumn('parent_id', 'integer', [ 'default' => null, 'limit' => 11, 'null' => true, ]); - $table->addColumn('created', 'datetime', [ - 'default' => 'CURRENT_TIMESTAMP', + $table->addColumn('lft', 'integer', [ + 'default' => null, + 'limit' => 11, + 'null' => true, + ]); + $table->addColumn('rght', 'integer', [ + 'default' => null, + 'limit' => 11, 'null' => true, ]); $table->create(); diff --git a/config/Migrations/20200820134407_AddCategoryIdToTags.php b/config/Migrations/20200820134407_AddCategoryIdToTags.php new file mode 100644 index 0000000000..bc0df96bcf --- /dev/null +++ b/config/Migrations/20200820134407_AddCategoryIdToTags.php @@ -0,0 +1,23 @@ +table('tags'); + $table->addColumn('category_id', 'integer', [ + 'default' => null, + 'limit' => 11, + 'null' => false, + ]); + $table->update(); + } +} diff --git a/config/auth_actions.php b/config/auth_actions.php index d979359679..ba107cd2cb 100644 --- a/config/auth_actions.php +++ b/config/auth_actions.php @@ -110,9 +110,8 @@ 'edit_correctness' => [ User::ROLE_ADMIN ], ], 'sentences_lists' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], - 'super_tags' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], 'tags' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], - 'tags_super_tags' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], + 'categories_tree' => [ '*' => User::ROLE_ADV_CONTRIBUTOR_OR_HIGHER ], 'transcriptions' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'user' => [ '*' => User::ROLE_CONTRIBUTOR_OR_HIGHER ], 'users' => [ '*' => [ User::ROLE_ADMIN ] ], diff --git a/src/Controller/CategoriesTreeController.php b/src/Controller/CategoriesTreeController.php new file mode 100644 index 0000000000..f0a26703cb --- /dev/null +++ b/src/Controller/CategoriesTreeController.php @@ -0,0 +1,147 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace App\Controller; + +use App\Controller\AppController; +use Cake\Event\Event; +use App\Model\CurrentUser; + + +class CategoriesTreeController extends AppController +{ + /** + * Controller name + * + * @var string + * @access public + */ + public $name = 'CategoriesTree'; + public $components = ['CommonSentence', 'Flash']; + public $helpers = ['Pagination']; + + public function manage(){ + $tags = []; + $this->loadModel('Tags'); + foreach ($this->Tags->find('all')->select(['id', 'name', 'nbrOfSentences', 'category_id']) as $value) { + $category = ($value['category_id'] == null) ? -1 : $value['category_id']; + if (!array_key_exists($category, $tags)) { + $tags[$category] = []; + } + array_push($tags[$category], [ + 'id' => $value['id'], + 'name' => $value['name'], + 'nbrOfSentences' => $value['nbrOfSentences'] + ]); + + } + + $treeList = $this->CategoriesTree->find('treeList'); + + $tree = []; + $depths = []; + foreach ($treeList as $key => $value) { + $d = 0; + while ($d < strlen($value) && $value[$d] == '_') + $d++; + array_push($depths, $d); + + array_push($tree, [ + 'id' => $key, + 'name' => substr($value, $d), + 'children' => [] + ]); + } + $maxDepth = max($depths); + + for ($d=$maxDepth; $d > 0; $d--) { + $i = 0; + while ($i < sizeof($tree)) { + if ($i < sizeof($depths) && $depths[$i] == $d) { + array_push($tree[$i-1]['children'], $tree[$i]); + unset($tree[$i]); + unset($depths[$i]); + $tree = array_values($tree); + $depths = array_values($depths); + } else + $i++; + } + } + + $this->set('tags', $tags); + $this->set('tree', $tree); + } + + public function createCategory() { + $name = $this->request->data('name'); + $description = $this->request->data('description'); + $parentName = $this->request->data('parentName'); + + $res = $this->CategoriesTree->create($name, $description, $parentName); + + return $this->redirect([ + 'controller' => 'categories_tree', + 'action' => 'manage', + '?' => ['createCategory' => $res], + ]); + } + + public function removeCategory($categoryId) { + $res = $this->CategoriesTree->remove($categoryId); + + return $this->redirect([ + 'controller' => 'categories_tree', + 'action' => 'manage', + '?' => ['removeCategory' => $res], + ]); + } + + public function attachTagToCategory() { + $tagName = $this->request->data('tagName'); + $categoryName = $this->request->data('categoryName'); + + $this->loadModel('Tags'); + $res = $this->Tags->attachToCategory($tagName, $categoryName); + + return $this->redirect([ + 'controller' => 'categories_tree', + 'action' => 'manage', + '?' => ['attachTagToCategory' => $res], + ]); + } + + public function detachTagFromCategory($tagId) { + $this->loadModel('Tags'); + $res = $this->Tags->detachFromCategory($tagId); + + return $this->redirect([ + 'controller' => 'categories_tree', + 'action' => 'manage', + '?' => ['detachTagFromCategory' => $res], + ]); + } + + public function autocomplete($search) { + $results = $this->CategoriesTree->Autocomplete($search); + + $this->loadComponent('RequestHandler'); + $this->set('results', $results); + $this->set('_serialize', ['results']); + $this->RequestHandler->renderAs($this, 'json'); + } +} \ No newline at end of file diff --git a/src/Controller/SuperTagsController.php b/src/Controller/SuperTagsController.php deleted file mode 100644 index 99bf1a79cd..0000000000 --- a/src/Controller/SuperTagsController.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -namespace App\Controller; - -use App\Controller\AppController; -use Cake\Event\Event; -use App\Model\CurrentUser; - - -class SuperTagsController extends AppController -{ - /** - * Controller name - * - * @var string - * @access public - */ - public $name = 'SuperTags'; - public $components = ['CommonSentence', 'Flash']; - - /** - * Add a super tag - * - * @return void - */ - public function createSuperTag(){ - $name = $this->request->data('name'); - $description = $this->request->data('description'); - $userId = CurrentUser::get('id'); - - $added = $this->SuperTags->create($name, $description, $userId); - - return $this->redirect([ - 'controller' => 'tags_super_tags', - 'action' => 'manage', - '?' => ['superTagAdded' => $added], - ]); - } - - public function removeSuperTag($superTagId){ - $this->SuperTags->remove($superTagId); - - return $this->redirect([ - 'controller' => 'tags_super_tags', - 'action' => 'manage', - ]); - } - - public function autocomplete($search) { - $results = $this->SuperTags->Autocomplete($search); - - $this->loadComponent('RequestHandler'); - $this->set('results', $results); - $this->set('_serialize', ['results']); - $this->RequestHandler->renderAs($this, 'json'); - } -} diff --git a/src/Controller/TagsSuperTagsController.php b/src/Controller/TagsSuperTagsController.php deleted file mode 100644 index fab66667b5..0000000000 --- a/src/Controller/TagsSuperTagsController.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -namespace App\Controller; - -use App\Controller\AppController; -use Cake\Event\Event; -use App\Model\CurrentUser; - - -class TagsSuperTagsController extends AppController -{ - /** - * Controller name - * - * @var string - * @access public - */ - public $name = 'TagsSuperTags'; - public $components = ['CommonSentence', 'Flash']; - public $helpers = ['Pagination']; - - public function manage(){ - $all_tags = []; - $this->loadModel('Tags'); - foreach ($this->Tags->find('all')->select(['id', 'name', 'nbrOfSentences']) as $key => $value) - $all_tags[$value['id']] = [ - 'name' => $value['name'], - 'nbrOfSentences' => $value['nbrOfSentences'] - ]; - - $all_superTags = []; - $all_tagsLinks = []; - $all_superTagsLinks = []; - $this->loadModel('SuperTags'); - foreach ($this->SuperTags->find('all')->select(['id', 'name']) as $value) { - $superTagId = $value['id']; - $all_superTags[$superTagId] = $value['name']; - $all_tagsLinks[$superTagId] = []; - $all_superTagsLinks[$superTagId] = []; - } - - $remaining_links = 0; - $non_leaves = []; - foreach ($this->TagsSuperTags->find('all')->select(['parent', 'child', 'child_type']) as $value) { - if ($value['child_type'] == 'tag') - array_push($all_tagsLinks[$value['parent']], $value['child']); - else { - array_push($all_superTagsLinks[$value['child']], $value['parent']); - $non_leaves[$value['parent']] = 1; - $remaining_links += 1; - } - } - - $tree = []; - $leaves = array_diff(array_keys($all_superTags), array_keys($non_leaves)); - foreach ($leaves as $leave) - $tree[$leave] = []; - - while ($remaining_links) { - foreach ($all_superTagsLinks as $child => $parents) { - if (count($parents) && array_key_exists($child, $tree)){ - // 1. copy - $subtree = $tree[$child]; - // 2. remove - unset($tree[$child]); - // 3. replace - foreach ($parents as $parent) { - if (!array_key_exists($parent, $tree)) - $tree[$parent] = []; - $tree[$parent][$child] = $subtree; - } - // 4. remove used links - $remaining_links -= count($parents); - $all_superTagsLinks[$child] = []; - } - } - } - - $this->set('all_tags', $all_tags); - $this->set('all_super_tags', $all_superTags); - $this->set('all_tags_links', $all_tagsLinks); - $this->set('all_super_tags_links', $tree); - } - - public function createTagSuperTag(){ - $parent = $this->request->data('parent'); - $child = $this->request->data('child'); - $childType = ($this->request->data('childType') == 0) ? 'tag' : 'superTag'; - $userId = CurrentUser::get('id'); - - $added = $this->TagsSuperTags->create($parent, $child, $childType, $userId); - - return $this->redirect([ - 'controller' => 'tags_super_tags', - 'action' => 'manage', - '?' => ['tagSuperTagAdded' => $added], - ]); - } - - public function removeTagSuperTag($parent, $child, $childType){ - $this->TagsSuperTags->remove($parent, $child, $childType); - - return $this->redirect([ - 'controller' => 'tags_super_tags', - 'action' => 'manage', - ]); - } -} \ No newline at end of file diff --git a/src/Model/Table/SuperTagsTable.php b/src/Model/Table/CategoriesTreeTable.php similarity index 60% rename from src/Model/Table/SuperTagsTable.php rename to src/Model/Table/CategoriesTreeTable.php index 41c2fbcdb3..2504da24a8 100644 --- a/src/Model/Table/SuperTagsTable.php +++ b/src/Model/Table/CategoriesTreeTable.php @@ -18,88 +18,85 @@ */ namespace App\Model\Table; -use App\Model\CurrentUser; use Cake\Database\Schema\TableSchema; use Cake\ORM\Table; use Cake\Event\Event; +use Cake\Utility\ClassRegistry; -class SuperTagsTable extends Table +class CategoriesTreeTable extends Table { public function initialize(array $config) { - $this->hasMany('TagsSuperTags'); - $this->belongsToMany('SuperTags'); - - $this->belongsTo('Users'); + $this->hasMany('Tags')->setForeignKey('category_id'); + + $this->addBehavior('Tree'); $this->addBehavior('Autocompletable', [ 'fields' => ['name', 'id'], - 'order' => [], + 'order' => [] ]); } /** - * Select a supertag - * - * @param String $name - * - * @return Integer - */ - public function getIdFromName($name) { - $result = $this->find('all') - ->where(['name' => $name]) - ->select(['id']) - ->first(); - - return $result ? $result->id : null; - } - - /** - * Add a supertag + * Add a tag category * * @param String $name * @param String $description - * @param int $userId + * @param Integer $parentId * * @return bool */ - public function create($name, $description, $userId) + public function create($name, $description, $parentName) { - if (!empty($name)){ - $exists = $this->find('all') + if (empty($name)) + return false; + else { + $count = $this->find('all') ->where(['name' => $name]) ->count(); - if ($exists > 0) + if ($count > 0) return false; $data = $this->newEntity([ 'name' => $name, 'description' => $description, - 'user_id' => $userId + 'parent_id' => $this->getIdFromName($parentName) ]); - $added = $this->save($data); - return $added; + return $this->save($data); } - else - return false; } /** - * Remove a supertag + * Remove a tag category * - * @param int $id + * @param Integer $categoryId */ - public function remove($superTagId) + public function remove($categoryId) { - // check whether the supertag has no children - $children = $this->TagsSuperTags->find('all') - ->where(['parent' => $superTagId]) - ->count(); - if ($children == 0){ - $this->TagsSuperTags->deleteAll(['child' => $superTagId, 'child_type' => 'superTag']); - $this->deleteAll(['id' => $superTagId]); - } + // check this category contains no tag + $tagsCount = $this->Tags->find('all', [ + 'conditions' => ['category_id' => $categoryId] + ])->count(); + if ($tagsCount > 0) + return false; + + // check this category contains no other category + $categoriesCount = $this->find('all', [ + 'conditions' => ['parent_id' => $categoryId] + ])->count(); + if ($categoriesCount > 0) + return false; - return ($children == 0); + return $this->deleteAll(['id' => $categoryId]); } + + public function getIdFromName($name) { + $result = $this->find('all') + ->where(['name' => $name]) + ->select(['id']) + ->first(); + + return $result ? $result->id : null; + } + } diff --git a/src/Model/Table/TagsSuperTagsTable.php b/src/Model/Table/TagsSuperTagsTable.php deleted file mode 100644 index 7012217956..0000000000 --- a/src/Model/Table/TagsSuperTagsTable.php +++ /dev/null @@ -1,123 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -namespace App\Model\Table; - -use App\Model\CurrentUser; -use Cake\Database\Schema\TableSchema; -use Cake\ORM\Table; -use Cake\Event\Event; -use Cake\Utility\ClassRegistry; - -class TagsSuperTagsTable extends Table -{ - public function initialize(array $config) - { - $this->belongsTo('Users'); - $this->belongsTo('Tags'); - $this->belongsTo('SuperTags'); - } - - /** - * Add a link - * - * @param String $parent - * @param String $child - * @param String $childType - * @param int $userId - * - * @return bool - */ - public function create($parent, $child, $childType, $userId) - { - // parent must reference SuperTag and child must reference childType (tag || superTag) - $idParent = $this->SuperTags->getIdFromName($parent); - if ($childType == 'tag') - $idChild = $this->Tags->getIdFromName($child); - else - $idChild = $this->SuperTags->getIdFromName($child); - - // both child and parent must be true references - if ($idParent == null || $idChild == null) - return false; - - // duplicates are not allowed - $exists = $this->find('all') - ->where([ - 'parent' => $idParent, - 'child' => $idChild, - 'child_type' => $childType - ])->count(); - if ($exists > 0) - return false; - - if ($childType == 'superTag') { - // child and parent must be different - if ($idParent == $idChild) - return false; - - // there must not already exist a path from child to parent - $candidates = array($idChild); - $cycle = false; - while (!$cycle && count($candidates)) { - $new_candidates = array(); - $result = $this->find('all') - ->where([ - 'parent IN' => $candidates, - 'child_type = ' => 'superTag' - ]) - ->select(['child']); - foreach ($result as $key => $value) - array_push($new_candidates, $value['child']); - - $candidates = $new_candidates; - $cycle = in_array($idParent, $candidates); - } - if ($cycle) - return false; - } - - $data = $this->newEntity([ - 'parent' => $idParent, - 'child' => $idChild, - 'child_type' => $childType, - 'user_id' => $userId - ]); - $added = $this->save($data); - return $added; - } - - /** - * Remove a link - * - * @param int $parent - * @param int $child - * @param String $childType - */ - public function remove($parent, $child, $childType) - { - // if $childType == tag, detach child (tag) from parent (superTag) - // else if $childType == superTag, detag child (superTag) from parent (superTag), along with all its children - $this->deleteAll([ - 'parent' => $parent, - 'child' => $child, - 'child_type' => $childType - ]); - return true; - } -} diff --git a/src/Model/Table/TagsTable.php b/src/Model/Table/TagsTable.php index b701615ef3..2474e991ea 100644 --- a/src/Model/Table/TagsTable.php +++ b/src/Model/Table/TagsTable.php @@ -49,9 +49,7 @@ public function getOKTagName() public function initialize(array $config) { - $this->hasMany('TagsSuperTags'); - $this->belongsToMany('SuperTags'); - + $this->belongsTo('CategoriesTree')->setForeignKey('category_id'); $this->hasMany('TagsSentences'); $this->belongsToMany('Sentences'); $this->belongsTo('Users'); @@ -137,9 +135,6 @@ public function removeTagFromSentence($tagId, $sentenceId) { $sentenceId ); - if ($this->getNameFromId($tagId) == null) - $this->TagsSuperTags->deleteAll(['child' => $tagId, 'child_type' => 'tag']); - return $count; } @@ -211,4 +206,20 @@ public function getNameFromId($tagId) { return $result ? $result->name : null; } + + public function attachToCategory($tagName, $categoryName) { + $query = $this->query(); + return $query->update() + ->set(['category_id' => $this->CategoriesTree->getIdFromName($categoryName)]) + ->where(['id' => $this->getIdFromName($tagName)]) + ->execute(); + } + + public function detachFromCategory($tagId) { + $query = $this->query(); + return $query->update() + ->set(['category_id' => null]) + ->where(['id' => $tagId]) + ->execute(); + } } diff --git a/src/Template/TagsSuperTags/manage.ctp b/src/Template/CategoriesTree/manage.ctp similarity index 65% rename from src/Template/TagsSuperTags/manage.ctp rename to src/Template/CategoriesTree/manage.ctp index fc8de62f47..a53ac574db 100644 --- a/src/Template/TagsSuperTags/manage.ctp +++ b/src/Template/CategoriesTree/manage.ctp @@ -25,40 +25,50 @@ * @link https://tatoeba.org */ -$this->set('title_for_layout', $this->Pages->formatTitle(__('Super tags management'))); +$this->set('title_for_layout', $this->Pages->formatTitle(__('Tags categories management'))); echo $this->Html->css('autocompletion.css'); $this->AssetCompress->script('sentences-block-for-members.js', ['block' => 'scriptBottom']); $this->Html->script('autocompletion.js', ['block' => 'scriptBottom']); -$this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']); +$this->Html->script('categoriesTree.js', ['block' => 'scriptBottom']); +$messages = [ + 'createCategory' => __('This tags category could not be created (category names should be unique and cycles are forbidden).'), + 'removeCategory' => __('This tags category could not be removed (a category should be empty to be deleted).'), + 'attachTagToCategory' => __('This tag could not be attached to this category.'), + 'detachTagFromCategory' => __('This tag could not be detached from its category.') +]; ?>
-
+ request['?'])) { + $action = array_keys($this->request['?'])[0]; + $error = ($this->request['?'][$action] == '0'); + if (array_key_exists($action, $messages) && $error) { + ?> +
+ +
+ + +
-

+

- request['?']) && isset($this->request['?']['superTagAdded']) && $this->request['?']['superTagAdded'] == '0'){ - ?> -
- -
- - Form->create( - 'CreateSuperTag', + 'CreateCategory', [ 'url' => [ - 'controller' => 'superTags', - 'action' => 'createSuperTag' + 'controller' => 'categoriesTree', + 'action' => 'createCategory' ], ] ); @@ -86,6 +96,19 @@ $this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']); ?> + + Form->input( + 'parentName', + [ + 'id' => 'parentName', + 'label' => __('Parent (tags category)') + ] + ); + ?> +
+
+ @@ -100,27 +123,18 @@ $this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']);
-

+

- request['?']) && isset($this->request['?']['tagSuperTagAdded']) && $this->request['?']['tagSuperTagAdded'] == '0'){ - ?> -
- -
- - Form->create( - 'CreateTagSuperTag', + 'AttachTag', [ 'url' => [ - 'action' => 'createTagSuperTag' + 'controller' => 'categories_tree', + 'action' => 'attachTagToCategory' ], ] ); @@ -129,39 +143,27 @@ $this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']); Form->input( - 'parent', - [ - 'label' => __('Parent (super tag)') - ] - ); - ?> -
-
- - - Form->select( - 'childType', - [__('Tag'), __('Super tag')], + 'categoryName', [ - 'label' => __('Child type'), - 'id' => 'child_type', - 'empty' => __('Child type (choose one)'), + 'id' => 'categoryName', + 'label' => __('Tags category') ] ); ?> +
Form->input( - 'child', + 'tagName', [ - 'label' => __('Child (tag or super tag)') + 'id' => 'tagName', + 'label' => __('Tag') ] ); ?> -
+
@@ -175,14 +177,13 @@ $this->Html->script('tagsSuperTags.js', ['block' => 'scriptBottom']);
-
+
$children) { - echo $this->element('superTag', [ + foreach ($tree as $category) { + echo $this->element('categories_tree', [ 'root' => -1, 'depth' => 0, - 'parent' => $parent, - 'children' => $children, + 'category' => $category ]); } ?> diff --git a/src/Template/Element/categories_tree.ctp b/src/Template/Element/categories_tree.ctp new file mode 100644 index 0000000000..c5d1d5198b --- /dev/null +++ b/src/Template/Element/categories_tree.ctp @@ -0,0 +1,93 @@ +Url->build([ + 'controller' => 'categories_tree', + 'action' => 'removeCategory', + $categoryId +]); + +?> + + + + + + + + + + + delete + + + + + + 0) { + ?> + + {{vm.hiddenReplies[] ? 'expand_more' : 'expand_less'}} + + + + + + + + + + + + Url->build([ + 'controller' => 'categories_tree', + 'action' => 'detachTagFromCategory', + $tag['id'] + ]); + ?> + + + + reply + + + + + + + 0) { + ?> +
+ element('categories_tree', [ + 'root' => $categoryId, + 'depth' => $depth + 1, + 'category' => $subcategory + ]); + } + ?> +
+ +
+
\ No newline at end of file diff --git a/src/Template/Element/superTag.ctp b/src/Template/Element/superTag.ctp deleted file mode 100644 index f2d5927efd..0000000000 --- a/src/Template/Element/superTag.ctp +++ /dev/null @@ -1,110 +0,0 @@ -Url->build([ - 'controller' => 'tagsSuperTags', - 'action' => 'removeTagSuperTag', - $root, - $parent, - 'superTag' -]); - -$deleteUrl = $this->Url->build([ - 'controller' => 'superTags', - 'action' => 'removeSuperTag', - $parent -]); -?> - - - - - - - - - - - reply - - - - - - - - delete - - - - - - 0) { - ?> - - {{vm.hiddenReplies[] ? 'expand_more' : 'expand_less'}} - - - - - - - - - - - - Url->build([ - 'controller' => 'tagsSuperTags', - 'action' => 'removeTagSuperTag', - $parent, - $tag, - 'tag' - ]); - ?> - - - - reply - - - - - - - 0) { - ?> -
- $new_children) { - echo $this->element('superTag', [ - 'root' => $parent, - 'depth' => $depth + 1, - 'parent' => $new_parent, - 'children' => $new_children, - ]); - } - ?> -
- -
-
\ No newline at end of file diff --git a/src/Template/Element/top_menu.ctp b/src/Template/Element/top_menu.ctp index d306ca1024..5df5d41336 100644 --- a/src/Template/Element/top_menu.ctp +++ b/src/Template/Element/top_menu.ctp @@ -71,8 +71,8 @@ $menuElements = array( "controller" => "tags", "action" => "view_all" ), - __('Manage supertags') => array( - "controller" => "tagsSuperTags", + __('Manage tags categories') => array( + "controller" => "categories_tree", "action" => "manage" ), /* @translators: menu item on the top (verb) */ diff --git a/webroot/js/categoriesTree.js b/webroot/js/categoriesTree.js new file mode 100644 index 0000000000..ebc87b0a7a --- /dev/null +++ b/webroot/js/categoriesTree.js @@ -0,0 +1,32 @@ +(function(){ + 'use strict'; + + new AutocompleteBox("categories_tree/autocomplete", function(suggestion) { + return suggestion.name; + }, "parentName", "autocompletionParent"); + + new AutocompleteBox("categories_tree/autocomplete", function(suggestion) { + return suggestion.name; + }, "categoryName", "autocompletionCategory"); + + new AutocompleteBox("tags/autocomplete", function(suggestion) { + return suggestion.name + '(' + suggestion.nbrOfSentences + ')'; + }, "tagName", "autocompletionTag"); + + angular + .module('app') + .controller('CategoriesTreeController', [ + '$http', '$location', '$anchorScroll', CategoriesTreeController + ]); + + function CategoriesTreeController($http, $location, $anchorScroll) { + var vm = this; + vm.expandOrCollapse = expandOrCollapse; + vm.hiddenReplies = {}; + + function expandOrCollapse(id) { + console.log("e"); + vm.hiddenReplies[id] = !vm.hiddenReplies[id]; + } + } +})(); \ No newline at end of file diff --git a/webroot/js/tagsSuperTags.js b/webroot/js/tagsSuperTags.js deleted file mode 100644 index ab80a910c3..0000000000 --- a/webroot/js/tagsSuperTags.js +++ /dev/null @@ -1,38 +0,0 @@ -(function(){ - 'use strict'; - - new AutocompleteBox("super_tags/autocomplete", function(suggestion) { - return suggestion.name; - }, "parent", "autocompletionParent"); - - $("#child_type").on('change', function(){ - if (parseInt(this.value) == 0) { - new AutocompleteBox("tags/autocomplete", function(suggestion) { - return suggestion.name + " (" + suggestion.nbrOfSentences + ")"; - }, "child", "autocompletionChild"); - } else { - new AutocompleteBox("super_tags/autocomplete", function(suggestion) { - return suggestion.name; - }, "child", "autocompletionChild"); - } - }); - - - - angular - .module('app') - .controller('TagsSuperTagsController', [ - '$http', '$location', '$anchorScroll', TagsSuperTagsController - ]); - - function TagsSuperTagsController($http, $location, $anchorScroll) { - var vm = this; - vm.expandOrCollapse = expandOrCollapse; - vm.hiddenReplies = {}; - - function expandOrCollapse(id) { - console.log("e"); - vm.hiddenReplies[id] = !vm.hiddenReplies[id]; - } - } -})(); \ No newline at end of file From 0bb1c9994eaa3e7b51a8939982932ea69b6adc87 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Thu, 20 Aug 2020 22:41:26 +0200 Subject: [PATCH 06/11] Removed unnecessary files --- src/Model/Entity/Contribution.php | 43 ------------------------------ src/Model/Entity/UsersSentence.php | 43 ------------------------------ src/Model/Entity/Wall.php | 40 --------------------------- 3 files changed, 126 deletions(-) delete mode 100644 src/Model/Entity/Contribution.php delete mode 100644 src/Model/Entity/UsersSentence.php delete mode 100644 src/Model/Entity/Wall.php diff --git a/src/Model/Entity/Contribution.php b/src/Model/Entity/Contribution.php deleted file mode 100644 index 4a08cc51cb..0000000000 --- a/src/Model/Entity/Contribution.php +++ /dev/null @@ -1,43 +0,0 @@ -. - */ -namespace App\Model\Entity; - -use Cake\ORM\Entity; - -class Contribution extends Entity -{ - protected function _getOldFormat() - { - return [ - 'Contribution' => [ - 'id' => $this->id, - 'sentence_id' => $this->sentence_id, - 'sentence_lang' => $this->sentence_lang, - 'translation_id' => $this->translation_id, - 'translation_lang' => $this->translation_lang, - 'script' => $this->script, - 'text' => $this->text, - 'action' => $this->action, - 'user_id' => $this->user_id, - 'type' => $this->type, - 'datetime' => $this->datetime, - ] - ]; - } -} \ No newline at end of file diff --git a/src/Model/Entity/UsersSentence.php b/src/Model/Entity/UsersSentence.php deleted file mode 100644 index d33d5be5f7..0000000000 --- a/src/Model/Entity/UsersSentence.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * @category PHP - * @package Tatoeba - * @author HO Ngoc Phuong Trang - * @license Affero General Public License - * @link https://tatoeba.org - */ -namespace App\Model\Entity; - -use Cake\ORM\Entity; - -class UsersSentence extends Entity -{ - protected function _getOldFormat() - { - return [ - 'UsersSentences' => [ - 'id' => $this->id, - 'user_id' => $this->user_id, - 'sentence_id' => $this->sentence_id, - 'correctness' => $this->correctness, - 'dirty' => $this->dirty - ] - ]; - } -} \ No newline at end of file diff --git a/src/Model/Entity/Wall.php b/src/Model/Entity/Wall.php deleted file mode 100644 index 272e7cd510..0000000000 --- a/src/Model/Entity/Wall.php +++ /dev/null @@ -1,40 +0,0 @@ -. - */ -namespace App\Model\Entity; - -use Cake\ORM\Entity; - -class Wall extends Entity -{ - protected function _getOldFormat() - { - return [ - 'Wall' => [ - 'id' => $this->id, - 'owner' => $this->owner, - 'date' => $this->date, - 'parent_id' => $this->parent_id, - 'content' => $this->content, - 'modified' => $this->modified, - 'lft' => $this->lft, - 'rght' => $this->rght, - ] - ]; - } -} \ No newline at end of file From e4c25abddb96ebf6766a1fe4aa219e6ef9728486 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Mon, 24 Aug 2020 12:01:13 +0200 Subject: [PATCH 07/11] Fixed empty max --- src/Controller/CategoriesTreeController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/CategoriesTreeController.php b/src/Controller/CategoriesTreeController.php index f0a26703cb..5b1f3586e5 100644 --- a/src/Controller/CategoriesTreeController.php +++ b/src/Controller/CategoriesTreeController.php @@ -67,7 +67,7 @@ public function manage(){ 'children' => [] ]); } - $maxDepth = max($depths); + $maxDepth = (count($depths)) ? max($depths) : -1; for ($d=$maxDepth; $d > 0; $d--) { $i = 0; From 71e6d2eddb458da5ec49448e1c2b7dcc489999b1 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Mon, 24 Aug 2020 22:45:08 +0200 Subject: [PATCH 08/11] Removed unnecessary imports, simplified code and cleaned AutocompletableBehaviour --- src/Controller/CategoriesTreeController.php | 8 ++---- .../Behavior/AutocompletableBehavior.php | 20 +++++--------- src/Model/Table/CategoriesTreeTable.php | 26 ++++--------------- src/Model/Table/TagsTable.php | 5 +++- webroot/js/autocompletion.js | 4 +-- 5 files changed, 20 insertions(+), 43 deletions(-) diff --git a/src/Controller/CategoriesTreeController.php b/src/Controller/CategoriesTreeController.php index 5b1f3586e5..012c1ea4a5 100644 --- a/src/Controller/CategoriesTreeController.php +++ b/src/Controller/CategoriesTreeController.php @@ -1,7 +1,7 @@ + * Copyright (C) 2020 Tatoeba Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,8 +19,6 @@ namespace App\Controller; use App\Controller\AppController; -use Cake\Event\Event; -use App\Model\CurrentUser; class CategoriesTreeController extends AppController @@ -56,9 +54,7 @@ public function manage(){ $tree = []; $depths = []; foreach ($treeList as $key => $value) { - $d = 0; - while ($d < strlen($value) && $value[$d] == '_') - $d++; + $d = strspn($value, '_'); array_push($depths, $d); array_push($tree, [ diff --git a/src/Model/Behavior/AutocompletableBehavior.php b/src/Model/Behavior/AutocompletableBehavior.php index 2be3033323..d67419a73c 100644 --- a/src/Model/Behavior/AutocompletableBehavior.php +++ b/src/Model/Behavior/AutocompletableBehavior.php @@ -40,9 +40,9 @@ class AutocompletableBehavior extends Behavior protected $_defaultConfig = [ 'implementedMethods' => ['Autocomplete' => 'Autocomplete'], - 'index' => ['name' => 'name', 'type' => 'string'], - 'fields' => ['name', 'id', 'nbrOfSentences'], - 'order' => ['nbrOfSentences' => 'DESC'], + 'index' => 'name', + 'fields' => ['id', 'name'], + 'order' => [], 'limit' => 10 ]; @@ -58,21 +58,15 @@ public function initialize(array $config) public function Autocomplete($search) { $query = $this->getTable()->find(); - $query->select($this->config('fields')); + $query->select($this->getConfig('fields')); if (!empty($search)) { $pattern = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $search).'%'; - $query->where([$this->config('index')['name'].' LIKE :search']) - ->bind(':search', $pattern, $this->config('index')['type']); + $query->where(["{$this->getConfig('index')} LIKE" => $pattern]); } - if (count($this->config('order'))) { - $query->order($this->config('order')); - } - - if ($this->config('limit') > 0) { - $query->limit($this->config('limit')); - } + $query->order($this->getConfig('order')); + $query->limit($this->getConfig('limit')); return $query->all(); } diff --git a/src/Model/Table/CategoriesTreeTable.php b/src/Model/Table/CategoriesTreeTable.php index 2504da24a8..3b7fcd9a63 100644 --- a/src/Model/Table/CategoriesTreeTable.php +++ b/src/Model/Table/CategoriesTreeTable.php @@ -1,7 +1,7 @@ + * Copyright (C) 2020 Tatoeba Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -18,10 +18,7 @@ */ namespace App\Model\Table; -use Cake\Database\Schema\TableSchema; use Cake\ORM\Table; -use Cake\Event\Event; -use Cake\Utility\ClassRegistry; class CategoriesTreeTable extends Table { @@ -31,8 +28,7 @@ public function initialize(array $config) $this->addBehavior('Tree'); $this->addBehavior('Autocompletable', [ - 'fields' => ['name', 'id'], - 'order' => [] + 'order' => ['name'] ]); } @@ -47,15 +43,9 @@ public function initialize(array $config) */ public function create($name, $description, $parentName) { - if (empty($name)) + if (empty($name) || $this->exists(['name' => $name])) return false; else { - $count = $this->find('all') - ->where(['name' => $name]) - ->count(); - if ($count > 0) - return false; - $data = $this->newEntity([ 'name' => $name, 'description' => $description, @@ -74,17 +64,11 @@ public function create($name, $description, $parentName) public function remove($categoryId) { // check this category contains no tag - $tagsCount = $this->Tags->find('all', [ - 'conditions' => ['category_id' => $categoryId] - ])->count(); - if ($tagsCount > 0) + if ($this->Tags->exists(['category_id' => $categoryId])) return false; // check this category contains no other category - $categoriesCount = $this->find('all', [ - 'conditions' => ['parent_id' => $categoryId] - ])->count(); - if ($categoriesCount > 0) + if ($this->exists(['parent_id' => $categoryId])) return false; return $this->deleteAll(['id' => $categoryId]); diff --git a/src/Model/Table/TagsTable.php b/src/Model/Table/TagsTable.php index 2474e991ea..c24f1309bc 100644 --- a/src/Model/Table/TagsTable.php +++ b/src/Model/Table/TagsTable.php @@ -55,7 +55,10 @@ public function initialize(array $config) $this->belongsTo('Users'); $this->addBehavior('Timestamp'); - $this->addBehavior('Autocompletable'); + $this->addBehavior('Autocompletable', [ + 'fields' => ['id', 'name', 'nbrOfSentences'], + 'order' => ['nbrOfSentences' => 'DESC'] + ]); } public function beforeMarshal($event, $data, $options) diff --git a/webroot/js/autocompletion.js b/webroot/js/autocompletion.js index 849e4edd8e..e7ea79ac48 100644 --- a/webroot/js/autocompletion.js +++ b/webroot/js/autocompletion.js @@ -23,7 +23,7 @@ class AutocompleteBox { this.currentSuggestPosition = -1; this.countBeforeRequest = 0; - this. suggestLength = 0; + this.suggestLength = 0; this.previousText = ''; this.isSuggestListActive = false; @@ -110,7 +110,7 @@ class AutocompleteBox { var that = this; if (tag != this.previousText) { $.get( - this.url + tag, + this.url + encodeURIComponent(tag), function(data) { that.suggestShowResults(data); } From 3346d6b77b80795534b685ac06adb08f4aa59b0a Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Mon, 24 Aug 2020 23:00:51 +0200 Subject: [PATCH 09/11] Cleaned copyright --- src/Template/CategoriesTree/manage.ctp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Template/CategoriesTree/manage.ctp b/src/Template/CategoriesTree/manage.ctp index a53ac574db..893f63f52f 100644 --- a/src/Template/CategoriesTree/manage.ctp +++ b/src/Template/CategoriesTree/manage.ctp @@ -1,7 +1,7 @@ + * Copyright (C) 2020 Tatoeba Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -15,14 +15,6 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - * - * PHP version 5 - * - * @category PHP - * @package Tatoeba - * @author Alexandre Magueresse - * @license Affero General Public License - * @link https://tatoeba.org */ $this->set('title_for_layout', $this->Pages->formatTitle(__('Tags categories management'))); From 9388add8ec7ea7ab57b4cc81a3f334ddf0bde425 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Tue, 25 Aug 2020 13:24:33 +0200 Subject: [PATCH 10/11] Cleaned js files and sanitized user input, mades branches closed by default --- src/Template/Element/categories_tree.ctp | 12 ++++++------ webroot/js/autocompletion.js | 2 +- webroot/js/categoriesTree.js | 17 ++++++----------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/Template/Element/categories_tree.ctp b/src/Template/Element/categories_tree.ctp index c5d1d5198b..28abe6b5d0 100644 --- a/src/Template/Element/categories_tree.ctp +++ b/src/Template/Element/categories_tree.ctp @@ -20,7 +20,7 @@ $deleteUrl = $this->Url->build([ - + safeForAngular($categoryName) ?> Url->build([ if (count($categoryChildren) > 0) { ?> - {{vm.hiddenReplies[] ? 'expand_more' : 'expand_less'}} - + {{vm.displayedBranches[] ? 'expand_less' : 'expand_more'}} + - + @@ -61,7 +61,7 @@ $deleteUrl = $this->Url->build([ ]); ?> - + safeForAngular($tag['name']).' ('.$tag['nbrOfSentences'].')' ?> reply @@ -75,7 +75,7 @@ $deleteUrl = $this->Url->build([ 0) { ?> -
+
element('categories_tree', [ diff --git a/webroot/js/autocompletion.js b/webroot/js/autocompletion.js index e7ea79ac48..5204e78f48 100644 --- a/webroot/js/autocompletion.js +++ b/webroot/js/autocompletion.js @@ -74,7 +74,7 @@ class AutocompleteBox { } changeActiveSuggestion(offset) { - $("#suggestItem" + this.currentSuggestPosition).removeClass("selected"); + $("#suggestedItem" + this.currentSuggestPosition).removeClass("selected"); this.currentSuggestPosition = (this.currentSuggestPosition + offset) % this.suggestLength; if (this.currentSuggestPosition < 0) { this.currentSuggestPosition = this.suggestLength - 1; diff --git a/webroot/js/categoriesTree.js b/webroot/js/categoriesTree.js index ebc87b0a7a..205ec595c5 100644 --- a/webroot/js/categoriesTree.js +++ b/webroot/js/categoriesTree.js @@ -10,23 +10,18 @@ }, "categoryName", "autocompletionCategory"); new AutocompleteBox("tags/autocomplete", function(suggestion) { - return suggestion.name + '(' + suggestion.nbrOfSentences + ')'; + return suggestion.name + ' (' + suggestion.nbrOfSentences + ')'; }, "tagName", "autocompletionTag"); - angular - .module('app') - .controller('CategoriesTreeController', [ - '$http', '$location', '$anchorScroll', CategoriesTreeController - ]); - - function CategoriesTreeController($http, $location, $anchorScroll) { + angular.module('app').controller('CategoriesTreeController', [CategoriesTreeController]); + + function CategoriesTreeController() { var vm = this; vm.expandOrCollapse = expandOrCollapse; - vm.hiddenReplies = {}; + vm.displayedBranches = {}; function expandOrCollapse(id) { - console.log("e"); - vm.hiddenReplies[id] = !vm.hiddenReplies[id]; + vm.displayedBranches[id] = !vm.displayedBranches[id]; } } })(); \ No newline at end of file From cf655c40eecfc8af63bfd4821f53828ec038d989 Mon Sep 17 00:00:00 2001 From: Alexandre Magueresse Date: Tue, 25 Aug 2020 15:03:58 +0200 Subject: [PATCH 11/11] Enabled tags category edition, optimized the tree creation, sorted tags and tags categories alphabetically --- src/Controller/CategoriesTreeController.php | 53 ++++++--------------- src/Model/Table/CategoriesTreeTable.php | 10 +++- src/Template/CategoriesTree/manage.ctp | 26 +++++----- 3 files changed, 36 insertions(+), 53 deletions(-) diff --git a/src/Controller/CategoriesTreeController.php b/src/Controller/CategoriesTreeController.php index 012c1ea4a5..2a76bd7cb0 100644 --- a/src/Controller/CategoriesTreeController.php +++ b/src/Controller/CategoriesTreeController.php @@ -34,61 +34,38 @@ class CategoriesTreeController extends AppController public $helpers = ['Pagination']; public function manage(){ - $tags = []; $this->loadModel('Tags'); - foreach ($this->Tags->find('all')->select(['id', 'name', 'nbrOfSentences', 'category_id']) as $value) { - $category = ($value['category_id'] == null) ? -1 : $value['category_id']; + $tags = []; + $tagList = $this->Tags->find('all') + ->select(['id', 'name', 'nbrOfSentences', 'category_id']) + ->order(['name']); + foreach ($tagList as $tag) { + $category = ($tag['category_id'] == null) ? -1 : $tag['category_id']; if (!array_key_exists($category, $tags)) { $tags[$category] = []; } array_push($tags[$category], [ - 'id' => $value['id'], - 'name' => $value['name'], - 'nbrOfSentences' => $value['nbrOfSentences'] + 'id' => $tag['id'], + 'name' => $tag['name'], + 'nbrOfSentences' => $tag['nbrOfSentences'] ]); } - - $treeList = $this->CategoriesTree->find('treeList'); - - $tree = []; - $depths = []; - foreach ($treeList as $key => $value) { - $d = strspn($value, '_'); - array_push($depths, $d); - - array_push($tree, [ - 'id' => $key, - 'name' => substr($value, $d), - 'children' => [] - ]); - } - $maxDepth = (count($depths)) ? max($depths) : -1; - - for ($d=$maxDepth; $d > 0; $d--) { - $i = 0; - while ($i < sizeof($tree)) { - if ($i < sizeof($depths) && $depths[$i] == $d) { - array_push($tree[$i-1]['children'], $tree[$i]); - unset($tree[$i]); - unset($depths[$i]); - $tree = array_values($tree); - $depths = array_values($depths); - } else - $i++; - } - } + + $tree = $this->CategoriesTree->find('threaded', [ + 'order' => 'name' + ]); $this->set('tags', $tags); $this->set('tree', $tree); } - public function createCategory() { + public function createorEditCategory() { $name = $this->request->data('name'); $description = $this->request->data('description'); $parentName = $this->request->data('parentName'); - $res = $this->CategoriesTree->create($name, $description, $parentName); + $res = $this->CategoriesTree->createOrEdit($name, $description, $parentName); return $this->redirect([ 'controller' => 'categories_tree', diff --git a/src/Model/Table/CategoriesTreeTable.php b/src/Model/Table/CategoriesTreeTable.php index 3b7fcd9a63..9ecef4ad04 100644 --- a/src/Model/Table/CategoriesTreeTable.php +++ b/src/Model/Table/CategoriesTreeTable.php @@ -41,9 +41,9 @@ public function initialize(array $config) * * @return bool */ - public function create($name, $description, $parentName) + public function createOrEdit($name, $description, $parentName) { - if (empty($name) || $this->exists(['name' => $name])) + if (empty($name)) return false; else { $data = $this->newEntity([ @@ -51,6 +51,12 @@ public function create($name, $description, $parentName) 'description' => $description, 'parent_id' => $this->getIdFromName($parentName) ]); + + $query = $this->find('all')->select(['id'])->where(['name' => $name]); + if (!$query->isEmpty()) { + $id = $query->first()['id']; + $data->set('id', $id); + } return $this->save($data); } diff --git a/src/Template/CategoriesTree/manage.ctp b/src/Template/CategoriesTree/manage.ctp index 893f63f52f..17ee9ef391 100644 --- a/src/Template/CategoriesTree/manage.ctp +++ b/src/Template/CategoriesTree/manage.ctp @@ -49,18 +49,18 @@ $messages = [
-

+

Form->create( - 'CreateCategory', + 'CreateOrEditCategory', [ 'url' => [ 'controller' => 'categoriesTree', - 'action' => 'createCategory' + 'action' => 'createOrEditCategory' ], ] ); @@ -103,7 +103,7 @@ $messages = [ - + Form->input( - 'categoryName', + 'tagName', [ - 'id' => 'categoryName', - 'label' => __('Tags category') + 'id' => 'tagName', + 'label' => __('Tag') ] ); ?> -
+
- + Form->input( - 'tagName', + 'categoryName', [ - 'id' => 'tagName', - 'label' => __('Tag') + 'id' => 'categoryName', + 'label' => __('Tags category') ] ); ?> -
+