Табличная часть в виде HTML

11 3

Когда-то очень давно появилась в разделе идей вот эта запись: http://club.directum.ru/idea/Prosmotr-i-dobavlenie-zapisi-tablicy-v-vide-formy.aspx. В комментариях я предложил альтернативную схему реализации (которую подсмотрел у нашей самописной программы-редактора бюджетных заявок) в виде представления каждой строки таблицы маленькой формой. Денис Баранов предложил тогда реализовать такую схему в веб-браузере на форме. Понадобилось без малого три года на то, чтобы вместе сложились время, место, обстоятельства и возможности, чтобы появилась в свет такая реализация. Технология не прошла еще боевого крещения на пользователях, но получен первый положительный от владельца бизнес-процесса, поэтому спешу представить вашему вниманию вариант реализации. Может быть в процессе обсуждения всплывут какие-то полезные вещи и что-то можно будет исправить до процесса широкой эксплуатации.

Итак, получается примерно вот такая картина:

Сразу приведу плюсы и минусы технологии. Преимущества:

  • То, о чем так долго мечтали многие разработчики, свершилось - кастомизировать таблицу можно почти в неограниченных пределах. Размеры, цвет, вид и так далее - все что можно сделать в рамках html и той версии Internet Explorer, которая установлена на компьютере конечного пользователя. Возможность добавить вычисления на Javascript еще больше усиливает эйфорию.
  • Отсутствие горизонтальной прокрутки в таблице многократно увеличивает удобство работы пользователя.

Недостатки:

  • Не будет работать в веб-доступе. У нас все территориально удаленные подразделения сидят через VPN на толстом клиенте, так что для нас это не проблема.
  • Многократно бОльшая сложность проектировки интерфейса. При этом WYSIWYG-редакторы генерируют такой html-текст, что лучше разобраться в верстке самостоятельно и писать руками в любом удобном редакторе.
  • Сложности со связыванием DIRECTUM и html-таблицы в области двустороннего обмена данных. Для каждого типа данных в DIRECTUM надо фактически разрабатывать отдельный контрол на html.
  • Подключение для отображения таблицы сторонней программы в виде Internet Explorer увеличивает риски багов, несовместимостей, костылей и странного поведения программ.
  • Уменьшение таблицы по горизонтали сильно увеличивает ее по вертикали. В условиях нерезиновых форм DIRECTUM и большого количества данных в таблице это может стать существенной проблемой.

Переходим к технической части. Стандартный дисклеймер: я не являюсь гуру верстки и дизайна, а JS первый раз использовал где-то в начале этого года. Поэтому код может содержать неточности, ошибки и подводные камни, особенно с учетом того, что одну и ту же задачу на html + js можно решить иногда десятью разными способами. Мне в быстром изучении html, css и js очень помог ресурс http://learn.javascript.ru - мало воды, много практических примеров, многое оттуда перенесено напрямую сюда.

Техническая часть из себя будет представлять простыни кода с необходимыми пояснениями. Разработка велась в PyCharm 3.1 (он у меня уже был установлен), где есть сворачивание блоков кода, подсветка, автокомплит и так далее. Рекомендую пользоваться системой контроля версий, благо есть возможность. В дальнейшем речь будет идти о справочнике, хотя все это справедливо и для форм электронных документов.

Делаем заготовку html-файла в каком-нибудь веб-узле на IIS или просто на диске в файле (для работы пользователей в DIRECTUM предлагается разместить конечный html-файл в самом DIRECTUM и выгружать его во временную папку - так будет обойдена проблема возможной недоступности веб-ресурса):



    
        
    

    
        
        
        

Элемент input нужен для пересоздания таблицы без полной перезагрузки страницы, будем по нему виртуально щелкать. Такая полная перезагрузка, почему-то, стабильно роняет DIRECTUM в сочетании Win7 + IE11. В других сочетаниях может тоже падает, не проверял. Блок style размещаем для удобства прямо в html-файле, чтобы не возиться с несколькими файлами, при желании можно вынести его в отдельный css. Элемент div у нас будет содержать создаваемую табличную часть, а script - код на Javascript (тоже не выносим в отдельный файл).

