Есть динамический список с группировками, и очень хочется навести порядок с помощью обычного драг-эн-дропа, перетаскивая нужные объекты или целые группировки туда-сюда. Идея, думаю, ясна, полезность её мне тоже видится немалой. Теории на тему поведения "демонических списков" в случае перетаскивания уже изрядно (например, //infostart.ru/public/419306/), поэтому предлагаю работающее решение, точнее, оснастку для него.
Состав оснастки: технические процедуры и обработчики событий элемента этого списка, которые надо будет подключить, а также одна-единственная безконтекстная процедура собственно изменения данных - она пишется на усмотрение разработчика согласно логике прикладного решения.
1. Позволяет перетаскивать один или несколько выделенных объектов.
2. При перетаскивании показывает, куда оно "упадёт".
3. Позволяет "бросить" и над элементом (тогда перенесётся в его непосредственного "родителя" по иерархии), и над группировкой любого уровня.
4. При перетаскивании "элемент взяли - над элементом бросили" не дёргает сервер низачем, кроме самого изменения данных объекта.
#Область ПравкаДинамическогоСпискаПеретаскиванием
#Область Описание
// Если ТекущиеДанные и ТекущаяСтрока не перепозиционируется, то до конца перетаскивания хранят
// исходную строку; в параметре Строка фигурирует значение (ТекущаяСтрока), над которой тащим в данный момент.
// Если ТекущийЭлемент не перепозиционируется, то до конца перетаскивания хранит исходную колонку;
// в параметре Поле фигурирует имя поля, над которым тащим в данный момент.
//
// Переустановка Действия=Перемещение ничего не даёт, несмотря на ДопустимыеДействия.КопированиеИПеремещение
// естественно, курсор не меняется, как нам надо, а меняется сам, когда находится над группировкой (а если его начать
// переопределять, то вообще не меняется, иконка отмены).
//
// В массиве Значение можно передавать любую клиентско-существующую информацию.
//
// Событие Перетаскивание не вызывается и не используется.
#КонецОбласти
#Область СлужебныеМеханизмы
&НаСервере
Функция ПолучитьДанныеБлокаНаСервере(рИмяЭлемента,соотЗначенияПолей)
Попытка
рСКД=Элементы[рИмяЭлемента].ПолучитьИсполняемуюСхемуКомпоновкиДанных();
рНастройки=Элементы[рИмяЭлемента].ПолучитьИсполняемыеНастройкиКомпоновкиДанных();
//
// нам нужны только Ссылка, но проще отфильтровать их по заполненности, чем перенастраивать Структуру
//
Для каждого киз Из соотЗначенияПолей Цикл
// по-хорошему ещё бы проверку, а нет ли других отборов на это поле; черновик!
отб=рНастройки.Отбор.Элементы.Добавить(Тип("ЭлементОтбораКомпоновкиДанных"));
отб.ЛевоеЗначение=Новый ПолеКомпоновкиДанных(киз.Ключ); // уже содержит путь к данным через точку
отб.ВидСравнения=ВидСравненияКомпоновкиДанных.Равно;
отб.ПравоеЗначение=киз.Значение;
отб.Использование=Истина;
КонецЦикла;
//
КомпоновщикМакета=Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки=КомпоновщикМакета.Выполнить(рСКД,рНастройки,,,Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
ПроцессорКД=Новый ПроцессорКомпоновкиДанных;
ПроцессорКД.Инициализировать(МакетКомпоновки,,,Истина);
ПроцессорВывода=Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
трез=Новый ТаблицаЗначений;
ПроцессорВывода.УстановитьОбъект(трез);
трез=ПроцессорВывода.Вывести(ПроцессорКД,Истина);
//
Возврат трез.ВыгрузитьКолонку("Ссылка");
Исключение
Сообщить("ПолучитьДанныеБлокаНаСервере, ошибка: "+ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
КонецФункции
&НаСервереБезКонтекста
Функция ИзменитьЗначенияГруппировочныхПолейДляОбъектаНаСервере(рСсылка,соотЗначенияПолей)
рОбъект=рСсылка.ПолучитьОбъект();
// использовать ЗаполнитьЗначенияСвойств(рОбъект,соотЗначенияПолей) не рекомендуется
//<ВНИМАНИЕ! ЗДЕСЬ ВСТАВИТЬ КОНКРЕТИКУ ИЗМЕНЕНИЯ ПОЛЕЙ СОГЛАСНО ЛОГИКЕ ПРИКЛАДНОГО РЕШЕНИЯ!>
//
// В простейшем случае:
инфо="";
Для каждого киз Из соотЗначенияПолей Цикл
инфо=инфо+Символы.ПС+"Изменяется "+киз.Ключ+" = "+Строка(киз.Значение);
//рОбъект[киз.Ключ]=киз.Значение; // например, так
КонецЦикла;
Сообщить(инфо);
рОбъект.ОбменДанными.Загрузка=Истина;
Попытка
рОбъект.Записать();
Возврат Истина;
Исключение
Сообщить("Ошибка: "+ОписаниеОшибки());
Возврат Ложь;
КонецПопытки;
КонецФункции
&НаКлиенте
Функция ПолучитьЗначенияПолейГруппировок(ЭлементДС,рЭтоГруппировка=Неопределено)
текдан=ЭлементДС.ТекущаяСтрока;
Если ТипЗнч(текдан)=Тип("СтрокаГруппировкиДинамическогоСписка") Тогда // находимся где-то на узле дерева
рЭтоГруппировка=Истина;
рДанные=текдан;
Иначе // находимся на конечном элементе дерева
рЭтоГруппировка=Ложь;
рДанные=ЭлементДС.ТекущиеДанные.РодительскаяГруппировкаСтроки;
КонецЕсли;
//
// хотя, конечно, можно использовать ЭлементДС.Группировка, но там только поля, а тут сразу ещё и их значения
соотДанных=Новый Соответствие;
Пока Истина Цикл
Если рЭтоГруппировка Тогда // группировочное значение - другого типа, у него может не быть этих полей
соотДанных.Вставить(рДанные.ИмяГруппировки,рДанные.Ключ);
Иначе
мПутьКДанным=СтрРазделить(рДанные.ИмяГруппировки,".",Ложь);
Если мПутьКДанным.Количество()=1 Тогда // прямое получение
соотДанных.Вставить(рДанные.ИмяГруппировки,рДанные.Ключ);
Иначе // разыменование, нужно только первое поле; все эти поля были в колонках, иначе бы не работала группировка
рПоле=мПутьКДанным.Получить(0);
Попытка соотДанных.Вставить(рПоле,текдан[рПоле]) Исключение КонецПопытки;
КонецЕсли;
КонецЕсли;
// к следующей итерации вверх по иерархии
рДанные=рДанные.РодительскаяГруппировка;
Если рДанные=Неопределено Тогда Прервать КонецЕсли;
КонецЦикла;
//
Возврат соотДанных;
КонецФункции
#КонецОбласти
#Область ОбработчикиСобытийПеретаскивания
&НаКлиенте
Процедура СписокНачалоПеретаскивания(Элемент, ПараметрыПеретаскивания, Выполнение)
рЭтоГруппировка=Неопределено; // фиксируем исходную ситуацию
соотДанных=ПолучитьЗначенияПолейГруппировок(Элемент,рЭтоГруппировка);
//
Если рЭтоГруппировка Тогда // тащут не конечную ветку, а группировку, придётся смотреть на сервере
мСсылок=ПолучитьДанныеБлокаНаСервере(Элемент.Имя,соотДанных);
Если ТипЗнч(мСсылок)=Тип("Массив") и мСсылок.Количество()>0 Тогда
// переопределеяем перетаскиваемый состав данных
ПараметрыПеретаскивания.Значение.Очистить();
Для каждого знч Из мСсылок Цикл
Если не ЗначениеЗаполнено(знч) Тогда Продолжить КонецЕсли; // группировочное пусто в Ссылке
ПараметрыПеретаскивания.Значение.Добавить(знч);
КонецЦикла;
// и переопределяем данные для них, т.к. для элемента другие пути и поля
Элемент.ТекущаяСтрока=мСсылок.Получить(0); // они все одинаковы в этом смысле
соотДанных=ПолучитьЗначенияПолейГруппировок(Элемент);
Иначе
Выполнение=Ложь; Возврат;
КонецЕсли;
Иначе
// ничего не изменяем в перетаскиваемом составе данных
КонецЕсли;
//
ПараметрыПеретаскивания.Значение.Добавить(соотДанных); // дописываем к самим значениями
КонецПроцедуры
&НаКлиенте
Процедура СписокПроверкаПеретаскивания(Элемент, ПараметрыПеретаскивания, СтандартнаяОбработка, Строка, Поле)
Элемент.ТекущаяСтрока=Строка; // заодно удобно видеть, куда бросаем (при неудобном указателе мыши)
КонецПроцедуры
&НаКлиенте
Процедура СписокОкончаниеПеретаскивания(Элемент, ПараметрыПеретаскивания, СтандартнаяОбработка)
спЗначение=ПараметрыПеретаскивания.Значение;
соотПоляИсходного=спЗначение.Получить(спЗначение.Количество()-1); // добавляется последним, служебно
Если ТипЗнч(соотПоляИсходного)<>Тип("Соответствие") Тогда Возврат КонецЕсли;
//
рЭтоГруппировка=Неопределено;
соотПоляТекущего=ПолучитьЗначенияПолейГруппировок(Элемент,рЭтоГруппировка); // смотрим итоговую ситуацию
Если рЭтоГруппировка Тогда // бросили над группой
мРазыменуемых=Новый Массив;
Для каждого киз Из соотПоляТекущего Цикл
Если Найти(киз.Ключ,".")<>0 Тогда мРазыменуемых.Добавить(киз.Ключ) КонецЕсли;
КонецЦикла;
Если мРазыменуемых.Количество()>0 Тогда // придётся разыменовать и посмотреть значения исходных полей,
// а для этого придётся получить фрагмент данных и взять любое его поле, чтобы получить исходное значение.
// можно сделать для этого отдельный запрос (ПЕРВЫЕ 1), чтобы не тащить возможно большой лишний кусок.
мСсылок=ПолучитьДанныеБлокаНаСервере(Элемент.Имя,соотПоляТекущего);
Если ТипЗнч(мСсылок)=Тип("Массив") и мСсылок.Количество()>0 Тогда
рСсылкаВГруппировке=мСсылок.Получить(мСсылок.Количество()-1); // допустим, последний в выборке
Для каждого знчРазыменуемое Из мРазыменуемых Цикл
рПоле=СтрРазделить(знчРазыменуемое,".",Ложь).Получить(0);
Попытка соотПоляТекущего.Вставить(рПоле,рСсылкаВГруппировке[рПоле]) Исключение КонецПопытки;
КонецЦикла;
КонецЕсли;
КонецЕсли; // если были поля группировок, являющиеся разыменованиями
КонецЕсли;
// Сравниваем (меняются только те поля, которые действительно отличают группировку,
// т.е. если бросили в старшей группе, то меняется её значение у элемента, а значения всех
// младших групп при этом не меняются. Если бросили в своей группе, ничего не меняется.
соотИзменённых=Новый Соответствие;
Для каждого кизИсходного Из соотПоляИсходного Цикл
знчТекущего=соотПоляТекущего.Получить(кизИсходного.Ключ);
Если знчТекущего=Неопределено Тогда Продолжить КонецЕсли; // не нашли...
Если знчТекущего<>кизИсходного.Значение Тогда
соотИзменённых.Вставить(кизИсходного.Ключ,знчТекущего); // новое значение
КонецЕсли;
КонецЦикла;
//
Если соотИзменённых.Количество()=0 Тогда Возврат КонецЕсли;
// собственно, изменяем поля объекта на сервере
рНадоОбновить=Ложь;
Для й=0 По спЗначение.Количество()-2 Цикл
рКогоТащим=спЗначение.Получить(й);
Если ИзменитьЗначенияГруппировочныхПолейДляОбъектаНаСервере(рКогоТащим,соотИзменённых) Тогда
рНадоОбновить=Истина;
КонецЕсли;
КонецЦикла;
Если рНадоОбновить Тогда
Элемент.Обновить();
КонецЕсли;
КонецПроцедуры
#КонецОбласти
#КонецОбласти
Ограничение текущего решения - нельзя одновременно выделять и концевые элементы, и группировки, и нельзя выделять элементы из разных группировок. Это уже несложно допилить, имея общий принцип.
Некоторая специфика в том, что источник и получатель события - один и тот же элемент формы. Тестировалось на 8.3.6 в толстом и тонком клиенте.
Если это кому пригодится, будет неплохо)