├── .editorconfig
├── .gitignore
├── assets
├── advantages-section-en.png
├── advantages-section-ru.png
├── browser-compatibility-section-en.png
├── browser-compatibility-section-ru.png
├── community-section-en.png
├── community-section-ru.png
├── connection-section-en.png
├── connection-section-ru.png
├── cover.png
├── description-section-en.png
├── description-section-ru.png
├── installation-section-en.png
├── installation-section-ru.png
├── navigation-section-en.png
├── navigation-section-ru.png
├── parameters-section-en.png
├── parameters-section-ru.png
├── usage-section-en.png
└── usage-section-ru.png
├── license.md
├── package.json
├── readme-ru.md
├── readme.md
└── src
└── transfer-elements.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | demo/
2 | dist/
3 |
--------------------------------------------------------------------------------
/assets/advantages-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/advantages-section-en.png
--------------------------------------------------------------------------------
/assets/advantages-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/advantages-section-ru.png
--------------------------------------------------------------------------------
/assets/browser-compatibility-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/browser-compatibility-section-en.png
--------------------------------------------------------------------------------
/assets/browser-compatibility-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/browser-compatibility-section-ru.png
--------------------------------------------------------------------------------
/assets/community-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/community-section-en.png
--------------------------------------------------------------------------------
/assets/community-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/community-section-ru.png
--------------------------------------------------------------------------------
/assets/connection-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/connection-section-en.png
--------------------------------------------------------------------------------
/assets/connection-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/connection-section-ru.png
--------------------------------------------------------------------------------
/assets/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/cover.png
--------------------------------------------------------------------------------
/assets/description-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/description-section-en.png
--------------------------------------------------------------------------------
/assets/description-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/description-section-ru.png
--------------------------------------------------------------------------------
/assets/installation-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/installation-section-en.png
--------------------------------------------------------------------------------
/assets/installation-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/installation-section-ru.png
--------------------------------------------------------------------------------
/assets/navigation-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/navigation-section-en.png
--------------------------------------------------------------------------------
/assets/navigation-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/navigation-section-ru.png
--------------------------------------------------------------------------------
/assets/parameters-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/parameters-section-en.png
--------------------------------------------------------------------------------
/assets/parameters-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/parameters-section-ru.png
--------------------------------------------------------------------------------
/assets/usage-section-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/usage-section-en.png
--------------------------------------------------------------------------------
/assets/usage-section-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SineYlo/transfer-elements/2d980bd78f8cb0b76ebfd01e9741d1a7cb7749f0/assets/usage-section-ru.png
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 SineYlo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transfer-elements",
3 | "version": "1.0.7",
4 | "type": "module",
5 | "description": "Moves elements from one place to another.",
6 | "keywords": [
7 | "lightweight",
8 | "performance",
9 | "responsive",
10 | "adaptive",
11 | "breakpoints",
12 | "vanilla",
13 | "javascript",
14 | "pure",
15 | "library",
16 | "dom-manipulation",
17 | "no-dependencies"
18 | ],
19 | "author": "SineYlo",
20 | "license": "MIT",
21 | "exports": "./dist/transfer-elements.esm.min.js",
22 | "browser": "./dist/transfer-elements.min.js",
23 | "files": [
24 | "./dist/"
25 | ],
26 | "homepage": "https://github.com/SineYlo/transfer-elements",
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/SineYlo/transfer-elements.git"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/SineYlo/transfer-elements/issues"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/readme-ru.md:
--------------------------------------------------------------------------------
1 |
`, а если целый раздел? В добавок на элементе может быть атрибут `id`, тогда ещё и его придётся изменять, так как два одинаковых `id` в разметке быть не может.
51 |
52 | Второй — использовать абсолютное позиционирование. Этот вариант кажется неплохим, но на самом деле он ещё хуже, так как ломает доступность. Да, визуально элемент будет находиться там, где нам нужно, но вот в `DOM` он останется на прежнем месте. А программы для чтения с экрана в подавляющем большинстве ориентируется на `DOM`, а не на визуальное расположение, ибо оно может быть каким угодно.
53 |
54 | Так вот библиотека **решает** эту проблему. Она просто «берёт» элемент из `DOM` и переносит его туда, куда вам нужно. При этом нет дублирования в разметке или использования абсолютного позиционирования.
55 |
56 | ---
57 |
58 | Основная **цель** данной библиотеки — дать свободу для творчества веб-дизайнерам. Я считаю, что не они под нас должны подстраиваться, а мы под них. Чем больше начнёт появляться инструментов, позволяющих реализовать их задумки, тем быстрее мы придём к тому, когда каждый сайт будет выглядеть современно, а самое главное станет удобным для использования с любого устройства.
59 |
60 |
61 |
62 |
63 |
64 | - **Без зависимостей**. Весь код написан с нуля и в нём отсутствуют сторонние решения.
65 | - **Инновационная технология**. Библиотека основана на собственной технологии «Transfer Elements Simulation» (TES). Благодаря ей последовательный перенос (поднятие всей цепочки брейкпоинтов) выполняется только один раз.
66 | - **Множественный перенос**. Максимальное количество брейкпоинтов не ограничено. Добавляйте их столько, сколько требуется для вашего проекта.
67 | - **Двухэтапная проверка данных**. В библиотеку добавлено множество разнообразных проверок пользовательских данных. На первом этапе данные проверяются на соответствие необходимому типу. После этого наступает второй этап и проверяется возможность вставки в целевой элемент. Также в TES встроена дополнительная проверка на редкие случаи. Если что-то пойдёт не так, вы получите подробное сообщение об ошибке.
68 | - **Доступность**. У брейкпоинтов отсутствует связь с какими-либо единицами измерения CSS. Несмотря на то, что есть некоторое сходство с пикселями (`px`), весь код библиотеки работает с обычным числом. Поэтому при изменении настроек шрифта в браузере ничего не сломается.
69 | - **Скорость**. Помимо разового поднятия всей цепочки брейкпоинтов, поиск самого брейкпоинта, в момент основного переноса, выполняется за логарифмическое время `O(log n)`.
70 |
71 |
72 |
73 |
74 |
75 | > [!IMPORTANT]
76 | > Если вы не используете модули или используете, но хотите импортировать не из `node_modules`, пропустите данный раздел и переходите к следующему.
77 |
78 | В зависимости от используемого вами менеджера пакетов выберите команду и запустите её в терминале.
79 |
80 | ```
81 | npm install transfer-elements
82 | ```
83 |
84 | ```
85 | yarn add transfer-elements
86 | ```
87 |
88 |
89 |
90 |
91 |
92 | ### Модули
93 |
94 | Если вы установили библиотеку, можете импортировать её из `node_modules`.
95 |
96 | ```JS
97 | import TransferElements from 'transfer-elements';
98 | ```
99 |
100 | Если вы не установили библиотеку, можете импортировать её из CDN.
101 |
102 | ```JS
103 | import TransferElements from 'https://cdn.jsdelivr.net/npm/transfer-elements@1.0.7/dist/transfer-elements.esm.min.js'
104 | ```
105 |
106 | ### Тег \
112 | ```
113 |
114 | Ссылка короче, чем для модулей, потому что файл запрашивается по умолчанию `transfer-elements.min.js`. Так или иначе вы можете указать полную ссылку.
115 |
116 | ```HTML
117 |
118 | ```
119 |
120 | ### Прочее
121 |
122 | Если все выше перечисленные варианты не подходят по какой-либо причине, можете [скачать файлы](https://registry.npmjs.org/transfer-elements/-/transfer-elements-1.0.7.tgz) и подключить библиотеку так как вам нужно.
123 |
124 |
125 |
126 |
127 |
128 | > [!NOTE]
129 | > В библиотеке добавлено достаточно большое количество проверок. Это нужно для того, чтобы в основной механизм поступали только правильные данные. Поэтому, даже если вы что-то сделаете не так, то в DevTools в разделе консоль увидите ошибку.
130 |
131 | Первое, что понадобится сделать после подключения — вызвать конструктор.
132 |
133 | ```JS
134 | new TransferElements();
135 | ```
136 |
137 | Он принимает набор объектов `{}` с параметрами. То есть вы можете указать как один объект, так и несколько, если вам нужно перенести разные элементы. Для примера я укажу только один.
138 |
139 | ```JS
140 | new TransferElements(
141 | {
142 |
143 | }
144 | );
145 | ```
146 |
147 | После этого добавим в объект первый и пожалуй самый важный параметр — `sourceElement`. Это элемент, который будет перенесён в другие элементы или в тот же самый, но с изменённой позицией, на брейкпоинтах, которые мы чуть позже укажем.
148 |
149 | Так как значением этого параметра должен быть объект типа `Element`, то вы можете воспользоваться любым методом возвращающим такое значение. Я же чаще всего использую `document.getElementById()`, поэтому применю его.
150 |
151 | ```JS
152 | new TransferElements(
153 | {
154 | sourceElement: document.getElementById('id-1')
155 | }
156 | );
157 | ```
158 |
159 | > [!TIP]
160 | > Если вам потребуется перенести несколько элементов, то указывать объекты с ними вы можете в любом порядке. Механизм сам скорректирует порядок вставки, основываясь на порядке расположения этих элементов в DOM.
161 |
162 | Затем добавим параметр `breakpoints`, значением которого должен быть объект типа `Object`. В нём будут храниться все брейкпоинты, на которых должен быть перенесён `sourceElement`.
163 |
164 | ```JS
165 | new TransferElements(
166 | {
167 | sourceElement: document.getElementById('id-1'),
168 | breakpoints: {
169 |
170 | }
171 | }
172 | );
173 | ```
174 |
175 | > [!TIP]
176 | > В основу библиотеки заложен подход `Desktop First`. Это значит, что перенос элементов будет осуществляться от большего брейкпоинта к меньшему. Но, не смотря на это, вы можете указывать брейкпоинты в любом порядке.
177 |
178 | Сам брейкпоинт состоит из триггера типа `string`, то есть ключа, и объекта типа `Object`, который является значением. В качестве триггера допускается указать практически любое значение, которое может быть преобразовано к числу, за исключением нуля (при условии, что это целое число), отрицательных чисел и чисел выходящих за пределы `Number.MAX_SAFE_INTEGER`. Теперь я добавлю случайный брейкпоинт `990`.
179 |
180 | ```JS
181 | new TransferElements(
182 | {
183 | sourceElement: document.getElementById('id-1'),
184 | breakpoints: {
185 | 990: {
186 |
187 | }
188 | }
189 | }
190 | );
191 | ```
192 |
193 | Далее в объекте, относящемуся к брейкпоинту, нужно указать параметр `targetElement`. Его значением должен быть также, как и у `sourceElement` — объект типа `Element`. Этот параметр отвечает за элемент, в который должен быть перенесён `sourceElement`. По аналогии с `sourceElement` я воспользуюсь методом `document.getElementById()`.
194 |
195 | ```JS
196 | new TransferElements(
197 | {
198 | sourceElement: document.getElementById('id-1'),
199 | breakpoints: {
200 | 990: {
201 | targetElement: document.getElementById('id-2')
202 | }
203 | }
204 | }
205 | );
206 | ```
207 |
208 | Если вы всё сделали правильно и на этом этапе не появилось никаких ошибок, то, переключившись на указанный вами брейкпоинт, увидите, что `sourceElement` перенёсся в `targetElement` и находится в нулевой позиции. Счёт начинается с нуля, и важно помнить об этом.
209 |
210 | Настало время поговорить про последний параметр — `targetPosition`. Это позиция, в которой `sourceElement` должен находиться в `targetElement`. Значением этого параметра может быть число от нуля до общего количества элементов в `targetElement` (включительно).
211 |
212 | Допустим в `targetElement` находятся следующие элементы: `A, B, C`. Таким образом для `targetPosition` можно указать только: `0, 1, 2, 3`. Если вы укажете максимальную позицию, то есть `3`, то `sourceElement` будет вставлен в самый конец. Во всех остальных случаях `sourceElement` окажется на месте того элемента, который на данный момент занимает указанную позицию. Если у `targetElement` вообще нет дочерних элементов, то максимальной позицией будет `0` и в таком случае нет смысла указывать `targetPosition`. Это связано с тем, что параметр не является обязательным, а его значение по умолчанию — `0`.
213 |
214 | Я укажу этот параметр со значением `1`. У вас возможно будет какая-то другая позиция.
215 |
216 | ```JS
217 | new TransferElements(
218 | {
219 | sourceElement: document.getElementById('id-1'),
220 | breakpoints: {
221 | 990: {
222 | targetElement: document.getElementById('id-2'),
223 | targetPosition: 1
224 | }
225 | }
226 | }
227 | );
228 | ```
229 |
230 |
231 |
232 |
233 |
234 | | Название | Тип объекта | По умолчанию | Обязательный | Описание |
235 | | -------- | ------------ | -------- | ---------- | ---------- |
236 | | `sourceElement` | [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) | | Да | Элемент, который должен быть перенесён. |
237 | | `breakpoints` | [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) | | Да | Брейкпоинты, на которых должен быть перенесён `sourceElement`. |
238 | | `targetElement` | [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) | | Да | Элемент, в который должен быть перенесён `sourceElement`. |
239 | | `targetPosition` | [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) | `0` | Нет | Позиция, в которой `sourceElement` должен находиться в `targetElement`. |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | Chrome 120 |
250 | Edge 120 |
251 | Safari 17 |
252 | Firefox 121 |
253 | Opera 106 |
254 |
255 |
256 |
257 |
258 | ✅ |
259 | ✅ |
260 | ✅ |
261 | ✅ |
262 | ✅ |
263 |
264 |
265 |
266 |
267 |
268 |
271 |
272 | Если у вас есть какие-то идеи, как можно улучшить библиотеку или что-то стало непонятно на каком-либо из этапов при чтении документации, не стесняйтесь и пишите в разделе «Issues», либо на почту: sineylodev@gmail.com. Я заинтересован в развитии своих продуктов, поэтому постараюсь максимально оперативно отвечать на все вопросы. Вместе мы сможем сделать процесс разработки интерфейсов в разы приятнее ✨
273 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 | ❄️ Transfer Elements ❄️
3 |
4 |
5 | 
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | The future belongs to those who are preparing for it today. (Malcolm X)
21 |
22 |
23 |
24 | Documentation in Russian
25 |
26 |
27 | ## 
28 |
29 | - [Description](#description)
30 | - [Advantages](#advantages)
31 | - [Installation](#installation)
32 | - [Connection](#connection)
33 | - [Usage](#usage)
34 | - [Parameters](#parameters)
35 | - [Browser compatibility](#browser-compatibility)
36 | - [Community](#community)
37 |
38 |
39 |
40 |
41 |
42 | “Transfer Elements” — a library that allows you to dynamically transfer any elements from one place to another on breakpoints.
43 |
44 | ### Why might this be necessary?
45 |
46 | Let's imagine that the designer initially prepared a layout only for computers, in which there is a complex header consisting of several rows. In one of these rows there is an input field for searching for products and some other elements. After some time, the designer starts designing the first layout for the adaptive and realizes that the search field does not have enough space and decides to move it to a more free row.
47 |
48 | As developers, we need to solve this problem somehow, and without using JS, there are two options that are not very good.
49 |
50 | The first — to duplicate the markup. The disadvantage of this method is that the code is bloated. And okay, if you need to move one `
`, but if the whole section? In addition, the element may have an `id` attribute, then it will also have to be changed, since there cannot be two identical `id` in the markup.
51 |
52 | The second — to use absolute positioning. This option seems good, but in fact it is even worse, as it breaks accessibility. Yes, visually the element will be where we need it, bun in the `DOM` it will remain in the same place. And screen readers overwhelmingly focus on the `DOM` rather than the visual location, because it can be anything.
53 |
54 | So the library **solves** this problem. It just “takes” an element from the `DOM` and moves it to where you need it. At the same time, there is no duplication in the markup or the use of absolute positioning.
55 |
56 | ---
57 |
58 | The main **purpose** of this library — to give freedom for web designers to create. I believe that they should not adapt to us, but we should adapt to them. The more tools that allow us to implement their ideas begin to appear, the faster we will come to a point where each site will look modern, and most importantly, it will be convenient to use from any device.
59 |
60 |
61 |
62 |
63 |
64 | - **Without dependencies**. All the code is written from scratch and there are no third-party solutions.
65 | - **Innovative technology**. The library is based on own technology “Transfer Elements Simulation” (TES). Thanks to it, the sequential transfer (lifting the entire breakpoint chain) is performed only once.
66 | - **Multiple transfer**. The maximum number of breakpoints is unlimited. Add as many of them as required for your project.
67 | - **Two-step data validation**. A wide variety of user data checks have been added to the library. At the first stage, the data is checked for compliance with the required type. After that, the second stage begins and the possibility of inserting into the target element is checked. There is also an additional check for rare cases built into TES. If something goes wrong, you will receive a detailed error message.
68 | - **Accessibility**. Breakpoints have no connection to any CSS units of measurement. Despite the fact that there is some similarity to pixels (`px`), all the library code works with a regular number. Therefore, when you change the font settings in the browser, nothing will break.
69 | - **Speed**. In addition to raising the entire breakpoint chain once, the search for the breakpoint itself, at the time of the main transfer, is performed in logarithmic time `O(log n)`.
70 |
71 |
72 |
73 |
74 |
75 | > [!IMPORTANT]
76 | > If you don't use modules or you do, but you want to import not from `node_modules`, skip this section and move on to the next one.
77 |
78 | Depending on the package manager you are using, select a command and run it in the terminal.
79 |
80 | ```
81 | npm install transfer-elements
82 | ```
83 |
84 | ```
85 | yarn add transfer-elements
86 | ```
87 |
88 |
89 |
90 |
91 |
92 | ### Modules
93 |
94 | If you have installed the library, you can import it from `node_modules`.
95 |
96 | ```JS
97 | import TransferElements from 'transfer-elements';
98 | ```
99 |
100 | If you haven't installed the library, you can import it from the CDN.
101 |
102 | ```JS
103 | import TransferElements from 'https://cdn.jsdelivr.net/npm/transfer-elements@1.0.7/dist/transfer-elements.esm.min.js'
104 | ```
105 |
106 | ### Tag \
112 | ```
113 |
114 | The link is shorter than for modules because the file is requested by default `transfer-elements.min.js `. Anyway, you can specify the full link.
115 |
116 | ```HTML
117 |
118 | ```
119 |
120 | ### Other
121 |
122 | If all of the above options are not suitable for any reason, you can [download the files](https://registry.npmjs.org/transfer-elements/-/transfer-elements-1.0.7.tgz) and connect the library the way you need.
123 |
124 |
125 |
126 |
127 |
128 | > [!NOTE]
129 | > A fairly large number of checks have been added to the library. This is necessary in order for the main mechanism to receive only the correct data. Therefore, even if you do something wrong, you will see an error in DevTools in the console section.
130 |
131 | The first thing you need to do after connecting is to call the constructor.
132 |
133 | ```JS
134 | new TransferElements();
135 | ```
136 |
137 | It accepts a set of `{}` objects with parameters. That is, you can specify either one object or several if you need to transfer different elements. For example, I will specify only one.
138 |
139 | ```JS
140 | new TransferElements(
141 | {
142 |
143 | }
144 | );
145 | ```
146 |
147 | After that, we will add the first and perhaps the most important parameter to the object — `sourceElement`. This is an element that will be moved to other elements or to the same one, but with a changed position, on breakpoints, which we will specify later.
148 |
149 | Since the value of this parameter must be an object of type `Element`, you can use any method that returns such a value. I use `document.getElementById()` most often, so I'll use it.
150 |
151 | ```JS
152 | new TransferElements(
153 | {
154 | sourceElement: document.getElementById('id-1')
155 | }
156 | );
157 | ```
158 |
159 | > [!TIP]
160 | > If you need to move several elements, then you can specify objects with them in any order. The mechanism will adjust the insertion order itself, based on the order of these elements in the DOM.
161 |
162 | Then add the `breakpoints` parameter, the value of which should be an object of type `Object`. It will store all the breakpoints where the `sourceElement` should be moved.
163 |
164 | ```JS
165 | new TransferElements(
166 | {
167 | sourceElement: document.getElementById('id-1'),
168 | breakpoints: {
169 |
170 | }
171 | }
172 | );
173 | ```
174 |
175 | > [!TIP]
176 | > The library is based on the `Desktop First` approach. This means that the transfer of elements will be carried out from a larger breakpoint to a smaller one. But despite this, you can specify breakpoints in any order.
177 |
178 | The breakpoint itself consists of a trigger of type `string`, that is, a key, and an object of type `Object`, which is a value. As a trigger, you can specify almost any value that can be converted to a number, except zero (assuming it's an integer), negative numbers, and numbers beyond `Number.MAX_SAFE_INTEGER`. Now I will add a random breakpoint `990`.
179 |
180 | ```JS
181 | new TransferElements(
182 | {
183 | sourceElement: document.getElementById('id-1'),
184 | breakpoints: {
185 | 990: {
186 |
187 | }
188 | }
189 | }
190 | );
191 | ```
192 |
193 | Next, in the breakpoint object, you need to specify the `targetElement` parameter. Its value should be the same as that of `sourceElement` — an object of type `Element`. This parameter is responsible for the element to which the `sourceElement` should be moved. By analogy with `sourceElement` I will use the `document.getElementById()` method.
194 |
195 | ```JS
196 | new TransferElements(
197 | {
198 | sourceElement: document.getElementById('id-1'),
199 | breakpoints: {
200 | 990: {
201 | targetElement: document.getElementById('id-2')
202 | }
203 | }
204 | }
205 | );
206 | ```
207 |
208 | If you did everything correctly and no errors appeared at this stage, then switching to the breakpoint you specified, you will see that the `sourceElement` has moved to the `targetElement` and is in the zero position. The account starts from zero, and it is important to remember this.
209 |
210 | It's time to talk about the last parameter — `targetPosition`. This is the position where the `sourceElement` should be in the `targetElement`. The value of this parameter can be a number from zero to the total number of elements in the `targetElement` (inclusive).
211 |
212 | Let's say the following elements are in the `targetElement`: `A, B, C`. Thus, for `targetPosition`, you can specify only: `0, 1, 2, 3`. If you specify the maximum position, i.e. `3`, then the `sourceElement` will be inserted at the very end. In all other cases, the `sourceElement` will be in place of the element that currently occupies the specified position. If `targetElement` has no child elements at all, then the maximum position will be `0` and in this case it makes no sense to specify `targetPosition`. This is because the parameter is optional, and its default value is `0`.
213 |
214 | I will specify this parameter with the value `1`. You may have some other position.
215 |
216 | ```JS
217 | new TransferElements(
218 | {
219 | sourceElement: document.getElementById('id-1'),
220 | breakpoints: {
221 | 990: {
222 | targetElement: document.getElementById('id-2'),
223 | targetPosition: 1
224 | }
225 | }
226 | }
227 | );
228 | ```
229 |
230 |
231 |
232 |
233 |
234 | | Name | Object type | Default | Required | Description |
235 | | -------- | ---------- | ------------ | ---------- | ---------- |
236 | | `sourceElement` | [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) | | Yes | The element that needs to be transferred. |
237 | | `breakpoints` | [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) | | Yes | Breakpoints based on which the `sourceElement` should be transferred. |
238 | | `targetElement` | [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) | | Yes | The element to which the `sourceElement` should be transferred. |
239 | | `targetPosition` | [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) | `0` | No | The position where the `sourceElement` should be in the `targetElement`. |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | Chrome 120 |
250 | Edge 120 |
251 | Safari 17 |
252 | Firefox 121 |
253 | Opera 106 |
254 |
255 |
256 |
257 |
258 | ✅ |
259 | ✅ |
260 | ✅ |
261 | ✅ |
262 | ✅ |
263 |
264 |
265 |
266 |
267 |
268 |
271 |
272 | If you have any ideas on how to improve the library or something became unclear at any stage when reading the documentation, do not hesitate and write in the “Issues” section, or by email: sineylodev@gmail.com. I am interested in developing my products, so I will try to answer all questions as quickly as possible. Together we can make the interface development process much more enjoyable ✨
273 |
--------------------------------------------------------------------------------
/src/transfer-elements.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Dynamic transfer of elements from one place to another at breakpoints.
3 | * @copyright SineYlo, 2024
4 | * @version 1.0.7
5 | * @license MIT
6 | */
7 |
8 | class TransferElements {
9 | constructor(...objectsWithParameters) {
10 | if (objectsWithParameters.length === 0) {
11 | throw TypeError('at least one object with parameters must be specified for the constructor');
12 | }
13 |
14 | const sourceElements = [];
15 |
16 | const validatedObjectsWithParameters = objectsWithParameters.map(
17 | (objectWithParameters) => {
18 | if (
19 | this.#getObjectType(objectWithParameters) !== '[object Object]'
20 | ) {
21 | throw TypeError(`the arguments specified for the constructor must be objects of type 'Object'`);
22 | }
23 |
24 | ['sourceElement', 'breakpoints'].forEach((parameterKey) => {
25 | if (!(Object.hasOwn(objectWithParameters, parameterKey))) {
26 | throw TypeError(`the '${parameterKey}' parameter is not specified for the main object`);
27 | }
28 | });
29 |
30 | const { sourceElement, breakpoints } = objectWithParameters;
31 |
32 | if (!(sourceElement instanceof Element)) {
33 | throw TypeError(`the value specified for the 'sourceElement' parameter must be an object of type 'Element'`);
34 | }
35 |
36 | if (sourceElements.includes(sourceElement)) {
37 | throw TypeError(`there can only be one object in the constructor with such a 'sourceElement': '${sourceElement.cloneNode().outerHTML}'`);
38 | }
39 |
40 | sourceElements.push(sourceElement);
41 |
42 | objectWithParameters.breakpoints = this.#assembleBreakpoints(
43 | breakpoints,
44 | sourceElement
45 | );
46 |
47 | return objectWithParameters;
48 | }
49 | );
50 |
51 | const sortedBreakpointTriggers = [...(
52 | validatedObjectsWithParameters.reduce(
53 | (collection, { breakpoints }) => {
54 | Object.keys(breakpoints).forEach((breakpointTrigger) => {
55 | if (Number(breakpointTrigger)) {
56 | collection.add(breakpointTrigger);
57 | }
58 | });
59 |
60 | return collection;
61 | },
62 |
63 | new Set()
64 | ).add('default')
65 | )].sort((a, b) => a - b);
66 |
67 | const storageOfBreakpoints = sortedBreakpointTriggers.reduce(
68 | (storage, breakpointTrigger) => {
69 | storage.set(breakpointTrigger, []);
70 |
71 | return storage;
72 | },
73 |
74 | new Map()
75 | );
76 |
77 | validatedObjectsWithParameters.forEach(
78 | ({ sourceElement, breakpoints }) => {
79 | Object.entries(breakpoints).forEach(
80 | ([breakpointTrigger, { targetElement, targetPosition }]) => {
81 | storageOfBreakpoints.get(breakpointTrigger).push({
82 | sourceElement,
83 | targetElement,
84 | targetPosition
85 | });
86 | }
87 | );
88 | }
89 | );
90 |
91 | storageOfBreakpoints.forEach((breakpointObjects) => {
92 | this.#sortBreakpointObjects(breakpointObjects);
93 |
94 | this.#removeSourceElements(breakpointObjects);
95 | this.#insertSourceElements(breakpointObjects, true);
96 |
97 | breakpointObjects.length = 0;
98 |
99 | sourceElements.forEach((sourceElement) => {
100 | breakpointObjects.push(this.#generateBreakpointObject(
101 | sourceElement,
102 | true
103 | ));
104 | });
105 |
106 | this.#sortBreakpointObjects(breakpointObjects);
107 | });
108 |
109 | let previousBreakpointTrigger = 'default';
110 |
111 | const resizeObserver = new ResizeObserver(
112 | ([{ borderBoxSize: [{ inlineSize }], target }]) => {
113 | const currentWidth = inlineSize + this.#getScrollbarWidth(target);
114 |
115 | const currentBreakpointTrigger = this.#getBreakpointTrigger(
116 | sortedBreakpointTriggers,
117 | currentWidth
118 | );
119 |
120 | if (previousBreakpointTrigger !== currentBreakpointTrigger) {
121 | const breakpointObjects = storageOfBreakpoints.get(
122 | currentBreakpointTrigger
123 | );
124 |
125 | this.#removeSourceElements(breakpointObjects);
126 | this.#insertSourceElements(breakpointObjects, false);
127 |
128 | previousBreakpointTrigger = currentBreakpointTrigger;
129 | }
130 | }
131 | );
132 |
133 | resizeObserver.observe(document.documentElement);
134 | }
135 |
136 | #assembleBreakpoints(breakpoints, sourceElement) {
137 | if (this.#getObjectType(breakpoints) !== '[object Object]') {
138 | throw TypeError(`the value specified for the 'breakpoints' parameter must be an object of type 'Object'`);
139 | }
140 |
141 | const breakpointEntries = Object.entries(breakpoints);
142 |
143 | if (breakpointEntries.length === 0) {
144 | throw TypeError(`at least one breakpoint must be specified for the 'breakpoints' object`);
145 | }
146 |
147 | const validatedBreakpoints = Object.fromEntries(
148 | breakpointEntries.map(
149 | ([breakpointTrigger, breakpointObject]) => {
150 | const breakpointTriggerAsNumber = Number(breakpointTrigger);
151 |
152 | if (
153 | !breakpointTriggerAsNumber ||
154 | breakpointTriggerAsNumber <= 0 ||
155 | breakpointTriggerAsNumber > Number.MAX_SAFE_INTEGER
156 | ) {
157 | throw RangeError(`the breakpoint trigger must be a safe (integer or fractional) number greater than zero`);
158 | }
159 |
160 | if (this.#getObjectType(breakpointObject) !== '[object Object]') {
161 | throw TypeError(`the breakpoint object must be of type 'Object'`);
162 | }
163 |
164 | if (!Object.hasOwn(breakpointObject, 'targetElement')) {
165 | throw TypeError(`the 'targetElement' parameter is not specified for the breakpoint object`);
166 | }
167 |
168 | const { targetElement, targetPosition } = breakpointObject;
169 |
170 | if (!(targetElement instanceof Element)) {
171 | throw TypeError(`the value specified for the 'targetElement' parameter must be an object of type 'Element'`);
172 | }
173 |
174 | if (sourceElement === targetElement) {
175 | throw TypeError(`the value specified for the 'targetElement' parameter must be different from the value specified for the 'sourceElement' parameter`);
176 | }
177 |
178 | if (this.#isTargetElementDescendantOfSourceElement(
179 | targetElement, sourceElement
180 | )) {
181 | throw TypeError(`the element that is specified as the value for the 'targetElement' parameter must not be a descendant of the element specified as the value for the 'sourceElement' parameter`);
182 | }
183 |
184 | if (this.#isTagOfTargetElementSelfClosing(targetElement)) {
185 | throw TypeError(`the element specified as the value for the 'targetElement' parameter must be a paired tag`);
186 | }
187 |
188 | if (Object.hasOwn(breakpointObject, 'targetPosition')) {
189 | if (typeof targetPosition !== 'number') {
190 | throw TypeError(`the value specified for the 'targetPosition' parameter must be of type 'number'`);
191 | }
192 |
193 | if (targetPosition < 0 || !Number.isSafeInteger(targetPosition)) {
194 | throw RangeError(`the number specified as the value for the 'targetPosition' parameter must be a non-negative safe integer`);
195 | }
196 | }
197 |
198 | return [
199 | breakpointTriggerAsNumber,
200 | {
201 | targetPosition: targetPosition ?? 0,
202 |
203 | ...breakpointObject
204 | }
205 | ];
206 | }
207 | )
208 | );
209 |
210 | validatedBreakpoints.default = this.#generateBreakpointObject(
211 | sourceElement,
212 | false
213 | );
214 |
215 | return validatedBreakpoints;
216 | }
217 |
218 | #getChildElementsOfTargetElement(targetElement) {
219 | return targetElement.children;
220 | }
221 |
222 | #getBreakpointTrigger(breakpointTriggers, currentWidth) {
223 | let startIndex = 0;
224 | let endIndex = breakpointTriggers.length - 2;
225 | let savedBreakpointTrigger;
226 |
227 | while (startIndex <= endIndex) {
228 | const middleIndex = Math.floor((startIndex + endIndex) / 2);
229 | const guessedBreakpointTrigger = breakpointTriggers[middleIndex];
230 |
231 | if (guessedBreakpointTrigger == currentWidth) {
232 | return guessedBreakpointTrigger;
233 | } else if (guessedBreakpointTrigger > currentWidth) {
234 | endIndex = middleIndex - 1;
235 | } else {
236 | startIndex = middleIndex + 1;
237 | }
238 |
239 | if ((guessedBreakpointTrigger - currentWidth) > 0) {
240 | savedBreakpointTrigger = guessedBreakpointTrigger;
241 | }
242 | }
243 |
244 | return savedBreakpointTrigger ?? 'default';
245 | }
246 |
247 | #getScrollbarWidth(observableElement) {
248 | const viewportWidth = window.innerWidth;
249 | const widthOfObservableElement = Math.min(
250 | observableElement.clientWidth,
251 | observableElement.offsetWidth
252 | );
253 |
254 | let scrollbarWidth = 0;
255 |
256 | if (widthOfObservableElement !== viewportWidth) {
257 | scrollbarWidth += viewportWidth - widthOfObservableElement;
258 | }
259 |
260 | return scrollbarWidth;
261 | }
262 |
263 | #getObjectType(object) {
264 | return Object.prototype.toString.call(object);
265 | }
266 |
267 | #isTargetElementDescendantOfSourceElement(
268 | targetElement,
269 | sourceElement
270 | ) {
271 | while (targetElement = targetElement.parentElement) {
272 | if (targetElement === sourceElement) {
273 | return true;
274 | }
275 | }
276 |
277 | return false;
278 | }
279 |
280 | #isTagOfTargetElementSelfClosing(targetElement) {
281 | return !new RegExp(/<\/[a-zA-Z]+>$/).test(targetElement.outerHTML);
282 | }
283 |
284 | #sortBreakpointObjects(breakpointObjects) {
285 | if (breakpointObjects.length > 1) {
286 | breakpointObjects.sort((a, b) => (
287 | a.targetPosition - b.targetPosition
288 | ));
289 | }
290 | }
291 |
292 | #removeSourceElements(breakpointObjects) {
293 | breakpointObjects.forEach(({ sourceElement }) => {
294 | sourceElement.remove();
295 | });
296 | }
297 |
298 | #insertSourceElements(
299 | breakpointObjects,
300 | hasCheckOfMaximumTargetPosition
301 | ) {
302 | breakpointObjects.forEach(
303 | ({ sourceElement, targetElement, targetPosition }) => {
304 | const childElementsOfTargetElement = (
305 | this.#getChildElementsOfTargetElement(targetElement)
306 | );
307 |
308 | if (hasCheckOfMaximumTargetPosition) {
309 | this.#throwExceptionIfMaximumTargetPositionIsExceeded(
310 | childElementsOfTargetElement,
311 | targetPosition
312 | );
313 | }
314 |
315 | const childElementOfTargetElement = (
316 | childElementsOfTargetElement[targetPosition]
317 | );
318 |
319 | if (childElementOfTargetElement) {
320 | childElementOfTargetElement.before(sourceElement);
321 | } else {
322 | targetElement.append(sourceElement);
323 | }
324 | }
325 | );
326 | }
327 |
328 | #throwExceptionIfMaximumTargetPositionIsExceeded(
329 | childElementsOfTargetElement,
330 | targetPosition
331 | ) {
332 | const maximumTargetPosition = childElementsOfTargetElement.length;
333 |
334 | if (targetPosition > maximumTargetPosition) {
335 | throw RangeError(`the number specified as the value for the 'targetPosition' parameter exceeds the maximum allowed value of '${maximumTargetPosition}'`);
336 | }
337 | }
338 |
339 | #generateBreakpointObject(sourceElement, isComplete) {
340 | const parentElementOfSourceElement = sourceElement.parentElement;
341 |
342 | const breakpointObject = {
343 | targetElement: parentElementOfSourceElement,
344 | targetPosition: [
345 | ...parentElementOfSourceElement.children
346 | ].findIndex(
347 | (childElementOfSourceElement) => (
348 | childElementOfSourceElement === sourceElement
349 | )
350 | )
351 | };
352 |
353 | if (isComplete) {
354 | breakpointObject.sourceElement = sourceElement;
355 | }
356 |
357 | return breakpointObject;
358 | }
359 | }
360 |
--------------------------------------------------------------------------------