В событие "Форма" - "Показ" справочника помещаем код, который будет нам загружать нашу страницу:

  filePath = "http://servername/filename.html"
  WebBrowserControl = Object.Form.Controls.FindControl("STWebBrowserControl1") 
  if Assigned(WebBrowserControl)
    if Assigned(WebBrowserControl.Document)
      WebBrowserControl.Document.getElementById('refreshButton').click()
    else
      WebBrowserControl.Navigate(FilePath)
    endif
  endif

Есть небольшая хитрость. Если у пользователя проблемы с отображением веб-контрола или всей страницы, то можно ему показать старую табличную часть. Один из вариантов - можно завести роль, в которой у нас будет список пользователей, для которых отображается старая табличная часть. Тогда размещаем веб-контрол и таблицу в одном месте и регулируем их скрытие/появление (вот только установка Visible в 4.9.1 для веб-контрола не работает, поэтому сдвигаем его за пределы видимости, используя свойство Left).

В событии "Запись" - "Отмена после" размещаем код, который будет обновлять таблицу, если пользователь отменил изменения:

  WebBrowserControl = Object.Form.Controls.FindControl("STWebBrowserControl1") 
  if not VarIsNull(WebBrowserControl)
    WebBrowserControl.Document.getElementById('refreshButton').click()
  endif

Начинаем заполнять html-файл. Первой создаем "входную функцию", с которой начнется построение таблицы, и сразу ее вызываем. Эта функция делает три вещи. Во-первых, она определяем необходимые для работы данные: датасет, набор доступных для редактирования реквизитов и флаг доступности таблицы для вставки/удаления строк (это параметры для моего случая; для других случаев возможно уменьшение или увеличение параметров для работы функции, неизменным остается только датасет). Если html-файл открыт на форме справочника, то данные берутся из DIRECTUM, в противном случае, мы создаем тестовые заглушки, чтобы наш файл без потери функциональности можно было открывать в браузере. Это дает нам простоту тестирования без привязки к DIRECTUM и консоль/отладку по клавише F12 (выводить информацию в консоль можно, написав в js вызов console.log(<текст>); или можно показывать сообщение, аналогичное ShowMessage(), с помощью alert(<текст>)). Во-вторых, наша стартовая функция формирует таблицу. Ну и в-третьих, добавляется (в случае возможности для текущего пользователя редактировать таким образом таблицу) ссылка "Добавить строку". Текст функции (функции в секцию script можно вставлять в любом порядке - браузер сначала ищет определения и только потом начинает исполнение кода):

    function createHTML(){
        if (window.external.Form){ //if directum
            var ref = window.external.Form.View.Component;
            var dds = ref.DetailDataset(1);
            var editableControlsList = ref.Params.FindItem('EditableControlsList');
            var allowEditTable = (ref.Params.FindItem("AllowEditTable") == 'Yes');
            var recordOpened = ref.RecordOpened;
        }
        else { //for testing
            var dds = new getDDSForTesting();
            var editableControlsList = new getEditableControlsList();
            var allowEditTable = true;
            var ref = {};
            var recordOpened = true;
        }

        if (recordOpened) {
            dds.First();

            var firstDiv = document.getElementById('firstdiv');
            for (var i = 0; i < dds.RecordCount; i++){
                createNewHtmlRow(firstDiv, dds, true, editableControlsList, allowEditTable);
                dds.Next();
            }

            if (allowEditTable) {
                var link = document.createElement('A');
                link.onclick = addNewRowToHTMLTable;
                link.directumDataset = dds;
                link.editableControlsList = editableControlsList;
                link.allowEditTable = allowEditTable;
                link.innerHTML = 'Добавить новую строку';
                link.className = 'addNewRowBtn';
                link.id = 'linkAddNewRow';

                firstDiv.appendChild(link);
            }
        }
    }

