Однажды тихими вечером... Впрочем, отбросим литературные изыски. В нашей организации довольно много различных бизнес-процессов, которые различаются реквизитами, документами и при этом используют один вид задач для движения по точкам. Задачи показываются в одном списке, пользователи их видят, нажимают кнопочки, исполняют, все как обычно. Колонки в списке задач были жестко заданы, показывая информацию из различных процессов. До определенного момента, пока количество ключевых параметров в бизнес-процессах примерно соответствовало, это всех устраивало. Но в один "прекрасный" момент у нас появились задачи с неизвестным заранее количеством ключевых реквизитов (как пример сведения об исполнителях групповых задач) и список перестал устраивать. Попытка пойти по пути СКД не увенчалась успехом - слишком долго перерисовывалась картинка при обновлении. Динамический список в обычной форме отсутствует, а управляемую форму в обычном клиенте нельзя (или я не знаю как) закрепить на экране. Такие мелкие казалось бы недостатки привлекали внимание руководства и обе идеи забраковали. Остался один вариант - создать заполняемый HTMLДокумент. Опыт создания такого документа, а точнее способ избежать все ошибки, которые я совершил при его создании, предлагаю уважаемым читателям. Сразу отмазка ака дисклеймер. Все справедливо и проверено для windows и обычных форм. Динамические не тестировались, линукс тем более.
При написании списка использовалась публикация Инфостарта: Javascript и 1С. Кросс-платформенное взаимодействие. Спасибо автору и участникам дискуссии.
Итак, сначала советы, куда без них.
Совет первый: Обязательно ориентируйтесь на версии IE, установленные на клиентских машинах, которые предполагают использование вашей разработки. Фрик-шоу стандартов от Микрософт, которая как тот сержант, одна идет в ногу, изрядно подпортит вам кровь при написании. Все, что ниже 9 версии, сразу ставит крест на разработке, ибо кривая поддержка css и обработки событий делает трудозатраты непропорциональными выхлопу. У меня два основных браузера были IE10 и 11 и это уже вносило достаточно разнообразия в рутинный процесс программирования. Обязательная строка в заголовке, если вы не хотите скатиться во встроенный в 1С IE6:
<meta http-equiv="X-UA-Compatible" content="IE=edge">
Первые кавычки расскажут браузеру, что нужно использовать HTML5 по возможности, вторые заставят поддерживать самую новую версию браузера, установленную на компьютере.
Совет второй: Для общения с 1с используйте javascript глобальные переменные и JSON нотацию. Любая функция js нашего документа возвращает в 1с неинициализированный COMobject, который мы не можем обработать. Возможно есть способы обойти эту ситуацию, мне они неизвестны. Поэтому любой вызов функций HTML документа лучше делать в два приема: 1.Вызов функции, которая что-то запишет в переменную документа в виде строки json, затем обращение к этой строке.
Совет третий: По максимуму используйте события click и dblclick в 1с для получения данных, остальные же события инициируйте и обрабатывайте скриптами документа. И я объясню почему: Единственный вариант вызвать событие в 1с умер вместе с IE9. Это метод fireEvent. Он пробивал броню parentWindow и мог вызвать некое событие в 1С. Метод js document.dispatchEvent стартует событие на странице, но за пределы документа событие не выходит и в 1с его обработчик не срабатывает. Плюс большая часть событий, которые являются свойствами поля HTMLдокумента, давно отменены в стандартах HTML и не поддерживаются тем же IE11.
Совет четвертый: забудьте про новомодные стрелочные функции, хитрые врапперы и замыкания. Чем дубовее код, тем лучше. Нет, врапперы и замыкания существуют, но я даже боюсь предположить какой стандарт js поддерживают IE в различных изводах. Постоянно будете натыкаться на то, что те или иные приемы не работают. Например для меня открытием стало, что this в IE10 всегда обозначает глобальный контекст, даже если его запихнуть во враппер, а объявление переменной let работает вообще непонятно как.
Совет пятый, вытекающий из предыдущих: Никогда не забывайте про сайты типа mdn или w3Schools, на которых вы можете узнать какие методы поддерживаются той или иной версией браузера.
Итак, сама разработка html для работы с 1с.
Первое что я начал делать: css стили, чтобы переложить отрисовку элементов на браузер.
Сначала нам нужно опеределить стиль для контейнера, который будет содержать информацию о задаче. Поскольку тег div будет использоваться много где, то для контейнера определим отдельный класс.
Небольшое отступление: Все, что я сейчас делаю со стилями - это мой личный вкус, настройка вида списка и его элементов может быть какой угодно. Если результат вам не понравился, смело экспериментируйте.
//чтобы не расписывать, какие селекторы что обозначают,
//отправлю к справочнику по css https://www.w3schools.com/cssref/css_selectors.asp
.container {
-ms-user-select: none;
border: 2px solid transparent;
background-color: hsl(0, 0%, 95%);
border-radius: 10px;
padding: 10px 15px 10px;
margin: 10px 0;
cursor: pointer;
min-width:120px;
}
Определим определим чередование цветов для контейнера. Обратите внимание, я использую функцию hsl чтобы не заморачиваться с вычислением цветов. Берем один цвет, и просто меняем его яркость.
.container:nth-child(even) {
background-color: hsl(0, 0%, 90%);
}
Выделим рамкой элемент, на который указывает курсор
.container:hover {
border-color: hsl(0,0%,60%);
}
Для задач у нас выставлены приоритеты, которые определяются атрибутом контейнера priority. Будем выделять их цветом и подкрашивать контейнер разными оттенками в зависимости от чередования
.container[priority='1'] {
background-color: hsl(120, 92%, 90%);
}
.container[priority='1']:nth-child(even) {
background-color: hsl(120, 92%, 85%);
}
.container[priority='2'] {
background-color: hsl(203, 92%, 90%);
}
.container[priority='2']:nth-child(even) {
background-color: hsl(203, 92%, 85%);}
Устанавливаем стиль для заголовка контейнера.
.tooltip{
margin: 1px;
line-height: 0.9em;
color: hsl(0, 0%, 15%);}
Стиль для тега time. Поскольку он будет использоваться только для одной опции, определяем стиль для всего тега
time {
float: right;
color: #aaa;
}
Теперь добавим стиль таблицы, в которой будем выводить информацию о задаче.
table {
table-layout:fixed;
white-space: pre-wrap;
width:100%;
min-width:90px;
text-align: left;
margin: 10px 0px;
border-collapse: separate;
border-spacing: 5px;
background: transparent;
color: #656665;
border: transparent;
}
Определим стиль ячеек и заголовков таблицы.
table th {
font-size: 12px;
padding: 1px;
margin: 1px;
}
table td {
font-size: 12px;
background: #fff;
padding: 5px;
}
И добавим описание стиля содержимого ячеек. В ячейки таблицы я вкладываю контейнеры div, потому что для ячеек <td> есть ограничения по настройке стиля.
table div{
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
}
Теперь нужно добавить всплывающую подсказку, на случай если содержимое не влезает в ячейку (в самой ячейке текст будет забиваться точками). Обратите внимание на z-index. Он нужен, чтобы всплывающая подсказка оказалась поверх всех элементов контейнера.
table td span {
display: none;
position: absolute;
z-index:2;
background-color: cornsilk;
color: inherit;
margin: 10px 0;
text-align: center;
border-radius: 6px;
padding: 8px 0;
}
td:hover span {
display: block;
}
table div{
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
}
Добавим описание контекстного меню. Само меню не должно быть привязано к элементу и должно быть поверх всех элементов, поэтому установим position:fixed и z-index:99;
.contmenu {
position:fixed;
background-color: hsl(240, 20%, 92%);
list-style-type: none;
margin: 0px;
float:right;
padding: 5px 5px 5px 0px;
border-radius: 6px;
z-index:99;
font-size:medium;
white-space: pre-wrap;
}
Выбранные пункты меню нужно выделять:
.contmenu>li {
background-color: hsl(240, 20%, 95%);
margin: 0px 0px 1px 15px;
padding: 10px 30px 10px 10px;
}
.contmenu>li:hover{background-color: hsl(240, 20%, 80%); color:white}
Получившийся результат выглядит так:
Чтобы контекстное меню на странице работало корректно, в 1с в свойствах формы списка укажем свойство контекстное меню: отсутствует.
На этом со стилями все. Почти. Мы не учли такую штуку, как размер окна, когда при уменьшении ширины наша прекрасная картинка начинает превращаться в нечто неудобочитаемом. Добавим еще немного стиля.
@media screen and (max-width:400px){
td { display:flex; text-decoration:underline; width:100%}
th { display:none;}
td:hover span {left:20px;}
.contmenu
{display:block;
font-size:small;
overflow-x: hidden;
text-overflow: ellipsis}
time {display:none}
}
Этим стилем мы показываем, что при размере окна меньше 400 пикселей нам надо свернуть информацию в блок, спрятать заголовки и немножко урезать осетра в шрифтах, чтобы контекстное меню можно было прочитать:
Вот теперь стили полностью готовы, можно переходить к скриптам.
Первым делом определим переменные, которые будут отвечать у нас за общение с 1С
var arr=''; //это будет переменная, в которую 1С будет закачивать строку json с данными
var retval=''; //переменная, в которую мы пишем результат выбора на странице
var cur='';//при обновлении экран иногда(не понял в каких случаях)уезжает к первому элементу
//здесь мы будем хранить элемент на который надо спозиционироваться при обновлении страницы.
var filter=new Object; //Отборы в списке. Будем на них строить фильтрацию в документе.
Эти переменные будут доступны в 1С через свойство полеHTMLдокумента.документ.parentWindow.
Создаем несколько функций, которые будут основой для работы со страницей.
1.Удаление всех контейнеров. Я решил не заморачиваться с отрисовкой каждого отдельного контейнера при обновлении, потому что в таком случае нужно будет хранить где-то позиции элементов, или искать их по неким атрибутам, чтобы добавлять последовательно данные. Работы много, а конечный пользователь разницы не увидит. Поэтому, при каждом обновлении списка задач мы просто убиваем все элементы списка и добавляем заново по новым данным из 1С.
function deleteAllTasks() {
var x=document.querySelectorAll('div[id]');
if (x.lenght!=0){
for (i = 0; i < x.length; i++) {document.body.removeChild(x[i]);}
}
}
2. Присваивание элемента выбора переменной, из которой мы получаем данные в 1С.
function addValue(event) {
retval = event.currentTarget.id;
event.currentTarget.focus();
//фокус тут для манипуляций с курсором ожидание/нажатие.
//в статье я этой темы не коснулся как маловажной, поэтому строку просто игнорьте %)
}
Заметьте, я здесь и далее использую свойство currentTarget вместо target. Это сделано для того, чтобы получать элемент (контейнер) к которому обработчик прикреплен, а не элемент, вызвавший событие (например ячейка таблицы).
3.Функция очистки значений. Добавлена чтобы при частично заполненной странице при нажатии на пустое пространство не вызывалась последняя нажатая задача и для того, чтобы запоминать последнюю выбранную задачу (для запоминания последней позиции при автообновлении). У функции есть параметр, в зависимости от которого поведение меняется.
function clearVal(event,mean) {
if (mean) {(event.stopPropagation) ? event.stopPropagation() : event.cancelBubble = true;
cur=event.currentTarget.id}
else{
retval=''};
}
4. Функция обработки одного нажатия. Нужна для работы с курсором и для контекстного меню
function oneClick(event) {
event.currentTarget.blur();
closeMenu(); //удаляем меню при нажатии
}
Раз уж мы коснулись контестного меню, то следующие функции будут для работы с ним. Я решил что хранить контекстное меню в элементе и прятать/открывать его неудобно. Будем меню создавать по клику и удалять после нажатия. На быстродействие это не влияет в заметных масштабах, поэтому считаю такой вариант оптимальным в плане сложности создания.
5. Функция включения отборов из контекстного меню. В ней мы работаем с глобальной переменной filter.
function enableFilter(event) {
var action=event.currentTarget.getAttribute('action');
var curFilter=event.currentTarget.getAttribute('name');
switch (action) {
case 'add':
filter[curFilter]=event.currentTarget.getAttribute('value');
break;
case 'deleteAll':
filter=new Object;
break;
default:
delete filter[curFilter]
break;
}
}
6. Удаление контекстного меню. Все просто. Нашли по id, удалили.
function closeMenu(){
var x=document.querySelectorAll('.contmenu');
if (x.length!=0){
for (i = 0; i < x.length; i++) {x[i].parentNode.removeChild(x[i]);}
}
}
7. Создание контекстного меню.
function contextMenu(event) {
closeMenu();
var menu=document.createElement('ul');
menu.className='contmenu'; //присвоим ранее определенный в css класс.
menu.style.left=event.clientX+'px'; //настроим позицию относительно пользовательского экрана
menu.style.top=event.clientY+'px';//меню будет появляться в точке, где была нажата мышка.
var type=event.currentTarget.getAttribute('name'); //здесь я добавил только один пункт. Остальные отборы целиком на вашей фантазии
var hasFilter=0; //переменная, проверяющаяя, нужно ли добавить пункт "снять все отборы"
if (type!=''){
var action='add';//у нас есть три типа действия, которы потом обрабатываются в функции enableFilter
var actionName='Отбор по типу задачи: ';
if (filter.hasOwnProperty('type')){hasFilter++; if (filter['type']==type){action='delete';actionName='Отключить отбор по типу задачи: '}}
var li=document.createElement('li');
li.innerHTML=actionName+type;
li.setAttribute('id','contmenu');
li.setAttribute('name','type');
li.setAttribute('value',type);
li.setAttribute('action',action);
li.addEventListener('click',enableFilter,true); //добавим обработчик нажатия.
menu.appendChild(li);}
if (hasFilter!=0) //если есть отборы, добавим пункт "отключить все отборы"
{var li=document.createElement('li');
li.innerHTML='Отключить все отборы';
li.setAttribute('id','contmenu');
li.setAttribute('name','');
li.setAttribute('value','');
li.setAttribute('action','deleteAll');
li.addEventListener('click',enableFilter,true);
menu.appendChild(li);}
event.currentTarget.appendChild(menu);
event.currentTarget.blur();//снимем фокус с контейнера.
}
Теперь перейдем к функциям непосредственного создания списка.
8. Создание списка задач. id объекта в данном случае - строковое представление UID задачи, полученное из 1С
function createPage() {
var tableObject=JSON.parse(arr); //обработаем полученную из 1С строку
for (var k in tableObject) {
var obj=tableObject[k];
if (document.getElementById(obj.taskId)==undefined){ //если элемента с таким id нет (это аппендикс от попыток обновлять странцу через отдельные элементы))
var div=document.createElement('div');
var d=new Date(obj.taskDate);
if (filter.hasOwnProperty('type')) {if (obj.taskName!=filter.type){continue;}} //проверяем на фильтр. Если отбор установлен, пропускаем все что не соответствует.
div.setAttribute('id',obj.taskId); //устанавливаем атрибуты по которым элемент можно найти и
div.setAttribute('name',obj.taskName);//отфильтровать (используются в вызове контекстного меню)
div.setAttribute('dateTime',d);
div.setAttribute('priority',parseInt(obj.taskPriority)); //для расцвечивания и возможного отбора по важности
div.setAttribute('tabindex',-1);// табиндекс не позволяет переходить на элемент и фокусироваться на нем. фокус только скриптом.
div.className='container';
div.addEventListener('dblclick',addValue,true); //присваивание ид возвращаемому значению.
div.addEventListener('mouseover',function(e){return clearVal(e,true)},true); //вот тут делаем два враппера очистки значения,
div.addEventListener('mouseout',function(e){return clearVal(e,false)},true);//когда мышь над элементом и уходит с него
div.addEventListener('click',oneClick,true);
div.addEventListener('contextmenu',return contextMenu,true); //вызов контекстного меню
var t=document.createElement('time');
t.innerHTML=d.toLocaleString();//нарисуем дату задчи.
div.appendChild(t);
var p=document.createElement('p');
p.className='tooltip';
p.innerHTML=obj.taskName;
div.appendChild(p);
var info=obj.info; //для задач которым нечего сказать миру таблицу делать не будем.
if (info.lenght!=0){
createTable(div,info)};
document.body.appendChild(div);}
}
if (cur!=''){ //а вот тут нам пригодилась переменная на которую позиционируемся.
//Вышло не очень удачно, потому что по факту позиционирование идет на последний элемент над которым пробежала мышка
//Выход есть, но в этой стаье я его не рассматриваю, потому что у меня в списке нет позиционирования по одному клику.
var elem= document.getElementById(cur);
if (elem!=undefined)
{elem.scrollIntoView();}
else {cur=''}}
}
9.Создание таблицы. Сразу хочу предупредить: при передаче таблиц, если вам нужен определенный порядок их следования, не используйте в 1С структуру или соответствие. Они в json строке располагают свои ключи в произвольном порядке. Пользуйтесь массивами, где вы можете располагать пары ключ-значение структур или соответствий в нужном вам порядке.
function createTable(container,source) {
var table=document.createElement('table');
var trH=document.createElement('tr');
var trD=document.createElement('tr');
if (source.length==1){trH.style.display='none'}// таблицу из одной колонки делаем без заголовка
for (var n in source){
var pair=source[n];//получили сложный объект который представляет нам одну колонку таблицы
var keys=Object.keys(pair);//поскольку мы не знаем названий колонок выводимых в задаче, получим их из объекта
var th=document.createElement('th');
var td=document.createElement('td');
var div=document.createElement('div');
th.setAttribute('width',pair[keys[0]]['width']);//в паре колонка-значение в 1С мы добавляем еще
td.setAttribute('width',pair[keys[0]]['width']);//различные реквизиты, помогающие нам оформить таблицу. В данном случае - ширину ячеек
div.innerHTML=keys[0];//заголовок
th.appendChild(div);
var div=document.createElement('div');
var span=document.createElement('span');
div.innerHTML=pair[keys[0]]['value'];//установим значение
span.innerHTML=pair[keys[0]]['value'];
td.appendChild(div);
td.appendChild(span);
trH.appendChild(th);
trD.appendChild(td);
};
table.appendChild(trH);
table.appendChild(trD);
container.appendChild(table);
}
Все ключевые функции написаны. Оформляем в html шаблон, и используем.
В 1С нам нужна процедура выдачи данных для странички (фоновое задание как вариант), обработчик события "ondblclick", через который мы узнаем УИД задачи которую нужно открыть (глобальные переменные, retval, помните?) и обработчик события "onclick" для обработки событий контекстного меню. Когда мы создавали меню на страничке, мы присвоили ему id contmenu. В обработчике события "onclick" поставим условие:
Если pEvtObj.srcElement.id="contmenu" Тогда
....
КонецЕсли;
Обновление списка можно повесить например на скрытый справочникСписок и процедуру "приОбновленииОтображения". Для этого достаточно на обычную форму добавить скрытый элемент и указать ему период автообновления. Вся форма будет обновляться, независимо от того, виден элемент или нет.
На этом все. Буду рад, если статья поможет кому-то не набить тех же шишек, что и я.