Open nin-jin opened 6 years ago
выход за границы и наложение получается
выход за границы и наложение получается
после нажатия кнопки Comment все стало нормально отображаться...
не успел проверить в режиме инкогнито с отключенными расширениями - возможно в Chrome расширения что-то портят
Это же гитхаб, его стили мне не подконтрольны.
https://page.hyoo.ru/#!=6vhts9_pxb81u
Здравствуйте, меня зовут Дмитрий Карловский и я.. обожаю математику. Однажды мне не спалось и я запилил сервис для таких же отбитых как и я - легковесную электронную таблицу с пользовательскими формулами, шарингом и скачиванием.
Живой пример с расчётом кредита:
А дальше я расскажу, как сотворить такое же за вечер используя фреймворк $mol...
Это что за покемон?
$mol - современный фреймворк для быстрого создания кроссплатформенных отзывчивых веб-приложений. Он базируется на архитектуре MAM устанавливающей следующие правила для всех модулей:
/my/file/
и/my/file2/
. Это позволит использовать оба интерфейса не путаясь в них.Рабочее окружение
Начать разработку на $mol очень просто. Вы один раз разворачиваете рабочее окружение и далее клепаете приложения/библиотеки как пирожки.
Для начала вам потребуется установить:
Если вы работаете под Windows, то стоит настроить GIT, чтобы он не менял концы строк в ваших исходниках:
Теперь следует развернуть MAM проект, который автоматически поднимет вам девелоперский сервер:
Всё, сервер разработчика запущен, можно открывать редактор. Обратите внимание, что в редакторе нужно открывать именно директорию MAM проекта, а не проекта конкретного приложения или вашей компании.
Как видите, начать разрабатывать на $mol очень просто. Основной принцип MAM архитектуры - из коробки всё должно работать как следует, а не требовать долгой утомительной настройки.
Каркас приложения
Для конспирации наше приложение будет иметь позывной
$mol_app_calc
. По правилам MAM лежать оно должно соответственно в директории/mol/app/calc/
. Все файлы в дальнейшем мы будем создавать именно там.Первым делом создадим точку входа - простой
index.html
:Ничего особенного, разве что мы указали точку монтирования приложения специальным атрибутом
mol_view_root
в котором обозначили, что монтировать надо именно наше приложение. Архитектура $mol такова, что любой компонент может выступать в качестве корня приложения. И наоборот, любое $mol приложение - не более, чем обычный компонент и может быть легко использовано внутри другого приложения. Например, в галерее приложений.Обратите внимание, что мы уже сразу прописали пути к скриптам и стилям - эти бандлы будут собираться автоматически для нашего приложения и включать в себя только те исходные коды, что реально ему необходимы. Забегая вперёд стоит заметить, что общий объём приложения составит каких-то 36KB без минификации, но с зипованием:
Итак, чтобы объявить компонент, который будет нашим приложением, нам нужно создать файл
calc.view.tree
, простейшее содержимое которого состоит всего из одной строчки:Второе слово - имя базового компонента, а первое - имя нашего, который будет унаследован от базового. Таким образом каждый компонент является преемником какого-либо другого. Самый-самый базовый компонент, от которого происходят все остальные - $mol_view. Он даёт всем компонентам лишь самые базовые стили и поведение. В нашем случае, базовым будет компонент $mol_page представляющий собой страницу с шапкой, телом и подвалом.
Из
calc.view.tree
будет автоматически сгенерирован TypeScript класс компонента и помещён в-view.tree/calc.view.tree.ts
, чтобы среда разработки могла его подхватить:Собственно, сейчас приложение уже можно открыть по адресу
http://localhost:8080/mol/app/calc/
и увидеть пустую страничку c позывным в качестве заголовка:Синтаксис view.tree довольно необычен, но он прост и лаконичен. Позволю себе процитировать один из отзывов о нём:
Так что не пугаемся, а погружаемся! И начнём с общей раскладки страницы - она будет состоять у нас из шапки, панели редактирования текущей ячейки и собственно таблицы с данными.
У каждого компонента есть свойство
sub()
, которое возвращает список того, что должно быть отрендерено непосредственно внутри компонента. У $mol_page туда рендерятся значения свойствHead()
,Body()
иFoot()
, которые возвращают соответствующе подкомпоненты:В данном коде опущены детали реализации подкомпонент, чтобы была видна суть. Объявляя подкомпонент (он же "Элемент" в терминологии БЭМ) мы указываем его имя в контексте нашего компонента и имя класса, который должен быть инстанцирован. Созданный таким образом экземпляр компонента будет закеширован и доступен через одноимённое свойство. Например,
this.Body()
в контексте нашего приложения вернёт настроенный экземпляр $mol_scroll. Говоря паттернами, свойствоBody()
выступает в качестве локальной ленивой фабрики.Давайте преопределим свойство
sub()
, чтобы оно возвращало нужные нам компоненты:Тут мы оставили шапку от $mol_page, добавили $mol_bar в качестве панельки редактирования текущей ячейки, в качестве тела страницы использовали $mol_grid - компонент для рисования виртуальных таблиц, а подвал так и вовсе убрали, так как он нам без надобности.
Давайте взглянем, как изменился сгенерированный класс:
Визитная карточка $mol - очень "читабельный" код. Это касается не только генерируемого кода, но и кода модулей самого $mol, и прикладного кода создаваемых на его базе приложений.
Возможно вы обратили внимание на то, что объекты создаются не прямым инстанцированием по имени класса
new $mol_grid
, а черезthis.$
. Поле$
есть у любого компонента и возвращает глобальный контекст или реестр, говоря паттернами. Отличительной особенностью доступа ко глобальным значениям через поле$
является возможность любому компоненту переопределить контекст для всех вложенных в него на любую глубину компонентов. Таким образом $mol в крайне практичной и ненавязчивой форме реализует инверсию контроля, позволяющую подменять реализации использующиеся где-то в глубине переиспользуемого компонента.Формирование таблицы
Что ж, давайте нарастим немного мясца и настроим вложенные компоненты под себя: гриду нужно объяснить, какие у нас будут идентификаторы столбцов, какие идентификаторы строк, а также списки ячеек в шапке и теле таблицы.
Генерируемый класс расширится следующим описанием:
Как видите, мы просто переопределили соответствующие свойства вложенного компонента на свои реализации. Это очень простая, но в то же время мощная техника, позволяющая реактивно связывать компоненты друг с другом. В синтаксисе
view.tree
поддерживается 3 типа связывания:Для иллюстрации двустороннего связывания, давайте детализируем панель редактирования текущей ячейки:
Как видно оно у нас будет состоять у нас из двух полей ввода:
enabled
- оставим этот функционал на будущее.value
поля ввода и наше свойствоformula_current
, которое мы тут же и объявляем, указав значение по умолчанию - пустую строку.Код свойств
Edit
иformula_current
будет сгенерирован примерно следующий:Благодаря реактивному мемоизирующему декоратору $mol_mem, возвращаемое методом
formula_current
значение кешируется до тех пока пока оно кому-нибудь нужно.Пока что у нас было лишь декларативное описание композиции компонент. Прежде чем мы начнём описывать логику работы, давайте сразу объявим как у нас будут выглядеть ячейки:
Заголовки строк и колонок у нас будут плавающими, поэтому мы используем для них компонент $mol_float, который отслеживает позицию скроллинга, предоставляемую компонентом $mol_scroll через контекст, и смещает компонент так, чтобы он всегда был в видимой области. А для ячейки заводим отдельный компонент
$mol_app_calc_cell
:Этот компонент у нас будет кликабельным, поэтому мы наследуем его от $mol_button. События кликов мы направляем в свойство
select
, которое в дальнейшем у нас будет переключать редактор ячейки на ту, по которой кликнули. Кроме того, мы добавляем сюда пару атрибутов, чтобы по особенному стилизовать выбранную ячейку и обеспечить ячейкам числового типа выравниванием по правому краю. Забегая верёд, стили для ячеек у нас будут простые:Обратите внимание на одноимённый компоненту селектор
[mol_app_calc_cell]
- соответствующий атрибут добавляется dom-узлу автоматически, полностью избавляя программиста от ручной работы по расстановке css-классов. Это упрощает разработку и гарантирует консистентность именования.Наконец, чтобы добавить свою логику, мы создаём
calc.view.ts
, где создаём класс в пространстве имён$.$$
, который наследуем от одноимённого автоматически сгенерированного класса из пространства имён$
:Во время исполнения оба пространства имён будут указывать на один и тот же объект, а значит наш класс с логикой после того как отнаследуется от автогенерированного класса просто займёт его место. Благодаря такой хитрой манипуляции добавление класса с логикой остаётся опциональным, и применяется только, когда декларативного описания не хватает. Например, переопределим свойство
select()
, чтобы при попытке записать в него объект события, оно изменяло свойствоselected()
наtrue
:А свойство
type()
у нас будет возвращать тип ячейки, анализируя свойствоvalue()
:Но давайте вернёмся к таблице. Аналогичным образом мы добавляем логику к компоненту
$mol_app_calc
:Первым делом нам надо сформировать списки идентификаторов строк
row_ids()
и столбцовcol_ids()
:Они зависят от свойства
dimensions()
, которое мы будем вычислять на основе заполненности ячеек, так, чтобы у любой заполненной ячейки было ещё минимум две пустые справа и снизу:Методы
string2number()
иnumber2string()
просто преобразуют буквенные координаты колонок в числовые и наоборот:Размерность таблицы мы вычисляем на основе реестра формул, который берём из свойства
formulas()
. Возвращать оно должно json вида:А сами формулы мы будем брать и строки адреса, вида
#A1=12/B1=%3DA1*2
:Как видно, свойство
formulas()
изменяемое, то есть мы можем через него как прочитать формулы для ячеек, так и записать обновление в адресную строку. Например, если выполнить:this.formulas({ 'B1' : '24' })
, то в адресной строке мы увидим уже#A1=12/B1=24
.Адресная строка
Кроссплатформенный модуль $mol_state_arg позволяет нам работать с параметрами приложения как со словарём, но как правило удобнее получать и записывать конкретный параметр по имени. Например, позволим пользователю изменять название нашей таблицы, которое мы опять же будем сохранять в адресной строке:
Как можно заметить, если в адресной строке имя таблицы не задано, то будет взято имя заданное в родительском классе, который генерируется из
calc.view.tree
, который мы сейчас обновим, добавив в шапку вместо простого вывода заголовка, поле ввода-вывода заголовка:head()
- свойство из $mol_page, которое возвращает список того, что должно быть отрендерено внутри подкомпонентаHead()
. Это типичный паттерн в $mol - называть вложенный компонент и его содержимое одним и тем же словом, с той лишь разницей, что имя компонента пишется с большой буквы.Tools()
- панель инструментов из $mol_page, отображаемая с правой стороны шапки. Давайте сразу же заполним и её, поместив туда кнопку скачивания таблицы в виде CSV файла:$mol_link - компонент для формирования ссылок. Если ему указать
file_name()
, то по клику он предложит скачать файл по ссылке, сохранив его под заданным именем. Давайте же сразу сформируем это имя на основе имени таблицы:Локализация
Обратите внимание на символ собачки перед значением по умолчанию на английском языке:
Вставка этого символа - это всё, что вам необходимо, чтобы добавить вашему приложению поддержку локализации. В сгенерированном классе не будет строки "Download" - там будет лишь запрос за локализованным текстом:
А сами английские тексты будут автоматически вынесены в отдельный файл
-view.tree/calc.view.tree.locale=en.json
:Как видно, для текстов были сформированы уникальные человекопонятные ключи. Вы можете отдать этот файл переводчикам и переводы от них поместить в фалы вида
*.locale=*.json
. Например, добавим нашему компоненту переводы на русский язык в файлcalc.locale=ru.json
:Теперь, если у вас в браузере выставлен русский язык в качестве основного, то при старте приложения, будет асинхронно подгружен бандл с русскоязычными текстами
-/web.locale=ru.json
. А пока идёт загрузка, компоненты, зависящие от переводов, будут автоматически показывать индикатор загрузки.Заполняем ячейки
Итак, у нас есть идентификаторы строк и столбцов. Давайте сформируем списки ячеек. Сперва заголовки колонок:
Обратите внимание, мы добавили лишнюю колонку вначале, так как в ней у нас будут располагаться заголовки строк. А вот и ячейки для строк:
Далее, вспоминаем, про свойства, которые мы провязывали для ячеек:
У ячейки это просто обычные свойства, а у нас они принимают ключ - идентификатор ячейки.
Введём свойство
current()
которое будет хранить идентификатор текущей ячейки:А в реализации
selected()
мы просто будем сравнивать ячейку по переданному идентификатору и по текущему:Разумеется, если в
selected()
переданоtrue
, то будет установлен новый идентификатор в качестве текущего и сравнение ячеек тоже дастtrue
.Последний штрих - при выборе ячейки было бы не плохо переносить фокус с её самой на редактор значения:
Тут мы с помощью $mol_defer ставим отложенную задачу перенести фокус на редактор всякий раз когда меняется идентификатор текущей ячейки. Отложенные задачи выполняются в том же фрейме анимации, а значит пользователь не увидит никакого мерцания от перефокусировки. Если бы мы перенесли фокус сразу, то подписались бы на состояние сфокусированности редактора и при перемещении фокуса - сбрасывался бы и идентификатор текущей ячейки, что нам, разумеется, не надо.
Клавиатурная навигация
Постоянно тыкать мышью в ячейки для перехода между ними не очень-то удобно. Стрелочками на клавиатуре было бы быстрее. Традиционно в электронных таблицах есть два режима: режим навигации и режим редактирования. Постоянно переключаться между ними тоже напрягает. Поэтому мы сделаем ход конём и совместим редактирование и навигацию. Фокус будет постоянно оставаться на панели редактирования ячейки, но при зажатой клавише Alt, нажатие стрелочек, будет изменять редактируемую ячейку на одну из соседних. Для подобных выкрутасов есть специальный компонент $mol_nav, который является компонентом-плагином.
В $mol есть 3 вида компонент:
Добавляются плагины через свойство
plugins()
. Например, добавим клавиатурную навигацию нашему приложению:Тут мы указали, что навигироваться мы будем по горизонтали и по вертикали, по идентификаторам столбцов и колонок, соответственно. Текущие координаты мы будем синхронизировать со свойствами
current_col()
иcurrent_row()
, которые мы провяжем с собственноcurrent()
:Всё, теперь нажатие
Alt+Right
, например, будет делать редактируемой ячейку справа от текущей, и так пока не упрётся в самую правую ячейку.Копирование и вставка
Так как ячейки у нас являются ни чем иным, как нативными
td
dom-элементами, то браузер нам здорово помогает с копированием. Для этого достаточно зажатьctrl
, выделить ячейки и скопировать их в буфер обмена. Текстовое представление содержимого буфера будет ни чем иным, как Tab Separated Values, который легко распарсить при вставке. Так что мы смело добавляем обработчик соответствующего события:И реализуем тривиальную логику:
Славно, что всё это работает не только в рамках нашего приложения - вы так же можете копипастить данные и между разными табличными процессорами, такими как Microsoft Excel или LibreOffice Calc.
Выгрузка файла
Частая хотелка - экспорт данных в файл. Кнопку мы уже добавили ранее. Осталось лишь реализовать формирование ссылки на экспорт. Ссылка должна быть data-uri вида
data:text/csv;charset=utf-8,{'url-кодированный текст файла}
. Содержимое CSV для совместимости с Microsoft Excel должно удовлетворять следующим требованиям:После установки новой ссылки, мы форсируем запуск отложенных задач, чтобы произошёл рендеринг в dom-дерево до выхода из текущего обработчика событий. Нужно это для того, чтобы браузер подхватил свежесгенерированную ссылку, а не предлагал скачать предыдущую версию файла.
Формулы
Самое главное в электронных таблицах - не сами данные, а формулы, через которые можно связывать значения одних ячеек со значениями других. При этом за актуальностью вычисляемых значений электронная таблица следит сама, реактивно обновляя значения в ячейках зависимых от редактируемой в данный момент пользователем.
В нашем случае пользователь всегда редактирует именно формулу. Даже если просто вводит текст - это на самом деле формула, возвращающая этот текст. Но если он начнёт свой ввод с символа
=
, то сможет использовать внутри различные математические выражения и, в том числе, обращаться к значениям других ячеек.Реализовывать парсинг и анализ выражений - довольно сложная задача, а вечеринке уже мерещится ДедЛайн, так что мы не долго думая воспользуемся всей мощью JavaScript и позволим пользователю писать любые JS выражения. Но, чтобы он случайно не отстрелил ногу ни себе, ни кому-то ещё, будем исполнять его выражение в песочнице $mol_func_sandbox, которая ограничит мощь JavaScript до разрешённых нами возможностей:
Как видите, мы разрешили пользователю использовать математические функции и константы, а также предоставили пару функций: для получения формулы ячейки и вычисленного значения ячейки по её идентификатору.
Песочница позволяет нам преобразовывать исходный код выражения в безопасные функции, которые можно безбоязненно вызывать.
Заставлять пользователя писать вызов функции
result
вручную - слишком жестоко. Поэтому мы слегка изменяем введённую формулу, находя комбинации символов, похожие на кодовые имена ячеек видаAB34
, и заменяя их на вызовыresult
. Дополнительно, вместо значения, можно будет получить формулу из ячейки, приписав спереди собачку:@AB34
. Создание таких функций - не бесплатно, так что если в ячейке у нас просто текст, а не выражение, то мы так его и возвращаем безо всяких песочниц.Осталось дело за малым - реализовать свойство
result()
с дополнительной постобработкой для гибкости:Тут мы избавились от возможного значения
undefined
, а так же добавили преобразование строк похожих на числа в собственно числа.Финальный аккорд
На этом основная программа нашей вечеринки подходит к концу. Полный код приложения $mol_app_calc доступен на ГитХабе. Но прошу вас не спешить расходиться. Давайте каждый возьмёт по электронной таблице в свои руки и попробует сделать с ней что-нибудь эдакое. Вместе у нас может получиться интересная галерея примеров её использования. Итак...