Идем дальше. Функция создания заглушки датасета getDDSForTesting(). Объяснять тут в общем-то нечего, она эмулирует IReference и добавляет три строки в виртуальную таблицу для тестирования.

  function getDDSForTesting() {
      this.currentRowNum = -1;
      this.RecordCount = 0;
      this.dataset = [];
    
      //IReference.Next()
      this.Next = function() {
          this.currentRowNum++;
      }
    
      //IReference.Requisites()
      this.Requisites = function (requisiteName) {
          if (this.RecordCount == 0) {
              alert('Набор данных пуст. Установка реквизита ' + requisiteName + ' невозможнa');
              return undefined;
          }
          //gets current table row from dataset
          var tableRow = this.dataset[this.currentRowNum];
   
          //gets data from table row by requisite name
          if (requisiteName in tableRow) {
              return tableRow[requisiteName];
          }
          else {
              var directumRequisite = {};
              directumRequisite.AsString = "";
              directumRequisite.Value = "";
              directumRequisite.Dataset = this;
              tableRow[requisiteName] = directumRequisite;
              return tableRow[requisiteName];
          }
      }
    
      //IReference.Locate()
      this.Locate = function (requisiteName, requisiteValue) {
          var tableRow;
          var reqValue;
          for (var i = 0; i < this.RecordCount; i++) {
              tableRow = this.dataset[i];
              reqValue = tableRow[requisiteName].AsString;
              if (reqValue === requisiteValue) {
                  this.currentRowNum = i;
                  return true;
              }
          }
          return false;
      }
    
      //IReference.Delete()
      this.Delete = function () {
          var iterSteps = this.RecordCount - 1;
          for (var i = this.currentRowNum; i < iterSteps; i++) {
              this.dataset[i] = this.dataset[i+1];
          }
          this.RecordCount--;
          this.dataset.length--;
          if (this.RecordCount == this.currentRowNum) {
              this.currentRowNum--;
          }
      };
    
      //IReference.Insert()
      this.Insert = function() {
          var newIndex;
          var rowNum;
          if (this.RecordCount == 0) {
              newIndex = 0;
              rowNum = '0';
          }
          else {
              newIndex = this.dataset.length;
              rowNum = this.dataset[newIndex - 1]['НомСтр'].AsString;
          }
          this.dataset[newIndex] = {};
    
          //adds one requisite with number of row
          this.dataset[newIndex]['НомСтр'] = {};
          this.dataset[newIndex]['НомСтр'].AsString = +rowNum + 1;
          this.dataset[newIndex]['НомСтр'].Value = +rowNum + 1;
          this.dataset[newIndex]['НомСтр'].Dataset = this;
          //------
    
          this.currentRowNum = newIndex;
          this.RecordCount++;
      };
    
      //IReference.First()
      this.First = function() {
          this.currentRowNum = 0;
      };
    
      //adds 3 elements to dataset
      this.Insert();
      this.Requisites('SIN_StringT').AsString = 'Первый';
      this.Requisites('SIN_String2T').AsString = 'Первый 2Т';
   
      this.Insert();
      this.Requisites('SIN_StringT').AsString = 'Второй';
      this.Requisites("SIN_YesNoT").AsString = 'Нет';
   
      this.Insert();
      this.Requisites("SIN_StringT").AsString = 'Третий';
      this.Requisites('SIN_YesNoT').AsString = 'Да';
  }

Функция получается несколько тяжелая для понимая с учетом того, что IReference содержит в себе и итератор, и данные (в результате нижеприведенный пример выглядит странно), но можно в ней не разбираться, главное, что она работает.

  foreach rec in reference
    showmessage(reference.Наименование)
  endforeach

Следующей будет небольшая функция, которая заменит нам IStringList в простейшем варианте, который разрешает редактировать все контролы:

  function getEditableControlsList() {
      this.IndexOf = function(requisiteName) {
          return 0;
      }
  }

Можно сделать эту заглушку более интеллектуальной, в зависимости от requisiteName возвращая -1 (элемент недоступен для редактирования) или любое число большее или равное нулю (хотя сам индекс нам не нужен, но функции Find в IStringList, к сожалению, нет). В DIRECTUM этот список можно формировать в событии "Форма" - "Показ", добавляя имена реквизитов, которые разрешено редактировать.

