From f9fdc5d400907d7ca85ca8adad34a4adefc16028 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 19 Nov 2020 17:44:13 -0300 Subject: [PATCH] =?UTF-8?q?reordenar=20art=C3=ADculos=20closes=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/packs/application.js | 17 -- .../packs/controllers/reorder_controller.js | 190 ++++++++++++++++++ app/views/posts/index.haml | 21 +- config/locales/en.yml | 10 +- config/locales/es.yml | 12 +- package.json | 1 - yarn.lock | 73 ------- 7 files changed, 218 insertions(+), 106 deletions(-) create mode 100644 app/javascript/packs/controllers/reorder_controller.js diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index abae9073..09f88ffe 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -22,8 +22,6 @@ const application = Application.start() const context = require.context("./controllers", true, /\.js$/) application.load(definitionsFromContext(context)) -import tableDragger from 'table-dragger' - import {EditorState} from "prosemirror-state" import {EditorView} from "prosemirror-view" import {schema, defaultMarkdownParser, defaultMarkdownSerializer} from "prosemirror-markdown" @@ -103,19 +101,4 @@ document.addEventListener('turbolinks:load', () => { // Ocultar el area textArea.style.display = 'none' }) - - document.querySelectorAll('.table-draggable').forEach(table => { - tableDragger(table, { - mode: 'row', - onlyBody: true, - dragHandler: '.handle' - }).on('drop', (from, to, el, mode) => { - Array.from(document.querySelectorAll('.reorder')) - .reverse() - .map((o,i) => o.value = i); - - Array.from(document.querySelectorAll('.submit-reorder')) - .map(s => s.classList.remove('d-none')); - }); - }); }) diff --git a/app/javascript/packs/controllers/reorder_controller.js b/app/javascript/packs/controllers/reorder_controller.js new file mode 100644 index 00000000..a7024fb2 --- /dev/null +++ b/app/javascript/packs/controllers/reorder_controller.js @@ -0,0 +1,190 @@ +import 'core-js/stable' +import 'regenerator-runtime/runtime' +import { Controller } from 'stimulus' + +/* + * Permite reordenar las filas de una tabla. + * + * Cada fila tiene un selector que permite decidir si la fila se + * mantiene en su lugar o se mueve al presionar las teclas de subir y + * bajar. + * + * Se pueden mover varias juntas. + * + * El controlador está en la tabla y cada fila es un objetivo. Dentro + * de cada fila tiene que haber un input[type=checkbox] que determina si + * está seleccionada o no y un input[type=hidden] que contiene la + * posición actual que luego será guardada. + * + * La tabla tiene que estar rodeada de un formulario para poder enviar + * los datos. + * + * El objetivo es poder mover filas en tablas de miles de elementos. + */ +export default class extends Controller { + static targets = [ 'row', 'unselect', 'top', 'bottom', 'direction' ] + + connect () { + // Deseleccionar + this.unselectTarget.addEventListener('click', event => { + event.preventDefault() + event.stopPropagation() + + for (const r of Object.values(this.selected_rows)) { + r.row.querySelector('[data-reorder-handler]').click() + } + }) + + // Enviar arriba de todo + this.topTarget.addEventListener('click', event => { + event.preventDefault() + event.stopPropagation() + + if (this.empty) return + + const rows = this.sorted_rows() + const first = rows[0].row.parentElement.firstElementChild + + for (const r of rows) { + const row = r.row + + if (row === first) continue + + row.parentElement.insertBefore(row, first) + } + + // Reacomodamos el orden + this.reorder() + + // Mantenemos el primero a la vista + rows[0].row.scrollIntoViewIfNeeded() + }) + + // Enviar al final + this.bottomTarget.addEventListener('click', event => { + event.preventDefault() + event.stopPropagation() + + if (this.empty) return + + const rows = this.sorted_rows() + + for (const r of rows) { + const row = r.row + + row.parentElement.appendChild(row) + } + + // Reacomodamos el orden + this.reorder() + + // Mantenemos el primero a la vista + rows[0].row.scrollIntoViewIfNeeded() + }) + + this.rowTargets.forEach(row => { + // Al cambiar los inputs, mantener la lista de filas actualizadas. + // Necesitamos saber la posición para poder mover las filas en + // orden en lugar del orden en que fueron seleccionadas. + row.querySelector('[data-reorder-handler]').addEventListener('change', event => { + if (event.target.checked) { + this.selected_rows[row.id] = { + row, + order: this.rowTargets.indexOf(row) + } + } else { + delete this.selected_rows[row.id] + } + }) + }) + + this.directionTargets.forEach(dir => { + dir.addEventListener('click', event => { + event.preventDefault() + event.stopPropagation() + if (this.empty) return + + this.move(dir.dataset.direction) + }) + }) + + // Lo asociamos al documento porque en la tabla se pierde el foco + // luego del primer evento. + document.addEventListener('keydown', event => { + if (!this.codes.includes(event.keyCode)) return + if (this.empty) return + + event.preventDefault() + + this.move(event.keyCode === 38 ? 'up' : 'down') + }) + } + + get selected_rows () { + if (!this._selected_rows) this._selected_rows = {} + + return this._selected_rows + } + + // Arriba, abajo + get codes () { + if (!this._codes) this._codes = [ 38, 40 ] + + return this._codes + } + + get empty () { + return (Object.keys(this.selected_rows).length === 0) + } + + /* + * Aplica el nuevo orden en las filas y sus campos + */ + reorder () { + for (const r of Object.values(this.selected_rows)) { + this.selected_rows[r.row.id].order = this.rowTargets.indexOf(r.row) + } + + const length = this.rowTargets.length + + this.rowTargets.forEach((row, i) => { + row.querySelector('[data-reorder]').value = length - i + }) + } + + sorted_rows () { + return Object.values(this.selected_rows).sort((a,b) => a.order - b.order) + } + + move (direction) { + if (this.empty) return + + const up = direction === 'up' + const down = !up + const direction_sibling = up ? 'previousElementSibling' : 'nextElementSibling' + + // Los movemos en orden + const rows = this.sorted_rows() + if (down) rows.reverse() + + for (const r of rows) { + const row = r.row + const sibling = row[direction_sibling] + + // Estamos en el tope? + if (!sibling || sibling.tagName !== row.tagName) continue + + if (up) { + row.parentElement.insertBefore(row, sibling) + } else { + row.parentElement.insertBefore(sibling, row) + } + } + + // Reacomodamos el orden + this.reorder() + + // Mantenemos el primero a la vista + rows[0].row.scrollIntoViewIfNeeded() + } +} diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index 48c1d24a..e4129e8a 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -38,8 +38,6 @@ - else = form_tag site_posts_reorder_path, method: :post do .d-flex.justify-content-between.align-items-center - = submit_tag t('posts.reorder'), class: 'btn submit-reorder' - -# TODO: Pensar una interfaz mejor para cuando haya más de tres idiomas @@ -48,8 +46,17 @@ - @site.locales.each do |locale| = link_to t("locales.#{locale}.name"), site_posts_path(@site, locale: locale), class: "mr-2 mt-2 mb-2#{locale == @locale ? 'active font-weight-bold' : ''}" - %table.table.table-draggable + %table.table{ data: { controller: 'reorder' } } %caption.sr-only= t('posts.caption') + %thead + %tr + %th.border-0.background-white.position-sticky{ style: 'top: 0', colspan: '4' } + = submit_tag t('posts.reorder.submit'), class: 'btn' + %button.btn{ data: { target: 'reorder.unselect' } }= t('posts.reorder.unselect') + %button.btn{ data: { target: 'reorder.direction', direction: 'up' } }= t('posts.reorder.up') + %button.btn{ data: { target: 'reorder.direction', direction: 'down' } }= t('posts.reorder.down') + %button.btn{ data: { target: 'reorder.top' } }= t('posts.reorder.top') + %button.btn{ data: { target: 'reorder.bottom' } }= t('posts.reorder.bottom') %tbody - dir = t("locales.#{@locale}.dir") - @posts.each_with_index do |post, i| @@ -57,13 +64,13 @@ TODO: Solo les usuaries cachean porque tenemos que separar les botones por permisos. - cache_if @usuarie, post do - %tr + %tr{ id: post.uuid.value, data: { target: 'reorder.row' } } %td - .handle - = image_tag 'arrows-alt-v.svg' + %input{ type: 'checkbox', autocomplete: 'off', data: { reorder: { handler: true } } } -# Orden más alto es mayor prioridad = hidden_field 'post[reorder]', post.uuid.value, - value: @posts.length - i, class: 'reorder' + value: @posts.length - i, + data: { reorder: true } %td.w-100{ class: dir } %small = link_to @site.i18n.dig('layouts', post.layout.name.to_s) || post.layout.name.to_s.humanize, diff --git a/config/locales/en.yml b/config/locales/en.yml index 2d502481..3d0c9fae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -169,8 +169,6 @@ en: usuarie: edit: Edit my profile category: 'Category' - posts: - reorder: 'You can drag and drop articles by the arrow icon () and then press the "Reorder posts" button to save them in different order.' i18n: top: 'Back to top' index: "Here you can edit your site's texts that don't belong to a post, like its description, sections, buttons... If you change languages up there in the title to be the same, you can edit them. If they're different, you can translate from one into the other." @@ -471,7 +469,13 @@ en: image: label: Imagen destroy: Remove image - reorder: 'Save posts order' + reorder: + submit: 'Save order' + unselect: 'Deselected all' + top: 'Send to top' + bottom: 'Send to bottom' + up: 'Up' + down: 'Down' sort: by: 'Sort by' order: 'order' diff --git a/config/locales/es.yml b/config/locales/es.yml index 880acee4..f3a6dfb5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -165,10 +165,6 @@ es: usuarie: edit: Editar mi perfil category: 'Categoría' - posts: - reorder: 'Puedes arrastrar y soltar los artículos por el ícono de - las flechas () y luego presionar el - botón "Reordenar artículos" para guardarlos en ese orden.' i18n: top: 'Volver al principio' index: 'Aquí puedes editar todos los textos del sitio que no se @@ -486,7 +482,13 @@ es: image: label: Imagen destroy: 'Eliminar imagen' - reorder: 'Guardar orden de artículos' + reorder: + submit: 'Guardar orden' + unselect: 'Deseleccionar todos' + top: 'Enviar al principio' + bottom: 'Enviar al final' + up: 'Subir' + down: 'Bajar' sort: by: 'Ordenar por' order: 'posición' diff --git a/package.json b/package.json index e1937514..7dbb4563 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "prosemirror-markdown": "^1.4.5", "prosemirror-schema-basic": "^1.1.2", "stimulus": "^1.1.1", - "table-dragger": "git+https://0xacab.org/sutty/table-dragger.git", "zepto": "^1.2.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index edd83c52..d47af9bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -579,14 +579,6 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" -"@babel/polyfill@^7.4.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.7.0.tgz#e1066e251e17606ec7908b05617f9b7f8180d8f3" - integrity sha512-/TS23MVvo34dFmf8mwCisCbWGrfhbiWZSwBo6HkADTBhUa2Q/jWltyY/tpofz/b6/RIhqaqQcquptCirqIhOaQ== - dependencies: - core-js "^2.6.5" - regenerator-runtime "^0.13.2" - "@babel/preset-env@^7.4.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a" @@ -1173,10 +1165,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -atoa@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/atoa/-/atoa-1.0.0.tgz#0cc0e91a480e738f923ebc103676471779b34a49" - atob@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -1858,13 +1846,6 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -contra@1.9.4: - version "1.9.4" - resolved "https://registry.yarnpkg.com/contra/-/contra-1.9.4.tgz#f53bde42d7e5b5985cae4d99a8d610526de8f28d" - dependencies: - atoa "1.0.0" - ticky "1.0.1" - convert-source-map@^1.1.0: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" @@ -1907,16 +1888,6 @@ core-js-compat@^3.1.1: browserslist "^4.6.6" semver "^6.3.0" -core-js@^2.6.5: - version "2.6.10" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" - integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== - -core-js@^3.0.1: - version "3.3.6" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.3.6.tgz#6ad1650323c441f45379e176ed175c0d021eac92" - integrity sha512-u4oM8SHwmDuh5mWZdDg9UwNVq5s1uqq6ZDLLIs07VY+VJU91i3h4f3K/pgFvtUQPGdeStrZ+odKyfyt4EnKHfA== - core-js@^3.1.3: version "3.2.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" @@ -1992,12 +1963,6 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" which "^1.2.9" -crossvent@1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.4.tgz#da2c4f8f40c94782517bf2beec1044148194ab92" - dependencies: - custom-event "1.0.0" - crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -2205,10 +2170,6 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" -custom-event@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.0.tgz#2e4628be19dc4b214b5c02630c5971e811618062" - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -2434,14 +2395,6 @@ dot-prop@^4.1.1: dependencies: is-obj "^1.0.0" -dragula@^3.7.0: - version "3.7.2" - resolved "https://registry.yarnpkg.com/dragula/-/dragula-3.7.2.tgz#4a35c9d3981ffac1a949c29ca7285058e87393ce" - integrity sha1-SjXJ05gf+sGpScKcpyhQWOhzk84= - dependencies: - contra "1.9.4" - crossvent "1.5.4" - duplexify@^3.4.2, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -5899,11 +5852,6 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -ramda@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -6222,13 +6170,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" - integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== - dependencies: - tslib "^1.9.0" - safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -6842,16 +6783,6 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" -"table-dragger@git+https://0xacab.org/sutty/table-dragger.git": - version "2.0.2" - resolved "git+https://0xacab.org/sutty/table-dragger.git#a5199975398dca9b3a849f5d56220fd11e68733c" - dependencies: - "@babel/polyfill" "^7.4.0" - core-js "^3.0.1" - dragula "^3.7.0" - ramda "^0.26.1" - rxjs "^6.5.2" - tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -6916,10 +6847,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== -ticky@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ticky/-/ticky-1.0.1.tgz#b7cfa71e768f1c9000c497b9151b30947c50e46d" - timers-browserify@^2.0.4: version "2.0.11" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"