Пропущу пока функцию createNewHtmlRow() и перейду к ссылке/кнопке "Добавить новую строку". На ее событие onclick вешается несложная функция addNewRowToHTMLTable(), которая вызывает ту же самую createNewHtmlRow():

    function addNewRowToHTMLTable(evt) {
        var dds = this.directumDataset;
        var editableControlsList = this.editableControlsList;
        var allowEditTable = this.allowEditTable;
        dds.Insert();
        var firstDiv = document.getElementById('firstdiv');
        createNewHtmlRow(firstDiv, dds, false, editableControlsList, allowEditTable);
        window.scrollTo(0,document.body.scrollHeight);
    }

В последней строке происходит прокрутка экрана до конца страницы, чтобы новая добавленная строка было полностью видима.

Функция createNewHtmlRow() формирует нам строку таблицы. Именно там задается расположение полей и управляющие кнопки/ссылки для строки. Понятно, что набор полей будет уникальным в каждом конкретном случае, поэтому дам общие пояснения.

  • Для более удобного просмотра часть полей спрятаны в скрывающуюся область.
  • Вся строка представляет из себя элемент div, в котором поля расположены в элементах p (параграф).
  • Возможности добавлять дробные числа пока нет. Самое простое решение - давать вводить точку в поле номер - не подходит, т.к. может приводить к тексту вида 45.00.0, которое будет вызывать ошибку. Надо чуть более сложную обработку.
  • Возможности добавлять поля типа "Справочник" пока нет. Технически возможность есть - например, использовать реквизит Вычисление, который будет обрабатывать введенное текстовое значение и давать выбирать значение из справочника.

Текст функции вместе со вспомогательными функциями раздует статью еще в два раза, поэтому приводить его здесь не буду. Они есть в приложенном файле.

Функция rebuildTable(), которая указана для input в файле-заготовке. Это функция просто очищает доступным способом содержимое первого элемента div и вызывает новое построение таблицы:

    function rebuildTable() {
        var firstDiv = document.getElementById('firstdiv');
        try {
            firstDiv.innerHTML = '';
        } catch(e) {
            while(firstDiv.firstChild) {
                firstDiv.removeChild(firstDiv.firstChild);
            }
        }
        createHTML();
    }

Остался вопрос связи табличной части с html-формой.

  • Связь html -> DIRECTUM решается очень просто: каждое изменение поля в html вызывает копирование введенных данных в соответствующее поле в DIRECTUM.
  • Связь DIRECTUM -> html (например, если ввод данных в одном поле вызывает вычисления, которые изменяют данные в другом поле) чуть сложнее. Я реализовал ее следующим образом: в событие "Вычисление" реквизита, данные из которого надо передать в html, помещается следующий код:
  if Assigned(Object.View)
    WebBrowserControl = Object.Form.Controls.FindControl("STWebBrowserControl1")
    if Assigned(WebBrowserControl)
      if Assigned(Sender.Value)
        value = Sender.DisplayText
      else
        value = ''
      endif
      WebBrowserControl.Document.getElementById('orgElem' & Object.DetailDataset(1).НомСтр).value = value
    endif
  endif

Т.е. при построении таблицы в html, для элемента задается id, состоящий из условного наименования и номера строки. Тогда мы можем в нужный момент найти этот элемент в html и записать туда нужное нам значение.

Приложенный файл открывается в браузере и служит демонстрационной моделью. Для использования его в DIRECTUM нужно исправить функцию createNewHtmlRow(), задав нужный порядок полей и их коды.

tableDIRECTUM.html (35,54 Кб)

От администрации DIRECTUM Club. Материал подан как заявка на DIRECTUM Awards - поддерживайте участника.

Валентина Писанова

Андрей, это офигенно круто!

Андрей Куров
Андрей, это офигенно круто!

Спасибо :)

Павел Власов

Круто, но ведь нереально трудоемко :(
 

Авторизуйтесь, чтобы написать комментарий